mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
* feat(plugins): let a plugin put the cursor in any agent's composer
A pill could insert text but left the cursor elsewhere, so the user had
to click the composer before typing. Plugins can address any agent's
composer, and an author cannot know where the user is, so focus() owns
revealing the agent as well as focusing it.
A running agent's composer holds one pending request and navigates to
the agent, completing when an input that can take the keyboard attaches.
Drafts and the New workspace composer have nothing to navigate to and
must not pull the user back, so they reject when off screen.
* fix(app): stop treating a covered New workspace composer as on screen
The screen passed isPaneFocused={true}, so with Settings pushed over it
the input still counted as revealed. focus() on a held composer resolved
on native with no visible cursor, and rejected with the wrong error on web.
The screen now reports its real route focus.
* fix(app): reach a compact subagent's composer and keep a focus selection
A compact sheet has no composer, so focus() on a subagent without a tab
opened a read-only sheet and timed out. It now promotes the sheet to a
tab, the same path as Open as tab.
A pending focus ran inside the effect that attached the input, before a
later effect in that commit applied a prompt set alongside it, so its
selection was clamped against the old text. It now runs after the commit.
* fix(app): show a compact subagent's tab when focus promotes its sheet
navigateToAgent stops at the sheet, so nothing closed an Explorer panel
covering the promoted tab and focus() timed out. The promoted tab now
goes through the same reveal as any other navigated tab.
* fix(app): open focus targets as tabs and focus Explorer composers on compact
Promoting a sheet after navigating could not reach an open deferred
until the workspace loaded, so the request now asks for a tab up front
with asTab, which travels with the deferred open. That removes the
post-navigation promotion.
A compact composer counted as covered whenever the panel target was not
the agent panel, so one hosted in the Explorer could never take focus.
Coverage now follows the composer's own host.
* fix(app): judge compact composer coverage by where it is hosted
The Explorer dock always reported its pane as focused, so a workspace sheet
over an Explorer composer still counted as on screen and focus() put the
cursor behind the sheet. It now suspends pane focus like the main panes do.
A composer on a screen with no panes counted as a main pane, so an Explorer
selection left over from the workspace marked New workspace as covered and
focus() rejected on a visible input. Only the agent list can cover those
screens, because the Explorer lives in the workspace.
* fix(app): close the composer's own sheets and menus when a plugin focuses it
focus() judged a composer on screen by its pane and panel alone, so a sheet
or menu the composer opened over itself, like the compact controls sheet
that hosts plugin Actions or a Subagents track page, stayed up while the
cursor went behind it. Navigation cannot close those, so focus() now closes
them itself.
Sheets and menus register with the nearest OverlayDismissalScope while open.
The composer opens one, and the agent panel opens one around its whole chat
because the tracks render outside the composer; a nested scope joins the
enclosing one.
* fix(app): close a composer's open picker when a plugin focuses it
The thinking, provider-option and plugin selectors render Combobox, which
never joined the composer's overlay dismissal scope. focus() left the
picker open, resolved on the textarea's brief focus, and the picker's
focus enforcement then took the cursor back. Combobox now registers its
close while open, like sheets and menus.
* fix(app): close a composer's modals before focus places the cursor
An image preview opened from the composer stayed over it after focus()
succeeded: the shared Modal never joined the overlay dismissal scope.
Modal now registers its onRequestClose while visible. Closing it then
exposed a second race: a web modal hands focus back to the button that
opened it in the commit that removes it, after the cursor was placed.
Dismissal now resolves once every closed overlay has unregistered, and
the composer places the cursor after that, waiting at most 1s so a
plugin modal that refuses to close cannot hold the promise forever.
A focus request replaced by a newer one resolved immediately, even when
the composer never came on screen. It now settles with the newer one.
* fix(app): close every composer sheet on focus and keep the latest request
The context usage sheet opens through the bottom sheet visibility hook
directly, so it stayed over the composer after focus(). Registration
moves to that hook, which every controlled bottom sheet uses;
AdaptiveModalSheet registers only its desktop dialog.
Two focus() calls in one turn overwrote each other's wait for an
overlay to close, so the older one resumed a second later and put its
selection back. Callers now share each overlay's close, and the latest
request places the cursor while earlier ones settle with it.
On native, focus() reported success when the input refused focus, as a
non-editable input does during dictation. It now checks isFocused().
* fix(app): let a focus call made while the input attaches decide the selection
A queued focus was dispatched in a microtask after the input attached, so a
focus() made in between (from a mounting item, for example) was overwritten
by the older selection. Each call now carries its sequence number; a queued
request that is no longer the latest settles with the newer one instead of
placing the cursor.
* fix(app): close a workspace sheet that focus dismisses before it animates open
A bottom sheet modal that unmounts after presenting but before its first
animation is still INITIAL, and the library's portal unmount returned early
for that status. Its portal stayed on the host and kept sliding open with no
owner, so composer.focus() removed the sheet from the workspace but left it
covering the input. The patch unmounts the portal in that case.
The compact Explorer focus test also waits for the phone layout before
focusing, since focus otherwise resolved against the wide layout.
501 lines
22 KiB
TypeScript
501 lines
22 KiB
TypeScript
import { seedMockAgentWorkspace, openAgentRoute } from "../support/helpers/mock-agent";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { openFileFromExplorer } from "../support/helpers/file-explorer";
|
|
import { openSettingsSection, clickSettingsBackToWorkspace } from "../support/helpers/settings";
|
|
import { expect, test } from "../support/fixtures";
|
|
import {
|
|
gotoWorkspace,
|
|
pressNewTabShortcut,
|
|
expectTabTitleFits,
|
|
} from "../support/helpers/launcher";
|
|
import { seedWorkspace } from "../support/helpers/seed-client";
|
|
import {
|
|
ensureExplorerSidebar,
|
|
openFilesPanel,
|
|
waitForWorkspaceTabsVisible,
|
|
dragTabIntoExplorer,
|
|
} from "../support/helpers/workspace-tabs";
|
|
|
|
function explorerSidebar(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
return page.getByTestId("workspace-explorer-sidebar").filter({ visible: true });
|
|
}
|
|
|
|
async function expectExplorerActiveTabForeground(
|
|
page: Parameters<typeof ensureExplorerSidebar>[0],
|
|
) {
|
|
await openFilesPanel(page);
|
|
const main = page.getByTestId("workspace-pane-main");
|
|
await main.hover();
|
|
const activeMainLabel = main
|
|
.getByTestId("workspace-tabs-row")
|
|
.locator('[aria-selected="true"]')
|
|
.getByText("New tab", { exact: true });
|
|
const foreground = await activeMainLabel.evaluate((element) => getComputedStyle(element).color);
|
|
const activeFiles = explorerSidebar(page).getByRole("button", {
|
|
name: "Browse workspace files",
|
|
exact: true,
|
|
});
|
|
await expect(activeFiles).toHaveAttribute("aria-selected", "true");
|
|
await expect(activeFiles.getByText("Files", { exact: true })).toHaveCSS("color", foreground);
|
|
}
|
|
|
|
test.describe("Explorer sidebar", () => {
|
|
test("starts with Files and Changes, switches views, and toggles without changing main", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-sidebar-defaults-" });
|
|
|
|
try {
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await waitForWorkspaceTabsVisible(page);
|
|
const mainTabsBefore = await page
|
|
.getByTestId("workspace-pane-main")
|
|
.locator('[data-testid^="workspace-tab-"]')
|
|
.count();
|
|
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await expect(explorer.getByTestId("workspace-tab-files")).toBeVisible();
|
|
await expect(explorer.getByTestId("workspace-tab-changes_tree")).toBeVisible();
|
|
await expect(explorer.getByTestId("workspace-new-tab-button")).toHaveCount(1);
|
|
|
|
await expectExplorerActiveTabForeground(page);
|
|
await expect(explorer.getByTestId("file-explorer-tree-scroll")).toBeVisible();
|
|
|
|
await explorer.getByTestId("workspace-tab-changes_tree").click();
|
|
await expect(explorer.getByTestId("changes-tree-panel")).toBeVisible();
|
|
|
|
await page.getByTestId("workspace-explorer-toggle").first().click();
|
|
await expect(explorerSidebar(page)).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("workspace-pane-main").locator('[data-testid^="workspace-tab-"]'),
|
|
).toHaveCount(mainTabsBefore);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function launchExplorerPanel(
|
|
page: Parameters<typeof ensureExplorerSidebar>[0],
|
|
name: string,
|
|
) {
|
|
const explorer = explorerSidebar(page);
|
|
await explorer.getByRole("button", { name: "New tab", exact: true }).click();
|
|
await page.getByRole("menuitem", { name: new RegExp(`^${name}`) }).click();
|
|
await explorer.getByRole("button", { name: "New tab", exact: true }).click();
|
|
await expect(page.getByRole("menuitem", { name: new RegExp(`^${name}`) })).toHaveCount(0);
|
|
await page.keyboard.press("Escape");
|
|
}
|
|
|
|
async function closeExplorerFilesFromContextMenu(
|
|
page: Parameters<typeof ensureExplorerSidebar>[0],
|
|
) {
|
|
const explorer = explorerSidebar(page);
|
|
const files = explorer.getByRole("button", { name: "Browse workspace files", exact: true });
|
|
await files.hover();
|
|
await expect(explorer.getByTestId("workspace-files-close")).toHaveCount(0);
|
|
await explorer.getByTestId("workspace-tab-changes_tree").hover();
|
|
await expect(explorer.getByTestId("workspace-working-diff-close-changes_tree")).toHaveCount(0);
|
|
await files.click({ button: "right", position: { x: 12, y: 13 } });
|
|
await page.getByRole("menuitem", { name: "Close", exact: true }).click();
|
|
await expect(files).toHaveCount(0);
|
|
}
|
|
|
|
async function expectWorkspaceCloseOnHover(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
const main = page.getByTestId("workspace-pane-main");
|
|
const tab = main.locator('[data-testid^="workspace-tab-"][aria-selected="true"]');
|
|
const close = main.getByRole("button", { name: "Close", exact: true });
|
|
await explorerSidebar(page).hover();
|
|
await expect(close.locator("..")).toHaveCSS("opacity", "0");
|
|
await tab.hover();
|
|
await expect(close.locator("..")).toHaveCSS("opacity", "1");
|
|
}
|
|
|
|
async function closeOtherExplorerTabs(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
const files = explorerSidebar(page).getByRole("button", {
|
|
name: "Browse workspace files",
|
|
exact: true,
|
|
});
|
|
await files.click({ button: "right", position: { x: 12, y: 13 } });
|
|
const confirmation = page.waitForEvent("dialog").then((dialog) => {
|
|
expect(dialog.message()).toContain("close 1 tab");
|
|
return dialog.accept();
|
|
});
|
|
await page.getByRole("menuitem", { name: "Close other tabs", exact: true }).click();
|
|
await confirmation;
|
|
}
|
|
|
|
test("Explorer keeps Files and Changes close actions in the context menu", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-shared-tabs-" });
|
|
try {
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await waitForWorkspaceTabsVisible(page);
|
|
const main = page.getByTestId("workspace-pane-main");
|
|
const mainTabsBefore = await main.getByTestId("workspace-tabs-row").getByRole("button").count();
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await expectTabTitleFits(page, "Files", { min: 64, max: 90 });
|
|
|
|
await test.step("Explorer's + menu excludes Agent and terminal profiles", async () => {
|
|
await explorer.getByRole("button", { name: "New tab", exact: true }).click();
|
|
const menu = page.getByTestId("workspace-new-tab-menu").filter({ visible: true });
|
|
await expect(menu).toBeVisible();
|
|
await expect(menu.getByRole("menuitem", { name: /^Agent/ })).toHaveCount(1);
|
|
await expect(menu.getByRole("menuitem", { name: /^Files/ })).toHaveCount(0);
|
|
await expect(menu.getByRole("menuitem", { name: /^Changes/ })).toHaveCount(0);
|
|
await expect(menu.getByRole("menuitem", { name: /^Terminal/ })).toBeVisible();
|
|
const entries = await menu.getByRole("menuitem").allTextContents();
|
|
expect(entries.findIndex((entry) => entry.startsWith("Terminal"))).toBeLessThan(
|
|
entries.findIndex((entry) => entry.startsWith("Diff")),
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
await main.getByTestId("workspace-new-tab-button").click();
|
|
await expect(menu).toBeVisible();
|
|
await expect(menu.getByRole("menuitem", { name: /^Agent/ })).toBeVisible();
|
|
await expect(menu.getByText("Terminal profiles", { exact: true })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
});
|
|
|
|
await test.step("Files and Changes omit X, and + restores Files after context-menu close", async () => {
|
|
await expectWorkspaceCloseOnHover(page);
|
|
await closeExplorerFilesFromContextMenu(page);
|
|
await launchExplorerPanel(page, "Files");
|
|
await expect(
|
|
explorer.getByRole("button", { name: "Browse workspace files", exact: true }),
|
|
).toBeVisible();
|
|
await expect(explorer.getByTestId("file-explorer-tree-scroll")).toBeVisible();
|
|
});
|
|
|
|
await test.step("standard bulk-close menus affect only Explorer tabs", async () => {
|
|
await closeOtherExplorerTabs(page);
|
|
await expect(explorer.getByTestId("workspace-tab-changes_tree")).toHaveCount(0);
|
|
await expect(explorer.getByTestId("workspace-tab-files")).toBeVisible();
|
|
await expect(main.getByTestId("workspace-tabs-row").getByRole("button")).toHaveCount(
|
|
mainTabsBefore,
|
|
);
|
|
await launchExplorerPanel(page, "Changes");
|
|
await expect(explorer.getByTestId("changes-tree-panel")).toBeVisible();
|
|
});
|
|
|
|
await test.step("Cmd+T still belongs to the focused workspace pane", async () => {
|
|
await pressNewTabShortcut(page);
|
|
await expect(
|
|
main.getByTestId("workspace-new-tab-panel").filter({ visible: true }),
|
|
).toBeVisible();
|
|
await expect(explorer.getByTestId("workspace-new-tab-panel")).toHaveCount(0);
|
|
});
|
|
|
|
await testInfo.attach("shared-explorer-tabs", {
|
|
body: await page.screenshot(),
|
|
contentType: "image/png",
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("moves agents into Explorer and keeps the shared Agent launcher available", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-agent-drag-" });
|
|
try {
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
const agent = await workspace.client.createAgent({
|
|
provider: "mock",
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "Dragged agent",
|
|
modeId: "load-test",
|
|
model: "ten-second-stream",
|
|
});
|
|
const main = page.getByTestId("workspace-pane-main");
|
|
await expect(main.getByTestId(`workspace-tab-agent_${agent.id}`)).toBeVisible();
|
|
await dragTabIntoExplorer(page, `agent_${agent.id}`);
|
|
await expect(explorer.getByTestId(`workspace-tab-agent_${agent.id}`)).toBeVisible();
|
|
await expect(main.getByTestId(`workspace-tab-agent_${agent.id}`)).toHaveCount(0);
|
|
await explorer.getByRole("button", { name: "New tab", exact: true }).click();
|
|
const menu = page.getByTestId("workspace-new-tab-menu").filter({ visible: true });
|
|
await expect(menu).toBeVisible();
|
|
await expect(menu.getByRole("menuitem", { name: /^Agent/ })).toHaveCount(1);
|
|
await page.keyboard.press("Escape");
|
|
await testInfo.attach("agent-in-explorer", {
|
|
body: await page.screenshot(),
|
|
contentType: "image/png",
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("tablet prototype keeps Explorer tabs when resizing down to a phone", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "tablet-explorer-" });
|
|
try {
|
|
await page.setViewportSize({ width: 951, height: 669 });
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await ensureExplorerSidebar(page);
|
|
await expect(
|
|
explorerSidebar(page).getByRole("button", { name: "Browse workspace files", exact: true }),
|
|
).toBeVisible();
|
|
await page.screenshot({ path: testInfo.outputPath("tablet-951.png") });
|
|
await openExplorerAgentDraft(page);
|
|
await page.screenshot({ path: testInfo.outputPath("tablet-agent-draft.png") });
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await revealPhoneExplorer(page);
|
|
await expect(
|
|
explorerSidebar(page).getByRole("button", { name: "New tab", exact: true }),
|
|
).toBeVisible();
|
|
await page.screenshot({ path: testInfo.outputPath("phone-shared-explorer.png") });
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
async function openExplorerAgentDraft(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
await explorerSidebar(page).getByRole("button", { name: "New tab", exact: true }).click();
|
|
await page.getByRole("menuitem", { name: /^Agent/ }).click();
|
|
await expect(explorerSidebar(page).getByRole("textbox").first()).toBeVisible();
|
|
}
|
|
|
|
async function revealPhoneExplorer(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
await page.getByRole("button", { name: "Open Explorer sidebar", exact: true }).click();
|
|
await expect(explorerSidebar(page)).toBeVisible();
|
|
await expect(
|
|
explorerSidebar(page).getByRole("button", { name: "Close Explorer sidebar", exact: true }),
|
|
).toBeInViewport({ ratio: 1 });
|
|
}
|
|
|
|
async function cycleWorkspaceTheme(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
await page.keyboard.press("Control+Alt+t");
|
|
}
|
|
|
|
test("retains the Explorer host through appearance changes and folding", async ({ page }) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-lifetime-" });
|
|
try {
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await openFilesPanel(page);
|
|
const host = await explorer.elementHandle();
|
|
if (!host) throw new Error("Explorer must be mounted before changing appearance");
|
|
const previousColor = await explorer.evaluate(
|
|
(element) => getComputedStyle(element).backgroundColor,
|
|
);
|
|
await cycleWorkspaceTheme(page);
|
|
await expect
|
|
.poll(() => explorer.evaluate((element) => getComputedStyle(element).backgroundColor))
|
|
.not.toBe(previousColor);
|
|
expect(await host.evaluate((element) => element.isConnected)).toBe(true);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await ensureExplorerSidebar(page);
|
|
expect(await host.evaluate((element) => element.isConnected)).toBe(true);
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
await expect(explorer).toBeVisible();
|
|
expect(await host.evaluate((element) => element.isConnected)).toBe(true);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
async function reorderCompactExplorerTabs(page: Parameters<typeof ensureExplorerSidebar>[0]) {
|
|
const explorer = explorerSidebar(page);
|
|
const files = explorer.getByTestId("workspace-tab-files");
|
|
const changes = explorer.getByTestId("workspace-tab-changes_tree");
|
|
const source = await changes.boundingBox();
|
|
const target = await files.boundingBox();
|
|
if (!source || !target) throw new Error("Explorer tabs must be visible to reorder");
|
|
await page.mouse.move(source.x + source.width / 2, source.y + source.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(source.x + source.width / 2 - 12, source.y + source.height / 2);
|
|
await page.mouse.move(target.x + 3, target.y + target.height / 2, { steps: 20 });
|
|
await page.mouse.up();
|
|
await expect
|
|
.poll(async () => {
|
|
const afterFiles = await files.boundingBox();
|
|
const afterChanges = await changes.boundingBox();
|
|
return Boolean(afterFiles && afterChanges && afterChanges.x < afterFiles.x);
|
|
})
|
|
.toBe(true);
|
|
}
|
|
|
|
test("reorders compact Explorer tabs with a pointer without changing selection", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-compact-drag-" });
|
|
try {
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openFilesPanel(page);
|
|
await reorderCompactExplorerTabs(page);
|
|
await expect(
|
|
explorerSidebar(page).getByRole("button", { name: "Browse workspace files", exact: true }),
|
|
).toHaveAttribute("aria-selected", "true");
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
// Exercise appearance at its consumers while the real workspace navigator retains the panels.
|
|
test("updates populated code and diff appearance without losing the editor instance or draft", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-populated-appearance-" });
|
|
const source = Array.from(
|
|
{ length: 120 },
|
|
(_, index) => `export const line${index} = "retained ${index}";`,
|
|
).join("\n");
|
|
try {
|
|
await writeFile(`${workspace.repoPath}/appearance.ts`, source);
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem(
|
|
"@paseo:app-settings",
|
|
JSON.stringify({ codeFontSize: 13, theme: "light" }),
|
|
);
|
|
localStorage.setItem(
|
|
"@paseo:changes-preferences",
|
|
JSON.stringify({ inlineDiff: true, layout: "unified" }),
|
|
);
|
|
});
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await openFilesPanel(page);
|
|
await openFileFromExplorer(page, "appearance.ts");
|
|
const editor = page.getByTestId("file-source-editor").filter({ visible: true });
|
|
const text = editor.locator(".cm-content");
|
|
await expect(text).toBeVisible();
|
|
await text.fill(`${source}\n// unsaved local edit`);
|
|
const instance = await editor.elementHandle();
|
|
const scroller = editor.locator(".cm-scroller");
|
|
await scroller.evaluate((node) => {
|
|
node.scrollTop = 400;
|
|
});
|
|
const scrollBefore = await scroller.evaluate((node) => node.scrollTop);
|
|
await explorerSidebar(page).getByTestId("workspace-tab-changes_tree").click();
|
|
const diff = explorerSidebar(page).getByTestId("git-diff-canvas");
|
|
await expect(diff).toBeVisible();
|
|
await page.keyboard.press("Control+,");
|
|
await openSettingsSection(page, "appearance");
|
|
await page.getByLabel("Code font size", { exact: true }).fill("19");
|
|
await page.getByLabel("Code font size", { exact: true }).press("Tab");
|
|
await clickSettingsBackToWorkspace(page);
|
|
await expect(text).toHaveCSS("font-size", "19px");
|
|
expect(await instance!.evaluate((node) => node.isConnected)).toBe(true);
|
|
await expect(text).toContainText("unsaved local edit");
|
|
expect(await scroller.evaluate((node) => node.scrollTop)).toBe(scrollBefore);
|
|
await expect(diff).toHaveCSS("font-size", "19px");
|
|
await cycleWorkspaceTheme(page);
|
|
expect(await instance!.evaluate((node) => node.isConnected)).toBe(true);
|
|
await expect(text).toContainText("unsaved local edit");
|
|
await dragTabIntoExplorer(page, "file_appearance.ts");
|
|
await expect(explorerSidebar(page).getByTestId("file-source-editor")).toBeVisible();
|
|
const explorerEditor = await explorerSidebar(page)
|
|
.getByTestId("file-source-editor")
|
|
.elementHandle();
|
|
await text.fill(`${source}\n// retained Explorer edit`);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await ensureExplorerSidebar(page);
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
expect(await explorerEditor!.evaluate((node) => node.isConnected)).toBe(true);
|
|
await expect(text).toContainText("retained Explorer edit");
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("retains populated Explorer chat and terminal attachments through appearance, hide and fold", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedMockAgentWorkspace({
|
|
repoPrefix: "explorer-chat-appearance-",
|
|
title: "Retained chat",
|
|
initialPrompt: "Show the retention marker",
|
|
featureValues: {
|
|
mockAssistantResponse: "Explorer populated paragraph.\n\n```ts\nconst retained = 42;\n```",
|
|
},
|
|
});
|
|
const attaches: string[] = [];
|
|
page.on("websocket", (socket) =>
|
|
socket.on("framesent", (frame) => {
|
|
if (typeof frame.payload !== "string") return;
|
|
try {
|
|
const message = JSON.parse(frame.payload).message;
|
|
if (message?.type === "subscribe_terminal_request") attaches.push(message.terminalId);
|
|
} catch {
|
|
/* Binary terminal traffic is not a JSON request. */
|
|
}
|
|
}),
|
|
);
|
|
try {
|
|
await openAgentRoute(page, workspace);
|
|
await ensureExplorerSidebar(page);
|
|
await dragTabIntoExplorer(page, `agent_${workspace.agentId}`);
|
|
const explorer = explorerSidebar(page);
|
|
const paragraph = explorer.getByText("Explorer populated paragraph.", { exact: true });
|
|
await expect(paragraph).toBeVisible();
|
|
const composer = explorer.getByRole("textbox", { name: "Message agent..." });
|
|
await composer.fill("Unsubmitted Explorer draft");
|
|
const composerNode = await composer.elementHandle();
|
|
await page.keyboard.press("Control+,");
|
|
await openSettingsSection(page, "appearance");
|
|
await page.getByLabel("Content font size", { exact: true }).fill("20");
|
|
await page.getByLabel("Content font size", { exact: true }).press("Tab");
|
|
await clickSettingsBackToWorkspace(page);
|
|
await expect(paragraph).toHaveCSS("font-size", "20px");
|
|
expect(await composerNode!.evaluate((node) => node.isConnected)).toBe(true);
|
|
await expect(composer).toHaveValue("Unsubmitted Explorer draft");
|
|
const previousColor = await paragraph.evaluate((node) => getComputedStyle(node).color);
|
|
await cycleWorkspaceTheme(page);
|
|
await expect
|
|
.poll(() => paragraph.evaluate((node) => getComputedStyle(node).color))
|
|
.not.toBe(previousColor);
|
|
await explorer.getByRole("button", { name: "New tab", exact: true }).click();
|
|
await page
|
|
.getByRole("menuitem", { name: /^Terminal/ })
|
|
.first()
|
|
.click();
|
|
const terminal = explorer.getByTestId("terminal-surface");
|
|
await expect(terminal).toBeVisible();
|
|
await expect.poll(() => attaches.length).toBe(1);
|
|
const terminalNode = await terminal.elementHandle();
|
|
await page.getByTestId("workspace-explorer-toggle").first().click();
|
|
await ensureExplorerSidebar(page);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await ensureExplorerSidebar(page);
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
await cycleWorkspaceTheme(page);
|
|
expect(await terminalNode!.evaluate((node) => node.isConnected)).toBe(true);
|
|
expect(attaches).toHaveLength(1);
|
|
await explorer.getByTestId(`workspace-tab-agent_${workspace.agentId}`).click();
|
|
expect(await composerNode!.evaluate((node) => node.isConnected)).toBe(true);
|
|
await expect(composer).toHaveValue("Unsubmitted Explorer draft");
|
|
await expect(paragraph).toHaveCSS("font-size", "20px");
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("compact file sheets cover Explorer and return to its retained file list", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "explorer-sheet-layer-" });
|
|
try {
|
|
await writeFile(`${workspace.repoPath}/sheet-marker.txt`, "Explorer sheet marker\n");
|
|
await gotoWorkspace(page, workspace.workspaceId);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await openFilesPanel(page);
|
|
const host = await explorer.elementHandle();
|
|
await openFileFromExplorer(page, "sheet-marker.txt");
|
|
await expect(page.getByText("Explorer sheet marker", { exact: true })).toBeVisible();
|
|
// A real click also proves the sheet draws above the retained Explorer overlay.
|
|
await page.getByRole("button", { name: "Close", exact: true }).click();
|
|
await expect(page.getByText("Explorer sheet marker", { exact: true })).toHaveCount(0);
|
|
await expect(explorer.getByTestId("file-explorer-tree-scroll")).toBeVisible();
|
|
expect(await host!.evaluate((node) => node.isConnected)).toBe(true);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|