diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e5283924..4fceaf6e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,8 +9,8 @@ { "name": "codeman", "source": "./plugins/codeman", - "description": "Drive Codeman from inside a Claude Code session: spawn worker sessions, prompt them, wait for them, read their answers, clean up. Acts only inside a Codeman-managed session.", - "version": "1.33.2", + "description": "Drive Codeman from inside a Claude Code session: spawn worker sessions, prompt them, wait for them, read their answers, clean up. Acts inside a Codeman-managed session; outside one it only reads saved Codex conversations offline.", + "version": "1.48.0", "author": { "name": "Ark0N", "url": "https://github.com/Ark0N" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2ed1d8f1..02874dd1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -55,7 +55,9 @@ npm run test:all # literally everything, environmental failures included Expect `test:browser`/`test:mobile`/`test:perf` to fail where the machine cannot provide what they need; read that as "not runnable here", not as a regression. `config/test-suites.ts` holds the globs, and both configs derive from it, so the exclusions and those runners cannot drift apart. -If you add a test that binds a port, pick a unique one at 3150 or above (search the repo for `const PORT =` first). Never 3000. +The browser suite also runs nightly (and on demand) in `.github/workflows/browser-suite.yml`; it is informational, not a gate. + +If you add a test that binds a port, bind port 0 (`new WebServer(0, …)` + `server.boundPort`, or `listen({ port: 0 })` + `address().port`), or use `app.inject()` when no socket is needed; mobile tests call `createTestServer()` and read `server.boundPort`. `test/test-ports-guard.test.ts` fails a `WebServer` built on any other port and a raw `listen` on a fixed one. Never 3000. Tests are tmux-safe by design: under vitest, the tmux layer becomes an in-memory mock, so tests cannot touch real sessions. diff --git a/.github/workflows/browser-suite.yml b/.github/workflows/browser-suite.yml new file mode 100644 index 00000000..caadba59 --- /dev/null +++ b/.github/workflows/browser-suite.yml @@ -0,0 +1,50 @@ +name: Browser suite + +# The per-push CI gate deliberately skips the Playwright-driven suite (config/test-suites.ts), +# so a browser-only regression can merge green. This job runs that suite on a schedule and on +# demand, so such a regression (the Shift+Enter keypress bug was one) is caught within a day +# instead of by a user. It is NOT a merge gate: a red run means "look", and it never blocks a +# push or a PR. +# +# Needs: chromium (installed below), tmux, and the live server the tests start themselves. +# Not run here: test:mobile (per-machine PNG baselines), test:perf (wall-clock), and +# codex-predictive-echo (needs a real codex binary; it also skips itself without one). + +on: + schedule: + - cron: '29 3 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + browser: + name: Playwright browser suite + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install tmux + run: | + if ! command -v tmux >/dev/null; then + sudo apt-get update -qq + sudo apt-get install -y tmux + fi + + - name: Install chromium + run: npx playwright install --with-deps chromium + + - name: Run the browser suite + run: npm run test:browser -- --exclude test/codex-predictive-echo.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe86f671..4df79374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,6 +219,8 @@ jobs: run: npx vitest run working-directory: packages/xterm-zerolag-input +# The browser suite also runs nightly (and on demand) in .github/workflows/browser-suite.yml; +# that job is informational and never gates a push or a PR. # Note: three suites are excluded from CI, each with its own local runner: # npm run test:browser Playwright + chromium (+ a live server, and a real # codex binary for codex-predictive-echo) diff --git a/.gitignore b/.gitignore index 5113ac0a..ff7d124e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,9 @@ DECISIONS.md # Written by install.sh into end-user clones when setup finishes .install-complete -# Dependencies -node_modules/ +# Dependencies. No trailing slash: `node_modules/` matches only a directory, so a +# worktree's `node_modules` symlink to a shared install got committed (#658, #662). +node_modules # Build output dist/ @@ -84,6 +85,23 @@ tmp/ # added there). No trailing slash so it still matches the symlink, not just dirs. /public +# Video never goes to GitHub (docs/media-assets.md). The only tracked moving +# images are the GIFs a public page embeds, in docs/images/; masters, promo cuts, +# drafts and captures live in the local media library pr/media/ (ignored above). +# The promo branch tracks its Remotion render inputs, so those stay addable. +# A deliberate exception elsewhere takes `git add -f`. +*.mp4 +*.webm +*.mov +*.m4v +*.mkv +*.avi +*.gif +!/docs/images/*.gif +!/scripts/remotion/public/**/*.mp4 +!/scripts/remotion/public/**/*.webm +!/scripts/remotion/public/**/*.gif + # Opt-in gesture overlay runtime assets: large MediaPipe wasm + model (~27 MB) # fetched at build/install by scripts/fetch-gesture-assets.mjs, kept out of git. # (The gesture bundle itself, gesture-codeman.js, IS tracked — built from @@ -107,7 +125,9 @@ todo.md @fix_plan.md readme-preview.mjs -# Uploaded images land here under each session working dir (runtime artifact) +# Prompt uploads land here under each session working dir (runtime artifact); +# .claude-images/ is where they landed before the move. +.codeman-uploads/ .claude-images/ # Local-LLM harness smoke-test config (real IPs/keys) — see the .example.json diff --git a/.prettierignore b/.prettierignore index cdfaf71e..c4427d1c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,8 +24,9 @@ src/web/public/settings-ui.js src/web/public/sw.js src/web/public/terminal-ui.js src/web/public/voice-input.js -src/web/public/upload.html scripts/remotion/ # Hand-maintained; Prettier escapes underscores in glob paths and corrupts paragraphs. CLAUDE.md +src/web/CLAUDE.md +docs/releasing.md diff --git a/AGENTS.md b/AGENTS.md index 49991d86..a3b084da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,6 @@ Quick pointers: - Type check: `tsc --noEmit` · Lint: `npm run lint` · Format: `npm run format:check` - Tests: `npm test` (the CI gate, safe to run bare) or `npm test -- test/.test.ts` for one file -- Route tests use `app.inject()`; new tests needing ports must pick a unique `const PORT =` +- Route tests use `app.inject()`; new tests needing a socket bind port 0 (`new WebServer(0, …)` + `boundPort`); mobile tests use `createTestServer()` and read `server.boundPort` - Branch off `master` for all work; Conventional Commit-style messages (`fix(mobile): ...`) - Never commit secrets or local state from `~/.codeman/` diff --git a/CHANGELOG.md b/CHANGELOG.md index 53aeb9c8..a41256ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,463 @@ # aicodeman +## 1.48.0 + +### Minor Changes + +- 4960bee: ### Thanks + - @opticon454 for a big round: the two-way Discord bridge (#741), custom endpoint context limits, Codex and DeepSeek model selection and the model picker (#752, #755, #757), Clone Repo into a custom folder (#744), the New folder button (#745), and the wrapped `.conf` file links (#735). + - @Randalix for the Edit history panel (#693). + - @aakhter for relative links that follow your `cd` (#740). + - @JDProfresh for settings saves that say when they failed (#736), a visible press on the phone keys (#737), and two test harness fixes (#738, #742). + - @timkjr for a tab strip that keeps its shape whichever tab is selected (#748). + - @Hugo-DDT for the parallel fix to the same wrapped file link bug (#734), whose `Bash(` test rows landed with the guard below. + + **Talk to your agents from Discord (#741).** An opt-in two-way bridge gives each tab its own thread: alerts go out, and a reply from an allowlisted account is typed into that tab. An agent that needs a person can run `codeman notify "" [--urgent] [--file ]`, and files travel both ways. Claude learns the command through its system prompt. Setup is in `docs/discord-bridge.md`. + + **Edit history panel (#693).** An opt-in header button (per device, off by default, never on phones) shows every Edit and Write a local Claude session made, subagents included, with each diff inline, plus the images a Bash call produced. It reads transcripts only when the panel asks, and keeps its 0600 per-session store only as long as the session's record. + + **Custom endpoints go further (#752, #755, #757).** OpenCode and Codex now receive the endpoint's context window, Codex launches on the model you picked, and endpoint discovery reads each model's context size from `/v1/models`, with a per-endpoint default for gateways that state none. DeepSeek Harness moves to a per-session home with a generated patch, so it runs the exact model you chose. The model picker gained a search box and marks the model one of your running sessions is using on an endpoint that cannot report it, or says plainly that nothing is known. + + **Clone into a custom folder, and make a folder from the picker (#744, #745).** Clone Repo can clone straight into a folder you pick, empty or not yet created, and registers it as a linked case. The path picker in Link Existing gained a New folder button, so a brand-new project folder no longer means a trip to a shell. + + **Relative links follow your `cd` (#740).** A relative path printed in the terminal now resolves against the directory the session's pane is in, not the folder the session started in. Only the base changes: what can be opened is the same as before. Session state carries it as the new optional `paneCwd` field. + + **Fixes.** + - **File links keep the whole name (#735).** `.conf` files and parenthesised paths with spaces link as one file in the terminal and the Response Viewer, and a path wrapped over several rows stays clickable from every row. An ambiguous hard wrap tries the unspaced name first and looks up the spaced one only after a 404, on click, never on hover. + - **A failed settings save says so (#736).** Any write the server did not store now reports the reason and keeps App Settings open, where only two kinds of refusal did before. Run mode and respawn preset writes warn too, and a failed tunnel stop no longer claims the tunnel stopped. + - **Phone keys show a press (#737).** Every toolbar button, the voice, attach and settings icons and the accessory keys now react on all seven skins. + - **The L-shaped tab strip holds its shape (#748).** A too-wide first tab is trimmed to the room beside the toolbar, so the layout no longer changes with the selected tab. + - **Credential files stay out of the attachment routes.** `webhook.json`, `custom-model-hosts.json`, the generated custom-model configs and everything under `backups/` could still be read through the attachment routes, which in multi-user mode means by any user. A config backup was the widest gap, since it holds copies of `users.json` and `settings.json`. A custom `configBackupDir` is outside what the blocklist can name, so keep that folder out of reach of other users. + - **The OpenCode Console breach notice is retired.** A new install has no record of having seen it, so every new OpenCode user was still being warned about an October incident. + + **Fixes applied while landing.** `codeman notify --file` refuses a credential file such as `.env` or a key under `.ssh`, even inside the tab's own folder, and the Discord bot token cannot be read through the attachment routes (#741). A very wide table cell in an agent's last message no longer stalls the server, and a file sent from Discord is no longer reported as something the agent produced (#741). A `Bash(` row keeps its links, because it holds a command rather than a path, while `Read(` and `Update(` rows link a spaced name whole (#735). A model id the CLI cannot launch is refused before anyone is asked to evict a loaded model, on both the swap route and quick-start (#752). A cases-folder refusal in Clone Repo now points at Clone Repo, and OpenSSL's `SSL_connect` errors classify as unreachable again (#744). Abandoning a tunnel enable records the settings baseline (#736). A session's unreachable `DSH_HOME` is logged when the server's home is linked instead (#755). Clone Repo and New folder no longer risk a hang on a mount that stopped answering (#744, #745). + + **Docs and tests.** Discord setup guide and wiki page, the Edit history and `paneCwd` API sections, the custom endpoint guide, and the architecture notes for the new subsystems. Two Docker and TUI test harnesses now pass under a root runner and on tmux 3.2a (#738, #742). + +## 1.47.0 + +### Minor Changes + +- 1ab01bb: ### Thanks + - @timkjr for five PRs: a wrapped tab strip that uses the space under the toolbar (#721), three fixes that stop Codeman reading and watching a remote session's folder on the local disk (#722, #727, #728), and a quieter build (#729). + - @kdxcxs for bodyless requests that work through Cloudflare Tunnel (#725), traced through a real Access + Tunnel hostname. + - @Hugo-DDT for a desktop dead key typed once and a Ctrl+V that stays a paste after a replay (#726), and @hedburgaren for reporting both in #716. + - @opticon454 for rendered Markdown that fills a resized File Viewer (#730). + - @JDProfresh for a middle click on a file path that opens the file (#731). + + **The wrapped tab strip uses the room under the toolbar (#721).** When the classic header strip wraps onto more rows (automatically, or with the two-rows setting), only the rows beside the toolbar stop short of it now; every row below runs the full header width, so long tab names no longer end up one per row. A strip that fits on one row looks exactly as before, and a strip tall enough to scroll keeps the plain wrap. + + **Fixes.** + - **Bodyless requests work through Cloudflare Tunnel (#725).** cloudflared forwards a POST with no body as an empty chunked request with no Content-Type, which the server refused with `415` before the route ran, so actions such as opening a saved web tab failed through a tunnel. An empty, untyped POST under `/api/` now reaches its route exactly like a `Content-Length: 0` one; anything with a body or another method is still refused as before. + - **A desktop dead key is typed once, and Ctrl+V stays a paste after a replay (#726, #716).** The soft-keyboard recovery for Android IMEs now stays off on a desktop (a fine pointer that is not Android), so a dead-key backtick no longer arrives twice. Plain-text Ctrl+V is now framed as a bracketed paste by Codeman itself, because xterm stops adding the markers once a terminal replay clears its paste mode, and a copied newline then submitted the prompt. The paste still scrolls to the bottom and clears the selection, in the main pane, grid tiles and the split pane. + - **Remote sessions no longer touch the local disk at their path (#722, #727, #728).** A remote (SSH) session's folder is a path on the remote host, but the Ralph `@fix_plan.md` watcher and the image watcher watched it on this machine (logging `ENOENT` at every start, or watching an unrelated local folder of the same name), and the Ralph file routes read and wrote it locally. Both watchers now skip remote sessions (the Session Options image-watcher switch is hidden there), the Ralph menu's **Import from File** reads `@fix_plan.md` on the remote host over the same ssh path the File Viewer uses, and **Write to File** (and the `@ralph_prompt.md` write) is refused for a remote session instead of landing in a local folder. + - **Rendered Markdown fills a resized File Viewer (#730).** The document kept a fixed 960px column from before the window could be resized, so it stopped reflowing once you dragged or expanded the window past it. + - **A middle click on a file path opens the file (#731).** In the Response Viewer and the File Viewer's rendered Markdown, a middle click on a file path or an in-document link opened the dashboard root in a new tab. It now does what a left click does; real web links keep the browser's own middle click. + - **No more duplicate-key warning on build (#729).** The Chinese translation table carried `Idle` twice. + + **Fixes applied while landing.** A remote session's fix plan stays authoritative as it is for a local one, so a prose line such as "All tasks completed" cannot mark every todo done and end a remote Ralph loop when it cannot end a local one (#722). The Session Options image-watcher switch flips back and says so when the server refuses the change, instead of reporting success (#727). The remote `@fix_plan.md` read uses the File Viewer's own containment check rather than a copy of it (#728). ASCII-diagram blocks in the File Viewer's rendered Markdown follow the window instead of staying 720px wide (#730). A paste marker hidden inside another one (`ESC[20` + `ESC[201~` + `1~`) can no longer survive the strip and end the bracketed paste early, in Ctrl+V and in the phone composer alike, and an Android device with a mouse, a trackpad cover or a stylus keeps the soft-keyboard fixes, which turned off there because Chromium reports such a pointer as fine (#726). + + **Docs and tests.** The L-shaped strip in the frontend notes and the architecture notes, the remote-session rows for the Ralph file routes and the two watchers in the remote-sessions guide and the architecture notes, and an overview of the empty-POST parser in its module. + +## 1.46.0 + +### Minor Changes + +- de1aab9: ### Thanks + - @JDProfresh for five PRs: a phone keyboard that no longer costs Claude its transcript (#711, measured in #672), a + on each session tab that starts a session beside it (#712, shaped in Discussion #700), a tunnel that needs its own opt-in instead of the LAN one (#718, from their report #706), a settings.json that two writers can no longer tear (#719), and a DeepSeek test that passes on current Node (#710). + - @aakhter for six PRs: `codeman agent spawn` in a project directory (#708), worker ids kept when the readiness check fails (#709), the offline Codex history reader (#707), experimental bounded inspection and guarded submission for agents (#694, from Discussion #695), links that follow URLs and paths across wrapped rows and table cells (#686), and file paths with spaces linked as one path (#687). + - @opticon454 for the Fetch button and fetch interval in the Git status window (#714). + - @johoja12 for Cmd+K arrows that switch the CLI both ways after you type a case name (#717). + + **Fetch from the Git status window (#714).** The Git status window has a Fetch button next to Refresh. It runs `git fetch` for the repositories it lists and shows each one's result. An optional interval runs it for you: off by default, app-wide in App Settings, Bottom bar, with a per-session override in Session Options, Git. A fetch only moves remote-tracking refs (HEAD, the index and the working tree stay as they are), it never runs for a Docker or remote session, and it is bounded in time, output and concurrency. + + **Start a session beside any tab (#712, opt-in).** Turn on **New Session Button on Tabs** (App Settings, Tabs) and each session tab gets a +: pick any CLI its case can run (a Docker case offers what its container has) and the new session starts in that case or worktree and that tab's group, without touching the toolbar's case or Run mode. The row menu offers the same as "New session here…" for the keyboard. + + **The phone keyboard no longer costs Claude its transcript (#711).** Every keyboard open and close used to resize the PTY, and Claude Code answers each height change by reprinting only the tail that fits, so lines above it were lost from tmux for good (25 of 100 in five keyboard cycles). A local Claude session now keeps its rows while the keyboard is up and slides the screen so the composer stays above the keyboard. On by default as **Keep Size Under Keyboard** (App Settings, per device); off restores the old resize. Fullscreen Claude (mouse tracking on) still resizes as before. + + **Typing stays smooth in a long prompt (#713).** The local echo overlay (the instant preview of what you type) rebuilt every character of the prompt on each keystroke, so on a phone-class CPU a 4,000-character prompt took most of a second per key. It now rebuilds only the line that changed, with one paint layer per line, so a keystroke costs the same however long the prompt is. Every character still sits on its terminal cell. This ships in `xterm-zerolag-input` too. + + **Links survive wrapping and spaces (#686, #687).** A URL or path a CLI wrapped inside a table cell or an indented paragraph now links whole instead of in fragments, URLs keep balanced parentheses (`…/Team-Space(QA)Archive/…`), a full-width table divider no longer glues onto the row below, and OSC 8 hyperlinks open through Codeman's own opener after a confirmation. A file path with spaces (`/mnt/c/OneDrive - Contoso/report.docx`, `Saved to: ~/My Docs/plan.md`) links as one path in the terminal and the response viewer when the line bounds it, and a shell prompt that prints its working directory no longer turns the whole command line into one link. + + **`codeman agent spawn` in a project directory (#708).** Without a case name, `spawn` starts the worker in the current directory, or in an existing directory given with `--working-dir`, through `POST /api/v1/sessions` and the mode's start route. Named cases still use quick-start. `--json` prints `{ sessionId, workingDir, created: true }` plus `ready`, `readinessError` or `startupError`. An empty `--working-dir`, a bare spawn from your home directory (it would install Codeman's hooks into `~/.claude`; pass `--working-dir ~` to mean it) and an unknown `--mode` are refused with exit 4 before any request. + + **Read saved Codex conversations offline (#707).** `codeman history read` reads Codex's saved rollouts without a Codeman server: by file, by Codex session id, or a whole conversation with its sub-agents and forks, filtered by topic or UTC date with whole turns kept. Reads and output have separate, raisable budgets, and anything it could not read or attribute (a compressed `.jsonl.zst` rollout, a file still being written) is reported through `complete: false` instead of failing the lookup. The codeman skill routes to it. + + **Experimental: bounded inspection and guarded submission for agents (#694, opt-in).** `codeman agent inspect` reads bounded, cursor-paged evidence from another session (a local Codex session's attributed rollout records, otherwise a labelled pane snapshot), and `agent stage` / `agent submit` type one line into a verified local Codex composer and later send exactly one Enter against a hash-confirmed receipt. Off by default behind **Experimental agent coordination** (App Settings, Cross-session features); while off the three routes answer `403 FORBIDDEN` naming the setting. These routes and verbs are experimental under the versioning policy and may change in a minor or patch release. During the few milliseconds of a guarded submit, other input is held and written in order afterwards; a tagged input POST in that window answers `409 CONFLICT` so its sender retries, and an untagged one (the phone keys, the Stop button) is held and answered once written. + + **Cmd+K arrows switch the CLI both ways (#717).** After typing a case name in Cmd+K, plain Left and Right now both change the New session row's CLI whenever the caret is at either end of the query (only Right did before). Inside the query the arrows move the caret as usual. + + **Fixes.** + - **Security: a LAN install's acknowledgement no longer opens a public tunnel (#718).** `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK` (and `--allow-unauthenticated-network`), which the installer writes for every LAN install without a password, also let the Cloudflare tunnel start, so one click on the welcome screen's tunnel link published full terminal control to a public URL with no login and no confirm. It now covers the network bind only. An unauthenticated tunnel has its own opt-in, `CODEMAN_ALLOW_UNAUTHENTICATED_TUNNEL=1` or `codeman web --allow-unauthenticated-tunnel` (carried by `codeman web -d` and `codeman service install`), which the installer never writes; if you relied on the network variable to start the tunnel without a password, set one of those. With no password and no multi-user mode, the browser now asks before every tunnel enable, using the new `loginConfigured` field of `GET /api/tunnel/status`. Both acknowledgement variables and their flags are now listed as stable in the versioning policy. + - **settings.json can no longer be torn (#719).** A quick-start's `lastUsedCase` write was not awaited and not atomic, so it could overlap the next settings save and leave one write's bytes over the tail of the other, after which every reader saw `{}`. Every write to settings.json now goes through one serialized writer that writes a temp file and renames it into place, and a file that does not parse is refused with `409 CONFLICT` instead of being overwritten with only the writer's own keys. A case rename checks this before it ends any session. + - **A worker id is never lost (#709).** When `codeman agent spawn` created a worker but its readiness request failed, it exited without the new session id. It now prints the id (and `--json` carries it) and exits 1, so the caller can inspect or delete the worker. `codeman agent` and `codeman mcp` also name the error code (such as `ECONNREFUSED`) when a request fails with an empty message. + - **The DeepSeek transcript test passes on Node 24.22+ and 26.10+ (#710).** It pinned Node's old zstd decoder, which stopped at the first frame until nodejs/node#65865. + + **Fixes applied while landing.** The Git fetch interval field is no longer reset by a session update while you type in it, a session holding only half an override shows the app's value for the other half, reading a repository's `core.sshCommand` no longer doubles the fetch timeout, a `GIT_SSH` in the server's environment is honoured, and the Fetch button's disabled and busy styles no longer reach other icon buttons and stop pulsing under reduced motion (#714). A + launch keeps the toolbar on its own case, and pressing the + again closes its picker (#712). The fetch scheduler no longer logs the text of a malformed settings.json on every 15-second tick (the parser's message could quote an API key; it now says the file is not valid JSON, once), and a fetch is skipped while the Docker case list cannot be read, so it can never run inside a Docker workspace (#714). A case rename answers `409 CONFLICT` for a settings.json it cannot write before it ends any session (#719). The unauthenticated-tunnel warning no longer fires in multi-user mode, where the tunnel has a login in front of it (#718). Untagged input during a guarded submit is held instead of refused, and the skill's staging guide says so (#694). A shell prompt line (a spaced working directory followed by `$` included) and a labelled `../` spaced path link as they should, and a bold label or inline code before a path in the response viewer splits it as the terminal does (#687); a URL whose row soft-wraps no longer glues a trailing column onto the next path, and in a table a link in the column right of a wrapped URL stays clickable on the URL's first row (#686). The keyboard hold reads the terminal's height only while rows are held (#711). The README, the plugin's README and its marketplace description now say the codeman skill reads saved Codex conversations outside a Codeman session and refuses everything else there (#707). + + **Docs and tests.** New sections for the keyboard hold (next to the terminal geometry rule it is an exception to), the tab +, Git fetch, the settings writer, the tunnel opt-in, the directory spawn, the offline history reader and the experimental agent routes, in CLAUDE.md, the API reference, the architecture notes, the wiki and the codeman skill. + +## 1.45.0 + +### Minor Changes + +- ### Thanks + - @opticon454 for five PRs: renaming a case and its folder with the live sessions handled (#680), a resizable line between the tabs and the header toolbar (#689), opt-in GPU and CPU clock readings in the header (#692), and two Codex wheel fixes measured against the real CLI, a first notch that moves the transcript (#699, from #697) and a wheel that no longer moves a selection menu's choice (#698, from #696). + - @aakhter for five PRs: an agent picker on the Cmd+K New session row (#685), a login throttle that no longer locks a whole tunnel out after ten typos (#684), an SSE connect attempt that can no longer hang forever (#678), prompts that start with a hyphen (#677), and `req` made required on the session lookup helpers so an ownership check cannot be skipped by accident (#683). + - @JDProfresh for a phone swipe that follows the order the strip draws (#682) and an inline Claude tab whose history loads instead of paging the composer after a server restart (#681), both found and tested on an iPhone. + - @Randalix for the per-session agent inbox (#690), the shape agreed in Discussion #558. + - @Hugo-DDT for closing the last two ways a key binding could move a client onto another session (#688), and @christianhaberl for reporting both in #671. + - @johoja12 for remote hosts whose tmux lives in Homebrew or a per-user bin directory (#691), the follow-up promised in #645. + + **Rename a case and its folder (#680).** The case manager has a Rename action. A case under `~/codeman-cases` is renamed as a name and folder; a linked case can rename only its name or its real folder too. Live sessions that have the folder open are listed and only stopped if you agree, the folder is locked while it moves so nothing starts in it half-moved, a folder another program holds open is reported instead of forced, and Claude Code's conversation history (including the Resume list), the case order, the last used case and the default session group follow the new name. + + **A per-session agent inbox (#690).** Agents can leave each other messages without typing into anyone's pane: `codeman agent post ""`, `codeman agent inbox [--wait]` and `codeman agent ack `, over `POST`/`GET /api/sessions/:id/inbox`, `POST .../inbox/ack` and `GET /api/agent-inbox/summary`. A message stays until its receiver acknowledges it, so nothing is lost between a read and the ack, and `codeman agent ls` shows an INBOX column. Inboxes live in memory by default; **Keep agent inbox across restarts** (Settings, Cross-session features, off by default) writes them to a 0600 file in the data dir, kept for 12 hours. + + **Pick the agent from Cmd+K (#685).** The palette's New session row shows the agent it will start, with arrows to change it (Left/Right while the row is highlighted). It starts on the Run button's mode, offers the same agents the Run menu offers for that case (a Docker case's container agents included), and launches once without changing the Run button. + + **A resizable header toolbar (#689).** Drag the line between the session tabs and the header toolbar. Dragging it right narrows the toolbar, which then wraps onto a second line instead of squeezing out the tabs, and the usage readings shed their labels, then their provider names, then (in the Compact style) their percentages as it narrows; double-click or Home resets it, the arrow keys move it, and the width is remembered on the device. + + **GPU and CPU clock readings (#692, opt-in).** **GPU Stats** adds GPU load, graphics memory and the GPU clock after CPU and MEM (each only when the host reports it), and **CPU MHz** adds the live CPU clock. Both are in Settings, Header & Panels, off by default, and nothing is read unless the browser asks. NVIDIA is read through `nvidia-smi`, AMD through sysfs, and inside WSL any vendor through the Windows performance counters (no GPU clock there). The header also keeps polling its stats with no session open, so CPU and MEM no longer sit at `--`. + + **Fixes.** + - **A tunnel no longer locks everyone out after ten typos (#684, multi-user mode).** Behind `tailscale serve` or a tunnel every login arrives from one address, so ten wrong passwords from anyone used to lock every user out for 15 minutes. The per-address limit is now 50, and each account allows ten failed guesses per address, both checked before the password is verified. Another address can no longer lock an account out, a burst of parallel guesses counts in full, and identical logins in flight share one check, so a reload with many tabs open is never refused. Single-user mode is unchanged. + - **The first wheel notch moves a Codex transcript (#699).** When a CLI does not track the mouse, Codeman turns the wheel into PageUp/PageDown, and it used to send nothing until half a screen of travel had built up: 5 notches in Chrome, 7 in Firefox. The first notch now pages at once and the notches after it repay it, so the page rate is unchanged. + - **The wheel no longer moves a Codex menu's selection (#698).** At "Implement this plan?" one notch down used to land on "No, stay in Plan mode" and one up on "Yes, implement this plan", because those menus use the page keys. The wheel is now ignored while such a menu shows, in the main pane and in tiles, and pages the transcript again once it closes. + - **History loads after a restart instead of paging Claude (#681).** After a server restart, a second visit to an inline Claude tab on a phone turned every swipe into PageUp/PageDown for Claude, so the composer jumped around and old output never showed. The swipe now loads tmux's history, as the first visit does. + - **A phone swipe follows the strip (#682).** With the case or state layout, a sorted rail or named groups, swiping now moves to the session drawn next to the current one, not the next one in the hidden canonical order. Alt+[ / Alt+], Ctrl+Tab and Alt+1..9 keep the canonical order, matching the Alt+N badges. + - **Key bindings can no longer move a client onto another session (#688, fixes #671).** The tmux servers behind Docker cases and owned remote sessions keep only their send-prefix binding, as Codeman's own socket has since 1.44.0, and `codeman tui`'s Alt+1..9 switch only the TUI's own client: an Esc then a digit in a browser tab reaches its pane instead of switching it. + - **Remote tmux found in Homebrew or a per-user bin (#691).** Every remote tmux call (availability check, launch, attach, discovery, capture, cleanup) now runs on one widened, quoted PATH, so a host whose tmux works only in an interactive shell links and launches, home directories with spaces included. + - **Prompts that start with a hyphen (#677).** Text typed into a pane that began with `-` (`-x hello`, `--help`) was read by tmux as its own options and never arrived. + - **A connect attempt that never opens is retried (#678).** After a phone resumes on a VPN or tailnet, an event stream could sit in "connecting" forever with no error to trigger a retry. Each attempt now gets 15 seconds before it is closed and retried with the usual backoff. + + **Fixes applied while landing.** A case rename that fails half way now puts `settings.json` back too and says where the folder is; it merges Claude Code's history into a folder a deleted case of the new name left behind instead of silently leaving it at the old name, refuses a dangling symlink at the new name before any session is stopped, refuses to move a folder while a session is still being created in it, and the dialog says plainly that stopping closes the sessions (#680). The inbox keeps the mail of sessions on the reboot-restore offer and of a detached session that comes back, prints message text with control characters escaped, and flushes after the server stops accepting posts (#690). Cmd+K's agent list follows the case's own availability rule, so a Docker case can start a CLI only its container has, and with a single choice the row keeps the Run button's mode (#685). The divider's stored width can no longer go negative and Escape no longer resets it (#689). With the toolbar narrowed, the GPU, VRAM and clock readings keep their labels, so they can still be told apart from CPU and MEM, and at Compact's rings-only step a clock with nothing left to show is hidden instead of leaving a gap (#689, #692). The stats route no longer waits on Windows' RAM probe, and a hung `nvidia-smi` can no longer freeze the header's CPU and MEM readings: the `nvidia-smi` and Windows RAM probes now give up after 1.5 s (#692). A second `codeman tui` on a longer pts number can no longer remove the first one's guard, and a client that fails the guard gets its key instead of losing it (#688). A test that had started passing vacuously after the SSE change checks the connect URL again (#678). + + **Docs and tests.** `req` is now required on `findSessionOrFail` and `findPersistedSessionOrFail`, pinned by a test that also checks its exact type (#683). New sections for the agent inbox, the case rename, the header divider and the host sensors, the SSE connect timer next to the staleness watchdog, the login buckets in the security docs, and the remote PATH rule in the architecture notes. + +## 1.44.0 + +### Minor Changes + +- ### Thanks + - @opticon454 for five PRs: config backups you can compare and restore (#661, ten review rounds), a File Viewer you can resize, expand and walk Back and Forward through (#659), GitHub Copilot's premium-request quota in the plan-usage chip (#662), a warning when llama.cpp's parallel slots leave too little context per request (#658, from #641), and a bare `npx vitest` that can no longer write into your real `~/.codeman` (#660, from #640). + - @JDProfresh for four PRs: the opt-in prompt-cache keep-alive (#666, step 2 of #554), Markdown links to file paths that open in the File Viewer (#665), a phone keyboard that stays up while you press accessory keys (#673), and a session swipe that no longer taps or scrolls the session you are leaving (#674), with the iPhone testing behind both phone fixes. + - @christianhaberl for four PRs: `CODEMAN_MAX_SESSIONS` (#668), tmux prefix keys that can no longer move a browser tab onto another session (#669), multi-line prompts that keep their line breaks (#670), and rediscovered tmux sessions that keep their own folder (#667). Also for the earlier #224, #225, #226 and the session sidebar (#307), whose commits carried an agent identity that GitHub credits to another account. + - @johoja12, a first-time contributor, for scroll batches that stay with the tab that made them, smoother trackpad scrolling and wheel scrolling in fullscreen Codex (#645), measured against the real CLI through five review rounds. + - @ludovicfourrage for touch scrolling in apps that track the mouse themselves (#664), and the careful write-up in #663. + - @aakhter for a default tab group per case (#676). + + **Config backups (#661).** Codeman now keeps timestamped copies of the files that hold your setup: settings, CLI overrides, custom model endpoints, the webhook, linked cases and, in multi-user mode, the accounts. It takes one at startup and whenever one of them changes, into `~/.codeman/backups/config` by default. App Settings → System lets you pick the folder and the retention (keep N backups, drop ones older than N days), back up now, compare two backups or a backup with what is running (credentials are never shown, only whether they changed), and restore one. A restore first saves what it is about to replace, so it can be undone, and retention only ever deletes folders Codeman made and never the newest backup. + + **Keep an idle Claude session's prompt cache warm (#666, opt-in).** Claude Code's prompt cache expires after an hour of quiet, and the next prompt then pays to re-read the whole conversation. Switch on **Keep the cache warm while idle** in a session's Respawn tab (or **Keep prompt caches warm** in App Settings → System → Automation for every new session) and Codeman sends a one-line keep-alive turn about five minutes before the cache would expire. It only does that when nothing else is going on: an empty prompt, no open dialog, nobody typing, no respawn, Ralph loop or waiting agent, no usage-limit pause. It stops on its own a while after your last prompt (**Keep warm for at most**, default 2 hours). A session started with it on also launches with Claude Code's own idle compaction off (Claude Code 2.1.290 or newer). The keep-alive raises no alert, push, webhook or Away-digest row, and the readout's Status line says what it is doing. + + **A default tab group per case (#676).** Case settings has a **Default session group**: every new session for that case lands straight in that tab group, in the same write that creates it, so it never flashes in Ungrouped first. An explicit group on the create request and a spawned child following its parent still win. It is stored per user and survives a group rename. + + **A File Viewer you can resize, with Back and Forward (#659).** Drag the corner grip to resize the window, or press Expand to fill the screen (remembered on this device). Following a link from one file to another builds a history that the ← and → buttons and Alt+← / Alt+→ walk through. Phones keep the full-screen viewer. + + **Copilot's premium-request quota in the plan-usage chip (#662).** With the GitHub Copilot CLI installed and signed in, the header chip gains a Copilot row for the month's premium requests, with the counts and the reset date in the tooltip. Codeman reads it every 10 minutes from GitHub with the Copilot CLI's own sign-in token. It is on by default, like the Codex row, and turning Plan Usage Limits off in App Settings stops the request. + + **More (or fewer) sessions with `CODEMAN_MAX_SESSIONS` (#668).** The cap of 50 concurrent sessions can be set at startup, from 1 to 462. A value that is not a positive integer falls back to 50, and the per-user cap in multi-user mode follows it unless `CODEMAN_MAX_SESSIONS_PER_USER` is set. + + **Multi-line prompts keep their line breaks (#670).** A prompt sent through the API with `useMux` used to arrive as one welded line (`line oneline two`). Its lines now go into the pane as one bracketed paste, followed by the Enter that submits them together. The senders that run unattended (cron, respawn's update and kickstart prompts, the auto-compact focus, `codeman agent send`, the TUI and the skill's `sendwait`) stay single-line and join lines with a space, because the submit check cannot yet recognise a multi-line paste in the composer. + + **A warning for llama.cpp slots that split the context too thin (#658).** A custom llama.cpp model that runs several parallel slots splits its context between them, and four 40K-token Claude requests against a 4-slot, 131K pool all failed with "Context size has been exceeded". Discovery now records each model's slot count, and a Claude launch warns when one slot gets less than 40K tokens, naming the fix (`--parallel 1`, or a context large enough for every slot). + + **Fixes.** + - **tmux prefix keys stay out of browser tabs (#669).** Ctrl+B then `)`, `(`, `s` or `w` in a tab switched its tmux client to another session while the tab kept the first one's name. Codeman's tmux socket now keeps only Ctrl+B Ctrl+B (send-prefix) in its prefix table, set at session start and at boot. This also means `Ctrl+B d` no longer detaches a terminal attached to that socket: `codeman tui` and `scripts/tmux-manager.sh` detach with F1. + - **Scrolling stays with its tab, and fullscreen Codex scrolls (#645).** A queued wheel batch can no longer land in a tab you switched to, trackpads keep their fractional travel instead of dropping it, the first wheel report of a tab visit goes out at once, and Codex gets wheel reports while it has mouse tracking on (measured on Codex 0.163.0, where an approval prompt's selection is unaffected). + - **Touch scrolling in apps that track the mouse (#664).** A finger drag and its fling now scroll a program in the pane that asked for SGR mouse reports (a TUI in a shell tab, for example) instead of scrolling a local buffer that cannot show it. Other mouse encodings are left alone. + - **A session swipe only switches sessions (#674).** A sideways swipe on the terminal no longer also opens a link, taps a CLI menu or toggles the keyboard on the session you are leaving. A deliberate swipe gets 500 ms instead of 300, a selection or a second finger pauses it, and a scroll that starts on a sideways thumb arc still scrolls. + - **The phone keyboard stays up (#673).** Arrow, Tab, Esc and the other accessory keys that act on the terminal no longer blur it for a moment, so the toolbar stops bouncing on every tap. + - **Markdown links to files open in the File Viewer (#665).** A link like Codex's `[label](/abs/path.json)` in the response viewer now opens the file instead of a same-origin 404 tab, and its label keeps the prose font. + - **Rediscovered sessions keep their folder (#667).** A tmux session Codeman finds again on its socket (after `mux-sessions.json` was lost) now gets the directory tmux reports for its pane instead of the server's own, so a respawn goes back to the right project. + - **Web tabs send POST bodies to dashboards that read Content-Length.** Codeman's web-tab proxy now relays the browser's Content-Length, so a dashboard built on Python's `http.server` or a small Go or C server no longer sees an empty body on every POST made through a web tab. + + **Fixes applied while landing.** A restore that fails partway now says which files it restored, which it could not and where the pre-restore copy is, re-reads what was written and refills the Settings form; backup passes list the manifests once, and a pre-restore copy equal to the newest backup is not made twice (#661). A failed keep-alive write is retried before the expiry instead of never, an unreadable `settings.json` is logged once instead of every minute for every session, the keep-alive's own hooks stay out of the run summary, and saving App Settings re-plans keep-alives only when their settings changed (#666). The Copilot quota read checks the setting and the sign-in token before it ever probes for the CLI, so a machine without Copilot runs no login shell every 10 minutes, and drops a header the Copilot CLI itself does not send (#662). The prefix-table reset still binds send-prefix on an empty table and no longer reruns on every session for a prefix tmux lists escaped (#669). Claude worktree sessions follow the case's default group like every other CLI's (#676). The remote-history PATH change that rode along in #645 was left for its own PR. Smaller touches: the Expand button keeps its label while pressed and Alt+← / Alt+→ are listed in the shortcut overlay and help (#659); the scroll log names xterm's mouse encoding (#664); the response-viewer link test covers `?query` and folder links (#665). + + **Docs and tests.** A bare `npx vitest run` from the repo now uses the isolated CI config instead of running without the setup file, the helpers that delete test folders refuse to run outside a sandboxed HOME, and the temp-HOME teardown is bounded so a stuck writer cannot time the hook out (#660). New sections and lines for config backups, the keep-alive and the Copilot quota in the architecture notes and `CLAUDE.md`, a test that a deleted pane directory is never adopted (#667), the codeman skill preamble re-stamped 1.44.0, and browser tests for the touch-scroll and swipe fixes. + +## 1.43.0 + +### Minor Changes + +- ### Thanks + - @opticon454 for five PRs: GitHub Copilot CLI as a run mode (#592), an Antigravity tab that shows when it is working (#648, reported in #639), a WS pill that no longer flickers on every paste (#647, reported in #638), custom-folder cases anywhere below a broad cases folder (#567, reported in #637), and the opt-in "Pin tab names on the CLI" setting (#646). Also for filing #637 to #641 with the cause already worked out, and for measuring Copilot and `agy` live instead of guessing. + - @JDProfresh for four follow-ups to the document uploads in #621: a mixed pick, drop or paste that says how many files it skipped (#651), a text upload refused as "not UTF-8" instead of a type mismatch (#652), a parity test that keeps the browser's copies of the document list honest (#653), and link taps on a phone that land on the right row after a tab switch (#654). + - @irisitymichaelgrundberg for clickable `.diff` and `.patch` paths (#644). + + **Security notices for the CLIs you run.** Codeman can now warn you about a security incident in an agent CLI it runs, and it does so only on machines where that CLI is installed. After an update the warning sits inside the "Codeman updated" note, with a button to the vendor's own notice next to **See what's new**; on a new install, or if you missed it, it comes as a card of its own. It stays until you close it or open the link, and expires on its own. The first one is about OpenCode: on 17 October OpenCode reported unauthorized access to a legacy system, and provider API keys (OpenAI, Anthropic and others) saved in some OpenCode Console workspaces may have been exposed. If OpenCode emailed you about it, revoke and replace those keys and check their recent usage. If you got no email, OpenCode says you were not affected. + + **GitHub Copilot CLI is a run mode (#592).** Install `copilot` (`npm install -g @github/copilot`) and it appears in the Run menu, on the welcome screen, in cron and in CLI management. It runs in tmux like the other agent CLIs, shows working and idle and the model it reads off its own footer, takes the usual launch options (`copilotConfig`: model, resume, continue, and `allowAll`, which is clamped off for non-granted multi-user owners), works with Custom Model Endpoints through Copilot's BYOK variables, joins MCP sync, and works with `codeman agent spawn`, `send` and `read`. A plain drag selects text, so Auto Copy works, while the wheel and phone swipes scroll its transcript. Copilot's folder-trust dialog still needs one Enter the first time a folder is used. + + **Pin tab names on the CLI (#646, optional).** A new App Settings switch, off by default, launches every Claude session with `--name `, not only the ones you renamed, so Claude Code's prompt-box label, `/resume` entry and remote session list match your tabs. The cost, which the setting spells out: Claude stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. It applies from the next session start or respawn. + + **Antigravity tabs show when they are working (#648).** An `agy` turn now marks its tab working and a finished turn reads as idle again. A shell command waiting for your approval keeps showing as working until you answer it. + + **Fixes.** + - **Custom-folder cases below the cases folder (#567).** Create New with a custom folder refused every folder anywhere below the cases folder. Now only the cases folder itself and its direct children are refused, since those are the only ones listed as local cases, so a broad `CODEMAN_CASES_PATH` such as `/mnt/user/Scripts` no longer blocks the whole tree. The refusal now says which of the two it hit. + - **A steady WS pill (#647).** The ` · NB queued` suffix appears only once a backlog has lasted over a second, instead of flashing on every paste, IME commit or fast burst. + - **Clickable diff and patch files (#644).** A `.diff` or `.patch` path is now a link in the terminal and the response viewer, so an agent's review diff opens in one click. + - **Mixed uploads say what they skipped (#651).** A pick, drop or paste with some unsupported files attaches the rest and says how many it skipped; the Compose and paste-dialog pickers now report an unsupported pick instead of doing nothing. + - **"Not UTF-8" for refused text uploads (#652).** A text file that fails the UTF-8 check is told it is not UTF-8 (or UTF-16, when it starts with that byte order mark) and to save it as UTF-8, instead of reading like a corrupt file. + - **Link taps on a phone after a tab switch (#654).** With the view parked above the bottom, a URL typed into the prompt no longer opens on tap, and a link in the scrollback opens on its own row. + + **Fixes applied while landing.** The Copilot run mode clamps every `COPILOT_*` variable that bypasses approval, runs code or moves the token for non-granted multi-user owners, keeps a divider row inside an answer as part of what `codeman agent read` returns, and its Docker docs no longer promise a token you cannot set there (#592). Antigravity's working line only matches at the start of a row, so an answer that quotes the spinner no longer holds the tab busy (#648). The custom-folder refusal names the cases folder itself and a direct child separately (#567). The WS pill reads its queue once per update (#647). The "Pin tab names" setting is translated for zh-CN and listed in the Settings Reference (#646). JSON files the page fetches at runtime (the update note, the security notices) are now served `no-cache` instead of cached for a year, so a later notice always reaches a browser that already has the old copy. + + **Docs and tests.** New route tests for nested custom-folder cases and for Copilot's screen-read answers, a wrapped-composer touch test, stricter upload parity checks, and docs that move Antigravity and Copilot into the screen-checked idle group. + +## 1.42.0 + +### Minor Changes + +- 1238316: ### Thanks + - @JDProfresh for five PRs: prompt uploads that take documents (#621, from the plan in #553), a video Codeman cannot take refused before it uploads (#622), a prompt-cache readout kept to the CLIs that report one and a statusline report with many cache-miss causes no longer thrown away (#623), and two fixes for Claude's workspace trust dialog in a brand-new folder: idle waits that hold until the dialog is answered (#634) and a dialog that gets answered every time (#635). + - @Randalix for Codex prompts that really get submitted, sent as one bracketed paste and a separate Enter (#628), and for the careful measurements in #627 that found the cause. + - @ludovicfourrage, a first-time contributor, for reporting (#629) and fixing (#632) the nested tmux status bar colour that leaked into live output after a history reload. + - @opticon454 for three PRs: a one-time "what's new" note after each update (#618), Previous and Next Session as rebindable shortcuts that refuse a chord another action already uses (#617), and a DeepSeek install from CLI management that ends with a DeepSeek you can run (#616). + + **Git worktrees for a case (#625).** Give every agent on a repository its own checkout. Pick a case whose folder is a git repository, choose **Git worktrees…** in the case picker (**Worktrees** on the phone's case sheet), type a name and pick the CLI: Codeman creates `/.claude/worktrees/` on a new branch `worktree-` and opens a session there, in a tab named after the worktree. **Open** starts another session in a worktree you already have. Removing one runs plain `git worktree remove`, never `--force`, so uncommitted work stays where it is, and Close Session can kill the session and remove its worktree in one go. The layout is the one Claude Code's own `claude --worktree` uses, so each sees the other's worktrees. To keep the main checkout clean, Codeman lists the worktrees folder and the files it writes for sessions in the repository's local `.git/info/exclude`; your `.gitignore` is never touched. + + **Codex sessions turn red when Codex asks you something (#624).** Codex's command and edit approvals, its question dialogs and the trust prompt on a new repository are read straight off the screen and raised the way a Claude permission prompt is: red tab, desktop notification, push, webhook, the phone's NEEDS YOU row and an Approvals Inbox card that tells you to answer in the terminal. An agent driving a codex worker can now wait for that with `until=blocked`. + + **Prompt uploads take documents (#621).** Attach, drop or paste a PDF, a Word, PowerPoint or Excel file, a zip, or a text file (txt, md, csv, tsv, json, log, yaml, xml) and its path lands on your prompt, ready to send, like a photo or a video does. Every file is checked against its actual bytes on the way in, so a program renamed to `.pdf` is refused, and text files must be UTF-8. The saved name keeps a readable part of the original: `Q3 report.pdf` is saved as `paste-...-Q3-report.pdf`. The button in the Compose and paste dialogs now says Attach. + + **Previous and Next Session are rebindable (#617).** Previous Session (Alt/Option+[) and the alternate Next Session (Alt/Option+]) are now ordinary rows in App Settings > Shortcuts, so you can rebind or switch them off like everything else. Capturing a chord another action already uses is refused with a message naming that action (Alt+1 to 9, Ctrl/Shift+Enter and Ctrl/Cmd+V stay reserved), and re-enabling or resetting a shortcut runs the same check. A chord you rebound no longer also types into the terminal: a rebind to Alt+J used to switch tabs and send ESC j to the session too. Built-in defaults like Ctrl+L still reach the CLI. + + **A note after each update (#618).** The first time a browser loads a newer Codeman, a toast lists that release's headline changes, with a **See what's new** button that opens the full release notes. It shows once per version, and a browser that has never opened Codeman before just records the version and stays quiet, so the first note you will see is the one for the release after this one. The headlines are taken from the changelog when the release is cut. + + **Multi-user mode: only admins change server-wide settings.** In multi-user mode any signed-in regular user could rewrite the server's settings (feature switches, run mode, case order, the default model for cron and the orchestrator, the app config), start a self-update, rotate the tunnel QR code, clear stale session state, and sign every user out with Revoke All, admins included. They could also read the Deepgram API key an admin saved. Those writes and operations now answer 403 to a regular user, Revoke All signs them out of their own logins only, and their settings read leaves out the admin's key and case names. In the browser a regular user's per-device settings still save, a change to a server-wide setting shows one notice that only an admin can change it, instead of pretending it was saved, and their own run mode and respawn presets are kept on their device. Single-user mode is unchanged. + + **Fixes.** + - **Codex submits long prompts (#628).** Prompts sent through the API, `codeman agent send`, the MCP server, cron or the agent skill go into the pane as one bracketed paste followed by a separate Enter, so Codex no longer leaves them sitting unsent in its composer. A single character (a menu answer like `3` or `y`) is still typed, so numbered dialogs keep picking the option you sent, and a trailing `;` survives, so `find . -exec rm {} \;` arrives whole. + - **New Claude sessions in an untrusted folder (#634, #635).** An agent waiting for a fresh session to go idle no longer gets its answer while the workspace trust dialog is still up, so its first prompt can no longer press Enter on the highlighted "No, exit" and kill the session it just started. And the dialog is now answered every time: a screen read that came too soon after the previous one used to be dropped, which now and then left a new session sitting on the dialog. Deleting a session while the dialog is up also stops Codeman reading its dead pane for seconds afterwards. + - **Nested tmux colours after a history reload (#632).** Opening a remote or Docker case, or a shell running tmux inside it, no longer hands the nested tmux status bar's green background to whatever the agent prints next. + - **The codeman skill with arguments (#642).** The skill works again when Claude Code loads it with arguments ("use codeman to start 5 claude workers"). Claude Code swaps a skill's `$1` and `$2` for words of your request, which broke the pasted helpers; they now use `${1}` and `${2}`, and a cached copy written by the broken version is rewritten on the next run. + - **DeepSeek installs ready to run (#616).** Installing the terminal profile finds the `pnpm` next to `dsh` even when the server's own PATH lacks `~/.local/bin`, and the DeepSeek install command adds `pnpm` when the machine has none. + - **Unsupported videos refused up front (#622).** An Android `.3gp` or an `.mkv` gets "Unsupported file type" right away, instead of uploading up to a gigabyte and failing at the end. + - **Statusline reports (#623).** The prompt-cache readout is stored only for CLIs whose statusline reports one (Claude today), like the model badge, and a Claude report with more than 20 cache-miss causes keeps the first 20 instead of being refused, which used to blank the footer and the plan-usage chip. + + **Fixes applied while landing.** Removing a worktree that is, sits inside or holds a Docker case's workspace is refused before git runs; it would have deleted the folder a container still mounts (#625). Codex's trust prompt also reads as blocked in the variant reached through its "Update available" screen, which ends in "esc back" (#624). The DeepSeek install only adds `pnpm` when none is on PATH, so a machine with a corepack `pnpm` keeps it instead of the whole install failing (#616). Three statements in the upload docs were corrected: only a local agent and a Docker case share the uploads folder (remote sessions are refused), GIFs keep their frames instead of being re-encoded, and a developer note no longer names a release that never shipped (#621). The update note skips the Thanks list and group labels like "Fixes.", still works for a release with no headlines, and is translated for zh-CN (#618). + + **Docs.** The README and its Chinese translation caught up with 1.41.0 and 1.41.1 and show a sharper hero GIF, the Codeman Skill got a wiki page and two GIFs linked from both READMEs, CLAUDE.md was split into the root file, `src/web/CLAUDE.md` and `docs/releasing.md`, the release steps now include the MCP registry publish, and a new note says where video and GIF masters live (not on GitHub). + +## 1.41.1 + +### Patch Changes + +- ### Thanks + - @timkjr for four PRs: shells that reload from tmux's rendered history instead of replaying old full-screen repaints (#601), the full scrollback of a remote SSH session read from the remote tmux (#602) with no rows lost at the join (#609), and a phone shell Paste that inserts a multi-line block instead of running it line by line (#600). + - @aakhter for three PRs: Codex panes that page their transcript with the wheel and touch (#527, fixes #508), new and spawned tabs that land in tab groups (#528), and a Focus section of shortcuts at the top of the vertical rail (#582). + - @JDProfresh for three PRs: Claude Code's prompt-cache state per session (#607, step 1 of #554), the phone Attach button with camera-roll video uploads (#606, part two of #553), and an upload that no longer stalls the server on a dead network mount (#605). + - @Randalix for clickable `~/`, relative and hard-wrapped file paths plus a folder grid for browsing a whole folder of renders (#604, closes #603). + - @opticon454 for three PRs: a Working folder row with a Copy button in Session Options (#593), tab colour, pin and image watcher surviving a restart (#591), and Link Existing saying why it refused a case (#590). + - @ideaCompany, a first-time contributor, for rendering local HTML files as real pages in the File Viewer (#547), and for the careful security write-up behind it. + - @zomby1, a first-time contributor, for OSC 52 clipboard support (#611). + - @Hugo-DDT, a first-time contributor, for two PRs: MCP server sync reading Gemini's bare `url`, OpenCode's V2 layout and `opencode.jsonc` (#599, fixes #586), and DeepSeek permission prompts that no longer offer Approve/Deny buttons that cannot work (#610, from #598). + + **HTML files open as pages, and markdown opens rendered from the terminal (#547).** Click an `.html` path an agent printed (a test report, a coverage page, a dashboard) and the File Viewer now shows it as a real page, with its own scripts, styles, images and data files from its folder, instead of raw source. A **Page** pill switches to the source and its Edit button. The page runs in a sandbox with an opaque origin: it cannot read Codeman's cookies, use Codeman's API or read files outside its own folder. Network access is open, so a dashboard that loads Chart.js or fonts from a CDN works as it would opened from disk. Broad folders (`/`, home, `/tmp`, a drive's mount point) are refused, so put a report in a folder of its own. And a `.md` or `.markdown` path clicked in the terminal now opens the rendered markdown viewer everywhere (main terminal, tiles, split pane, touch), instead of the live log viewer that showed "streaming"; the MD pill still gives the source. Logs, `.txt`, `.json` and code keep the live log viewer. + + **`codeman mcp`: drive Codeman from any MCP client (#615).** A stdio Model Context Protocol server with seven tools (list, start, send, wait, read, interrupt, delete), so Claude Code, Codex, Gemini CLI, Cursor or Claude Desktop can start worker sessions in any installed agent CLI, prompt them, wait for them and read what they did. It is a client of the existing HTTP API with the same guards as `codeman agent`: it refuses control characters in input, never acts on the session it runs in, and by default only touches sessions it started. Setup lines for Claude Code, JSON clients and Codex are in the README and the wiki page "Driving Codeman From An Agent". + + **The codeman skill for every agent CLI (#613).** With **Codeman Agent Skill** on (App Settings > Agents & CLIs > All agent CLIs, still off by default), Codex, Gemini CLI, OpenCode, Antigravity, Grok Build, DeepSeek Harness and OMP sessions now get the same `codeman` skill Claude Code had, in `.agents/skills/codeman`, so an agent in any mode knows how to start and manage worker sessions. `codeman skill install --mode ` installs it by hand. + + **Tiles: move them by hand, or tear one off into its own window (#584).** With the camera gesture overlay on, pinch a tile and carry it onto another tile to swap them, onto an empty cell to move it, or carry a tab onto the grid to add it. With the new **Detach Tiles** setting (App Settings > Appearance, off by default), drag a tile's header out of the window and it pops out into its own window; drop it on another Codeman window's tiles and it moves there. Known limits: all Codeman windows in one browser share one saved tile layout, a drag between two browser profiles is not seen by the source window, and a drag-out held for more than a few seconds can be blocked as a pop-up. + + **Focus section on the vertical rail (#582).** "Add to Focus" in a row's menu (or right-click a web tab) pins a shortcut to that tab in a small collapsible section at the top of the rail. The tab itself stays where it is. + + **Tabs land in groups (#528).** A tab group's menu has **New session**, which starts a session straight into that group. A session an agent spawns lands right after its parent; App Settings > Appearance > Tabs > **Spawned tabs follow their parent** (off by default) keeps it moving with the parent. + + **Folder grid and more clickable paths (#604).** Paths written as `~/...`, relative to the session folder, or broken across terminal lines are now clickable in the terminal and the Response Viewer, and folders are too: a folder opens a grid of its files (renders, screenshots, reports) that you can page through with the arrow keys. + + **Phone Attach button and videos (#606).** On a phone the toolbar has an Attach button that opens the system file sheet directly, without raising the keyboard, and camera-roll videos (mp4, mov, webm, up to 1 GB) upload into `.codeman-uploads/` like images. + + **OSC 52 clipboard (#611).** A program in a session that sets the clipboard with OSC 52 (Claude Code's `/copy`, a tmux yank, vim) now puts the text on your browser's clipboard, in the main terminal and in every tile. Read requests are refused, and a replayed buffer never copies. + + **Prompt-cache readout (#607).** Claude sessions show whether their prompt cache is warm (`cache:until HH:MM`) or cold in the footer and in Session Options > Respawn, restored after a restart, with a reason when a launch will never report it. Display only, nothing changes behaviour. + + **Faster tab switches to fullscreen Claude (#588, #583).** Switching back to a fullscreen Claude tab now loads only its current screen (a few KB) instead of replaying a 1 MB tail of old redraws (about 1.3 to 1.7 s saved per switch). The "Showing the most recent 1.0 MB" bar now appears only when there really is older history and you scroll to the top looking for it, gives a line count, and stays closed once you close it. + + **Fixes.** + - **Shells reload cleanly (#601).** A shell tab, tile or split pane reloads from tmux's rendered history, so a prompt no longer appears four or five times and lines no longer go missing after a resize or a full-screen program. + - **Remote SSH sessions keep their scrollback (#602, #609).** Loading a remote session's full history reads it from the remote tmux in one bounded ssh round trip, where the local pane kept only a fraction (48 of 279 lines in one measurement, none after a reattach). + - **Codex pages with the wheel and touch (#527).** A Codex pane on its alternate screen pages its transcript with PageUp/PageDown from the first wheel notch or swipe, in the main terminal and in tiles, and never pages into an open permission dialog. + - **openpyxl spreadsheets preview (#612).** An `.xlsx` written by openpyxl with a cell comment, a table or a chart now opens in the File Viewer instead of failing. + - **Phone shell Paste (#600).** A multi-line paste in a shell is inserted as one bracketed paste and waits for Enter, instead of running each line as it arrives. + - **Restart keeps tab colour, pin and image watcher (#591).** + - **Session Options shows the working folder (#593)**, with a Copy button and a label that says when the folder is on a remote host or mounted into a Docker container. + - **Link Existing says why (#590)**, for example "Invalid case name format", instead of "Invalid request body". + - **Uploads on a dead mount (#605).** A prompt upload into a workspace on a network mount that stopped answering is refused at once instead of tying up a server thread. + - **MCP server sync (#599).** Gemini servers with a bare `url` sync as HTTP, OpenCode's V2 `mcp.servers` layout and `opencode.jsonc` are read. + - **DeepSeek approvals (#610).** DeepSeek Harness permission prompts show "Answer in the terminal" in the Approvals Inbox, the phone NEEDS YOU strip and push notifications, instead of Approve/Deny buttons the server always refused. + + **Fixes applied while landing.** Remote SSH and Docker tabs keep their scrollback on a tab switch (they were about to lose it with #588). No colour bleed where remote history meets the live screen (#602, #609). A bare `~` in a Docker case no longer lists the host's home folder, and shell prompts (`user@host:~/proj$`) no longer turn into broken folder links (#604). Spawned sibling tabs keep their creation order (#528). An OSC 52 copy over plain HTTP gives keyboard focus back to the terminal (#611). A refused video upload answers instead of hanging, and the zh-CN "Attach" label of the tile grid is back (#606). The Working folder row fits on phones, with zh-CN text (#593). OpenCode sync no longer stalls on a server named `servers` (#599). Moving a tile to another window no longer overwrites that window's saved layout (#584). HTML pages: a `~/` link to an `.html` file renders as a page instead of falling back to source, `/tmp` is refused even when `TMPDIR` points elsewhere, symlinks cannot get around the hidden-file and file-type rules, workspace confinement is re-checked on every request, media in a page can seek, and the service worker never caches a page (#547). Codex users of `codeman mcp`: the config now forwards the session id, so its self-protection works (#615). + +## 1.41.0 + +### Minor Changes + +- 87f1c9c: ### Thanks + - @Randalix for `codeman agent` (#557), the session verbs (`ls`, `spawn`, `send`, `wait`, `read`, `interrupt`, `rm`) for agents in every CLI mode, and for moving every test server onto an ephemeral port (#570), which finishes #440. Thanks also for reporting and fixing git's clone errors on non-English hosts (#568, shipped as #572 with your commit). + - @opticon454 for five PRs: MCP server sync for GitHub Copilot CLI (#581), an Apply button that saves Settings without closing them (#565), configurable toast and browser-notification display times (#564), in-document links in rendered markdown that scroll to their heading (#563), and npm-based CLI installs that work when the system npm prefix is root-owned (#562). + - @JDProfresh for three PRs: pasted and uploaded files moving into a hidden, self-ignoring `.codeman-uploads/` folder (#574, after your #553 proposal), local echo that keeps painting on phones when the view sits above the bottom (#576), and upload failures that say why they failed (#578). + - @aakhter for session search on the vertical tab rail (#580), built on the sidebar's existing filter instead of a second one, and for keeping alerted tabs visible during a search. + + ![Codeman tile grid: six live agents powering on and off with the CRT tile animation](https://raw.githubusercontent.com/Ark0N/Codeman/08694b5862534b2b1224e9e272ad9858c787ebef/release-1.41/tiles-crt-stats-800.gif) + + **Tile Animations (#571).** The tile grid from 1.40.0 can now open with a show. App Settings has a new **Animations** section (right after Appearance) that holds every animation setting: the Entrance Theme (moved out of Appearance), the new **Tile Animations** row, and a button that opens the animation lab. Tile styles: **CRT** (each tile switches on as a hot line in a diagonal wave, and switches off to a line and a dot), **Fly from tab** (each tile grows out of its session tab and flies back into it), **Deal** (dealt out of the Tiles button like cards), **Beam down**, **Cascade**, **Pop**, **Soft** and **None**. A styled tile plays in two beats: the frame enters in the tile style, then its screen powers on in the terminal style of your Entrance Theme. Picking a theme presets a matching tile style, and a new **Launch** theme flies the tiles out of their tabs. Off by default (the grid keeps its quick fade), per device, nothing moves under reduced motion, and the animations never cost an extra PTY resize. The terminal pane's Boot entrance style is gone; a saved Boot falls back to off. + + **Tiles open the sessions you care about, and keep your layout.** With no grid arranged yet, the Tiles button (or `Ctrl+Shift+G`) now fills the grid with the sessions that are working first (most recently started first), then the ones waiting on you (a permission prompt or a finished turn you have not seen), then the most recently used, instead of the tabs in strip order, which opened the oldest ones. The session you are on is still always included. Once you arrange a grid (which session sits where, empty cells, the tile count, divider sizes, the focused and zoomed tile), that layout is remembered in this browser and the Tiles button brings it back exactly, however the grid was closed, and so does a reload while it was open. A session that has gone since frees its place, which the same ranking fills. A grid larger than the window opens in full with the focused tile zoomed, instead of being trimmed (and losing the rest of your layout). + + **The wheel scrolls Claude inside a tile (#577).** In a tile or the split's second pane, a Claude session on its fullscreen renderer now scrolls its own conversation with the mouse wheel, exactly like the main terminal: the wheel goes to Claude as mouse reports, aimed at that tile's session and computed from the tile's own screen. Before, the wheel did nothing or scrolled the stale frames left over from loading the tile. Shift+wheel (scroll local history) works in every tile on Windows and Linux too; it was dead there. + + **`codeman agent`: session verbs for every CLI (#557).** Agents in any mode (Codex, OpenCode, Gemini, Pi and the rest, not just Claude) can now drive other Codeman sessions from the command line: `codeman agent ls | spawn | send | wait | read | interrupt | rm`. It is a thin client over the existing session API: every call names the session that made it, `wait` blocks on a signal (`--until stop,exit`) or a literal output marker (`--match`), and exit codes say what happened (`0` ok, `1` error, `2` timeout, `3` exited, `4` refused). Ids shorter than 8 characters are refused, so a stray `rm 9` can never pick a session at random, and `rm` never deletes the session it runs in. See the README section "`codeman agent`" and the wiki page Driving Codeman From An Agent. This is phase 1 of #445. + + **Settings: Apply (#565).** Next to Save, an Apply button saves the same way but keeps Settings open. Switching on MCP server sync makes its Preview and Sync usable straight away, and CLI management's add, enable and disable work without closing and reopening Settings. + + **MCP server sync reaches GitHub Copilot CLI (#581).** With MCP server sync on (App Settings, off by default), Copilot CLI's `~/.copilot/mcp-config.json` now takes part like the agent CLIs' own files: its servers are copied to the others and theirs to it, additively, with the previous file kept as `.codeman-bak`. Copilot joins only when it is installed or already has that file, a server switched off in Copilot is never copied, and `COPILOT_HOME` is followed. Copilot is a sync target only, not a new run mode. + + **Notifications stay up longer if you want (#564).** Settings → Notifications has a Toast display time and a Browser notification display time (1 second to 5 minutes, per device; the defaults stay 3 s and 8 s). + + **CLI logos on tabs can be switched off (#569).** App Settings → Appearance → Tabs → **CLI Logos on Tabs** hides the agent logo on every tab surface (header strip, rails, sidebar, phone chips, the desktop home list) on this device. On by default. Tile headers, split pane headers and the Run menus keep their logos. + + **Search sessions on the vertical rail (#580).** The vertical tab rail has a **Search sessions** box at the top: type part of a name and the rail narrows to the tabs that match (a web tab by its title), across every group, collapsed ones included, without touching your groups, their collapse or the tab order. A tab with an alert stays visible even when its name does not match, so a prompt waiting on you is never filtered away. Escape or × clears it, you can still drag a found tab into a group, and nothing is saved. The sidebar's filter box shares the same filter: a tab with an alert stays visible there too, and in the by-case tab layout a case with no match now hides. + + **Closing a tab is instant.** Closing a session used to take half a second or more before the tab went away. The tab, tile or split pane now goes (and the next session is selected) the moment you click, while the server shuts the session down in the background, and the server side is faster too (about 450 ms down to 200-260 ms for a Claude session): it no longer sleeps fixed intervals, no longer freezes for about 70 ms per close on a synchronous tmux call, and scans for a session's subagents once instead of once per subagent. If the server refuses the close, the tab comes back where it was with the error. This also fixes a bug where a failed close still said "Session closed" while the session kept running. + + **Fixes.** + - **Clone errors on non-English hosts (#572, from #568).** Cloning a repository as a case now classifies a failed clone correctly whatever the host's language: a missing branch or tag is "does not exist on the remote" (400) and a missing repository is a 404, instead of a generic 422 with git's German (or any other) error text. Git runs with `LC_ALL=C` for clones and repo status, so the repo status card's error text is English on every host as well. + - **Links within a markdown file (#563).** A link to another heading of the same document (`[Install](#installation)`) in the File Viewer or Response Viewer scrolls to that heading instead of doing nothing. Headings get GitHub-style slugs, repeated titles are numbered, and non-ASCII headings work. + - **npm CLI installs on a root-owned prefix (#562).** Installing an npm-based CLI from Settings (DeepSeek's `dsh`, pi, ...) no longer fails with EACCES when the system node keeps its global prefix under `/usr`: the install goes to `~/.local`, where Codeman already looks for CLIs. A prefix you set yourself, or one you can write to, is left alone, including when Codeman runs under `npm run`. + - **Uploads go to a hidden `.codeman-uploads/` folder (#574).** Images you paste or upload into a prompt are saved in `/.codeman-uploads/` instead of `.claude-images/`. The folder ignores itself in git (it carries a `.gitignore` of `*`), stays hidden in the Files panel, and is cleaned up as before: files older than 7 days in an hourly sweep, and the folder when the last session in that workspace closes. The old `.claude-images/` folder gets nothing new and is still swept and removed during 1.41.x. An upload to a remote (SSH) session is now refused with a clear message, since the file would land on the Codeman host where the remote agent cannot read it. + - **Typing on a phone after a tab switch (#576, fixes #575).** With local echo on (the default on touch devices), text typed while the terminal sat above the bottom was buffered but never painted, so the keyboard looked dead. The view sits there after every tab switch and after the keyboard closes. The overlay now paints whenever the prompt row is on screen, and hides only when you scroll the prompt out of view. + - **Upload failures say why (#578).** When a prompt image upload fails, the toast shows the server's reason (for example a rate limit) instead of only "1 failed". + - **The npm page shows the English README.** npmjs.com had been rendering the Chinese README, because npm picks the package's readme from an unsorted file match at publish time. The publish now moves `README.zh-CN.md` aside while it runs (the file and every link to it stay as they are), and the package description and homepage say what Codeman is and point at getcodeman.com. + - **New cases ask for clickable file paths.** The CLAUDE.md generated into a new case asks the agent to report every file it created as a full absolute path, which Codeman turns into a link that opens the File Viewer, and mentions the codeman skill for starting and managing worker sessions. + + **For contributors (#570).** Every in-process test server binds an ephemeral port, the mobile suite included, and the port guard now also refuses raw listeners on a fixed port, so two test runs on one machine never collide. + + **Fixes applied while landing.** zh-CN translations for the two new notification display-time settings and for the new Animations section. A session whose name matches an interface word ("Lab", "New session") is no longer translated in the tab strip when the interface is in Chinese. Plus test and doc cleanups left over from review. + +## 1.40.0 + +### Minor Changes + +- ### Thanks + - @opticon454 for four PRs in this release: the end of Android autocorrect duplicating a line (#541), a Run dropdown that fits and scrolls on any screen (#542), Git status that copes with big folders and slow shares (#543), and a nightly browser-suite workflow plus the three stale browser tests it needed (#540). Thanks also for confirming #550 on a live install. + - @JDProfresh, a first-time contributor, for four PRs: Respawn and Ralph back in Claude's Session Options (#550), removing the broken tunnel Upload URL page (#551), the wiki's Contributing page catching up with CONTRIBUTING.md (#552), and a remote-wake test that no longer pins its budget to the millisecond (#559). + - @Randalix for making opencode tabs selectable and scrollable again (#555), and for the server reporting the port it really bound, which let the test suite move off fixed ports (#556). + - @shenlvkang-collab for Default Codex model and reasoning effort settings (#546), and for the opt-in `window.CodemanHost` bridge that lets a native wrapper app pop a session or a file preview out into a window of its own (#432). + - @aakhter for `.xlsx` spreadsheets in the File Viewer (#502), parsed entirely in the browser behind admission limits that held up to round after round of review. + + This is the biggest visual overhaul Codeman has had: a tile grid for driving several sessions at once, a new header, CLI logos everywhere an agent is named, lineage trees, a calmer welcome screen and optional new tab layouts. The defaults are chosen so an upgrade looks familiar: the tab strip stays Classic, the header gets the Compact stats, and the Tiles button is there to try. + + **The tile grid: up to six live sessions side by side.** Click **Tiles** in the header (or press `Ctrl+Shift+G`) and the window splits into a grid of real terminals, each one a full session you can read and type in, with its own connection. Every tile has a small header naming the session, the agent (its logo) and the model it runs (`Claude on Opus 5.5`, `DeepSeek on qwen3.8-27b`, following an in-session `/model` switch), a status dot that turns red and pulses the tile's border when that session needs you, a session menu, zoom (`⤢` or `Alt+Shift+Enter`) and remove (`×`, the session keeps running). Right-click the button for a 2 / 4 / 6 count menu (remembered per device; a hover card explains both clicks). Add sessions by `Ctrl`/`Cmd`+clicking a tab, dragging a tab onto a tile or an empty cell, "Open group as tiles" in a tab group's menu, or just Run while the grid is open. Rearrange by dragging a tile by its header (onto another tile they swap, and an empty cell can sit anywhere), with `Ctrl+Shift+Arrows`, and resize with the column and row dividers; `Alt+Shift+Arrows`, `Ctrl+Tab` and `Alt+[` / `]` move the focus, and the focused tile is the session every panel follows (files, git status, respawn, subagent windows, voice, image paste). Picking a tab that is not tiled shows it on its own and one click brings the grid back; app-driven selections never collapse it. The grid survives a reload (per device, session ids only), opens and closes with a short animation (off under reduced motion), auto-zooms the focused tile when the window gets too small, shows an Attach overlay for a session that is not attached or whose agent exited, and is fully translated into 简体中文. It was built to stay fast with six busy agents: tiles paint first and load their terminals one per frame, a tile replays its history at xterm's own pace and never reads more scrollback than it keeps, the main terminal is parked (no SSE terminal stream) while tiles own the screen, and a window resize refits each tile exactly once. Desktop only (a window at least 1180px wide); the setting is App Settings → Header & Panels → Tiles, on by default on desktop and opt-in on touch tablets. The user guide is the new [Tile Grid](https://github.com/Ark0N/Codeman/wiki/Tile-Grid) wiki page. + + **Split view, rebuilt on the same terminal (#560).** The split's second pane is now a `TerminalTile`, the same component every tile is: its input goes through the exactly-once queue, so a dropped link can no longer lose or double a keystroke; it reconnects on its own after a drop or a server restart; it and its PTY never disagree about size; file paths are clickable and `Ctrl+V` pastes images into it; and app shortcuts, voice and image paste follow the pane you are in. Both panes now name their agent and model above them. + + **A new header.** The header stats come in three styles (App Settings → Header & Panels → Header Stats Style, per device): **Compact**, the new default, draws WS, CPU, MEM and the plan usage windows as two pills where every reading is a ring, a label and a value; **Tiles** gives each one its own box; **As before** keeps the old readout. The icon buttons beside them take the matching shape, and hover now moves the icon, never the button. + + **CLI logos everywhere an agent is named (#532).** The Run menus (toolbar, phone overview picker, custom endpoints, model picker), every agent tab (header strip, rail, sidebar, phone chips, the desktop home rail, Claude's tabs included), the tile and split headers and the welcome screen now show each CLI's own logo instead of a colour dot or a two-letter pill. The marks are inline SVG, follow every skin, and a CLI you added through `clis.json` gets a plain dot. + + **Lineage trees (#544).** The lines from a tab to the tabs it spawned are now one rounded tree per spawning tab, routed through the gaps between tab rows so they never cross a tab or reach the terminal. Every family is always drawn, the selected tab's family is drawn thicker and on top, and a dashed branch now means that child is working. + + **Tab layouts (#538, optional).** App Settings → Appearance → Tabs → **Tab Layout** adds three opt-in arrangements next to the default **Classic** strip: **By state** (a row each for Needs you, Waiting, Working and the rest, most urgent first, flippable with State Order), **By case** (one labelled box per case) and **Ledger** (an aligned grid of equal cells). By state and By case also group the vertical rail and the sidebar. Per device; phones keep their scrolling chip row. + + **A calmer welcome screen.** One primary launcher for the first agent in your catalog (Claude Code on a stock install, the next agent if it is disabled) with its real logo, every other CLI as a slim pill under it, and Cloudflare Tunnel as a quiet link above its QR. The toolbar's "+" and case gear moved into the case picker as "New or link a case…" and "Case settings…" rows, and the duplicate instance stepper is gone (#428). + + **Every session knows its model.** Sessions publish the model they run (`displayModel` on the session state): a custom endpoint's model id, else what the running CLI itself reports (Claude's statusLine, codex's, pi's and opencode's footers, the dsh status line), else the model its config pins (a DeepSeek route) or the one it was launched with. It is persisted, follows `/model` switches, and is stripped of control characters and capped. + + **Idle detection for every agent.** OpenCode, Gemini, Pi and OMP turns now end: their composer bars and spinners are read so a session goes idle when the agent is done instead of spinning "working" forever, codex 0.162's new footer row no longer hides its model or its background-terminal row, and a freshly started or re-attached agent pane now announces its idle to open pages (it used to stay `busy` until a reload). A pane prompted within its first seconds is never settled idle at launch, so send-and-wait cannot resolve before the turn starts. + + **Spreadsheets in the File Viewer (#502).** `.xlsx` files open as a read-only grid with sheet tabs, number formats, merged cells and colours, from the Files panel, attachments or a path an agent prints. They are parsed entirely in your browser in a worker (up to 10 MB) behind admission limits on everything the parser would expand. `.xls` and `.ods` stay download-only, and `file-content` now reports `type: 'spreadsheet'` for `.xlsx`. + + **Default Codex model and reasoning effort (#546).** App Settings has a Default Codex model and a Default Codex reasoning effort, applied to new local Codex sessions (Run menu, Resume and the HTTP API) unless the launch names its own; custom endpoints, Docker and remote sessions keep their own settings. + + **Pop-out windows for native wrapper apps (#432).** A native wrapper (for example an Android app on a foldable) can pop a session, a file preview or a web tab out into a window of its own beside the dashboard through an opt-in, experimental `window.CodemanHost` bridge. Browsers behave exactly as before. + + **Git status for big folders and slow shares (#543).** Two per-device settings under Settings → Bottom bar set how many repositories it lists (up to 50, default 12) and how long one git command may take (5 to 120 s, now 30 s by default), and a repository git cannot read is listed with the reason and counted as `? N` in the indicator instead of silently disappearing. + + **Behaviour change: `Ctrl+W` no longer closes a session.** It is delete-word in every shell and agent CLI, and muscle memory used to kill a session (its pane and CLI, with no confirm) mid-sentence. Close Session has no default key now; bind one in App Settings → Shortcuts if you want it. + + **Removed: the tunnel Upload URL page (#551).** Since the response envelope change it reported "Saved: undefined" and listed nothing. Use the in-app image paste instead. `POST /api/screenshots`, `GET /api/screenshots` and `GET /api/screenshots/:name` (and their `/api/v1` aliases) still work unchanged but log a one-time deprecation warning and will be removed in a future major release. + + **Fixes.** Android keyboards that autocorrect as you type (SwiftKey, Gboard) no longer duplicate the line in the prompt: the corrected word reaches the session exactly once, including when Enter arrives in the same keyboard transaction (#541). opencode tabs: a drag selects text again (so copy-on-select works and `Ctrl+C` copies instead of closing opencode), and the wheel and touch swipes page through opencode's conversation (#555). The Run dropdown no longer runs off the top of the screen: with many CLIs, endpoints and saved URLs it fits between the header and the toolbar, follows the on-screen keyboard and the iPhone safe areas, and scrolls (#542). Claude sessions show the Respawn and Ralph / Todo tabs and the auto-resume toggle in Session Options again, hidden since 1.33.0 (#550, fixes #549). The terminal refits when only its box changes size (a header that grows with the state rows or the lineage gutter used to leave the bottom rows clipped behind the toolbar until a tab switch). A device with any shortcut override no longer gets every App Settings save rejected while the toast still said "Settings saved". Image paste and dictation land in the session they started in. App Settings search finds Split and Tiles by what they do. The Run button family, the Help modal, the shortcut overlay leftovers and the exited-agent tab badge are translated into Chinese. + + **For contributors.** The server reports the port it actually bound (`boundPort`), the tests that shared fixed ports bind ephemeral ones, and a static guard keeps new fixed ports out, so two test runs on one machine no longer collide (#556). A nightly (and on-demand) Playwright browser-suite workflow runs the suites the CI gate cannot, informational and never a gate (#540). The wiki's Contributing page says `npm test` is the CI gate (#552). + + **A final review before shipping.** A last adversarially verified review of the whole release fixed these in the tile view: an image dropped on a tile uploads to that tile's session instead of navigating the browser away; a Claude session started into the grid keeps its welcome banner and transcript; a tile refresh never blanks the screen while it waits; a remote close or a reconnect no longer moves your keyboard into another session's tile; dictation and the phone keyboard's Path and Clear keys reach the focused tile or split pane; each tile caps its live-output backlog and recovers dropped output with one bounded refresh; Redraw on a tile forces the resize it reports; popping out the last tile leaves no frozen view; and "Open group as tiles" no longer pulls an open split into the grid. Also from that review: the By case layout stays one scrolling row on 600 to 767px tablets, the needs-you pulse animates opacity only, a codex session on the ultra effort shows its model, codex's launch defaults are CLI registry data instead of an id check, `npm run build` checks its dependencies before it deletes anything, and the release's new strings (case picker rows, Git status settings, toasts, tile and spreadsheet texts, the Redraw toasts) have zh-CN translations. + + **Fixes applied while landing.** Tiles and the split's Pane B got the same two fixes the main terminal got from contributors: opencode's wheel paging and click reports (#555), and the Android keyboard handling that stops autocorrect duplicating a line (#541). Android: a word composed right before Enter in the same keyboard transaction was sent twice; it now arrives once (#541). Codex: App Settings refuses a Default Codex model the server would reject instead of failing the whole save behind a "Settings saved" toast, and the two Codex rows stack under their labels on phones (#546). Run dropdown: its height follows the on-screen keyboard and both iPhone safe areas, a long custom-endpoint label no longer adds a horizontal scrollbar, and the open menu sits above the keyboard accessory bar (#542). Git status: a lone repository git cannot read is no longer shown as clean and empty, a repeated `timeout` query parameter no longer answers 500, and the timeout field accepts any whole number of seconds (#543). Spreadsheets: cells clipped to nothing at the edge of a very large sheet no longer grow its scroll area, and the preview's fixed texts have zh-CN translations (#502). Pop-out windows: the bridge refuses to pop out without a window channel (the tab could never re-dock), the tab menu follows the host-aware default, and Close window works inside a host window (#432). Every deprecated `/api/screenshots` route now logs its warning, pinned per route (#551). Across the new tab layouts and header styles: lineage lines run between the state labels and the tabs and never through a case box, an open Tiles or Split button stays highlighted in the boxed header styles, a phone keeps the active chip in view when it changes state band, and the Tab Layout and Header Stats Style settings are translated. + +## 1.35.0 + +### Minor Changes + +- 6f88e40: ### Thanks + - @opticon454 for four PRs in one night: the Git status indicator with its panel of uncommitted and unpushed work and per-file diffs (#537), `codeman doctor` in Settings (#536), creating a case in a custom folder (#535) and the detailed-rail rename clamp fix (#534). Both review rounds came back within half an hour with every item addressed. + - @aakhter for making the grouped vertical rail editable end to end (#525), the inline rename write queue and the long-prefix editor layout (#526), and bounded path probes so an unreachable network mount can no longer freeze the server (#516). Every round came back with tests that replay the exact sequences from the review, and the review nits were already fixed before landing. + + **Edit tab groups in the vertical rail (#525).** The grouped rail is now editable from the browser: create, rename, reorder and delete groups, and move tabs between groups or back to Ungrouped, from the row menu, a group menu (Shift+F10, ContextMenu, right-click, or the header glyph, which stays visible on touch screens; F2 renames inline) or a mouse/pen drag. A flat rail offers "Move to new group" to make the first one. Every edit is a named operation saved through the existing `PUT /api/tab-layout`, one write in flight at a time; a version conflict replays the pending operations onto the server's layout and retries, so a concurrent edit from another device survives, and unsaved edits survive a reload. A drag released outside the rail leaves nothing behind, and committing a group rename by clicking elsewhere leaves focus where you clicked. No server changes. + + **Inline rename that keeps up (#526).** Inline renames go through a per-session queue: one PUT at a time in the order they were made, the confirmed name applied even if the editor was reopened or cancelled meanwhile, an editor reopened over a rename in flight starts from that name, and a failed rename always shows its toast. A long `w-` prefix no longer pushes the editor out of its row in the rail or the sidebar, and the detailed rail no longer keeps its 3-line clamp around the editor (#534 found and fixed the same clamp independently). + + **Git status in the bottom bar (#537).** Turn on Settings → Header & Panels → Bottom bar → "Git status" (per-device, off by default) and a small indicator shows the active session's repository at a glance (`● 3` uncommitted files, `↑ 2` commits not pushed, `✓` when everything is committed and pushed). Click it for a draggable window listing the uncommitted files (staged, not staged, untracked, conflicts; click one for its diff; grouped under collapsible folders) and the unpushed commits. A folder holding several projects gets a section per repository found up to two levels down, and an unrelated repository above the workspace (a dotfiles repo in your home folder) is ignored. Read-only and offline: Codeman never fetches or changes the repository, and git never runs on a repository a Docker case can write to. Not shown for Docker or remote sessions. New `GET /api/sessions/:id/git-status` and `GET /api/sessions/:id/git-diff`. + + **Diagnostics in Settings (#536).** Settings → System → Diagnostics runs `codeman doctor` on the server (`GET /api/doctor`) and lists which agent CLIs, tmux, Node and the optional office tools are installed, with versions, paths and install hints. The probe runs in a child process, so a slow `--version` cannot freeze the server, and both the panel and the terminal `codeman doctor` now also look in each CLI's usual install directories, so a CLI installed outside a service's minimal PATH is found. Admin only in multi-user mode. + + **Create a case in a custom folder (#535).** Add Case → Create New has a "Create in a custom folder" option with a Browse button: the case folder is created inside the parent you pick, scaffolded like any other case and listed alongside the rest (deleting it unlinks, never removes files). `POST /api/cases` accepts an optional `path` for the same thing. The folder must not exist or must be empty, and system folders, the home folder, credential folders and the cases directory itself are refused. Nothing is left behind if creation fails part-way. Admin only in multi-user mode. + + **An unreachable mount no longer freezes the server (#516).** A linked case can live on a network mount, and when that mount goes away a hard mount makes `stat()` wait indefinitely; the synchronous probes in the case routes, the workspace hook and statusLine helpers and session creation used to freeze the whole server with it. Those probes now go through one bounded, tri-state probe (present, absent, or unknown when nothing answers in time): a stalled path costs one threadpool worker, paths on the same mount answer "unknown" without a new stat, and unrelated paths keep working. "Unknown" is never treated as "absent": `GET /api/cases/:name` reports an unreachable linked case with `unreachable: true` instead of NOT_FOUND, Run creates a case only on a real NOT_FOUND, and session creation answers OPERATION_FAILED for a folder that did not answer and never scaffolds over it. Tunable with `CODEMAN_PATH_PROBE_TIMEOUT_MS` (default 1500) and `CODEMAN_PATH_PROBE_MAX_STALLED`. + + **Fixes applied while landing.** Tab groups: an edit made while an earlier save was still in flight, and made inapplicable by that save's conflict (its group deleted on another device), is no longer dropped silently but reported like every other dropped edit, and the menus stop offering a new group once the 32-group limit is reached instead of failing with an untranslated error. Rail: a static CI check now pins that no rail or sidebar clamp out-ranks the rename unclamp (the browser test that caught it is outside the gate). Git status: a cached repository list is re-checked against the current Docker workspaces on every poll, a diff larger than 8 MB is cut short instead of failing, a diff click refreshes only that repository, a dotfiles repository above the workspace is identified with one `rev-parse` before any full status (a failing status there no longer hides the repositories below), and "Upstream is gone" now reads "Upstream not on remote", which is also true for a branch that was never pushed. Doctor: candidates are judged like the Run menu's own resolver (a wrong binary on the PATH no longer hides the right one in an install directory, a non-executable file or a relative directory reads as missing), probes are killed with SIGKILL on timeout, a missing optional tool shows ○ instead of ✗, and the contract test no longer runs the machine's installed CLIs. Custom-folder cases: the symlink-resolved target is judged against resolved roots too (home reached through a link, macOS `/private/etc`), a target inside the cases directory is refused, the success toast names the folder the server created, the new labels have zh-CN translations, and the route test can no longer delete a real `~/projects` or the live linked-cases registry when run outside `npm test`. + +## 1.34.0 + +### Minor Changes + +- 6aecc3b: ### Thanks + - @opticon454 for four PRs in one batch: webhook notifications (#523), MCP server sync (#521), the Shift+Enter keypress fix (#520) and the newline chord plus Key tester (#522). Every review item was answered in one round, and the merge-order map across all four made landing them together easy. + - @aakhter for the grouped vertical rail (#517) and its ARIA tree and full-row activation (#519), which give the owner tab-layout API its first frontend, and for the iOS IME composition preview (#499), carried through three careful review rounds including the overlay rework in the zerolag package. + - @irisitymichaelgrundberg for per-session Claude models on `POST /api/sessions` (#514) and Codex reasoning effort per session (#515), both kept registry-driven with no CLI id branching. + - @timkjr for keeping Pane B painting during a history pull and its "disconnected" marker last in every interleaving (#524), with an old-versus-new table measured in real Chrome. + + **Webhook notifications (#523).** Settings → Notifications → Webhook posts the same events as Web Push (permission prompts, questions, errors, idle) to ntfy, Slack, Discord or any JSON URL, so a headless server can reach a phone with no browser open. Off by default. The URL is a bearer secret: it lives in its own 0600 file (`~/.codeman/webhook.json`), is never returned by the API, and the routes (`GET`/`PUT /api/webhook`, `POST /api/webhook/test`) are admin only in multi-user mode. Delivery goes through the web-tab egress guard (link-local and cloud-metadata targets refused), does not follow redirects, times out after 5 s, dedupes repeats, and neutralises `@everyone`/Slack control characters in agent-supplied text. + + **MCP server sync (#521).** Opt-in (`mcpSyncEnabled`, synced, off by default; `GET`/`POST /api/mcp-sync` answer 403 until it is on). Settings → Agents & CLIs → MCP servers previews or copies each installed, enabled CLI's MCP servers into the others' own config files (Claude, Gemini, Codex, OpenCode, Antigravity). It only adds missing servers, never edits or removes one, skips servers you switched off, keeps a `.codeman-bak` of every file it changes, re-parses the result before writing, writes through symlinked dotfiles, leaves files that receive env values or headers readable by you only, and reports same-name conflicts instead of overwriting. CLIs with no known MCP config (Pi, Grok, OMP, DeepSeek) are listed as unsupported. Adds the `smol-toml` dependency to read Codex's `config.toml` safely. + + **Claude advisor tool.** Claude Code's experimental advisor (a stronger model the session's main model consults at decision points) can now be set per session: an `advisorModel` field on `POST /api/sessions`, `POST /api/quick-start` and `POST /api/ralph-loop/start` (`fable`, `opus`, `sonnet`, or a full id in those families), and a synced App Settings default under Models → Advisor. It rides the launch's one `--settings` JSON rather than the `--advisor` flag, because the flag exits at launch on any pairing the CLI refuses and would leave a dead pane on every respawn. It is persisted, so respawns and both restore paths keep it, and `/advisor` still switches it in-session. Agents using the codeman skill can give their claude workers one with `CODEMAN_WORKER_ADVISOR=opus`. + + **Per-session Claude model (#514) and Codex reasoning effort (#515).** `POST /api/sessions` takes an optional `model` that launches that one Claude session with `claude --model ` and writes nothing to disk (`modelOverride` still writes the case default). It is persisted, so both recovery paths relaunch on it. `codexConfig.reasoningEffort` starts a codex session at a chosen effort (`--config model_reasoning_effort=`), and it survives respawn and resume. + + **Grouped vertical rail (#517, #519).** When the owner has tab groups (`/api/tab-layout`), the vertical rail draws them as collapsible sections, with collapse remembered per device, the active row always visible, and lineage arcs anchored to a collapsed group's header. The grouped rail is an ARIA tree with one tab stop and the standard arrow-key model. With no groups, the rail is unchanged byte for byte. Editing groups from the browser comes in a follow-up. + + **iOS IME composition preview (#499).** On iOS Safari, the text an IME is composing (Japanese, Chinese, Korean, and the predictive composition on English keyboards) is now drawn in the terminal before it commits, inside the local-echo overlay when local echo is on. Inert on every other platform. The `xterm-zerolag-input` package gains `setComposition()`. + + **Key tester and newline chord (#522).** Settings → Terminal & Input has a Key tester that shows the keydown/keypress/keyup events the browser reports, to diagnose a device where a shortcut behaves differently. Keys pressed in it never trigger app shortcuts. Shift+Enter's newline chord is now CLI registry data (`capabilities.newline`, line feed by default); no stock CLI changes. + + **Fixes.** Shift+Enter no longer submits the prompt after inserting the newline: the key handler swallowed only `keydown`, so xterm's `keypress` still sent a bare `\r` (#520). Claude sessions created at the same moment (`spawn_workers`, a multi-tab Run) no longer fall out of tmux onto the direct-PTY fallback: the statusLine exporter's temp file name collided within one millisecond (#531). Pane B of the split view keeps painting during a history pull, and its "disconnected" marker stays the last line however a close, a pull and a refresh interleave (#524). + + **Fixes applied while landing.** Webhooks: the App Settings Save button now saves webhook edits too (a refused URL keeps the dialog open with a warning), Send test saves pending edits first, and a Remove URL button clears a saved URL. MCP sync: a config file that fails to parse is reported by line and column only, never by quoting its content, which can hold API keys; the sync follows `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `XDG_CONFIG_HOME` and `GEMINI_CLI_HOME` from the server's environment and skips a target it cannot place instead of writing a file the CLI never reads; Preview before saving says to save first; and the MCP group is hidden from non-admins in multi-user mode. Grouped rail: a collapsed group's header shows the red or yellow ring of a hidden row that needs you; layout reads rebuild the rail only when something it draws changed, and failed reads back off (5, 10, 20, 40 s) instead of retrying every 5 s forever; a corrupted collapse preference resets instead of disabling collapse; Ctrl+Shift+{ / } only moves a tab within its own group; tapping a group header or row no longer dismisses the phone keyboard; keys pressed on a row's own buttons no longer move tree focus; and screen-reader positions stay correct after a re-sort. Sessions: `model` on `POST /api/sessions` refuses a value starting with a dash, and `model` or `advisorModel` together with `attachRemoteSession` is now a 400 instead of being ignored; non-Claude sessions no longer report or persist Claude's default model. Split view: a refresh queued behind a history pull no longer leaves a second, stale "disconnected" marker above its replay. iOS IME: a composition on an empty prompt now follows the prompt when output or a resize moves it, and the `xterm-zerolag-input` README documents `setComposition()`. The Shift+Enter and Key tester browser tests now drive the shipped handlers instead of copies. + +## 1.33.3 + +### Patch Changes + +- f776ad8: ### Thanks + - @JDProfresh for rendering Markdown in the File Viewer (#503) through the chat's existing markdown pipeline and sanitizer rather than a second one, plus the Lines and Wrap toggles and the sanitizer fix that stops a document from clobbering `document.app`. + - @timkjr for bringing the Shell scroll-to-top history pull to the split view's second pane (#506), following #494's rules down to the back-off, with tests that fail on the code before each fix. + - @irisitymichaelgrundberg for the `#session=` dashboard link (#507), so a page that keeps one Codeman window open can switch it between sessions without reloading it. + - @dignfei for handing focus back when the Command Palette or the Session Manager closes (#509), and for the six-overlay measurement that showed exactly which two were broken. + + **Markdown files render in the File Viewer (#503).** Opening a `.md` or `.markdown` file now shows it as a document: headings, tables, code blocks with the same copy buttons as the chat, images relative to the file, and links to other documents that open inside the viewer. An `MD` pill switches back to the source, and Edit works from either view. Plain text gets a `Lines` gutter (never part of a copy) and a `Wrap` toggle, all three remembered per device. `.avif` images preview inline, and printed `.avif`/`.ico` paths open the viewer instead of the tail view. An in-workspace file path clicked in the terminal still opens the live tail view. + + **Link a dashboard window to a session (#507).** An outside page, such as a task board, that keeps one Codeman window open can now switch it to a session by pointing it at `/#session=`. Only the fragment changes, so the page stays loaded and the switch is an ordinary tab selection. A link to a session the dashboard does not list yet waits up to 30 seconds for it to appear and then shows "Session not found"; picking another tab, going Home or opening a web tab cancels the wait. Following a link does not count as looking at the session, so its idle alert stays armed. The fragment is documented in `docs/extending-codeman.md` and is now a stable surface under `docs/versioning-policy.md`. + + **Escape no longer strands the keyboard (#509).** Closing the Command Palette or the Session Manager now hands focus back to whatever held it before they opened, usually the terminal, so you can keep typing without clicking first. An Escape pressed while neither is open changes nothing. + + **Split view: a Shell Pane B scrolls back into tmux history (#506).** Wheel up at the top of a Shell session in the split view's second pane now pulls the most recent 1 MiB of its tmux history and keeps your place, the same as the primary pane since 1.33.2. + + **Fixes applied while landing.** Markdown opened from an attachment card no longer resolves relative images and links against the workspace root, where they could show a missing image or open a different file of the same name; they render as their alt text and link text instead. Rendered files no longer turn every source line break into a hard break the way chat messages do, so a README wrapped at 80 columns reads as flowing paragraphs. Absolute-path links inside a rendered document open in that document's session. A disconnected Pane B keeps its "disconnected" marker as the last line even when the socket closes in the middle of a history pull. Closing the Session Manager through a row's "Switch to session" or "Open folder" no longer pulls focus back from the terminal to the header button. + ## 1.33.2 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index 539f9b32..4f350fdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,14 +6,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co > > **This file is in `.prettierignore` on purpose.** Prettier's markdown printer escapes underscores inside the glob-heavy paths used throughout (`agent-*.jsonl` became `agent-\_.jsonl`, collapsing backtick spans and corrupting a whole paragraph). Do not remove the ignore entry, and do not run `prettier --write` on it. > -> **Repo root is kept short on purpose** (the README sits below the file listing on GitHub). Config lives in `config/` (`eslint.config.js`, `knip.json`, the vitest configs), Prettier's config is the `"prettier"` key in `package.json`, and `SECURITY.md` is under `.github/`. Root-only files are the ones tools genuinely require there: `CLAUDE.md` + `AGENTS.md` (loaded from the root by Claude Code / Codex), `CHANGELOG.md` (changesets writes it next to `package.json`), `tsconfig.json`, `.editorconfig`, `.nvmrc`/`.npmrc`, `.prettierignore` (resolved relative to cwd), `LICENSE` (GitHub detection), `.dockerignore` (the build context is the repo root, so Docker resolves it there and nowhere else) and `install.sh` (its raw URL is the published install one-liner). `.claude-plugin/marketplace.json` is root-only for the same reason: `/plugin marketplace add Ark0N/Codeman` reads it from the repo root and nowhere else, which makes the repo its own plugin marketplace. The one plugin it lists is `plugins/codeman/` (manifest + README + a MIRROR of `skills/codeman/`), and ⚠️ the plugin is a small separate directory on purpose: `claude plugin install` copies the plugin root into its cache, and a plugin root that carries a `package.json` gets an **npm install** at install time (measured with the repo root as plugin root: 832 MB, 511 packages and this repo's postinstall on every installer's machine), while a symlink to `skills/codeman` would dangle in the copy. `skills/codeman/` stays the single source; `scripts/sync-plugin.mjs` mirrors it and writes `package.json`'s version into both manifests inside `version-packages`, and `test/plugin-manifest.test.ts` pins the byte-identity, the versions, the absence of a `package.json` in the plugin root and that no other component (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`) rides along. `npm run check:plugin` runs that drift check plus both strict validations with EXPLICIT paths (`plugins/codeman`, `.claude-plugin/marketplace.json`): a bare `.` argument copied out of prose reads as a full stop and gets dropped, which surfaces as `missing required argument 'path'`. Needs the `claude` CLI, so it is a local check, not a CI step. Don't relocate those. +> **Repo root is kept short on purpose** (the README sits below the file listing on GitHub). Config lives in `config/` (`eslint.config.js`, `knip.json`, the vitest configs), Prettier's config is the `"prettier"` key in `package.json`, and `SECURITY.md` is under `.github/`. Root-only files are the ones tools genuinely require there: `CLAUDE.md` + `AGENTS.md` (loaded from the root by Claude Code / Codex), `CHANGELOG.md` (changesets writes it next to `package.json`), `tsconfig.json`, `.editorconfig`, `.nvmrc`/`.npmrc`, `.prettierignore` (resolved relative to cwd), `vitest.config.ts` (a one-line re-export of `config/vitest.ci.config.ts`: vitest finds its config by walking up from cwd, so without it a bare `npx vitest` ran with no setup file and wrote into the real `~/.codeman`, #640), `LICENSE` (GitHub detection), `.dockerignore` (the build context is the repo root, so Docker resolves it there and nowhere else) and `install.sh` (its raw URL is the published install one-liner). `.claude-plugin/marketplace.json` is root-only for the same reason: `/plugin marketplace add Ark0N/Codeman` reads it from the repo root and nowhere else, which makes the repo its own plugin marketplace. The one plugin it lists is `plugins/codeman/` (manifest + README + a MIRROR of `skills/codeman/`), and ⚠️ the plugin is a small separate directory on purpose: `claude plugin install` copies the plugin root into its cache, and a plugin root that carries a `package.json` gets an **npm install** at install time (measured with the repo root as plugin root: 832 MB, 511 packages and this repo's postinstall on every installer's machine), while a symlink to `skills/codeman` would dangle in the copy. `skills/codeman/` stays the single source; `scripts/sync-plugin.mjs` mirrors it and writes `package.json`'s version into both manifests inside `version-packages`, and `test/plugin-manifest.test.ts` pins the byte-identity, the versions, the absence of a `package.json` in the plugin root and that no other component (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`) rides along. `npm run check:plugin` runs that drift check plus both strict validations with EXPLICIT paths (`plugins/codeman`, `.claude-plugin/marketplace.json`): a bare `.` argument copied out of prose reads as a full stop and gets dropped, which surfaces as `missing required argument 'path'`. Needs the `claude` CLI, so it is a local check, not a CI step. Don't relocate those. ## Quick Reference | Task | Command | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dev server | `npm run dev` (or `npx tsx src/index.ts web`) | -| Type check | `npm run typecheck` (= `tsc --noEmit`) | +| Type check | `npm run typecheck` (= `tsc --noEmit` + `tsc -p config/tsconfig.scripts.json`) | | Lint | `npm run lint` (fix: `npm run lint:fix`) | | Format | `npm run format` (check: `npm run format:check`) | | Tests | `npm test` (the CI gate — safe to run bare) · one file: `npm test -- test/.test.ts` · see Testing for the excluded suites | @@ -44,47 +44,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 2. **Frontend changes**: Use Playwright to load the page and assert the UI renders correctly. Use `waitUntil: 'domcontentloaded'` (not `networkidle` — SSE keeps the connection open). Wait 3-4s for polling/async data to populate, then check element visibility, text content, and CSS values 3. **Only after verification passes**, proceed with COM -The production server caches static files for 1 year, `immutable` (`maxAge: '1y'` in `server.ts`). To avoid stale frontend after a deploy, `renderIndexHtml` runs `cacheBustAssets(html)` — it appends `?v=` to **every same-origin `.js`/`.css`** reference (mtime memoized ~1s so a burst of renders is cheap; external/already-versioned/missing refs untouched). Because `index.html` is served `no-cache`, a **normal reload now picks up edited modules/styles — no hard refresh needed** (the gesture bundle is injected separately with its own `?v=`). If you add an asset referenced by an _absolute_ URL or from JS rather than a `` via the exported `escapeScriptJson()` since `label` is a user-`clis.json`-settable string unlike the neighbouring booleans-only `__codemanCliAvailable`), never a hardcoded per-CLI id list in the frontend — the same "no branching on CLI id outside stock.ts" discipline the registry itself enforces. One entry per (capable, INSTALLED CLI, saved endpoint) pair, e.g. "Claude Code (llama.cpp)", filtered through `isCliAvailable()` like the stock entries. Clicking one calls `selectCustomModelEntry(mode, endpointId)` (`session-ui.js`), which re-fetches the endpoint (never trusts anything cached from the dropdown's render — the 5-minute sweep below or a settings edit may have changed it since) and decides the model: exactly one discovered model launches straight away, two or more open `#customModelPickModal` to ask, with `defaultModelId` marked but never auto-chosen (asking exists so ONE launch can deliberately differ from the saved default). -⚠️ **The picker promotes exactly one row to the top**: whichever model llama-swap reports `ready` right now (tagged "Currently loaded", queried via `GET /api/model-endpoints/:id/running-status`, client-side bounded to ~800ms via `Promise.race` so a sleeping/firewalled endpoint cannot leave the modal invisible for the route's own 5s server-side timeout) beats a merely remembered choice, and — only when nothing is currently loaded — the model actually launched last for this exact (harness, endpoint) pair (tagged "Last used", read from the per-device `codeman:customModelLastUsed::` localStorage key). Neither tag reorders past the top, and the "Default" pill is a SEPARATE span rather than a third value of the same slot, so a promoted row that is also the endpoint's `defaultModelId` shows both (on a single-purpose GPU box that is the common case; an exclusive slot silently dropped the Default marking for exactly that row). "Last used" is written by `_runCustomModelEntryViaRestart` (claude) and `_quickStartWithCustomModelConfirm` (every one-shot launch; `runCustomModelEntry` itself only dispatches between the two) only once the model is actually applied — never on the mere click — because a context-window-warning decline means this exact model cannot work with this CLI at all, and promoting a model that cannot launch would be actively wrong, not just premature. +⚠️ **The picker promotes exactly one row to the top**: whichever model llama-swap reports `ready` right now (tagged "Currently loaded", queried via `GET /api/model-endpoints/:id/running-status`, client-side bounded to ~800ms via `Promise.race` so a sleeping/firewalled endpoint cannot leave the modal invisible for the route's own 5s server-side timeout) beats a live session's pin (tagged "In use by a session": `_getCustomModelInUseBySession` walks `this.sessions` for one of your own sessions whose `customModel.endpointId` is that host, newest `createdAt` wins), which applies only when nothing is currently loaded and beats a merely remembered choice, the model actually launched last for this exact (harness, endpoint) pair (tagged "Last used", read from the per-device `codeman:customModelLastUsed::` localStorage key). ⚠️ The pin reads only `customModel`, never `displayModel` (screen-scraped display text: a terminal that merely printed a matching string is not evidence), and never counts a session with `status: 'stopped'`, a null `pid` (its PTY exited) or a recorded `paneExit`, the same live-pane test keep-warm applies. ⚠️ When nothing was promoted and the probe answered `isLlamaSwap: false` (the shape `getLlamaSwapStatus` gives for EVERY failure it catches, a down or refused endpoint included, so the line claims only that nothing was reported), the static `#customModelPickUnknown` line says so out loud; its `[hidden]` rule must keep `display: none !important` scoped to `#customModelPickModal`, because `.form-hint`'s `display: block` outranks the UA `[hidden]` rule. Neither tag reorders past the top, and the "Default" pill is a SEPARATE span rather than a third value of the same slot, so a promoted row that is also the endpoint's `defaultModelId` shows both (on a single-purpose GPU box that is the common case; an exclusive slot silently dropped the Default marking for exactly that row). "Last used" is written by `_runCustomModelEntryViaRestart` (claude) and `_quickStartWithCustomModelConfirm` (every one-shot launch; `runCustomModelEntry` itself only dispatches between the two) only once the model is actually applied, never on the mere click, because a context-window-warning decline means this exact model cannot work with this CLI at all, and promoting a model that cannot launch would be actively wrong, not just premature. Either way the actual launch (`runCustomModelEntry`) routes through `run()` itself via a temporary `_runMode` swap — never `setRunMode()`, which would persist it as the user's new default — rather than a parallel dispatch table, which is what gives a custom-model launch the same `_runInFlight` lock every other Run click gets and means a CLI whose injection recipe lands later needs no update here. It then GETs `/api/sessions/:id/wait?until=idle&timeout=20000` on that session BEFORE applying — measured live, a freshly launched CLI reports itself `busy` for its own startup (boot spinner, workspace-trust check) well before the apply call would otherwise reach it, and the apply route's `isBusy()` guard correctly can't tell that apart from a real turn in progress, so every fresh launch failed with `SESSION_BUSY` until this wait was added. A timeout there is a normal 200 per the wait endpoint's own contract, never an error, so a session still busy after 20s just reaches the apply call anyway and gets that route's own honest error. It then calls `POST /api/sessions/:id/custom-model` on the session `run()` produced, guarded by snapshotting `activeSessionId` before the call and requiring it to have actually changed after — every `run*()` handles its own failure internally and returns normally rather than throwing, so a declined/failed launch must not silently re-point and restart whatever session was already open. @@ -562,9 +662,9 @@ Either way the actual launch (`runCustomModelEntry`) routes through `run()` itse ⚠️ **That launch-time check cannot catch a swap caused by a DIFFERENT session's LATER, ordinary use** — confirmed live: a second Codex session picking a different model launched with no warning at all (nothing conflicted at that exact instant), yet it silently evicted the first session's model regardless, since llama-swap has no push notification of its own. `detectCustomModelSwapDisplacements()` (`custom-model-routes.ts`) is a separate periodic sweep (`server.ts`, `CUSTOM_MODEL_SWAP_CHECK_INTERVAL_MS` = 20s) that compares each live custom-model session's own `modelId` against what `/running` actually reports loaded, broadcasting a `custom-model:swapped-out` SSE event — shown as a global toast, never tied to the displaced session's own tab, since the whole point is telling the user before they type into it — the first time a mismatch appears, via a caller-owned de-dupe `Set` cleared once that session's own model is loaded and ready again so a later, genuinely new displacement notifies again rather than staying silently un-notified forever after the first one. -⚠️ **Context length is read from the REAL launch command, never `/props`** — `/props?model=`'s `default_generation_settings.n_ctx` was confirmed live to report a `--fit-ctx`-launched backend's theoretical/trained maximum rather than the real runtime-configured size (a measured 154112-vs-16384 discrepancy, caught only because the unfixed value still overflowed), so discovery parses the actual configured size straight out of `/running`'s own `cmd` field instead (`parseCtxFromCmd`: `--fit-ctx ` first, then plain llama.cpp `-c`/`--ctx-size`), falling back to `/props` only when `cmd` states no recognizable flag at all. +⚠️ **Context length is read from the REAL launch command, never `/props`** — `/props?model=`'s `default_generation_settings.n_ctx` was confirmed live to report a `--fit-ctx`-launched backend's theoretical/trained maximum rather than the real runtime-configured size (a measured 154112-vs-16384 discrepancy, caught only because the unfixed value still overflowed), so discovery parses the actual configured size straight out of `/running`'s own `cmd` field instead (`parseCtxFromCmd`: `--fit-ctx ` first, then plain llama.cpp `-c`/`--ctx-size`), falling back to `/props` only when `cmd` states no recognizable flag at all. The same rule covers the slot check: `/props` supplies `total_slots` only when `cmd` has no `--parallel`, and it NEVER widens the pool the slots divide (an earlier cut let a larger `/props` `n_ctx` become the pool and skip the warning; the 154112-vs-16384 measurement is exactly why that is wrong). `/props` refreshes llama-swap's last-use time for the model, so `discoverModels` probes it once per launch command (`propsByCmd`, in memory, keyed by host and model id and compared with `/running`'s `cmd`); probing on every 5-minute sweep would keep a model with a `ttl` loaded forever. With an explicit `--parallel N` llama.cpp reports `n_ctx` per slot, so a `/props` context beside it is stored with a slot count of 1: it is not divided a second time, and the 1 overwrites a count an earlier discovery round stored (`applyDiscoveredModels` merges onto what was learned before, so recording no count would keep the stale one). -⚠️ **Claude alone gets a context-window FLOOR check, on top of the ceiling `contextLengthVar` already fixes** — `exceedsSafeContextFloor()` (gated on the registry declaring `contextLengthVar`, so a no-op for every other CLI by construction) compares a model's discovered context against `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` (40000): confirmed live, twice, that Claude Code's own system prompt and tool schemas cost roughly 36.4K tokens on the very first message, before any conversation history exists to compact, so a smaller real context fails outright regardless of what `CLAUDE_CODE_MAX_CONTEXT_TOKENS` says (that var only controls when HISTORY gets compacted, and there is none yet on message one). Below the floor, the apply returns `{requiresContextWarning, modelId, contextLength, minSafeContextTokens}` instead of launching, shown as an in-app dialog naming the actual fix: give the model an explicit larger `-c`/`--ctx-size` in llama-swap's config instead of relying on `--fit-ctx` auto-fit, which optimizes for the biggest MODEL that fits rather than the biggest CONTEXT. +⚠️ **Claude alone gets a context-window FLOOR check, on top of the ceiling `contextLengthVar` already fixes** — `exceedsSafeContextFloor()` (gated on the registry declaring `contextLengthVar`, so a no-op for every other CLI by construction) compares a model's discovered context against `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` (40000): confirmed live, twice, that Claude Code's own system prompt and tool schemas cost roughly 36.4K tokens on the very first message, before any conversation history exists to compact, so a smaller real context fails outright regardless of what `CLAUDE_CODE_MAX_CONTEXT_TOKENS` says (that var only controls when HISTORY gets compacted, and there is none yet on message one). Below the floor, or when `contextLength / slotCount` (`perSlotContext()`, from `modelSlotCounts`; an unknown count is one slot) is below it, the apply returns `{requiresContextWarning, modelId, contextLength, minSafeContextTokens}` (plus `slotCount` when it is above 1) instead of launching, shown as an in-app dialog naming the actual fix: give the model an explicit larger `-c`/`--ctx-size` in llama-swap's config instead of relying on `--fit-ctx` auto-fit, which optimizes for the biggest MODEL that fits rather than the biggest CONTEXT. ⚠️ **A fresh, isolated `CLAUDE_CONFIG_DIR` looks like a brand-new Claude Code profile and replays its ENTIRE first-run sequence on every launch** — the theme picker, the security-notes screen, the per-project "trust this folder?" dialog, and (running with a bypass-permissions flag) a one-time warning about it, confirmed live, none of which a real, already-onboarded profile shows again. `skipFirstRunPrompts` (claude's entry only, requires `apiKeyTrustFile` since it reuses the same file) pre-seeds that same "already been through this" state: `hasCompletedOnboarding` and this session's own `projects[workingDir].hasTrustDialogAccepted` merge into the same `.claude.json` the API-key trust file already writes to, and `skipDangerousModePermissionPrompt` merges into `settings.json` (a different file, same corrupt-tolerant merge). @@ -572,7 +672,7 @@ Either way the actual launch (`runCustomModelEntry`) routes through `run()` itse ### Approvals Inbox -**Approvals Inbox** (cross-session queue of prompts waiting on a human; `approvalsInboxEnabled`, SYNCED, default OFF: every surface is opt-in; only the store and answer endpoints run regardless, so flipping it ON shows anything already pending): `web/approval-inbox.ts` is a `sessionWaits`-style singleton fed by `/api/hook-event`, holding at most ONE item per session (a new prompt supersedes), claude-mode only, in-memory. Cards are answered via `POST /api/approvals/:id/answer`, which sends a digit / Esc / idle-prompt text through `writeViaMux` (menu answers never carry `\r`). +**Approvals Inbox** (cross-session queue of prompts waiting on a human; `approvalsInboxEnabled`, SYNCED, default OFF: every surface is opt-in; only the store and answer endpoints run regardless, so flipping it ON shows anything already pending): `web/approval-inbox.ts` is a `sessionWaits`-style singleton fed by `/api/hook-event`, holding at most ONE item per session (a new prompt supersedes), in-memory; answerable items are claude-mode only, and the DeepSeek status bridge and codex's screen-read dialogs (`web/screen-blocked.ts`) open ALERT-ONLY items (`answerInTerminal`: the answer route refuses them BEFORE its hooks gate, push carries no `approvalId`, the card and overview strip say "answer in the terminal"). Cards are answered via `POST /api/approvals/:id/answer`, which sends a digit / Esc / idle-prompt text through `writeViaMux` (menu answers never carry `\r`). ⚠️ `option` digits are accepted ONLY when they match options parsed from the captured pane frame, and the answer path RE-CAPTURES the pane first (a dialog that no longer parses on screen means the keystroke would land in the composer, so refuse with 409). @@ -580,7 +680,7 @@ Either way the actual launch (`runCustomModelEntry`) routes through `run()` itse ⚠️ **Viewing a session ACKNOWLEDGES its idle item, it does not resolve it** (`POST /api/approvals/session/:sessionId/viewed` → `acknowledgedAt` → `approval:updated`): the item stays pending (still answerable, still Read My Mind context) and only stops arming the yellow tab alert. That flag is what makes the clear durable, since the view-clears-idle rule used to live in one browser's memory and `seedApprovals()` re-armed the alert on the next reload while other devices never heard about it at all; the local half is `markIdleAlertSeen()` (app.js), called from BOTH `selectSession` paths, including the already-active early return, where a click could otherwise never clear the alert. -⚠️ **Only a HUMAN opening a session acknowledges**: `selectSession(id, { auto: true })` marks the three selections the APP makes (boot restore, a solo window opening its target, the fallback after the active session is closed) and skips the acknowledgement, so a page load cannot silently spend an alert the user never saw. The flag defaults to user-initiated, so an untagged call site fails toward acknowledging rather than toward an alert nothing can clear; `test/session-select-ack-gate.test.ts` pins both the gate and the tagged call sites. Idle-only by construction (`acknowledge()` defaults to `['idle']`): looking at a permission/question dialog does not answer it. +⚠️ **Only a HUMAN opening a session acknowledges**: `selectSession(id, { auto: true })` marks the four selections the APP makes (boot restore, a solo window opening its target, a `#session=` link from another page, the fallback after the active session is closed) and skips the acknowledgement, so a page load cannot silently spend an alert the user never saw. The flag defaults to user-initiated, so an untagged call site fails toward acknowledging rather than toward an alert nothing can clear; `test/session-select-ack-gate.test.ts` pins both the gate and the tagged call sites. Idle-only by construction (`acknowledge()` defaults to `['idle']`): looking at a permission/question dialog does not answer it. ⚠️ Same rule on the input path: `_ackDelivery` (app.js) spends the IDLE alert only, via that same `markIdleAlertSeen()`. It used to `clearPendingHooks(sessionId)` with no kind, so one keystroke wiped a RED alert on that device while the dialog was still up, the other devices stayed red, and a reload re-seeded it. @@ -602,6 +702,20 @@ Either way the actual launch (`runCustomModelEntry`) routes through `run()` itse Frontend `readmymind-ui.js`: header 🧠 marker-hidden (`btn-readmymind--hidden`) until the setting is ON; phones hide it in mobile.css and get a keyboard-accessory 🧠 key instead (ships in BOTH bar templates, revealed by the `rmm-enabled` class on the BAR element — setMode() rebuilds button innerHTML, so per-key state would be wiped; synced at init + every `applyHeaderVisibilitySettings()`). Alternate suggestions render as tappable rows that swap into the editable field without losing edits; Rethink rejects the whole shown set and carries the optional steer note (`#readMyMindSteer`, sent as `steer`, shown in ready + empty-result phases, cleared on each open). Suggestions render via value/`textContent` ONLY and Send/Insert go through `POST /input` (server-side, so the sendEnterKey/local-echo trap does not apply) — nothing auto-sends, ever. User guide: `docs/readmymind.md`. +### Agent inbox + +**Agent inbox** (discussion #558): a per-session mailbox one agent posts to and another reads, so a coordinator can hand a worker text without typing into its pane. Store `web/agent-inbox.ts` (pure, `sessionWaits`-style singleton, no IO), routes `routes/inbox-routes.ts`, CLI verbs `codeman agent post|inbox|ack`, opt-in snapshot `web/agent-inbox-persistence.ts`. Nothing in it writes to a pane, and it is kept out of `toState()` so a post does not fan out a `session:updated` to every browser; the counts live behind `GET /api/agent-inbox/summary` (owner-scoped, counts only, never text), which `codeman agent ls` merges into its INBOX column. + +⚠️ **Read until ack.** A read removes nothing; the same messages come back until the receiver acks them BY ID (`POST …/inbox/ack`, idempotent: an id already gone is not an error). Acking by id rather than "everything I read" is what keeps a message posted between the read and the ack from being lost. + +⚠️ **Seen and ack belong to the receiver.** The caller is whatever `resolveParentSessionId()` makes of `X-Codeman-Parent-Session` (live session, same owner as the receiver), never a body field (`InboxPostSchema` is strict). Only a read whose caller IS the receiver marks messages seen; anyone else's read is a peek. An ack from anyone else is a 403, not a silent no-op, so a coordinator never believes it cleared a worker's mail. In multi-user mode this stops a non-admin caller from naming another owner's session (an admin passes `canAccessOwned` for every session, so an admin can act as any session of the receiver's owner); in single-user mode every local process holds the same credentials, so the header is the client's own claim about itself, which is all the seen/peek split needs. + +⚠️ **`?wait=` parks while nothing is UNSEEN**, not while nothing is pending: a receiver holding a seen-but-unacked message would otherwise get an immediate answer on every loop and spin. Inbox waiters are the registry's third waiter kind (`waitForInbox`/`notifyInbox`), so they count against the same per-session/owner/total caps and are ended by `cancelAll` like signal and output waiters. `notifyInbox` runs AFTER the store, so a woken reader finds the message. `parked` in the summary counts receiver waiters only. + +⚠️ **Caps refuse, they never drop.** 16 KiB per message (UTF-8 bytes), 512 KiB and 200 messages per inbox; a full inbox is 409 CONFLICT and nothing is stored. Dropping the oldest would lose exactly the message a slow receiver has not handled yet. TTL is 12 h from each message's own `createdAt`, swept lazily, and a restore keeps `createdAt`, so a restart does not restart the clock. + +⚠️ **Lifetime follows the session, not the pane.** `_doCleanupSession` drops the inbox only when `killMux` is set (a real delete); a detach or server shutdown keeps it, and `DELETE /api/sessions/:id` on a row that is no longer live (the persisted-only branch in `session-routes.ts`) drops what the detach kept. A detach also ends a parked read with `ended: true`, which is why `codeman agent inbox --wait` asks the session list before it exits 3. Persistence (`agentInboxPersistEnabled`, SYNCED, default OFF) writes `agent-inbox.json` via `dataPath()`, 0600 through tmp+rename, debounced and flushed on stop; off means no file, and switching it off deletes the snapshot while memory stays. It is never fed to `/api/search`, and `sensitive-path.ts` keeps the file viewer off it. Boot loads the snapshot before `restoreMuxSessions`, but prunes inboxes of sessions that no longer exist only in `finalizeRestoredState`'s SUCCESS path: a failed restore must not delete mail for sessions that are about to come back. The keep set is the live sessions, the records `state.json` still holds AND the sessions on the reboot-restore offer (`rebootRestoreRegistry`): `cleanupStaleSessions()` has already pruned an offered session's record by then, and a rebuild reuses its id, so leaving the offer out returned every rebuilt session with an empty inbox (the 12 h TTL bounds what is never rebuilt). `POST /api/cleanup-state` drops the inboxes of the records it removes, since it is a delete for a detached row, except a record whose tmux session the multiplexer still lists (`mux.getSession`): a detach keeps that pane, and the next boot re-adopts it under the same id even without a `state.json` record. Tests: `test/agent-inbox.test.ts`, `test/agent-inbox-persistence.test.ts`, `test/routes/inbox-routes.test.ts`, `test/agent-inbox-server-wiring.test.ts`, the inbox block in `test/session-wait-registry.test.ts`. + ### Voice dictation via Claude **Voice dictation via Claude** (`claudeVoiceEnabled`, SYNCED, default OFF): the mic button can transcribe through this machine's Claude Code login instead of a Deepgram key, using the same speech-to-text service the CLI's own `/voice` mode uses. @@ -626,7 +740,7 @@ Matching semantics: a query containing `/` matches the relative path, otherwise ### Raw file bodies: streamed and range-aware -**Raw file bodies are streamed and range-aware**: `file-raw`, the attachments `/raw` route and `GET /api/download` always advertise `Accept-Ranges: bytes` and answer a `Range` header with `206` + `Content-Range` (single-range only; parser is pure + unit-tested in `src/web/http-range.ts`, a malformed spec is ignored → 200 while an out-of-bounds one is a 416). +**Raw file bodies are streamed and range-aware**: `file-raw`, the attachments `/raw` route, `GET /api/download` and the rendered-HTML route `GET /html-view//...` (so a rendered page's own `