diff --git a/docs/API.md b/docs/API.md index d1306f4..7ef003c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1487,6 +1487,8 @@ 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. +`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). + **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 analytics, the Kanban board, and the Workflows page automatically. diff --git a/docs/LANES.md b/docs/LANES.md index 2cf00e5..8610117 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -948,6 +948,8 @@ curl -X POST http://localhost:4820/api/run \ The `initialPrompt` field is optional — if omitted, the tmux session is created/attached with no initial input, and you type into the terminal directly. Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The same tmux session persists across attach/detach cycles, so you can switch between the browser Workspace and `ccam lanes shell` seamlessly. +**Permission mode in a lane run.** `permissionMode` (one of `acceptEdits` — the default — `default`, `plan`, `bypassPermissions`) sets the mode the pane *starts* in. The pane's `claude` also gets `--allow-dangerously-skip-permissions`, which puts **bypass permissions** into the pane's own `shift+tab` cycle **without enabling it** — matching what you get in a real terminal, where the cycle would otherwise be limited to plan / auto / manual / accept-edits and bypass would be unreachable from inside a dashboard-started run. Bypass is still only ever entered by an explicit `shift+tab` in the pane or by asking for `permissionMode: "bypassPermissions"` up front; nothing skips permission checks by default. + **A killed run releases the lane.** When the tmux session is terminated (from the terminal, browser stop button, or external `kill`), the lane's `run_id` is cleared, its status returns to `idle`, and the change is broadcast as `lane_update`. Runs are recorded with the lane's id and are listable via `GET /api/run/history?laneId=`. ### stop diff --git a/server/__tests__/pty-run.test.js b/server/__tests__/pty-run.test.js index a9095c5..c6d9d18 100644 --- a/server/__tests__/pty-run.test.js +++ b/server/__tests__/pty-run.test.js @@ -116,6 +116,26 @@ describe("pty-run", () => { assert.ok(newSessionArgv.includes("abc12345")); }); + it("spawnRun offers bypass in the pane's shift+tab cycle without starting in it", () => { + let newSessionArgv = null; + tmux.__setExecImpl((args) => { + if (args[0] === "has-session") { + const e = new Error("gone"); + e.status = 1; + throw e; + } + if (args[0] === "new-session") newSessionArgv = args; + return ""; + }); + pty.spawnRun({ laneId: 4, cwd: "/tmp/repo" }); + assert.ok(newSessionArgv.includes("--allow-dangerously-skip-permissions")); + // ...as an OPTION only: the starting mode stays the requested one, and + // `--dangerously-skip-permissions` (which would enable it) is never passed. + assert.ok(!newSessionArgv.includes("--dangerously-skip-permissions")); + const modeAt = newSessionArgv.indexOf("--permission-mode"); + assert.equal(newSessionArgv[modeAt + 1], "acceptEdits"); + }); + it("spawnRun appends a positional initial prompt after argv flags", () => { let newSessionArgv = null; tmux.__setExecImpl((args) => { diff --git a/server/lib/pty-run.js b/server/lib/pty-run.js index 627ab3c..9c904d3 100644 --- a/server/lib/pty-run.js +++ b/server/lib/pty-run.js @@ -62,6 +62,13 @@ function makeErr(code, message) { function buildArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt }) { const argv = ["claude"]; argv.push("--permission-mode", permissionMode || "acceptEdits"); + // Puts "bypass permissions" INTO the pane's shift+tab cycle without turning + // it on: `claude --help` — "Enable bypassing all permission checks as an + // option, without it being enabled by default." Without it the cycle is only + // plan/auto/manual/acceptEdits, so a run started in any non-bypass mode could + // never reach bypass from inside the pane, while a real terminal could. + // The starting mode is still whatever --permission-mode says above. + argv.push("--allow-dangerously-skip-permissions"); if (model) argv.push("--model", model); if (effort && EFFORT_LEVELS.has(effort)) argv.push("--effort", effort); if (resumeSessionId) argv.push("--resume", resumeSessionId);