diff --git a/client/src/components/run/LaneConsolePane.tsx b/client/src/components/run/LaneConsolePane.tsx index a301bb5..3023d2a 100644 --- a/client/src/components/run/LaneConsolePane.tsx +++ b/client/src/components/run/LaneConsolePane.tsx @@ -117,6 +117,7 @@ export function LaneConsolePane({ // 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 // empty setup form the user then has to Start out of. + const autoAttachedForLane = useRef(null); useEffect(() => { const { activeRuns: runs } = latest.current; setPrompt(""); @@ -124,9 +125,27 @@ export function LaneConsolePane({ setError(null); setBusy(null); setHandle(runs?.items.find((r) => r.laneId === laneId && r.status === "running") ?? null); + autoAttachedForLane.current = null; 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 // path rather than on `laneId` alone: the pane can mount before the lane // list has loaded (split view restores its pane lanes from localStorage),