Keep agent panes mounted and focused
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,4 +23,13 @@ describe("shouldFocusPaneFromEventTarget", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(shouldFocusPaneFromEventTarget(null)).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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
const INTERACTIVE_TARGET_SELECTOR = [
|
const INTERACTIVE_TARGET_SELECTOR = [
|
||||||
"a",
|
"a",
|
||||||
"button",
|
"button",
|
||||||
"input",
|
|
||||||
"select",
|
"select",
|
||||||
"textarea",
|
|
||||||
"[role='button']",
|
"[role='button']",
|
||||||
"[role='link']",
|
"[role='link']",
|
||||||
"[contenteditable='true']",
|
"[contenteditable='true']",
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ export function SplitContainer({
|
|||||||
}
|
}
|
||||||
return { kind: "pane" as const, pane: focusedPane };
|
return { kind: "pane" as const, pane: focusedPane };
|
||||||
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
|
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
|
||||||
|
const renderRoot = useMemo(() => wrapRootPaneForStableMount(effectiveRoot), [effectiveRoot]);
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
const data = asWorkspaceTabDragData(event.active.data.current);
|
const data = asWorkspaceTabDragData(event.active.data.current);
|
||||||
@@ -560,7 +561,7 @@ export function SplitContainer({
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SplitNodeView
|
<SplitNodeView
|
||||||
node={effectiveRoot}
|
node={renderRoot}
|
||||||
workspaceKey={workspaceKey}
|
workspaceKey={workspaceKey}
|
||||||
uiTabs={uiTabs}
|
uiTabs={uiTabs}
|
||||||
focusedPaneId={layout.focusedPaneId}
|
focusedPaneId={layout.focusedPaneId}
|
||||||
@@ -1071,6 +1072,22 @@ function getNodeKey(node: SplitNode): string {
|
|||||||
return node.group.id;
|
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) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
group: {
|
group: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user