diff --git a/packages/app/e2e/workspace-pane-remount.spec.ts b/packages/app/e2e/workspace-pane-remount.spec.ts new file mode 100644 index 000000000..ef1c2be5a --- /dev/null +++ b/packages/app/e2e/workspace-pane-remount.spec.ts @@ -0,0 +1,64 @@ +import { buildHostAgentDetailRoute } from "@/utils/host-routes"; +import { expect, test } from "./fixtures"; +import { + archiveAgentFromDaemon, + connectArchiveTabDaemonClient, + createIdleAgent, +} from "./helpers/archive-tab"; +import { expectComposerVisible } from "./helpers/composer"; +import { createTempGitRepo } from "./helpers/workspace"; +import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; + +test.describe("Workspace pane mounting", () => { + test("opening the first split pane keeps the existing agent composer mounted", async ({ + page, + }) => { + test.setTimeout(90_000); + const serverId = process.env.E2E_SERVER_ID; + if (!serverId) { + throw new Error("E2E_SERVER_ID is not set."); + } + + const client = await connectArchiveTabDaemonClient(); + const repo = await createTempGitRepo("pane-remount-"); + let agentId: string | null = null; + + try { + const agent = await createIdleAgent(client, { + cwd: repo.path, + title: `pane-remount-${Date.now()}`, + }); + agentId = agent.id; + + await page.goto(buildHostAgentDetailRoute(serverId, agent.id, agent.cwd)); + await page.waitForURL( + (url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"), + { timeout: 60_000 }, + ); + await waitForWorkspaceTabsVisible(page); + await expectComposerVisible(page); + + const originalComposer = await page + .getByTestId("message-input-root") + .filter({ visible: true }) + .first() + .elementHandle(); + expect(originalComposer).not.toBeNull(); + + await page.getByRole("button", { name: "Split pane right" }).first().click(); + await expect(page.getByTestId("message-input-root").filter({ visible: true })).toHaveCount( + 2, + { timeout: 30_000 }, + ); + + const originalStillConnected = await originalComposer!.evaluate((node) => node.isConnected); + expect(originalStillConnected).toBe(true); + } finally { + if (agentId) { + await archiveAgentFromDaemon(client, agentId).catch(() => undefined); + } + await client.close().catch(() => undefined); + await repo.cleanup(); + } + }); +}); diff --git a/packages/app/src/components/split-container-pane-focus.test.ts b/packages/app/src/components/split-container-pane-focus.test.ts index 7e5f4541b..461364677 100644 --- a/packages/app/src/components/split-container-pane-focus.test.ts +++ b/packages/app/src/components/split-container-pane-focus.test.ts @@ -23,4 +23,13 @@ describe("shouldFocusPaneFromEventTarget", () => { ).toBe(true); expect(shouldFocusPaneFromEventTarget(null)).toBe(true); }); + + it("returns true for composer text inputs so focusing the composer focuses the pane", () => { + expect( + shouldFocusPaneFromEventTarget({ + closest: (selector: string) => + selector.includes("input") ? ({ tagName: "INPUT" } as Element) : null, + } as unknown as EventTarget), + ).toBe(true); + }); }); diff --git a/packages/app/src/components/split-container-pane-focus.ts b/packages/app/src/components/split-container-pane-focus.ts index c3cb53b7b..a63823f1f 100644 --- a/packages/app/src/components/split-container-pane-focus.ts +++ b/packages/app/src/components/split-container-pane-focus.ts @@ -1,9 +1,7 @@ const INTERACTIVE_TARGET_SELECTOR = [ "a", "button", - "input", "select", - "textarea", "[role='button']", "[role='link']", "[contenteditable='true']", diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 00f999f0b..ba6111bf8 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -406,6 +406,7 @@ export function SplitContainer({ } return { kind: "pane" as const, pane: focusedPane }; }, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]); + const renderRoot = useMemo(() => wrapRootPaneForStableMount(effectiveRoot), [effectiveRoot]); const handleDragStart = useCallback((event: DragStartEvent) => { const data = asWorkspaceTabDragData(event.active.data.current); @@ -560,7 +561,7 @@ export function SplitContainer({ onDragEnd={handleDragEnd} > ({ group: { flex: 1,