Skip to content

Commit 2d85a6c

Browse files
wesmuserFRMclaude
authored
feat(resume): session resume dropdown with terminal launch and directory actions (wesm#120)
## Summary Adds a session action dropdown to the breadcrumb bar that lets users resume sessions in a terminal, open the project directory in editors or file managers, and copy paths and commands to the clipboard. ### Backend - **`POST /sessions/{id}/resume`** — Builds the agent-specific resume command (`claude --resume`, `codex resume`, etc.), optionally launches it in a detected or user-specified terminal. Supports `command_only` for clipboard copy, `opener_id` for launching in a specific terminal, and flags like `skip_permissions` and `fork_session` for Claude. - **`GET /sessions/{id}/directory`** — Agent-agnostic directory resolution. Prefers the embedded `cwd` from the session file, falls back to the session's `project` field. Used by the frontend to determine dropdown visibility and copy the directory path. - **`GET /openers`** — Detects installed applications (terminals, editors, file managers) via app bundle paths on macOS and `PATH` lookup on Linux. Results are cached with a 60s TTL. - **`POST /sessions/{id}/open`** — Launches a detected opener with the session's resolved project directory. - **Terminal config** (`GET/POST /config/terminal`) — Persisted `auto`, `custom`, or `clipboard` mode. Custom mode supports a user-specified binary and argument template with `{cmd}` placeholder. - **macOS launch** uses `open -na <app>` for `.app` bundles and direct binary execution for PATH-detected installs, keeping detection and launch consistent. AppleScript for iTerm2 and Terminal.app. - **Session directory resolution** reads the first 20 lines of the session JSONL file for an embedded `cwd` field, with fallback to the session's `project` field. Both must be absolute paths pointing to existing directories. ### Frontend - **Resume dropdown** on the session breadcrumb bar, visible when the session has resume support, detected openers, or a resolvable directory: - Terminal openers (Ghostty, kitty, iTerm2, etc.) — launches the resume command directly - "Default terminal" — uses the server's auto-detect or custom terminal config - "Copy command" — copies the resume command to clipboard - "Copy directory path" — copies the resolved project directory - "Open in" section — detected editors (VS Code, Cursor, Zed, etc.) and file managers (Finder, etc.) - **Keyboard shortcut** `c` copies the resume command to clipboard from the session detail view. - **`resume.ts`** utility with per-agent command templates and POSIX shell quoting. - **Dropdown visibility** is based on actual capabilities: cached server-side directory resolution, detected openers, and agent resume support. No dropdown renders for sessions with nothing actionable. ### Supported agents | Agent | Resume command | |-------|---------------| | Claude | `claude --resume <id>` | | Codex | `codex resume <id>` | | Gemini | `gemini --resume <id>` | | OpenCode | `opencode --session <id>` | | Amp | `amp --resume <id>` | ### Detected terminals (macOS) Ghostty, iTerm2, kitty, Alacritty, WezTerm, Terminal.app — detected via app bundle path with PATH binary fallback for non-default installs. ### Detected terminals (Linux) Ghostty, kitty, Alacritty, WezTerm, GNOME Terminal, Konsole, Xfce Terminal, Tilix, xterm — detected via `PATH`. Respects `$TERMINAL` environment variable. --------- Co-authored-by: userFRM <frederic.miesegaes@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b31cfcc commit 2d85a6c

24 files changed

+2320
-15
lines changed

frontend/src/lib/api/client.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,34 @@ export function getExportUrl(sessionId: string): string {
330330
return `${BASE}/sessions/${sessionId}/export`;
331331
}
332332

333+
/* Resume in terminal */
334+
335+
export interface ResumeRequest {
336+
skip_permissions?: boolean;
337+
fork_session?: boolean;
338+
command_only?: boolean;
339+
opener_id?: string;
340+
}
341+
342+
export interface ResumeResponse {
343+
launched: boolean;
344+
terminal?: string;
345+
command: string;
346+
cwd?: string;
347+
error?: string;
348+
}
349+
350+
export function resumeSession(
351+
sessionId: string,
352+
flags: ResumeRequest = {},
353+
): Promise<ResumeResponse> {
354+
return fetchJSON(`/sessions/${sessionId}/resume`, {
355+
method: "POST",
356+
headers: { "Content-Type": "application/json" },
357+
body: JSON.stringify(flags),
358+
});
359+
}
360+
333361
/* Publish / GitHub config */
334362

335363
export function publishSession(sessionId: string): Promise<PublishResponse> {
@@ -392,6 +420,70 @@ export async function bulkStarSessions(
392420
}
393421
}
394422

423+
/* Session directory */
424+
425+
export function getSessionDirectory(
426+
sessionId: string,
427+
): Promise<{ path: string }> {
428+
return fetchJSON(`/sessions/${sessionId}/directory`);
429+
}
430+
431+
/* Openers — Conductor-style "Open in" */
432+
433+
export interface Opener {
434+
id: string;
435+
name: string;
436+
kind: "editor" | "terminal" | "files" | "action";
437+
bin: string;
438+
}
439+
440+
export interface OpenersResponse {
441+
openers: Opener[];
442+
}
443+
444+
export function listOpeners(): Promise<OpenersResponse> {
445+
return fetchJSON("/openers");
446+
}
447+
448+
export interface OpenResponse {
449+
launched: boolean;
450+
opener: string;
451+
path: string;
452+
}
453+
454+
export function openSession(
455+
sessionId: string,
456+
openerId: string,
457+
): Promise<OpenResponse> {
458+
return fetchJSON(`/sessions/${sessionId}/open`, {
459+
method: "POST",
460+
headers: { "Content-Type": "application/json" },
461+
body: JSON.stringify({ opener_id: openerId }),
462+
});
463+
}
464+
465+
/* Terminal config */
466+
467+
export interface TerminalConfig {
468+
mode: "auto" | "custom" | "clipboard";
469+
custom_bin?: string;
470+
custom_args?: string;
471+
}
472+
473+
export function getTerminalConfig(): Promise<TerminalConfig> {
474+
return fetchJSON("/config/terminal");
475+
}
476+
477+
export function setTerminalConfig(
478+
cfg: TerminalConfig,
479+
): Promise<TerminalConfig> {
480+
return fetchJSON("/config/terminal", {
481+
method: "POST",
482+
headers: { "Content-Type": "application/json" },
483+
body: JSON.stringify(cfg),
484+
});
485+
}
486+
395487
/* Analytics */
396488

397489
export interface AnalyticsParams {

0 commit comments

Comments
 (0)