fix(workspace): keep the console cwd on the selected lane's own folder

The cwd was synced only when laneId changed, but a pane can render before
GET /api/lanes has answered — split view restores its pane lanes from
localStorage, and layout 1 paints before the list loads. With no lane
resolved the field fell back to the home suggestion, and since laneId never
changed afterwards it stayed there. RunSetup submits that string verbatim to
POST /api/lanes/:id/start, so the run was started in the wrong folder.

Track the cwd in its own effect keyed on the resolved lane path rather than
on laneId, so it re-syncs as soon as the lane is known. The home default now
applies only while no lane is selected at all.
This commit is contained in:
2026-08-18 11:33:31 +07:00
parent 37adf983e3
commit ab6d6410d5
4 changed files with 184 additions and 2 deletions
+13 -2
View File
@@ -118,16 +118,27 @@ export function LaneConsolePane({
// 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.
useEffect(() => {
const { lanes: knownLanes, activeRuns: runs, defaultCwd: fallbackCwd } = latest.current;
const { activeRuns: runs } = latest.current;
setPrompt("");
setResumeSession(null);
setError(null);
setBusy(null);
setCwd(knownLanes.find((l) => l.id === laneId)?.cwd ?? fallbackCwd ?? "");
setHandle(runs?.items.find((r) => r.laneId === laneId && r.status === "running") ?? null);
refreshList();
}, [laneId, refreshList]);
// 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),
// and `laneId` never changes afterwards, so a laneId-only effect would leave
// the console pointing at the default directory. RunSetup submits this
// string verbatim, so a stale one starts the run in the wrong folder.
const laneCwd = currentLane?.cwd ?? null;
useEffect(() => {
if (laneCwd) setCwd(laneCwd);
else if (laneId === null) setCwd(latest.current.defaultCwd ?? "");
}, [laneId, laneCwd]);
const attachToRun = useCallback(
async (id: string) => {
if (busy) return;
@@ -161,6 +161,19 @@ describe("LaneConsolePane", () => {
await waitFor(() => expect(screen.queryByTestId("terminal-view")).not.toBeInTheDocument());
});
it("adopts the lane's cwd when the lane list arrives after the pane mounted", async () => {
// Split view restores its pane lanes from localStorage, so a pane can
// render with a laneId before GET /api/lanes has answered. laneId never
// changes afterwards — only the resolved lane does.
const props = { ...baseProps(), lanes: [], defaultCwd: "/home/tester" };
const { rerender } = render(<LaneConsolePane {...props} />);
const cwdInput = screen.getByPlaceholderText(/type to search/i) as HTMLInputElement;
expect(cwdInput.value).toBe("/home/tester");
rerender(<LaneConsolePane {...props} lanes={[LANE]} />);
await waitFor(() => expect(cwdInput.value).toBe(LANE.cwd));
});
it("shows a lane dropdown only when showLaneSelector is true", () => {
const { rerender } = render(<LaneConsolePane {...baseProps()} showLaneSelector />);
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();