diff --git a/client/src/components/run/PaneGrid.tsx b/client/src/components/run/PaneGrid.tsx index ca819d12..e73518c3 100644 --- a/client/src/components/run/PaneGrid.tsx +++ b/client/src/components/run/PaneGrid.tsx @@ -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ĩ */ @@ -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 ( +
+ {false} +
{node}
+
+ ); + } const r = rectOf.get(slot); const run = runOf(slot); const focused = layout.focus === slot; return (