feat(run): start a resume as soon as its session is picked

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.
This commit is contained in:
2026-08-18 13:59:31 +07:00
parent ab6d6410d5
commit 1fa52d1bfc
3 changed files with 48 additions and 6 deletions
+20 -6
View File
@@ -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) {
<div className="min-w-0 flex-1">
<SessionPicker
selected={props.resumeSession}
onSelect={props.onResumeSessionChange}
onSelect={(s) => {
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}
/>
</div>
@@ -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,
});
}
@@ -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 });