perf(workspace): switching workspaces keeps their terminals mounted

Panes were keyed by slot alone, so slot 0 of the next workspace reused the
previous one's pane: every switch disposed its terminals and attached new
ones (a WebSocket each, the whole screen replayed). The live panes of the
last 4 workspaces left now stay mounted as hidden siblings in the same
grid, keyed by paneKey (lane x 10 + slot), so going back is instant.

Measured on 3 workspaces x 2-3 shell panes, switching back to one already
opened: script 210 -> 52 ms, main thread 370 -> 120 ms, long tasks
120 -> 0 ms, terminal re-attaches 2-3 -> 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 15:03:27 +07:00
parent b08482c6aa
commit d72a255294
3 changed files with 128 additions and 3 deletions
@@ -675,6 +675,56 @@ describe("console tabs", () => {
expect(screen.getByTestId("console-tab-new")).toBeDisabled();
});
// Switching workspaces used to hand slot 0 of the new workspace the SAME
// pane as the old one's slot 0: its terminal was disposed and a new one
// attached, replaying the whole screen — the lag of every switch.
it("keeps a workspace's terminals mounted (hidden) while another workspace is shown", async () => {
withRuns([live(0), { ...live(0), id: "agenthub-lane-2", laneId: 2, slot: 0 }]);
function Go() {
const [, setParams] = useSearchParams();
return (
<>
{[1, 2].map((id) => (
<button
key={id}
data-testid={`go-${id}`}
onClick={() => setParams({ lane: String(id), run: `agenthub-lane-${id}` })}
/>
))}
</>
);
}
render(
<MemoryRouter initialEntries={["/workspace?lane=1&run=agenthub-lane-1"]}>
<Go />
<Workspace />
</MemoryRouter>
);
await settle();
await settle();
const term = (id: string) =>
screen.getAllByTestId("terminal-view").find((el) => el.dataset.runId === id);
const first = term("agenthub-lane-1");
expect(first).toBeDefined();
const user = userEvent.setup();
await user.click(screen.getByTestId("go-2"));
await settle();
const second = term("agenthub-lane-2");
expect(second?.closest(".hidden")).toBeNull();
// Lane 1's terminal is the same element, only hidden.
expect(term("agenthub-lane-1")).toBe(first);
expect(first!.closest(".hidden")).not.toBeNull();
await user.click(screen.getByTestId("go-1"));
await settle();
expect(term("agenthub-lane-1")).toBe(first);
expect(first!.closest(".hidden")).toBeNull();
expect(term("agenthub-lane-2")).toBe(second);
// Only the shown workspace's tabs are in the tab bar.
expect(
screen.getAllByRole("tab").filter((t) => t.dataset.testid?.startsWith("console-tab-"))
).toHaveLength(1);
});
it("with tabs from every workspace on, shows another workspace's session and selects it on click", async () => {
window.localStorage.setItem("agenthub.workspace.allTabs", "1");
withRuns([live(0), { ...live(0), id: "agenthub-lane-2-1", laneId: 2, slot: 1 }]);