mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 18:35:17 +00:00
* Put sheet tabs on the one sheet stack Sheets are one primitive with one stack. A sheet opened from any sheet stacks on top of it, and closing it uncovers the sheet underneath. The compact workspace's file and subagent sheets were an in-tree Gorhom BottomSheet with their own stacking, so a subagent chosen from the subagents list sheet replaced the list instead of covering it. - SheetHostScope: a subtree hosts the sheets presented inside it, so the sheet tabs draw inside the compact column and read the workspace providers while still joining the app-wide stack. Needs Gorhom's BottomSheetModalInternalProvider, which the patch exports. - IsolatedBottomSheetModal takes enableDismissOnClose; false keeps the content mounted through a close so the unsaved-changes guard can veto it. Its backdrop then closes the sheet rather than dismissing the top modal. The Gorhom patch also keeps a dismissing modal's status through its final onChange(-1), without which such a modal unmounting while presented never left the portal. - SheetTabs (was WorkspaceSheetHost) renders one IsolatedBottomSheetModal per store entry. The store never reorders sheets; a reveal of a covered sheet drops the unmodified sheets above it. - Subagent rows that open a sheet keep the list open (closeOnSelect false); rows that open a tab still close it. - A menu closes when its retained panel goes inactive, instead of hiding and reappearing with the panel on the next tab switch. Co-Authored-By: Claude <noreply@anthropic.com> * Show an agent's model and thinking in its sheet, and fit Open as tab in the header rail A Paseo agent shown in a sheet has no composer, so the facts the composer's model and thinking controls would show now sit in one line under the sheet header, the same row provider subagents already get from the daemon. The line resolves exactly as the controls do (resolveAgentModelSelection), from the session agent and the providers snapshot, and shows wherever the pane host is a sheet. Open as tab becomes the full-screen glyph beside Close, drawn by the shared SheetHeaderIconButton, so the title keeps its width. The tab switcher sheet is titled "Workspace tabs" so it reads as what it lists. Co-Authored-By: Claude <noreply@anthropic.com> * Show mode and context usage in the sheet facts line The line under a sheet's header now carries everything the composer's controls and context meter show: model, thinking, mode, and used/max context tokens with the share, each resolved as its control resolves it. Co-Authored-By: Claude <noreply@anthropic.com> * Re-render subagent rows when the workspace's tabs change A row decides at render time whether choosing it opens a sheet or reveals a tab, and keeps the list open only for a sheet. The hook behind it only subscribed to the layout mode, so a row rendered while its agent was a full tab kept closing the list after that tab was gone: on iOS, promoting a managed child, closing its tab, and reopening it from the list dropped the list under the sheet. Subscribe to the workspace's layout too. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { expect, test } from "../support/fixtures";
|
|
import { expectComposerVisible } from "../support/helpers/composer";
|
|
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
|
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
import {
|
|
expectSubagentRowVisible,
|
|
openSubagentFromCompactTrack,
|
|
seedSubagent,
|
|
} from "../support/helpers/subagents";
|
|
import {
|
|
closeSheet,
|
|
expectOpenTabCount,
|
|
openSheetAsTab,
|
|
expectDeclinedDiscardPrompt,
|
|
expectSheetAtSnap,
|
|
replaceSheetFileText,
|
|
selectTabFromSwitcher,
|
|
sheet,
|
|
sheetHeader,
|
|
tapSheetBackdrop,
|
|
} from "../support/helpers/sheet";
|
|
|
|
const FILE_LINK = "linked.ts:3";
|
|
|
|
function promptMentioning(target: string): string {
|
|
return [
|
|
"Generate a title and a git branch name for a coding agent from the user prompt and attachments.",
|
|
"Return JSON only with fields 'title' and 'branch'.",
|
|
"",
|
|
"<user-prompt>",
|
|
`Open \`${target}\` now`,
|
|
"</user-prompt>",
|
|
].join("\n");
|
|
}
|
|
|
|
test("compact opens files and subagents in a sheet, leaving the tabs for agents", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
const workspace = await seedMockAgentWorkspace({
|
|
repoPrefix: "compact-workspace-sheet-",
|
|
title: "Sheet parent",
|
|
initialPrompt: promptMentioning(FILE_LINK),
|
|
});
|
|
try {
|
|
const gate = await installDaemonWebSocketGate(page);
|
|
const linkedPath = path.join(workspace.cwd, "linked.ts");
|
|
await writeFile(linkedPath, "one\ntwo\nthree\n", "utf8");
|
|
const child = await seedSubagent(
|
|
{ client: workspace.client, repoPath: workspace.cwd, workspaceId: workspace.workspaceId },
|
|
{ parentId: workspace.agentId, title: "Sheet child" },
|
|
);
|
|
await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: workspace.agentId });
|
|
await expectOpenTabCount(page, 1);
|
|
|
|
await test.step("a chat file link opens the file in a sheet", async () => {
|
|
const fileLink = page.getByText(FILE_LINK, { exact: true });
|
|
await expect(fileLink).toBeVisible({ timeout: 15_000 });
|
|
await fileLink.click();
|
|
|
|
const fileSheet = sheet(page, "linked.ts");
|
|
await expect(fileSheet).toBeVisible();
|
|
await expect(fileSheet.getByText("three")).toBeVisible();
|
|
await expectSheetAtSnap(page, "linked.ts");
|
|
await expect(
|
|
sheetHeader(page, "linked.ts").getByRole("button", { name: "Open as tab" }),
|
|
).toHaveCount(0);
|
|
await expectOpenTabCount(page, 1);
|
|
await closeSheet(page, "linked.ts");
|
|
});
|
|
|
|
await test.step("a subagent opens in a sheet over the subagents list", async () => {
|
|
await openSubagentFromCompactTrack(page, child.id);
|
|
|
|
const childSheet = sheet(page, child.title);
|
|
await expect(childSheet).toBeVisible();
|
|
await expectSheetAtSnap(page, child.title);
|
|
await expect(childSheet.getByRole("textbox", { name: "Message agent..." })).toHaveCount(0);
|
|
// The composer's model, thinking, and mode stand in for it, resolved the same way. An idle
|
|
// seeded agent has no context usage yet.
|
|
await expect(childSheet.getByTestId("agent-pane-subtitle")).toHaveText(
|
|
"Ten second stream · Low · Load test",
|
|
);
|
|
await expectOpenTabCount(page, 1);
|
|
});
|
|
|
|
await test.step("closing the subagent's sheet uncovers the list", async () => {
|
|
await closeSheet(page, child.title);
|
|
await expectSubagentRowVisible(page, child.id);
|
|
});
|
|
|
|
await test.step("Open as tab promotes the subagent to a full tab and ends the list", async () => {
|
|
await page.getByTestId(`subagents-track-row-${child.id}`).click();
|
|
await expectSheetAtSnap(page, child.title);
|
|
await openSheetAsTab(page, child.title);
|
|
await expect(page.getByTestId(`subagents-track-row-${child.id}`)).toHaveCount(0);
|
|
await expectOpenTabCount(page, 2);
|
|
await expectComposerVisible(page);
|
|
});
|
|
|
|
await test.step("the list the subagent was opened from closed with its tab", async () => {
|
|
await selectTabFromSwitcher(page, "Sheet parent");
|
|
await expect(page.getByTestId(`subagents-track-row-${child.id}`)).toHaveCount(0);
|
|
});
|
|
|
|
await test.step("once its tab closes, the subagent reopens in a sheet that keeps the list", async () => {
|
|
await page
|
|
.getByRole("button", { name: /Switch tabs/ })
|
|
.filter({ visible: true })
|
|
.click();
|
|
await expect(page.getByText("Workspace tabs", { exact: true })).toBeVisible();
|
|
const menuBase = `workspace-tab-menu-agent_${child.id}`;
|
|
await page.getByTestId(`${menuBase}-trigger`).click();
|
|
await page.getByTestId(`${menuBase}-close`).click();
|
|
await expectOpenTabCount(page, 1);
|
|
// Closing a tab keeps the switcher open, with the parent as the tab left. Its backdrop is
|
|
// tapped once the row menu's own sheet has gone.
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(1);
|
|
await tapSheetBackdrop(page);
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(0);
|
|
|
|
await openSubagentFromCompactTrack(page, child.id);
|
|
await expectSheetAtSnap(page, child.title);
|
|
await closeSheet(page, child.title);
|
|
await expectSubagentRowVisible(page, child.id);
|
|
await tapSheetBackdrop(page);
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(0);
|
|
});
|
|
|
|
await test.step("a sheet with unsaved edits closes only through the guarded close", async () => {
|
|
await page.getByText(FILE_LINK, { exact: true }).click();
|
|
const fileSheet = sheet(page, "linked.ts");
|
|
await expect(fileSheet).toBeVisible();
|
|
// Hold the autosave so the edit stays unsaved.
|
|
gate.holdNextClientRequest("fs.file.write.request");
|
|
await replaceSheetFileText(page, "linked.ts", "edited in the sheet");
|
|
await gate.waitForHeldClientRequest();
|
|
|
|
await expectDeclinedDiscardPrompt(page, () => tapSheetBackdrop(page));
|
|
await expect(fileSheet.locator(".cm-content")).toHaveText("edited in the sheet");
|
|
await expectDeclinedDiscardPrompt(page, () =>
|
|
sheetHeader(page, "linked.ts").getByRole("button", { name: "Close" }).click(),
|
|
);
|
|
await expect(fileSheet.locator(".cm-content")).toHaveText("edited in the sheet");
|
|
|
|
gate.releaseHeldClientRequest();
|
|
await expect.poll(() => readFile(linkedPath, "utf8")).toBe("edited in the sheet");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|