From 1fa52d1bfc5c69c11de64fc2410a275f2d8a1c84 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 18 Aug 2026 13:59:31 +0700 Subject: [PATCH] feat(run): start a resume as soon as its session is picked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picking a session in the setup form's resume picker only staged the selection: the user still had to type a prompt and press Run before the lane's tmux session was started with `--resume`. A resume carries its own transcript, so there was nothing to type. The picker now fires the start directly with the picked session (passed explicitly, since the parent's state has not landed on that tick), sends the session's own cwd — which is what the locked cwd field already displays — and the Run button no longer requires a prompt while a resume is selected. Fresh runs still require one. --- client/src/components/run/RunSetup.tsx | 26 ++++++++++++++----- .../run/__tests__/RunSetup.test.tsx | 26 +++++++++++++++++++ docs/LANES.md | 2 ++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/client/src/components/run/RunSetup.tsx b/client/src/components/run/RunSetup.tsx index 8308057..3c6b94d 100644 --- a/client/src/components/run/RunSetup.tsx +++ b/client/src/components/run/RunSetup.tsx @@ -9,7 +9,9 @@ * model / permission-mode / effort fields, plus the concurrency hint and * the Start button. Its disabled state is driven by the `binaryFound` prop, * so a missing `claude` binary is a surfaced state here rather than a probe - * of its own. + * of its own. Picking a session to resume starts that run immediately — + * a resume carries its own history, so there is nothing to type first; + * the prompt stays optional for resumes and required for fresh runs. * - the pickers the panel owns: `CwdAutocomplete`, `SessionPicker`, * `ModelPicker`, and the small `Field` layout helper. * @@ -154,7 +156,13 @@ export function RunSetup(props: RunSetupProps) {
{ + props.onResumeSessionChange(s); + // Pass the picked session explicitly: the parent's state + // update has not landed yet on this tick, so reading + // props.resumeSession here would resume nothing. + if (s && !props.busy) handleStart(props, s); + }} cwd={props.laneCwd} />
@@ -247,7 +255,8 @@ export function RunSetup(props: RunSetupProps) { onClick={() => handleStart(props)} disabled={ !props.binaryFound || - !props.prompt.trim() || + // A resume needs no prompt — the session it continues is the input. + (!props.resumeSession && !props.prompt.trim()) || props.busy || atCap || (resumePicked && !props.resumeSession) || @@ -270,14 +279,19 @@ export function RunSetup(props: RunSetupProps) { ); } -function handleStart(props: RunSetupProps) { +/** `session` overrides `props.resumeSession` for the auto-start fired straight + * out of the picker, before the parent's state has caught up. */ +function handleStart(props: RunSetupProps, session?: Session) { + const resume = session ?? props.resumeSession; props.onStart({ laneId: props.laneId, - cwd: props.cwd || undefined, + // A resume is pinned to its own session's folder — that's what the locked + // cwd field shows, so it's what gets sent. + cwd: resume?.cwd || props.cwd || undefined, model: props.model || undefined, permissionMode: props.permissionMode || undefined, effort: props.effort || undefined, - resumeSessionId: props.resumeSession?.id || undefined, + resumeSessionId: resume?.id || undefined, initialPrompt: props.prompt || undefined, }); } diff --git a/client/src/components/run/__tests__/RunSetup.test.tsx b/client/src/components/run/__tests__/RunSetup.test.tsx index 4f7245d..e979700 100644 --- a/client/src/components/run/__tests__/RunSetup.test.tsx +++ b/client/src/components/run/__tests__/RunSetup.test.tsx @@ -201,6 +201,32 @@ describe("RunSetup — resume picker scopes sessions to the selected lane", () = ); }); + it("starts the resume immediately when a session is picked", async () => { + const { api } = await import("../../../lib/api"); + vi.mocked(api.sessions.list).mockResolvedValue({ + sessions: [ + { id: "sess-in-lane", cwd: "/Users/tester/lane-a", started_at: "", status: "completed" }, + ], + total: 1, + limit: 100, + offset: 0, + } as never); + + const { spies } = renderSetup({ laneCwd: "/Users/tester/lane-a", prompt: "" }); + fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption"))); + fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession"))); + fireEvent.click(await screen.findByText("/Users/tester/lane-a")); + + expect(spies.onResumeSessionChange).toHaveBeenCalledWith( + expect.objectContaining({ id: "sess-in-lane" }) + ); + // No prompt typed, no Run click - the pick itself is the start, and it + // carries the session's own cwd rather than the form's. + expect(spies.onStart).toHaveBeenCalledWith( + expect.objectContaining({ resumeSessionId: "sess-in-lane", cwd: "/Users/tester/lane-a" }) + ); + }); + it("lists everything when no lane is selected", async () => { const { api } = await import("../../../lib/api"); renderSetup({ laneCwd: undefined }); diff --git a/docs/LANES.md b/docs/LANES.md index 91fb0fb..2cf00e5 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -319,6 +319,8 @@ Run history is per lane, queryable via `GET /api/run/history?laneId=`. The **working directory** field tracks the selected lane's own `cwd` specifically, and re-syncs as soon as that path is known rather than only when the selection changes — a pane can render before `GET /api/lanes` has answered (split view restores its pane lanes from `localStorage`), and its lane id never changes afterwards. `RunSetup` submits that string verbatim to `POST /api/lanes/:id/start`, so a cwd left over from the previous lane or from the home default would start the run in the wrong folder. The home suggestion is used only while no lane is selected at all. +**Picking a session to resume starts it right away.** In the setup form's Fresh/Resume switch, choosing a session from the resume picker fires the start immediately — `POST /api/lanes/:id/start` with that `resumeSessionId` and the session's own `cwd` — and the pane switches straight to the terminal. A resume carries its own transcript, so there is nothing to type first: the prompt box stays optional for resumes (the Run button no longer requires it) and required for fresh runs. Type into the tmux terminal once it's attached. + ### Active runs list The **Active runs** button in the console header opens the merged run list. It shows three sources in one place, newest first: