From 24de68cfe128c6dd1a90bfa1494a51598d9b1856 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 27 Aug 2026 10:54:16 +0700 Subject: [PATCH] feat(lanes): badge codex lanes and document the lane tool --- client/src/components/lanes/LaneCard.tsx | 15 ++++++--- client/src/components/lanes/LaneStripCard.tsx | 5 +++ .../lanes/__tests__/LaneCard.test.tsx | 12 +++++++ docs/API.md | 33 +++++++++++++++---- docs/LANES.md | 28 ++++++++++++++++ 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/client/src/components/lanes/LaneCard.tsx b/client/src/components/lanes/LaneCard.tsx index 6d5396a9..826cb31f 100644 --- a/client/src/components/lanes/LaneCard.tsx +++ b/client/src/components/lanes/LaneCard.tsx @@ -381,11 +381,18 @@ export default function LaneCard({ )} - {lane.ci_status && ( + {(lane.ci_status || lane.tool === "codex") && (
- - CI {lane.ci_status} - + {lane.ci_status && ( + + CI {lane.ci_status} + + )} + {lane.tool === "codex" && ( + + Codex + + )}
)} diff --git a/client/src/components/lanes/LaneStripCard.tsx b/client/src/components/lanes/LaneStripCard.tsx index cd447b72..a5e1d97c 100644 --- a/client/src/components/lanes/LaneStripCard.tsx +++ b/client/src/components/lanes/LaneStripCard.tsx @@ -69,6 +69,11 @@ export default function LaneStripCard({ {lane.stage} + {lane.tool === "codex" && ( + + Codex + + )} {detectionLeads(lane) && ( = {}): Lane { cwd: "/work/demo", branch: "lane/demo", kind: "adopted", + tool: "claude", source_repo: null, pipeline: "default", session_id: null, @@ -85,6 +86,17 @@ function makeLane(overrides: Partial = {}): Lane { } as Lane; } +describe("LaneCard lane tool badge", () => { + it("badges a codex lane and leaves a claude lane unbadged", () => { + const { rerender } = render( + + ); + expect(screen.getByText("Codex")).toBeInTheDocument(); + rerender(); + expect(screen.queryByText("Codex")).not.toBeInTheDocument(); + }); +}); + describe("LaneCard child worktrees", () => { it("lists worktrees provisioned from this lane and jumps to one on click", async () => { const onSelectLane = vi.fn(); diff --git a/docs/API.md b/docs/API.md index 52fe9889..d7b85611 100644 --- a/docs/API.md +++ b/docs/API.md @@ -135,7 +135,7 @@ POST /api/lanes/ensure ``` ```json -{ "cwd": "/absolute/path/to/work", "title": "App package", "pipeline": "ship-feature" } +{ "cwd": "/absolute/path/to/work", "title": "App package", "pipeline": "ship-feature", "tool": "codex" } ``` `cwd` is optional — when missing or empty the server falls back to @@ -147,6 +147,11 @@ optional and, like `title`, applies **only when a lane is actually created** `POST /api/lanes/worktree` and `POST /api/lanes` take the same optional field. An id no template defines returns `400 EBADPIPELINE`. +`tool` (`"claude"` or `"codex"`, default `"claude"`) is the CLI this lane's +runs will spawn. Immutable once the lane is created; an attempt to update it on +an existing lane returns `400 EBADTOOL`. The tool choice appears as an immutable +badge on the lane card (Codex lanes only). + Idempotent lookup by working directory — the Workspace page opens on a `cwd`, not on a lane id. Returns the lane whose own `cwd` is that path, or the longest path-boundary parent of it, as `{ "lane": {...}, "created": false }` with `200`. @@ -194,10 +199,15 @@ existing `lane_update` message when the lane becomes `idle` or `failed`. { "sourceRepo": "/absolute/path/to/repository", "title": "Criteria form", - "base": "main" + "base": "main", + "tool": "claude" } ``` +`tool` (`"claude"` or `"codex"`, default `"claude"`) is the CLI this lane's +runs will spawn. Immutable once the lane is created; an attempt to update it on +an existing lane returns `400 EBADTOOL`. + `sourceRepo` must exist, be absolute, and be a git repository. When `base` is omitted, it defaults to the `LANE_BASE_BRANCH` env var, or `main` when that is also unset. The feature branch is named `` (prefix @@ -1571,23 +1581,32 @@ The `/api/run/*` namespace starts and manages `claude` subprocesses in tmux-back ```http GET /api/run List all live runs (computed fresh from tmux state) GET /api/run/tmux { available: bool } — whether tmux is on PATH -GET /api/run/binary { found, path } for the `claude` binary +GET /api/run/binary?tool= { found, path } for the `claude` or `codex` binary GET /api/run/cwds Suggested cwds (dashboard, home, recent) GET /api/run/history?limit=&laneId= Persisted run history; laneId narrows to one lane's runs GET /api/run/files?cwd=&q= Fuzzy file search inside cwd for the @-file autocomplete (skips node_modules, .git, dist, build, .next, .cache, coverage, vendor) -POST /api/run Start/attach run — Body: { laneId, initialPrompt?, ... } +POST /api/run Start/attach run — Body: { laneId, initialPrompt?, sandbox?, approval?, resumeLast?, ... } GET /api/run/:id Run handle (returns live run state) DELETE /api/run/:id Kill (SIGTERM → SIGKILL after 5 s) ``` -**`POST /api/run` (start/attach):** Requires `laneId` (the lane this run belongs to). Creates or attaches an existing tmux session named `ccam-lane-` in the lane's working directory. If that session already exists and its pane is idling at a shell prompt, the `claude` command line (including `--resume` and any initial prompt) is typed into that pane instead of being dropped; if the pane is running a program, the request adopts the session unchanged. Optionally accepts `initialPrompt` to immediately type/send into the session (if empty or omitted, the session is created/attached with no initial input). Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The dashboard self-heals a lane's `run_id`/`status` on every read if the tmux session has been killed externally. +**`GET /api/run/binary`:** Probes for the binary (`claude` or `codex`). Optional query parameter `?tool=codex` probes for Codex; omitted or `?tool=claude` probes for Claude. Returns `{ found: bool, path: string | null }`. -`permissionMode` (`acceptEdits` default, or `default` / `plan` / `bypassPermissions`) is the mode the pane starts in. The pane's argv also carries `--allow-dangerously-skip-permissions`, which only makes **bypass permissions** selectable in the pane's `shift+tab` cycle — it does not enable it. See [docs/LANES.md § start](LANES.md). +**`POST /api/run` (start/attach):** Requires `laneId` (the lane this run belongs to). Creates or attaches an existing tmux session named `ccam-lane-` in the lane's working directory. If that session already exists and its pane is idling at a shell prompt, the appropriate command line (including `--resume` and any initial prompt) is typed into that pane instead of being dropped; if the pane is running a program, the request adopts the session unchanged. Optionally accepts `initialPrompt` to immediately type/send into the session (if empty or omitted, the session is created/attached with no initial input). Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The dashboard self-heals a lane's `run_id`/`status` on every read if the tmux session has been killed externally. + +For **Claude lanes**, `permissionMode` (`acceptEdits` default, or `default` / `plan` / `bypassPermissions`) is the mode the pane starts in. The pane's argv also carries `--allow-dangerously-skip-permissions`, which only makes **bypass permissions** selectable in the pane's `shift+tab` cycle — it does not enable it. + +For **Codex lanes**, three additional body fields control the spawn behavior: +- **`sandbox`** (boolean, default `false`) — whether to enable Codex's restricted filesystem/network mode (`-s` flag) +- **`approval`** (boolean, default `false`) — whether to prompt for confirmation before tool execution (`-a` flag) +- **`resumeLast`** (boolean, default `false`) — whether to resume the last Codex run (`--last` flag instead of starting new) + +See [docs/LANES.md § Lane tool](LANES.md#lane-tool-claude-or-codex) for full details. **PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `ccam lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view. -Every history row carries `lane_id`: the lane whose run it belongs to. `GET /api/run/history?laneId=` returns only that lane's runs. Spawned `claude` processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, the Dashboard's Analytics tab, and the Workflows page automatically. +Every history row carries `lane_id` (the lane whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=` returns only that lane's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, the Dashboard's Analytics tab, and the Workflows page automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`. --- diff --git a/docs/LANES.md b/docs/LANES.md index b6e7c463..8cb75d08 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -45,6 +45,34 @@ The modal has a segmented toggle mirroring the CLI's two modes: Both modes' path field has a **Browse** button next to it, opening a small folder browser (`GET /api/lanes/browse?path=`) instead of a native OS picker — a browser cannot hand a web page an absolute filesystem path from a native dialog, so this dashboard (local-first, server and browser on the same machine) lists directories server-side instead: click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field. Git repos are marked in the listing. +## Lane tool: Claude or Codex + +A lane is driven by a tool: **Claude** (default) or **Codex**, chosen in the "+ Add lane" modal and immutable once the lane exists. A lane's tool determines which CLI is spawned when a run starts. + +- **`claude` (default)** — `claude` binary is invoked; the session's hooks, messages, and tool calls flow through the dashboard. Claude lanes render Sessions, Analytics, cost, and stage-detection badges; liveness is tracked by the Claude Code session. +- **`codex` (experimental)** — `codex` binary is invoked instead. The Run Setup panel shows model, sandbox approval, and reasoning effort toggles instead of the Claude session picker, and offers "Resume last Codex run" instead of picking a session. Resume uses `codex resume --last`. + +### How Codex arguments map + +The Run Setup panel's model, sandbox, approval, and reasoning-effort toggles map directly onto `codex` command-line arguments: + +- **Model** → `codex -m ` (e.g. `codex -m claude-opus-5`) +- **Sandbox** → `-s` (enables restricted filesystem/network mode) +- **Approval** → `-a` (prompts for confirmation before tool execution) +- **Reasoning effort** → `-c model_reasoning_effort=` (e.g. `medium`) + +A codex lane's run console shows the full command line before execution. + +### Limitations of Codex lanes + +Codex does not emit Claude Code hooks (sessions, agent events, tool-execution tracking), so the following surfaces remain empty for a Codex lane: + +- **Sessions** and **Analytics** tabs — no session history or aggregated statistics +- **Cost** surface — no per-run or per-model pricing data +- **Stage detection** — automatic stage inference from tool events (`stage-detect`) never fires + +**These are not bugs; they reflect Codex's architecture.** The stage you declare via `ccam stage` (or that the driving agent calls itself) still works; liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented. + ## Destructive lane actions Reset a managed worktree, remove one, or purge the lane's eligible session history with the CLI: