mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
A subagent pane now names its parent and takes you back to it. With a composer, a pill leads the rail above it; without one, a parent box takes the composer's place. Both open the parent through navigateToAgent.
173 lines
7.3 KiB
TypeScript
173 lines
7.3 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's row shows the facts its sheet shows", async () => {
|
|
await page.getByTestId("subagents-track-header").click();
|
|
await expect(page.getByTestId(`subagents-track-row-facts-${child.id}`)).toHaveText(
|
|
"Ten second stream · Low · Load test",
|
|
);
|
|
await tapSheetBackdrop(page);
|
|
});
|
|
|
|
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("the parent box in a subagent's sheet closes it onto the parent", async () => {
|
|
await page.getByTestId(`subagents-track-row-${child.id}`).click();
|
|
const childSheet = sheet(page, child.title);
|
|
await expectSheetAtSnap(page, child.title);
|
|
await expect(childSheet.getByTestId("composer-parent-agent-pill")).toHaveCount(0);
|
|
await childSheet.getByTestId("parent-agent-callout-open").click();
|
|
await expect(childSheet).toHaveCount(0);
|
|
await expectOpenTabCount(page, 1);
|
|
});
|
|
|
|
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();
|
|
}
|
|
});
|