fix(run): re-attach a lane's live terminal after the Workspace page remounts

The lane-switch effect only synced `handle` from whatever activeRuns
snapshot was already loaded, so navigating away and back to /run (a
fresh mount with activeRuns still null) left a running lane's console
stuck on the setup form until the user switched lanes and back.
This commit is contained in:
2026-08-18 16:28:36 +07:00
parent 8e49d2b300
commit 8aaa200dd9
@@ -117,6 +117,7 @@ export function LaneConsolePane({
// away when the new lane already has a live run: each lane sticks to its own // away when the new lane already has a live run: each lane sticks to its own
// tmux session, and the switch should land on that session rather than on an // tmux session, and the switch should land on that session rather than on an
// empty setup form the user then has to Start out of. // empty setup form the user then has to Start out of.
const autoAttachedForLane = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
const { activeRuns: runs } = latest.current; const { activeRuns: runs } = latest.current;
setPrompt(""); setPrompt("");
@@ -124,9 +125,27 @@ export function LaneConsolePane({
setError(null); setError(null);
setBusy(null); setBusy(null);
setHandle(runs?.items.find((r) => r.laneId === laneId && r.status === "running") ?? null); setHandle(runs?.items.find((r) => r.laneId === laneId && r.status === "running") ?? null);
autoAttachedForLane.current = null;
refreshList(); refreshList();
}, [laneId, refreshList]); }, [laneId, refreshList]);
// The switch effect above only sees whatever `activeRuns` the page already
// had loaded at that instant. Remounting this page (navigating away and
// back) starts `activeRuns` at null again, so a lane with a live run would
// otherwise show the setup form until the user switched lanes and back —
// the only path that re-ran the effect after the poll caught up. Re-check
// once `activeRuns` actually arrives, but only once per lane so it never
// fights a user-initiated "New Run".
useEffect(() => {
if (handle || laneId === null) return;
if (autoAttachedForLane.current === laneId) return;
const running = activeRuns?.items.find((r) => r.laneId === laneId && r.status === "running");
if (running) {
autoAttachedForLane.current = laneId;
setHandle(running);
}
}, [activeRuns, laneId, handle]);
// The cwd tracks the lane's own folder separately, keyed on the resolved // The cwd tracks the lane's own folder separately, keyed on the resolved
// path rather than on `laneId` alone: the pane can mount before the lane // path rather than on `laneId` alone: the pane can mount before the lane
// list has loaded (split view restores its pane lanes from localStorage), // list has loaded (split view restores its pane lanes from localStorage),