Keep agent panes mounted and focused

This commit is contained in:
Mohamed Boudra
2026-05-18 11:24:56 +07:00
parent 69fc6fe754
commit 82319f5805
4 changed files with 91 additions and 3 deletions

View File

@@ -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();
}
});
});

View File

@@ -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);
});
});

View File

@@ -1,9 +1,7 @@
const INTERACTIVE_TARGET_SELECTOR = [
"a",
"button",
"input",
"select",
"textarea",
"[role='button']",
"[role='link']",
"[contenteditable='true']",

View File

@@ -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}
>
<SplitNodeView
node={effectiveRoot}
node={renderRoot}
workspaceKey={workspaceKey}
uiTabs={uiTabs}
focusedPaneId={layout.focusedPaneId}
@@ -1071,6 +1072,22 @@ function getNodeKey(node: SplitNode): string {
return node.group.id;
}
function wrapRootPaneForStableMount(node: SplitNode): SplitNode {
if (node.kind === "group") {
return node;
}
return {
kind: "group",
group: {
id: `root:${node.pane.id}`,
direction: "horizontal",
children: [node],
sizes: [1],
},
};
}
const styles = StyleSheet.create((theme) => ({
group: {
flex: 1,