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:
@@ -19,6 +19,9 @@
|
||||
* the close as one menu. Dividers resize by pointer or by arrow keys; a drag
|
||||
* moves only the line and commits the ratio on release, so the layout is
|
||||
* saved and the terminals refit once, not on every pointer move.
|
||||
* `kept` panes (another workspace's live sessions) are hidden children of
|
||||
* the SAME list, keyed by `keyOf`, so a pane moves between shown and kept
|
||||
* without remounting — a workspace switch never re-attaches its terminals.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -365,8 +368,14 @@ export function PaneGrid({
|
||||
onNewSplit,
|
||||
stateOf,
|
||||
onStop,
|
||||
keyOf = (slot) => slot,
|
||||
kept = [],
|
||||
}: {
|
||||
layout: PaneLayout;
|
||||
/** A pane's React key: unique across `mounted` and `kept` (a `paneKey`). */
|
||||
keyOf?: (slot: number) => number;
|
||||
/** Panes kept mounted, always hidden — never in the layout. */
|
||||
kept?: { key: number; node: ReactNode }[];
|
||||
/** Stops the session in `slot` (the header's X). */
|
||||
onStop?: (slot: number) => void;
|
||||
/** A live AI run's state for its header dot (none for a shell). */
|
||||
@@ -490,13 +499,25 @@ export function PaneGrid({
|
||||
onChange((l) => applyDrop(l, src, z.slot, z.zone));
|
||||
}}
|
||||
>
|
||||
{mounted.map((slot) => {
|
||||
{[
|
||||
...mounted.map((slot) => ({ key: keyOf(slot), slot, node: null as ReactNode })),
|
||||
...kept.map((k) => ({ key: k.key, slot: null, node: k.node })),
|
||||
].map(({ key, slot, node }) => {
|
||||
if (slot === null) {
|
||||
// The same element shape as a hidden pane below, so React keeps it.
|
||||
return (
|
||||
<div key={key} data-testid={`pane-kept-${key}`} className="hidden">
|
||||
{false}
|
||||
<div className="flex min-h-0 flex-1 flex-col">{node}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const r = rectOf.get(slot);
|
||||
const run = runOf(slot);
|
||||
const focused = layout.focus === slot;
|
||||
return (
|
||||
<div
|
||||
key={slot}
|
||||
key={key}
|
||||
data-testid={`pane-${slot}`}
|
||||
data-focused={r && focused ? "true" : undefined}
|
||||
// A menu opened in this pane stays inside it (`menuRoom`).
|
||||
|
||||
Reference in New Issue
Block a user