From 24f13911fe6bfe7d144567391a25680ad3fbe925 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 12 Aug 2026 10:24:45 +0700 Subject: [PATCH] feat(run): replace RunHandle/RunStartArgs types and api.run for the tmux backend --- client/src/lib/api.ts | 177 ++++++++-------------------------------- client/src/lib/types.ts | 53 +++--------- 2 files changed, 42 insertions(+), 188 deletions(-) diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 268222e..c8e5cc4 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -1474,110 +1474,32 @@ export const api = { /** Spawn/manage headless or conversational `claude` CLI child processes * launched from the dashboard's Run page, and stream their output. */ run: { - /** - * GET /api/run - currently tracked runs (in-memory handles) plus - * concurrency limits. - * @returns {@link RunListResponse} — live handles + `maxConcurrent`/`activeCount`. - */ + /** GET /api/run - lanes with a live tmux-backed run, computed fresh from tmux state. */ list: () => request("/run"), - /** - * GET /api/run/history - persisted run history from the `dashboard_runs` - * table, including runs whose in-memory handle has since been reaped. - * Optionally filter by lane. - * - * `limit` defaults to 50 when the caller omits it and is always sent as a - * query param. - * - * @param limit Max history rows to return (default 50). - * @param options Optional filters like laneId. - * @returns `{ items }` — {@link DashboardRunHistoryItem} rows, newest-first. - */ + /** GET /api/run/history - persisted run history from `dashboard_runs`. */ history: (limit = 50, options?: { laneId?: number }) => { const qs = new URLSearchParams({ limit: String(limit) }); if (options?.laneId !== undefined) qs.set("laneId", String(options.laneId)); return request<{ items: DashboardRunHistoryItem[] }>(`/run/history?${qs.toString()}`); }, - /** - * GET /api/run/binary - whether a `claude` executable was found on PATH. - * - * Lets the Run page disable/enable the "start" affordance and show where the - * CLI resolved from (or that it's missing). - * - * @returns `{ found, path }` — whether a binary was located and its path. - */ + /** GET /api/run/binary - whether `claude` was found on PATH. */ binary: () => request<{ found: boolean; path: string | null }>("/run/binary"), - /** - * GET /api/run/cwds - suggested working directories for the cwd picker. - * @returns `{ items }` — {@link CwdSuggestion} entries (dashboard/home/recent). - */ + /** GET /api/run/tmux - whether the `tmux` binary was found on PATH. */ + tmuxAvailable: () => request<{ available: boolean }>("/run/tmux"), + /** GET /api/run/cwds - suggested working directories for the cwd picker. */ cwds: () => request<{ items: CwdSuggestion[] }>("/run/cwds"), - /** - * GET /api/run/files - path-completion suggestions under `cwd`, filtered - * by an optional query fragment `q`. - * - * Backs the file/@-mention autocomplete when composing a run prompt: `cwd` - * is always sent; `q` is appended only when non-empty to narrow matches. - * - * @param cwd The directory to complete paths within. - * @param q Optional partial fragment to filter suggestions by. - * @returns `{ items }` — matching path strings under `cwd`. - */ + /** GET /api/run/files - path-completion suggestions under `cwd`. */ files: (cwd: string, q?: string) => { const qs = new URLSearchParams({ cwd }); if (q) qs.set("q", q); return request<{ items: string[] }>(`/run/files?${qs.toString()}`); }, - /** - * POST /api/run - spawn a new `claude` child process. - * - * Sends {@link RunStartArgs} (prompt, mode, and optional cwd/model/ - * permission-mode/resume/effort). The server spawns the CLI and returns the - * initial {@link RunHandle}; subsequent output is streamed over the - * `run_stream` WebSocket message rather than this response. - * - * @param args The spawn parameters. - * @returns {@link RunHandle} — the freshly created run's handle. - */ + /** POST /api/run - start (or adopt, if already live) a lane's terminal run. */ start: (args: RunStartArgs) => request("/run", { method: "POST", body: JSON.stringify(args) }), - /** - * GET /api/run/:id - one run's current handle; pass `envelopes: true` to - * also include its buffered stream-json envelopes (for a page refresh - * mid-run, since the WS `run_stream` history isn't otherwise replayed). - * - * The `envelopes` flag is translated to `?envelopes=1`. Use it when - * re-hydrating the Run page after a reload: the WebSocket only pushes *new* - * envelopes, so the buffered ones must be pulled once to backfill the view. - * - * @param id The run id. - * @param opts Optional `{ envelopes }` — include buffered stream-json envelopes. - * @returns {@link RunHandle} — the run's handle (with `envelopes` when requested). - */ - get: (id: string, opts?: { envelopes?: boolean }) => - request(`/run/${encodeURIComponent(id)}${opts?.envelopes ? "?envelopes=1" : ""}`), - /** - * POST /api/run/:id/message - write `text` to the run's stdin (conversation - * mode only); acked via the `run_input_ack` WS message. - * - * Only meaningful for a run started in "conversation" mode (stdin left - * open). The HTTP response returns just the `messageId`; the actual - * delivery/echo is confirmed asynchronously over the WebSocket. - * - * @param id The run id to send input to. - * @param text The user's follow-up message written to the CLI's stdin. - * @returns `{ messageId }` — id correlating this input with its `run_input_ack`. - */ - send: (id: string, text: string) => - request<{ messageId: string }>(`/run/${encodeURIComponent(id)}/message`, { - method: "POST", - body: JSON.stringify({ text }), - }), - /** - * DELETE /api/run/:id - forcibly terminate a running process. - * - * @param id The run id to kill. - * @returns `{ ok: true }` — acknowledgement that termination was requested. - */ + /** GET /api/run/:id - one run's current handle. */ + get: (id: string) => request(`/run/${encodeURIComponent(id)}`), + /** DELETE /api/run/:id - kill the tmux session. */ kill: (id: string) => request<{ ok: true }>(`/run/${encodeURIComponent(id)}`, { method: "DELETE" }), }, @@ -2506,78 +2428,49 @@ export interface CcHookScripts { // mirror the CLI's own vocabulary so the dashboard can drive the CLI faithfully. // ───────────────────────────────────────────────────────────────────────────── -/** "headless" runs to completion unattended and streams only output; - * "conversation" keeps stdin open so the user can send follow-up messages. */ -export type RunMode = "headless" | "conversation"; -/** Lifecycle of a spawned `claude` process, mirrored in `RunHandle.status` - * and `RunStatusPayload.status`. "abandoned" is applied by server cleanup - * when a handle is reaped without a clean exit ever being observed. */ -export type RunStatus = "spawning" | "running" | "completed" | "error" | "killed" | "abandoned"; /** Maps 1:1 to the `claude --permission-mode` CLI flag. */ export type PermissionMode = "acceptEdits" | "default" | "plan" | "bypassPermissions"; /** Maps 1:1 to the `claude --effort` CLI flag; "" omits the flag (model default). */ export type EffortLevel = "" | "low" | "medium" | "high" | "xhigh" | "max"; +/** "running" — a live tmux session exists; "gone" — it doesn't (killed, + * crashed, claude exited and closed the pane). Computed fresh from tmux + * state on every read, never cached. */ +export type RunStatus = "running" | "gone"; -/** Body for POST /api/run - parameters for spawning a new `claude` process. */ +/** Body for POST /api/run - parameters for starting a lane's terminal run. */ export interface RunStartArgs { - /** Initial prompt/task text passed to the CLI. */ - prompt: string; - mode: RunMode; - /** Working directory to launch in; server default applies if omitted. */ + laneId: number; cwd?: string; - /** `--model` value; omitted inherits the CLI's own default (settings.json). */ model?: string; permissionMode?: PermissionMode; /** Resume an existing Claude Code session id (`--resume`) instead of starting fresh. */ resumeSessionId?: string; effort?: EffortLevel; + /** Sent as `claude`'s first positional message once the pane boots; omit + * to just open the pane and let the user type. */ + initialPrompt?: string; } -/** In-memory (or freshly-fetched) handle for one spawned `claude` process, - * from POST/GET /api/run - the live counterpart to {@link DashboardRunHistoryItem}. - * Where {@link DashboardRunHistoryItem} is the persisted DB row (snake_case, - * survives handle reaping), this is the richer live handle (camelCase, carries - * argv/tails/envelope counters) that only exists while the server tracks it. */ +/** A lane's tmux-backed terminal run — one per lane, id is the tmux session + * name (`ccam-lane-`). */ export interface RunHandle { id: string; - /** OS process id; null before the process has actually spawned. */ - pid: number | null; - mode: RunMode; - cwd: string; - model: string | null; - permissionMode: PermissionMode; - effort: EffortLevel | null; - prompt: string; - /** Full argv the server invoked the CLI with, for debugging. */ - argv: string[]; - resumeSessionId: string | null; + laneId: number | null; status: RunStatus; - /** Epoch-ms timestamp the process was spawned. */ - startedAt: number; - /** Epoch-ms timestamp the process exited; null while still running. */ - endedAt: number | null; - exitCode: number | null; - /** POSIX signal that killed the process (e.g. "SIGTERM"); null otherwise. */ - signal: string | null; - error: string | null; - /** Claude Code session id the run created/resumed, once known. */ + cwd: string | null; + model: string | null; + permissionMode: PermissionMode | null; + effort: EffortLevel | null; + resumeSessionId: string | null; + /** Claude Code session id this run created/resumed, once known. */ sessionId: string | null; - /** Count of stream-json envelopes emitted so far. */ - envelopeCount: number; - /** Last chunk of captured stdout, for a quick inline preview. */ - stdoutTail: string; - /** Last chunk of captured stderr, for a quick inline preview. */ - stderrTail: string; - envelopes?: unknown[]; // present when fetched with ?envelopes=1 + /** ISO timestamp the tmux session was created. */ + startedAt: string | null; } /** Response shape of GET /api/run. */ export interface RunListResponse { items: RunHandle[]; - /** Server-configured cap on simultaneously running processes. */ - maxConcurrent: number; - /** Count of runs currently in "spawning"/"running" state. */ - activeCount: number; } /** @@ -2591,23 +2484,17 @@ export interface RunListResponse { */ export interface DashboardRunHistoryItem { id: string; - /** Claude Code session id the run created/resumed; null if never captured. */ session_id: string | null; - mode: RunMode; cwd: string; model: string | null; permission_mode: PermissionMode | null; effort: EffortLevel | null; resume_session_id: string | null; - /** Truncated leading excerpt of the original prompt, for the history list. */ prompt_preview: string | null; - status: RunStatus; + status: "running" | "killed" | "abandoned"; exit_code: number | null; started_at: string; ended_at: string | null; - /** True when an in-memory {@link RunHandle} for this row still exists (so - * the UI can offer live actions like "send message"/"kill"); false once - * the handle has been reaped and only the DB row remains. */ isLive: boolean; } diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index a65d90c..07068c9 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -1277,52 +1277,19 @@ export interface UpdateStatusPayload { fetch_error?: string; } -// ───── Interactive run streaming ───── -// Payloads for the "run a `claude` process from the dashboard" feature. A run is -// started via POST /api/run and identified by a `RunHandle` id; the server then -// streams stdout envelopes, status transitions, and stdin acks back over the WS. +// ───── Terminal run status ───── +// A lane's terminal run is a tmux session; its own live/dead state is polled +// via GET /api/run, not pushed over the WS envelope path. This payload only +// covers the one thing worth pushing live: a run ending (killed or the pane +// process exiting) so an open Workspace tab can update its badge/switcher +// without polling. -/** Payload for the `run_stream` WebSocket message: one streamed JSON envelope - * from a headless/conversation `claude` process started via POST /api/run. */ -export interface RunStreamPayload { - /** Id of the `RunHandle` this envelope belongs to. Lets the UI route the chunk - * to the right run panel when several runs stream at once. */ - id: string; - /** Raw stream-json envelope emitted by the Claude Code CLI (assistant text - * deltas, tool_use/tool_result blocks, etc.) - shape varies by event type. - * Typed as `unknown` because it's forwarded verbatim and narrowed at render. */ - envelope: unknown; -} -/** Payload for the `run_status` WebSocket message: a lifecycle transition for - * a run started via POST /api/run (mirrors `RunHandle.status`). */ +/** Payload for the `run_status` WebSocket message. */ export interface RunStatusPayload { - /** Id of the `RunHandle` whose status changed. */ + /** The run id (tmux session name, `ccam-lane-`). */ id: string; - /** New run lifecycle state; terminal states are "completed"/"error"/"killed". - * "spawning" → the child is being started; "running" → streaming output; - * "killed" → the run was cancelled by the user. */ - status: "spawning" | "running" | "completed" | "error" | "killed"; - /** Epoch-ms timestamp of this status transition (NOT an ISO string, unlike - * most timestamps in this file). */ - at: number; - /** Process exit code; present once status reaches "completed"/"error". 0 means - * a clean exit. */ - exitCode?: number; - /** Claude Code session id resumed/created by this run, once known. Lets the UI - * link a run to the {@link Session} it produced. */ - sessionId?: string | null; - /** Failure message; present when status is "error". Surfaced in the run panel. */ - error?: string; -} -/** Payload for the `run_input_ack` WebSocket message: confirms a message sent - * via POST /api/run/:id/message was written to the child process's stdin. */ -export interface RunInputAckPayload { - /** Id of the `RunHandle` the input was delivered to. */ - id: string; - /** Echoes the id returned by the `send` call this acks, so the UI can clear - * the matching "sending…" pending state. */ - messageId: string; - /** Epoch-ms timestamp the input was delivered (not an ISO string). */ + status: "running" | "gone"; + /** Epoch-ms timestamp of this transition. */ at: number; }