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:
@@ -148,6 +148,9 @@ const CHANGES_DRAGGED_KEY = "agenthub.workspace.changesDragged";
|
||||
const CONTEXT_OPEN_KEY = "agenthub.workspace.contextOpen";
|
||||
/** localStorage key for the last workspace shown, restored on a bare /workspace. */
|
||||
const SELECTED_KEY = "agenthub.workspace.selected";
|
||||
/** Workspaces left most recently whose live panes stay mounted (hidden), so
|
||||
* switching back to one shows its terminals as they were, with no re-attach. */
|
||||
const KEEP_LANES = 4;
|
||||
|
||||
/** The Changes panel's two preset widths. Its header has two controls, like a
|
||||
* window's: Full screen (Restore once at full screen, back to the last
|
||||
@@ -198,7 +201,10 @@ function slotOf(runId: string | null | undefined): number {
|
||||
* (Settings → Appearance) the tabs hold EVERY workspace's live runs, stored
|
||||
* once (`all`): a leaf is a `paneKey` (lane × 10 + slot), each pane is bound
|
||||
* to its own run's lane, and focusing one selects its workspace (the URL's
|
||||
* `?lane=` follows `?run=`). */
|
||||
* `?lane=` follows `?run=`). Without it, the live panes of the last
|
||||
* `KEEP_LANES` workspaces left stay mounted too — hidden siblings in the same
|
||||
* grid, each keyed by its `paneKey` — so a workspace switch neither disposes
|
||||
* nor re-attaches a terminal. */
|
||||
function ConsoleArea({
|
||||
lanes,
|
||||
selectedLaneId,
|
||||
@@ -481,6 +487,51 @@ function ConsoleArea({
|
||||
const mounted = [...new Set([...shown, ...tabs.map((r) => r.slot ?? 0)])].sort((a, b) => a - b);
|
||||
const focusSlot = view.focus;
|
||||
|
||||
// The workspaces just left, newest first — updated DURING the render that
|
||||
// switches, because the switch's own commit would otherwise unmount the
|
||||
// panes before an effect could keep them. Idempotent on a repeat render.
|
||||
const leftLanes = useRef<number[]>([]);
|
||||
const lastLane = useRef(selectedLaneId);
|
||||
if (lastLane.current !== selectedLaneId) {
|
||||
const prev = lastLane.current;
|
||||
if (prev !== null)
|
||||
leftLanes.current = [prev, ...leftLanes.current.filter((id) => id !== prev)].slice(
|
||||
0,
|
||||
KEEP_LANES
|
||||
);
|
||||
lastLane.current = selectedLaneId;
|
||||
}
|
||||
const kept = allTabs
|
||||
? []
|
||||
: (activeRuns?.items ?? [])
|
||||
.filter(
|
||||
(r) =>
|
||||
r.laneId !== null &&
|
||||
r.laneId !== selectedLaneId &&
|
||||
r.status === "running" &&
|
||||
leftLanes.current.includes(r.laneId)
|
||||
)
|
||||
.map((r) => {
|
||||
const id = r.laneId as number;
|
||||
const slot = r.slot ?? 0;
|
||||
return {
|
||||
key: paneKey(id, slot),
|
||||
node: (
|
||||
<LaneConsolePane
|
||||
lanes={lanes}
|
||||
laneId={id}
|
||||
slot={slot}
|
||||
binaryStatus={binaryStatus}
|
||||
activeRuns={activeRuns}
|
||||
onNewSession={onNewSession}
|
||||
onRunExit={onRunsChanged}
|
||||
autoFocus={false}
|
||||
attachRequest={null}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Alt+1–4 focus a pane, Alt+\ splits the focused one right with a new
|
||||
// terminal, Alt+W takes it out into a tab of its own. Session view only, and never from inside a
|
||||
// terminal or a field, whose keys are their own (Ctrl+M lets Tab out of a
|
||||
@@ -578,6 +629,9 @@ function ConsoleArea({
|
||||
onNewSplit={(target, edge, tool) => void newSplit(target, edge, tool)}
|
||||
stateOf={stateOf}
|
||||
onStop={(slot) => void stopPane(slot)}
|
||||
// Keyed by workspace too, so a kept pane coming back is the same one.
|
||||
keyOf={allTabs ? undefined : (slot) => paneKey(selectedLaneId ?? 0, slot)}
|
||||
kept={kept}
|
||||
renderPane={(slot) => (
|
||||
<LaneConsolePane
|
||||
lanes={lanes}
|
||||
|
||||
Reference in New Issue
Block a user