# aicodeman ## 1.28.2 ### Patch Changes - **Terminal font weight** (#417, from discussion #403). App Settings → Terminal → Font gains two per-device rows, Normal font weight and Bold font weight, each a select from Default plus 100 to 900. Claude Code marks bold with a bare `ESC[1m` and no colour change, so with a family that ships only a regular and a bold face a bold heading reads as body text; setting normal to 300 turns that one small step into an obvious one. Both slots resolve against their own xterm default (an unset bold never inherits normal), apply live to the terminal, both echo overlays and open Agent Teams panes, and the bundled JetBrains Mono `@font-face` is declared over the font's real 100 to 800 axis instead of 400 to 700, without which every weight below 400 rendered identically to 400 on a stock install. **Phones up to 599px get the phone layout** (#390, fixes #389). The phone tier's cutoff moves from 430px to 600px in the JS classifier, mobile.css and every test and doc that pins it, so the iPhone Plus and Pro Max sizes, the Pixel Pro and the Z Fold cover display (430 to 460px) get the phone header, the Enter key and the accessory bar instead of the tablet layout. Verified on a real iPhone 17 Pro Max; a Safari page zoom below 100% widens the reported viewport, which is why the cutoff is 600 rather than 480. **The plan-usage statusline exporter no longer touches your settings files** (#361, diagnosed in #405). Codeman used to write its exporter into a workspace's `.claude/settings.local.json`, which Claude Code ranks above `~/.claude/settings.json`, so it replaced your own statusline for ANY `claude` run in that directory, including outside Codeman, and rendered the bare word `codeman` when run by hand. The exporter is now passed to `claude` as an ephemeral `--settings` flag when Codeman spawns it and is never written to disk; your own statusline (project-local, project, then `~/.claude/settings.json`) is wrapped and printed through inside Codeman sessions, and a hand-run `claude` sees nothing of Codeman. Workspaces an older Codeman wrote to self-heal the first time a session starts there. Telemetry collection follows the Plan Usage chip setting, read fresh at every Claude session create and respawn; an absent setting means on, and a device writes the switch only when it flips the chip, so a phone (chip off by default) saving its font size can no longer switch collection off for the desktop. The exporter prints nothing when it cannot reach Codeman, the telemetry route answers an unknown session with an empty body, and the footer is empty rather than a brand word. Known limit: sessions inside a Docker case do not feed the chip yet (the flag rides local spawns only; the chip is account-wide, so any local Claude session covers it). **`install.sh` and the Docker agent image read the CLI catalogue** (#380). Adding a CLI to `src/config/cli-registry/stock.ts` and running `npm run generate:cli-catalog` wires it into the installer's detection, install menu and closing reminder, and into the agent image's npm layer; each of those was a separate hand-kept list before, and OMP had been missing from the installer's detection entirely. The install menu offers every enabled CLI that can drive a pane (eight, rather than the fixed two), DeepSeek is deliberately withheld because `npm install -g @deepseek-ai/dsh` installs only a launcher with no runnable profile, a wget-only host keeps the entries that never needed curl, and the agent image respects `enabled`. The script stays bash 3.2 compatible and CI now executes it inside a real `bash:3.2` container. Choosing "s" (Skip) in the menu continues to the clone and build instead of aborting. **iPhone Duo support** (#407). A visual-viewport resize that changes the WIDTH is the device changing shape and is never read as the virtual keyboard: closing an iPhone Duo (626 to 466pt wide) or rotating any phone used to latch the keyboard layout with no keyboard on screen, sticky until the device was opened again. The seven centred overlays keep their dialogs out of the hinge through the CSS Viewport Segments variables (inert on devices that do not fold), the phone path picker and preview stay flush under 600px, and a shape change with the keyboard up baselines to the layout viewport so the settle event after a rotation no longer closes the keyboard layout. Two Duo device profiles join the test matrix. **Codeman is its own Claude Code plugin marketplace.** `/plugin marketplace add Ark0N/Codeman` followed by `/plugin install codeman@codeman` installs the codeman agent skill as a plugin, from `plugins/codeman/` (a mirror of `skills/codeman/` kept byte-identical by a test), which is a small separate directory on purpose: a plugin root carrying a `package.json` gets an `npm install` on every installer's machine. A Claude Code holding both the plugin and a user-level or per-case copy lists the skill twice; pick one route. Housekeeping: the maintainer's Telegram PR bot moved out of this repository (it is a client of the HTTP API like any other), the COM flow gained a Discussions announcement step, and the changelog's Thanks sections were backfilled for 1.22.0 to 1.28.1. ### Thanks - @irisitymichaelgrundberg for the font-weight analysis in #403 that this release implements, and the statusline diagnosis in #405 - @JDProfresh for the phone breakpoint fix (#390) - @timkjr for moving the statusline exporter off disk (#361) - @opticon454 for driving the installer and the agent image from the CLI catalogue (#380) ## 1.28.1 ### Patch Changes - 708cb2c: fix(tabs): let a wrapped desktop tab strip grow the header instead of clipping itself The wrapped tab strip carried fixed height caps (120px for the manual two-row layout, 96px for measured auto-wrap) that were row counts in disguise. A third row of tabs was clipped into a roughly 4px scroller, so the tab being looked for sat off-screen inside a container nothing invites you to scroll, while the header had the whole page below it to grow into. The header is `min-height` plus `flex-shrink: 0`, and terminal-ui's ResizeObserver refits the terminal on its own, so growing it costs nothing. Both wrapped layouts now share one rule capped at `var(--tab-strip-max-height, 40vh)`. That cap is a safety net for an absurd session count rather than a row limit: past it the scroller comes back, which still beats a header that swallows the terminal. Nothing sets `--tab-strip-max-height` yet, so today it is the 40vh fallback plus a hook for a future control. Desktop only in effect. `tabs-auto-wrap` is applied by `updateTabOverflowMode()`, which returns early for anything that is not a desktop viewport, and below 1024px `mobile.css` pins the header to `max-height: 48px` so it cannot grow at all. The two rules are comma-grouped rather than wrapped in `:is()`, so each arm keeps its own (0,2,0) specificity and `mobile.css`'s matching overrides still win on source order. ### Thanks 1.28.1 is a same-day follow-on to 1.28.0, so the thanks for this pair belong here too: - **@shenlvkang-collab** for the path picker's typed-path jump and name/date sort (#399), and for the care in the edges: the retry is bounded to one parent level, a typo keeps the listing you had instead of resetting to the root, and a full file path lands in its folder with the entry already selected. - **@irisitymichaelgrundberg** for Claude truecolor in panes (#409), and above all for flagging the one reading they could not prove: that suppressing truecolor may have made Claude's block collapse into the background rather than fixing anything. That paragraph is why this got measured instead of taken on trust, and the measurement changed the changelog. - **@timkjr** for trapping Ctrl+Z in agent sessions (#404), for finding that Caps Lock flips `ev.key` to `'Z'` without setting `shiftKey` so a plain `=== 'z'` check misses exactly the keystroke the guard exists for, and for stating up front that an agent CLI already holds its tty with ISIG off rather than overselling the fix. ## 1.28.0 ### Minor Changes - 58b4cb0: feat(files): let the path picker jump to a typed path and sort by name or date The picker's current-folder line was read-only, so reaching a deep folder meant tapping through every level, and its listing was fixed to name order, so the file an agent had just written was somewhere in a 500-entry list. The current folder is now an editable field (Enter or Go jumps there, a full file path lands in its folder with the file selected, and a typo keeps the listing you had instead of resetting to the root), the listing can be sorted by name or modified time in either direction with folders always first (the choice is remembered per device), and each entry shows a compact modified time. `GET /api/filesystem/browse` entries carry `mtimeMs` to make that possible, with one stat per entry. ### Patch Changes - c211461: fix(terminal): swallow Ctrl+Z in agent sessions so it cannot suspend a running CLI Ctrl+Z raises SIGTSTP on the pane's tty. In a `shell` session that is ordinary job control and is left alone, but in an agent session suspending the CLI stops an unattended loop dead with no visible output, the same failure shape as an XOFF freeze. The key is now swallowed in `attachCustomKeyEventHandler` for every non-shell mode, and unconditionally in the subagent/teammate terminals, which always run an agent CLI. The match is case-insensitive, because Caps Lock flips `ev.key` to `'Z'` without setting `shiftKey` and a plain `=== 'z'` check would let exactly the keystroke this exists to catch through. This is defence in depth rather than a fix for the steady state: an agent CLI holds its tty in raw mode with ISIG off, where ^Z is already inert. It covers the moments that are not the steady state: the window before the CLI takes the tty at startup, and any point where it hands the tty back. Two input paths are deliberately not covered and still reach the PTY: the mobile keyboard accessory bar's one-shot Ctrl, and the CJK composition textarea when `cjkInputEnabled` is on. Both are separate choke points to the PTY, and both are worth covering if this ever turns out to matter in practice. - 7767b16: fix(terminal): let Claude use truecolor so its themed backgrounds render Claude draws the user's own messages as a block of background color, and it renders as an approximation of the theme color at best. Claude's registry entry deleted `COLORTERM`, which left it the only agent CLI here besides `opencode` not asking for 24-bit color, so every RGB color its theme asks for was quantized down to whatever palette `TERM` alone implies. Claude now exports `COLORTERM=truecolor` like codex, gemini, antigravity, pi, grok, deepseek and omp already do, and the block renders in the color the theme actually names. How bad the quantization was depends on `TERM`, which is why this looks different on different machines. On tmux 3.2 and newer, whose `default-terminal` defaults to `tmux-256color`, supports-color reports 256 colors and `rgb(55, 55, 55)` lands on `ESC[48;5;237m`: visible, but not the color the theme asked for. Where `TERM` resolves to a 16-color entry instead (tmux older than 3.2, or a `~/.tmux.conf` setting `default-terminal screen`, which Codeman's tmux server does read), every dark background collapses to `ESC[40m`, the terminal's own black, and the block disappears entirely. That is the case this was reported from, and a custom Claude theme could change the color there with nothing on screen moving. Those seven CLIs also unset `NO_COLOR`; Claude does not, so a user who exports `NO_COLOR` globally keeps the monochrome panes they asked for. `CLAUDECODE` stays unset, because Claude reads it as a signal that it is running nested inside itself. `buildClaudeEnv()`, the direct-PTY fallback used when tmux is unavailable, now reads the same registry entry as the tmux pane and its attach client instead of deleting `COLORTERM` from a hand-maintained list of its own. It applies that entry before assigning Codeman's own variables, mirroring `buildEnvExports()`, so a `clis.json` override naming one of them cannot strip it on this path while the tmux pane keeps it. A remote pane still exports nothing, because `buildRemoteLaunchCommand()` never carried these declarations, so an SSH-remote Claude session keeps the old rendering. PR #3 introduced the `unset COLORTERM` in February, citing xterm.js#484 for the claim that xterm.js mishandles truecolor, and aiming to fall back to 256-color mode. xterm.js closed that issue in April 2019, Codeman now depends on `@xterm/xterm` 6, and `TmuxManager` sets `terminal-overrides ",*:Tc"` on its own tmux server, so 24-bit color already reaches the browser for the CLIs that ask for it. ### Thanks - **@shenlvkang-collab** for the path picker's typed-path jump and name/date sort (#399), and for the care in the edges: the retry is bounded to one parent level, a typo keeps the listing you had instead of resetting to the root, and a full file path lands in its folder with the entry already selected. - **@irisitymichaelgrundberg** for Claude truecolor in panes (#409), and above all for flagging the one reading they could not prove: that suppressing truecolor may have made Claude's block collapse into the background rather than fixing anything. That paragraph is why this got measured instead of taken on trust, and the measurement changed the changelog. - **@timkjr** for trapping Ctrl+Z in agent sessions (#404), for finding that Caps Lock flips `ev.key` to `'Z'` without setting `shiftKey` so a plain `=== 'z'` check misses exactly the keystroke the guard exists for, and for stating up front that an agent CLI already holds its tty with ISIG off rather than overselling the fix. ## 1.27.0 ### Minor Changes - Session lists that answer "which of these wants me next?", loopback links that work from a phone, and a batch of input and remote-session fixes. **The vertical tab rail sorts by activity and wears the home screen's cards.** A new per-device setting (App Settings → Appearance → Tabs → **Vertical Rail Order**, default _By activity_) orders rail rows with the same comparator both home screens use: whatever is blocked on you first, then whatever has been running longest, then the most recently quiet. Detailed rail rows become cards, with the state dot keeping its working ring and gaining the home rail's green halo. ⚠️ Existing vertical-rail users get sorting on upgrade, and a self-sorting list cannot also be drag-reorderable: choose _Manual_ to get your own order and drag-reordering back. The lineage bracket also moves 4px further from the rail's left edge, where its glow was being clipped by the window frame. **The Claude Response Viewer's brief view shows the whole last turn.** It used to render one row, so the eye button often showed the "Done." tail of an answer whose substance was in the rows above it. A multi-row turn now also opens at its newest text instead of its first narration line. **A `localhost` link in agent output opens as a proxied web tab.** An agent prints `http://localhost:5173/` and you tap it on a phone: that address only exists on the Codeman box, so the link was a guaranteed connection error from any other device. It now opens through the proxy, reusing a saved dashboard for the same dev server (one tab per server, not per host spelling) or saving one under its `host:port`. LAN and tailnet addresses still open directly, and on the box itself every link opens directly. `*.localhost` is deliberately not auto-routed: it is the only spelling that is a DNS name rather than an address literal, and these links come from agent output; add such a dashboard by hand instead. Trusted (non-sandboxed) dashboards are likewise never auto-reused by a tapped link. **Remote omp and remote claude sessions continue their conversation across a respawn or reattach.** Remote claude now launches an idempotent `--session-id || --resume` pair and remote omp respawns with `--continue`, instead of starting a fresh conversation each time. An omp session id is never resolved from the local `~/.omp` for a remote session, which would have pinned an unrelated local conversation. **Android and IME keyboards no longer drop committed characters.** Chrome on Android delivers a `composed: true` input event preceded by a keydown, which is exactly the shape xterm refuses to forward, so the character vanished. A recovery controller forwards it when, and only when, xterm produced nothing for that keystroke, so dictation and soft-keyboard input cannot be delivered twice either. ### Thanks - **@shenlvkang-collab** for the Response Viewer last-turn fix (#400) and for loopback links as web tabs (#401), both carefully measured, #400 against 285 real transcripts. - **@timkjr** for remote-omp resume/continue through respawn and reattach (#362), including dropping a half that had already landed and verifying the merge kept none of it. - **@aakhter** for the Android/IME input recovery (#388), and in particular for finding that an earlier version of their own browser test was passing vacuously, and saying so. ## 1.26.2 ### Patch Changes - Terminal rendering fixes, a Ctrl+V paste fix, an iOS Safari toolbar fix, a 2GB download cap, and a Blur entrance animation. ### Terminal rendering Three independent causes behind #398, where opening a session rendered a frame with characters spliced into each other and left the caret on the composer's border instead of its input line, until the CLI next wrote anything: - **The full-history replay now keeps row alignment** (#395). The linear capture path never restored the cursor, so every cursor-relative update the CLI sent afterwards was measured from the status line instead of the pane's real position, and four transforms that each can delete a line (trailing-blank stripping, redraw-bloat stripping, the pre-banner trim, leading-whitespace removal) shifted the frame out from under it. The full-history path now appends the pane's own cursor position and keeps every row, so row N of the reply is row N of the pane. The visible-frame and tail paths are untouched. - **The first fit waits for the terminal font** (#396). A cell measured against a fallback font gives the wrong column and row count, so the pane was sized twice and the CLI repainted for a shape that no longer matched the frame on screen. `selectSession` now holds for the font before measuring, bounded at 2s so a font that never arrives cannot strand a session, and it ends by re-measuring explicitly — `FitAddon.proposeDimensions()` divides by a cached cell size and nothing in it listens for font loading, so waiting alone would still divide by the fallback cell. - **A detached session's own window owns its pane size** (#397). Popping a session out left both windows sizing one PTY, and the dashboard's terminal is narrower than the popup because the session rail takes width the popup does not have, so the CLI drew frames that fit neither. The dashboard now withholds the resize send (never the local reflow) for a session showing in its own window, and takes sizing back on redock. ### Other fixes - **Ctrl+V no longer pastes twice** (#394). One keypress delivered two paste events to the clipboard trap: Firefox dispatches a trusted event for `document.execCommand('paste')` and then returns `false`, and the key's own default action fires another, because xterm's custom key handler returns false without cancelling the keydown. Right-click → Paste has no keydown, which is why only the keyboard duplicated. The trap now consumes exactly one event per keypress. - **iOS Safari: the phone toolbar sits on Safari's bottom bar** (#391, #392). The toolbar was lifted by `100vh - --app-height`, which on iPhone Safari measures the bar's collapsible height rather than an overlap — fixed elements there already stop above the bar — leaving an empty ~40px band and padding the terminal by the same amount. The lift is now `--chrome-overlap` (`innerHeight` minus the visual viewport height), which is 0 on iPhone Safari and equals the real overlap anywhere fixed elements do land behind the chrome. ### Downloads `file-raw`, the attachment `/raw` route and `GET /api/download` now cap at **2GB** instead of 50MB, configurable via `CODEMAN_MAX_DOWNLOAD_BYTES` (`0` = unlimited). The old cap was memory protection for a `readFile()` that no longer exists: those bodies stream and answer `Range` requests, so size costs a read stream rather than RSS (measured: a 600MB download moved peak RSS by ~37MB), and all the cap still did was refuse legitimate downloads of build artifacts, videos and archives. `/api/download` was the last route that really did buffer the whole file; it now streams, advertises `Accept-Ranges` and is resumable. Refusals move from `400` to `413`, the correct status for the case. ### Blur entrance animation A new opt-in `Blur` style on all four entrance surfaces (tabs, agent windows, the terminal pane, connection lines), plus a `Soft focus` theme that sets all four: an iOS-style focus pull where the thing arrives out of focus and the blur fades off it as the opacity comes up. App Settings → Appearance → Entrance Animations, or mix per surface at `?animlab=1`. Entrance animations stay off by default, so an untouched install is unchanged. ### Maintainer tooling The PR bot now fails fast when the review model's budget is spent, instead of hanging a review for the full 40-minute timeout and burning its retry cap. ### Thanks - @irisitymichaelgrundberg for #394, #395, #396 and #397, and for the #398 investigation that separated three causes behind one symptom - @JDProfresh for reporting #391 and fixing it in #392 ## 1.26.1 ### Patch Changes - Codex sessions no longer report idle for their entire life, and Codex conversations now appear in Past Sessions and can be resumed. **Per-CLI work detection (#385, irisitymichaelgrundberg).** The composer glyph and the working status line are now registry data (`capabilities.workDetect`) rather than Claude constants. Claude keeps its exact current pair, Codex declares `›` plus its `esc to interrupt` footer, and any CLI that declares neither falls back to Claude's, which is what every session used before. Work detection had been gated Claude-mode-only on the reasoning that an external CLI has no `❯`, which was true and still left every Codex session reporting `idle` from the moment it started. `workingLine` is config-supplied and its compiled pattern runs on the PTY hot path, so it goes through `compileVersionRegex()` in both the schema refine and the runtime compile: a nested quantifier there would backtrack on the event loop for the whole server. The Codex footer is matched case-insensitively on the E, so a future version capitalising it cannot make the fix silently inert. **Codex conversations in Past Sessions (#386, irisitymichaelgrundberg).** A bounded scanner reads codex's `~/.codex/sessions` rollout store, so the unified session list now merges three transcript stores rather than one (Claude's `~/.claude/projects`, omp's `~/.omp/agent/sessions`, codex's `~/.codex/sessions`). A scanned row carries a `resumeId`, the rollout's own thread id, which lets it resume through `codexConfig.resumeSessionId`; a live session never carries one, so a row without it stays a genuinely fresh session. Live and resumed Codex sessions fold into their rollout row through the existing alias map, including a `session_meta.originator` match for fresh panes, so a conversation never shows up twice. The phone overview carries `resumeId` through its own row projection, without which a tapped Codex past row started a fresh session on a thread already on disk. ### Thanks - @irisitymichaelgrundberg for both PRs (#385, #386), and for turning a full review round on #386 in a day. ## 1.26.0 ### Minor Changes - Tag the case directories agent workers create, and clean up what they leave behind. A long agent orchestration creates one case directory per worker, and deleting the sessions never removed them, so `~/codeman-cases` filled with scratch folders that looked exactly like real projects. - A case directory `POST /api/quick-start` **creates** for an agent-driven spawn now carries a `.codeman-agent-case.json` marker recording when it was made, by whom, from which session, and in which mode. Only the branch that creates the directory writes it, so a linked case, a cloned repo or any pre-existing path is never labelled, and deleting the marker file adopts a scratch case as a real one. - The label comes from the new `X-Codeman-Agent-Origin` header that the packaged agent skill sets on its shared curl invocation (preamble 1.22.0), or an `agentOrigin` body field, falling back to a resolved `parentSessionId` so workers spawned by an older skill copy are still labelled. - `GET /api/cases` publishes it as `agentCreated`, and the new read-only `GET /api/cases/agent-created` lists the scratch cases with `inUse` (a live session is still working in it) and `modifiedAt`. - Add Case -> Manage badges every agent-created case and adds a sticky **Clean up** entry point that names each directory in its confirmation and skips any case a running session is using. Removal still goes through `DELETE /api/cases/:name`. - The agent skill's per-session preamble cache (`~/.cache/codeman-agent-.sh`) is now removed with the session and swept at boot. One was written per Claude session and nothing ever deleted them (236 orphans on a working machine); the sweep keeps every live session's file and only takes orphans older than seven days. ### Thanks 1.26.0 carries no contributor PRs of its own. It lands the day after 1.25.0, so the thanks for that pair belong here too: - @mtiller for the reverse-proxy base URL (#381). - @dignfei for attaching cases to running containers (#357). - @shenlvkang-collab for the response viewer fix (#369), the first-hand conversation hook (#367) and the phone Add Case fix (#368). - @opticon454 for the case picker default (#383). ## 1.25.0 ### Minor Changes - Codeman can be mounted under a sub-path behind a reverse proxy (#381, @mtiller). `--base-url /codeman` (or `CODEMAN_BASE_URL`) makes the server strip the prefix on the way in, rebase redirects on the way out, inject `` and `window.__CODEMAN_BASE__` into the shell, and route web-tab proxying and WebSocket upgrades under the mount, so one TLS name can front several apps. A root install is byte-identical to before. Applied on top: the crash-diag beacon stays under the mount (sendBeacon is not fetch, so the base-aware wrapper never saw it), the test suite strips `CODEMAN_BASE_URL`, and a wiring test boots a real server under a prefix. A case can attach to a container that is already running (#357, @dignfei). `DockerCase.owned:false` mirrors the remote-SSH attach contract: Codeman only execs into such a container, never creates, starts, stops, removes, pauses or commits it, with the refusal enforced at string-construction time so no caller bug can reach `docker stop`. The Add Case dialog gets an attach panel with a container picker, the run menu takes its mode availability from the CLIs actually present in the container, and adoption is admin-only in multi-user mode. Three gaps closed after review: export no longer pauses or commits an adopted container, a freshly linked owned case no longer hides every agent mode behind a probe of a container that does not exist yet, and multi-user gating is explicit. The Claude response viewer renders one message per model message (#369, @shenlvkang-collab). The reader used to fuse every assistant row between two human prompts into one card and never read the attachment rows that hold a prompt typed mid-turn; measured over 57 real transcripts it now shows 1,806 messages instead of 356 and recovers 162 absorbed user prompts, with the assistant text unchanged row for row. A Claude pane learns its live conversation from the CLI's own `UserPromptSubmit` hook (#367, @shenlvkang-collab). The conversation id used to be re-derived by correlating `~/.claude/history.jsonl` against a stamp only Codeman's own input path set, so a pane driven straight from tmux stayed pinned to its launch conversation forever. The hook reports the id first-hand, addressed by the pane's own `$CODEMAN_SESSION_ID`, and the chain of conversations is persisted so a restart re-pins the right one. The new `hook:prompt_submitted` SSE event is registered (158 = 158), and it lands in the run summary only when the conversation actually moved. The Add Case modal can be submitted from a phone again (#368, @shenlvkang-collab). Since 1.16.4 the layout below 860px hid the modal footer, which held the only Create/Clone/Link button. A header submit button now sits beside the close button, dims while a submit is pending, and a static test pins the contract so it cannot silently disappear again. The Link Existing case picker opens in the Codeman Cases directory instead of Home (#383, @opticon454). Under Docker the two are unrelated trees and Home holds nothing but dot directories, so the picker showed no cases at all. The fallback chain is now Current Folder, then Codeman Cases, then `/mnt/d`, then the first root. A PR review bot for the maintainer (`scripts/pr-bot/`, guide in `docs/pr-bot.md`). It reviews every open pull request in its own Codeman session inside a private clone and reports the verdict, ranked findings and a recommendation to Telegram with action buttons; merge, close, post-comment and approve-CI happen only from a confirmed tap. Maintainer tooling, not part of the server or the CLI. ### Thanks - @mtiller for the reverse-proxy base URL (#381). - @dignfei for attaching cases to running containers (#357). - @shenlvkang-collab for the response viewer fix (#369), the first-hand conversation hook (#367) and the phone Add Case fix (#368). - @opticon454 for the case picker default (#383). ## 1.24.7 ### Patch Changes - The web-tab proxy refuses link-local and cloud-metadata targets. Its Test probe, the proxy itself and the WebSocket relay accepted any http(s) host, so a saved dashboard URL could reach `169.254.169.254` (in decimal, hex, IPv6-mapped or DNS-name form) through a capability and no cookie. Loopback and RFC1918 addresses stay allowed on purpose, since a localhost Grafana is the feature; only link-local and the fixed cloud-metadata addresses are refused, at the schema, at every connect site, and through a DNS lookup hook that judges the resolved addresses, which is what closes DNS rebinding. Adds `undici` so the proxy runs its fetch through its own agent. Proxy capabilities are revoked on logout. `revokeOwner()` had shipped with no caller, so a leaked proxy URL stayed valid for as long as anything kept polling it. `POST /api/logout`, the admin forced logout and user deletion now revoke the capabilities they should, and proxied responses carry `Referrer-Policy: same-origin` with the upstream's own policy dropped, so a dashboard on a loose referrer policy cannot hand the capability to a third-party host it links to. The Docker Compose deployment updates itself from App Settings again (#373, @opticon454). The checkout Compose builds from is bind-mounted at `/opt/codeman`, so an update's `git checkout` and rebuild land on the host and survive container recreation; build artefacts live in named volumes so container-compiled native modules never enter the host checkout; the image keeps devDependencies and a build toolchain; and the restart is the server exiting under `restart: unless-stopped`. An in-place update applies code only, so the updater refuses a release that changes `server.Dockerfile` or `docker-compose.yaml`, or that adds keys to `.env.example` the user's `.env` has no value for (Compose interpolates an unset variable to the empty string and starts anyway), and points at `docker/Start-Codeman.sh` on the host instead. The four global agent CLIs in the image are pinned. A follow-up makes the final step fail safe: the server exits only when the Compose file declares `CODEMAN_RESTART_BY_EXIT=1` or the daemon confirms an auto-restart policy, and otherwise the build is staged for a manual restart, so a container nothing would restart is never taken down. Details in `docs/docker-self-update.md`. The test suite strips `CODEMAN_INSTANCE`, `CODEMAN_DATA_DIR` and `CODEMAN_TMUX_SOCKET` before any application module loads (#371, @opticon454), with a two-half test whose static half reads `test/setup.ts` so a dropped line fails everywhere. This replaces the throwaway data dir #356 had set for the same variable. ### Thanks - @opticon454 for the Compose self-update (#373) and the test isolation fix (#371). ## 1.24.6 ### Patch Changes - CLI backends are now a data-driven registry (#347, @opticon454). Every run mode (Claude Code, Terminal/Shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek Harness and OMP) is a `CliEntry` in `src/config/cli-registry/`: binary discovery (search dirs, version and identity probes), the launch argv template, environment handling, the multi-user privileged-parameter and privileged-env-key clamps, the remote and Docker pane commands, and the capability flags the rest of the app reads instead of branching on a CLI's name. `~/.codeman/clis.json` can override any stock entry or add a custom CLI; it is read-only in this release, must be mode 0600, and every reason it was ignored is now logged once on first load (`docs/cli-registry.md`). Config never contains shell text: entries declare typed argv tokens, literals are validated at load time, and values resolve through patterns named in code. Registry data resolves at call time rather than at module import, so a CLI enabled while the server runs moves every surface at once, and a guard test fails the build if per-CLI-id branching reappears outside the stock catalog. This is an internal refactor. The spawn command every CLI receives is byte-identical to the previous hand-written builders, verified by pinned golden strings in the test suite and by diffing both implementations across 11,602 option combinations for all ten modes. Five small deliberate changes ride along: the in-container version probe derives the binary from the registry (`antigravity` runs `agy`), the remote version probe now covers Grok and DeepSeek, `codeman doctor`'s CLI rows are generated from the registry (Claude's install hint is the install command, five CLIs gain hints, the row order follows the catalog), OMP now requires tmux like its siblings instead of silently falling back to a direct PTY, and an OMP session's attach client now receives `COLORTERM=truecolor` like the other truecolor CLIs. Remote sessions are no longer auto-revived after a clean agent exit (#355, @timkjr). The reconnect watcher could not tell a transport drop from a Ctrl-C, Ctrl-D or `exit` inside the remote CLI, so a clean exit relaunched a fresh agent (OpenCode and OMP started a new conversation every time; Claude only looked fine because its `--resume` fallback masked it). The watcher now revives a dead pane only when the durable remote tmux session is verifiably still alive, via a `has-session` probe over ssh, and an unreachable host means do not revive. A follow-up classifies that probe by exit status, since `tmux has-session` prints nothing on success and reading its stdout had marked every live session as gone, forgets the cached answer whenever the pane is seen alive again so a stale result cannot revive a later clean exit, and caps the probe at one in flight per session. The test suite can no longer reach the production `~/.codeman` data dir (#356, @timkjr). `test/setup.ts` now points `CODEMAN_DATA_DIR` at a throwaway directory, which is the absolute override that bypasses the suite's temporary HOME when inherited from the shell, and every test that deletes a case tree goes through a containment gate that refuses paths outside the temporary HOME. A bare suite run had overwritten a real `remote-hosts.json` with a route test's fixture. The comments around it and CLAUDE.md's testing section now name that variable as the cause; `os.homedir()` itself does follow `$HOME`. ### Thanks - @opticon454 for the CLI registry (#347), the phased resubmission of #343, and the review rounds that hardened it. - @timkjr for the remote auto-revive fix (#355) and the test-isolation sweep (#356). ## 1.24.5 ### Patch Changes - Fable 5.1 is selectable in App Settings. `claude-fable-5-1` is in Claude Code's model catalog (display name "Fable 5.1", June 2026 knowledge cutoff), but the model picker only went up to Fable 5, so pinning it meant hand-editing a case's `.claude/settings.local.json`. It now appears as a card under **App Settings -> Models -> New Claude sessions**, and as an option in **Task routing** (Default for tasks, plus the Explore / Implement / Test / Review overrides). It is offered exactly the way Fable 5 already is: the "1M capable" badge, the 1M context window switch stays live for it, and base + switch compose into `claude-fable-5-1[1m]`. Both strings are accepted by the CLI. Deliberately not claimed: that a 1M window is what sets Fable 5.1 apart. The CLI's model catalog marks both fable entries as natively 1M with the same window, so an always-on window for 5.1 next to a switchable one for 5 would encode a difference the models do not have. ### Thanks - @shenlvkang-collab for #370, which surfaced that Fable 5.1 was missing from the picker. ## 1.24.4 ### Patch Changes - The Compose deployment image ships the Docker CLI instead of the whole Docker engine. `docker/server.Dockerfile` installed Debian's `docker.io` to get a client for the mounted host socket. That package is the full **engine**: even with `--no-install-recommends` it pulls 15 packages including containerd, runc, dmsetup and iptables, none of which a container that only talks to a socket can use. It also ships Docker 20.10.24, from 2023. The CLI and the buildx plugin are now copied from the official `docker:29-cli` image instead. Measured on the same `node:22-bookworm-slim` base: **266 MB → 108 MB**, a 158 MB saving, with the current CLI (29.7.2) in place of a two-year-old one. Verified by building the real image and running it: the binaries are static Go builds, so they work on this glibc image even though they come from an Alpine one, and `docker --version`, `docker ps` and `docker build` all succeed against a mounted host socket as the unprivileged runtime user. buildx is copied deliberately — `scripts/build-agent-image.mjs` shells out to `docker build` and Codeman auto-builds the agent image on the first Docker case, which without the plugin falls back to the classic builder Docker has deprecated. `docker-compose` is not copied; Codeman never shells out to it. ### Thanks 1.24.4 is a same-day follow-on to 1.24.3, so the thanks for that pair belong here too: - @opticon454 for #349, and for a write-up that made an infrastructure PR quick to review ## 1.24.3 ### Patch Changes - Docker Compose deployment, and the plan-usage chip stops losing its 5-hour window. **Run Codeman itself in a container** (#349, @opticon454). `docker/` now carries a local-image Compose deployment: copy `docker/.env.example` to `docker/.env`, set `CODEMAN_PASSWORD`, run `bash docker/Start-Codeman.sh`. Docker cases then start as **sibling** containers through the mounted host socket rather than nested ones, which inverts an assumption the bare-host path takes for granted: the daemon no longer shares Codeman's filesystem, so a bind source that is valid inside Codeman means nothing to it. `CODEMAN_DOCKER_HOST_HOME` translates sources under HOME into the daemon's namespace and `CODEMAN_CASES_PATH` points the cases dir at a host-absolute bind mount, so a workspace resolves to the same absolute path on both sides. `CODEMAN_DOCKER_DISABLE_SWAP_LIMIT=1` drops `--memory-swap` for hosts without swap accounting (`--memory` still applies) and filters only that one kernel warning. Guides: `docs/docker-compose.md`, `docker/README.md`. Three things were fixed while landing it: - **`docker/.env` was being baked into the image.** A `.dockerignore` pattern matches the whole context-relative path, so the bare `.env` line excluded only the root file while `COPY . .` picked up `docker/.env` — the file the deployment's own README tells you to fill with `CODEMAN_PASSWORD` and provider API keys — and left it at `/opt/codeman/docker/.env`. Now excluded via `**/.env`, verified in both directions against a real build context with a canary secret. - **`codeman skill install --case ` could not find a case under Compose.** `CODEMAN_CASES_PATH` moved the server's cases dir but not the CLI's, which still hardcoded `~/codeman-cases`. Both now resolve through one place. - **A Docker case handed its Claude conversation id to every other CLI.** `resumeOnStart` seeded `dockerResumeId` from `lastClaudeSessionId` regardless of mode, and `appendResumeFlag()` maps a resume id onto codex/gemini/pi/grok/deepseek/omp/antigravity. This one is a plain master bug, unrelated to Compose. **The plan-usage chip keeps its 5-hour slot.** It silently shrank from `5h 4% · 7d 52%` to a lone `7d 52%`, which reads as half the feature breaking. Nothing was broken: Claude Code ships `rate_limits.five_hour` "only while the API reports it and its resets_at has not passed", so between 5-hour session windows the key simply leaves the statusline payload. The slot now stays with a dimmed em dash and the tooltip says "no active session window". Claude only — a missing Codex bucket means that plan has no such limit, so those stay omitted. ### Thanks - @opticon454 for #349, and for a write-up that made an infrastructure PR quick to review ## 1.24.2 ### Patch Changes - Fix every new claude session dying on Claude Code 2.1.252's rewritten folder-trust dialog. That dialog used to offer `❯ 1. Yes, I trust this folder` / `2. No, exit`, so Codeman answered it by pressing Enter on the highlighted default. 2.1.252 dropped the numbers, reversed the options and highlights `No, exit`, so the same Enter now answers _exit_: a session in any directory claude had not seen before died (`Pane is dead (status 1)`) about six seconds after it started, before the agent ever drew a composer. - `trustDialogNextKey()` (`src/session-trust-dialog.ts`) now reads the `❯` marker off the rendered pane and returns ONE keystroke at a time: an arrow while the cursor is on the wrong option, Enter only once the screen shows it on the trust option. A frame it cannot read presses nothing. Both the 2.1.252 and the older numbered layout are handled, and the direction is derived from the frame rather than assumed, so a further reordering costs a repaint instead of a session. - The scan schedules its own follow-up read. It had only ever run from the PTY data handler, which was enough while one Enter answered the dialog; the arrow that moves the cursor is the last output the pane produces, so a two-keystroke answer would otherwise stall with the cursor sitting on the right option forever. The keystroke cap goes from 3 to 6 for the same reason. - The bundled `codeman` agent skill gets the same treatment (preamble 1.21.0): its `_accept_trust` fallback reads `terminal?full=1`, steers onto the trust option and confirms only after re-reading, instead of posting a blind `\r`. It sends those keystrokes under its own `clientId`, because input sequence numbers are monotonic per client and spending prompt numbers on dialog keys would make the next send-and-wait look like a stale duplicate and vanish silently. - Readiness recipes in `docs/extending-codeman.md`, `docs/api-reference.md` and the skill's own reference carry the corrected answer and a new symptom-table entry for a worker whose pane is dead seconds after the spawn. Also included: a CLAUDE.md audit against the tree, correcting counted drift (route modules, handler counts, frontend module count and app.js size, install.sh size) and documenting several subsystems that had no entry. ### Thanks 1.24.2 is a hotfix on top of 1.24.1, so the thanks for that pair belong here too: - @opticon454 for #350, with a reproduction that made this a confirmation rather than a hunt - @timkjr for reporting #352, and for finding it while verifying Docker support for someone else's PR ## 1.24.1 ### Patch Changes - The Docker agent base image builds again. **`docker/agent.Dockerfile` could not be built from a fresh checkout** (#352, fix in #350): the DeepSeek Harness step died with `dsh: pnpm not found on PATH` and exit 127, which took the whole image with it and, because Codeman auto-builds this image on the first Docker case, left Docker mode unusable on a clean host. `dsh plugin` does not bundle a package manager; it spawns a literal `pnpm` with no npm fallback, so pnpm is now installed alongside `dsh` and the layer proves it with `pnpm --version`. The profile install also passes `--config.dangerouslyAllowAllBuilds=true`, because pnpm, unlike npm, refuses dependency lifecycle scripts by default and fails the install over it (`ERR_PNPM_IGNORED_BUILDS`, exit 1). Which packages that hits moves between rebuilds, since the terminal profile is resolved by dist-tag rather than pinned: the tree that broke the build in August pulled `@google/genai`, today's does not. An allowlist of those names would have gone stale rather than prevented the next break, and running those scripts is the same exposure the image already accepts three layers up, where `npm install -g` runs the install scripts of every transitive dependency of the five CLIs above it with no gate at all. Documentation caught up with two things it had wrong: the image smoke test in `docs/docker-cases.md` now covers `dsh` and `omp`, and checks the dsh **profile** rather than only the binary (`dsh` is a launcher, so `dsh --version` says nothing about whether a session can start), and `docs/deepseek-integration.md` names pnpm as a prerequisite for installing a terminal profile at all, by hand or through the UI button. A comment in the `/api/deepseek/install-profile` route claimed the opposite of what this bug proved, and is corrected; the route's behaviour was already right, surfacing dsh's own "pnpm not found on PATH" line as the install error. ### Thanks - @opticon454 for #350, with a reproduction that made this a confirmation rather than a hunt - @timkjr for reporting #352, and for finding it while verifying Docker support for someone else's PR ## 1.24.0 ### Minor Changes - OMP (Oh My Pi) as a tenth run mode, mode-faithful Resume for external CLIs, and a cleaner plan-usage chip. **OMP (`omp`) run mode** (#353): Oh My Pi joins Claude Code, shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok Build and DeepSeek Harness as a run mode, in local, Docker and remote-SSH sessions: toolbar dropdown, welcome button, phone overview, command palette, clone-repo brain picker, cron agent types, tab badges and per-mode colours, plus `GET /api/omp/status`, a `codeman doctor` entry, install.sh detection and the docker agent image. The resolver leads with `~/.local/bin` (the upstream installer's real target) and demands `omp/` from `--version`, so an unrelated binary with the same three-letter name is never spawned. Past omp conversations appear in Past Sessions, read from omp's own session files (the header line carries the real working directory, so nothing has to reverse-engineer omp's directory mangling), and a respawned or resumed omp session is pinned to an exact conversation with `--resume ` instead of omp's newest-file `--continue`. Review hardening before merge: the pin is resolved only at the moment a respawn is actually confirmed (an eager resolve on boot recovery used to alias two omp tabs in one case directory onto one conversation), candidates are verified against their own header `cwd` and claimed process-wide so siblings cannot double-pin; `OMP_*` joins the env-override allowlist and `OMP_AUTH_BROKER_URL`/`OMP_AUTH_BROKER_TOKEN` are clamped for non-granted owners in multi-user mode, the same shape as `DEEPSEEK_BASE_URL`. Known and documented: omp's own knobs are mostly `PI_*` (it is a pi fork), its default `tools.approvalMode` is `yolo`, and in-container `--resume` pinning does not reach a Docker omp pane. **Resume keeps the row's own CLI** (#353): clicking Resume on an OpenCode, Pi, Grok, DeepSeek or OMP row used to create a plain Claude session, since the create request never carried the row's mode. Resume now relaunches in the row's own mode with that CLI's continue flag, and retires the stale row it came from so three clicks no longer leave three copies of the same name. Codex, Gemini and Antigravity rows have no continuation wired yet, so their rows are deliberately left in place. `DELETE /api/sessions/:id` accepts a persisted-only session (ownership enforced through the same helper as live lookups, 404 rather than 403 so nothing leaks) and broadcasts `session_deleted` so other tabs drop the row too. **Plan-usage chip drops the provider label when there is only one**: a machine with only Claude limits rendered `CLAUDE 5H 60% 7D 23%`, a 46px label naming the only thing it could be. The name exists to tell two rows apart, so it now appears only when both Claude and Codex have windows; the tooltip still names the provider either way. ### Thanks - @timkjr for #353, and for turning every review finding around within a day ## 1.23.2 ### Patch Changes - Codex plan usage in the header chip, a visible inline rename in the session sidebar, and an installer that no longer loses Tailscale access on a re-run. **Codex plan usage in the header chip** (#346): the plan-usage chip used to show Claude's 5-hour and weekly limits without saying they were Claude's, which stops being a detail the moment you run more than one CLI. It now renders one compact row per provider, Claude above Codex, each labelled and colour-coded by how much is used up. Claude's numbers still come from Codeman's marked `statusLine.command` exporter; Codex's come from the signed-in host CLI's read-only `account/rateLimits/read` app-server request at startup and every five minutes, so credentials stay inside the CLI and no auth material reaches the browser. Only the main `codex` bucket is read (model-specific buckets such as Spark are separate limits and are deliberately excluded), and the Codex row is omitted entirely when no 5-hour or weekly window is available, rather than inventing one. **Inline rename is visible in the session sidebar** (#345): starting a rename on a sidebar row opened a focused input you could not see. The row's ellipsis clamp was still painting over the live editor, so text and caret went in blind. The sidebar now gets the same unclamped editor layout the vertical tab rail already had. Covered by a Chromium regression test that asserts the painted `overflow` and the input's measured width, not just the class name. **install.sh keeps Tailscale access on a re-run**: a re-run whose build failed could drop a working Tailscale binding instead of preserving it. The installer now offers Tailscale setup again on re-run rather than losing it, and the README describes the three-way network-access prompt (Tailscale / LAN / local-only) as it actually behaves. ### Thanks - @JackStuart for #346 - @fibr for #345 - @tailong-wu for #342, whose analysis of the terminal refresh replay loop matched a fix that had landed on master a few hours earlier ## 1.23.1 ### Patch Changes - Fix a fresh-Linux install failure, and bound the browser terminal's live write queue. **install.sh now installs a build toolchain.** Reported against a stock Ubuntu 24 server: node-pty publishes prebuilt binaries for darwin and win32 only, so on Linux it is always compiled from source during `npm install`. The installer set up Node, tmux and git but never a compiler, so a machine without `build-essential` died deep inside node-gyp with `not found: make` — which reads like an npm bug rather than a missing system package. `make`, a C++ compiler and `python3` are now checked up front exactly like git and tmux, installed per distro (apt / dnf / pacman / apk / zypper) behind the same consent prompt, and re-verified afterwards rather than assumed. If `npm install` fails anyway — including on `install.sh update` — it now names the missing tools and the command that installs them instead of leaving a node-gyp stack trace as the last word. **Bounded live xterm backpressure** (#339): live output is now one chunk in flight at a time, released by xterm's own parse callback, so xterm's private WriteBuffer can no longer hide an unbounded backlog behind the browser's 128 KiB render cap; queued, loading and incoming bytes all count against that cap. Automatic drop recovery for a shell stays on the bounded 1 MiB tail — a 100k-line shell capture is tens of MiB, and parsing it on the main thread is the freeze the cap exists to prevent — while TUI modes still recover full history behind the existing downgrade guard. Duplicate SSE terminal events are dropped before JSON parsing while WebSocket owns terminal I/O, and recovery is single-flight per active session. Follow-up hardening: the three write-queue reset paths now also release the in-flight gate, so a parse callback that never lands cannot leave live output permanently stalled. **File Viewer searches the workspace** (#340): the File Viewer search box now queries the server-side file search endpoint with a 250 ms debounce and strict response validation, instead of filtering only the part of the tree already loaded. Tree and search state are scoped to the active session, the hidden-file preference and independent request epochs, so a stale response cannot repaint the panel; cached-tree restoration, directory results and reset behaviour survive session switches and both panel-hide paths. ### Thanks - @dignfei for #339 - @aakhter for #340 - 858b15e: Search the full session workspace from File Viewer while keeping results scoped to the active session and hidden-file preference. ## 1.23.0 ### Minor Changes - DeepSeek Harness as a ninth run mode, DeepSeek agent workers, and detailed rows for the vertical tab rail. **DeepSeek Harness (`dsh`) run mode** (#337): DeepSeek's plugin-native agent framework joins Claude Code, shell, OpenCode, Codex, Gemini, Antigravity, Pi and Grok as a run mode. The harness is a profile launcher rather than an agent, so availability is two questions (binary AND a pane-capable profile): the Run button gates on both, a missing terminal profile is offered as a one-click install (`POST /api/deepseek/install-profile`, the only endpoint in Codeman that installs third-party code, fenced accordingly), and the resolver demands the harness's own help banner so Debian's unrelated `dsh` (dancer's shell) can never be spawned. Its permission switch is the `DSH_PERMISSION_MODE` env export (the harness has no bypass flag), injected via tmux setenv and clamped for non-granted owners in multi-user mode, including the env-override path. The community TUI's supervisor-reporting contract makes deepseek the first non-Claude mode with REAL lifecycle signals: a generated status shim turns its idle/working/blocked reports into definitive `stop`/`permission_prompt`/`agent_working` hook events, so dsh sessions get real respawn triggers, real wait signals and red "needs you" alerts instead of output-stabilization guesswork. The vendor's browser UI opens as a managed web tab through a background `dsh web` fenced to Codeman's origin. Docker image support included. **DeepSeek agent workers** (#341): the codeman agent skill can spawn and drive dsh workers like claude ones — tasked, waited on and read with the same calls. `GET /api/sessions/:id/last-response` reads the harness's real zstd transcript (one frame per append; the reader walks frame boundaries itself, since a naive decode silently truncates to the first frame), distinguishes real prompts from plugin-injected context, and reports a failed turn's provider error instead of an empty answer. **Vertical tab rail: detailed rows** (#338): the vertical rail can now show the home screen's per-session line (created stamp, state duration, status pill) via the new per-device `tabRailDetail` setting (default detailed; `simple` restores the 1.22.0 rows). One shared row model and one gate (`isRichTabRows()`) keep the rail, the rich sidebar and both home screens in agreement about what "working" means. A never-sized rail opens at the 320px Wide preset; below 288px the created stamp is dropped, below 240px rows fall back to simple. Also fixes Escape during an inline tab rename committing an empty name (the session then displayed its folder name). **Review hardening across all three** (post-review commits on each PR): multi-user owners without the bypass grant can no longer redirect the server's forwarded `DEEPSEEK_API_KEY` via a `DEEPSEEK_BASE_URL` override; waits on `stop`/`blocked` are refused for docker/remote dsh sessions (their status bridge cannot reach the harness) and docker/remote dsh sessions keep the pane reader (their transcripts are not local); dsh approvals are alerts answered in the terminal, never blind keystrokes into a third-party TUI; the status shim forwards the contract's `--seq` token (stale retried reports are dropped server-side) and treats 4xx as permanent so a misconfigured session cannot rate-limit the hook endpoint for the whole instance; concurrent DeepSeek web-UI starts are serialized; cron deepseek jobs run the same launch gate as the HTTP paths; the installer's dsh identity probe is stdin-closed, bounded and memoized; transcript reads are memoized per (path, mtime, size) so 1s polling stops decoding unchanged files; the rail's width dialog, compact-threshold folder rows and reset affordances are rich-aware. ### Patch Changes - b330f1d: Vertical tab rail: detailed rows, and a rename cancel that no longer wipes the name. The vertical rail (Tab Orientation → Vertical) now draws the same per-session line the home screen and the rich sidebar draw — when the session was created, how long it has been in the state it is in, the folder it runs in, and a status pill — instead of just the name. New per-device setting **Vertical Rail Rows** (`tabRailDetail`, App Settings → Appearance → Tabs) with `Detailed` as the default and `Simple (name only)` as the opt-out. A rail that has never been sized now opens at 320px (the existing Wide preset) so the line fits; a narrower rail sheds the created stamp below 288px and falls back to simple rows below 240px. Also fixes a data-loss bug in the inline tab rename that predates the rail: pressing Escape cleared the input and blurred it, and the blur handler commits — so cancelling a rename stored an EMPTY session name and the tab fell back to its folder label. Escape now cancels without a request, in every layout. ### Thanks 1.23.0 carries no contributor PRs of its own. It lands the day after 1.22.0, so the thanks for that pair belong here too: - **@aakhter** built both halves of the new tab experience: the owner-scoped, server-authoritative tab-layout foundation with recipient-safe SSE publication and an unusually deep test suite (#335), and the resizable vertical session rail with accessible pointer/keyboard sizing and careful FitAddon handoff (#334). Fifth and sixth merged PRs, and the layout work also fixed real multi-user ordering leaks along the way. ## 1.22.0 ### Minor Changes - 3f8c8e9: Add Grok Build (xAI `grok`) as a seventh CLI run mode. SessionMode gains 'grok', with its own resolver (version-probed, since the name has npm squatters; GET /api/grok/status surfaces path + version), GrokConfig (model, alwaysApprove -> --always-approve, resume/continue), GROK*\*/XAI*\* env allowlist entries, the multi-user only-if-sent bypass clamp, Docker (own image step + per-file credential seeding) and remote-SSH command defaults, cron agentType, run-mode/welcome/tab UI with a charcoal identity, and docs (grok-integration.md + plan). Verified end to end against grok 1.0.5 on an isolated instance. - 74194e4: Add the owner-scoped tab-layout model, persistence, API, lifecycle repair, and synchronized legacy ordering foundation. - e3a2fb7: Add an optional resizable vertical session rail with responsive layout, complete labels, accessible controls, and stable inline rename. ### Patch Changes - Fix the file preview's dead pop-out control: a real detach button now opens the previewed file in a browser tab (raw route for PDFs/images/media/text, converted-PDF preview for docx/pptx) and the copy button reports when a preview has no text to copy instead of silently doing nothing. Review-driven hardening for the new tab features: PUT /api/session-order drops unknown ids again instead of rejecting the whole write (a session deleted inside the browser's debounce window could silently lose the user's reorder), a failed mux restore no longer blocks explicit session/webview deletion for the process lifetime (the automated stale sweep stays fail-closed), and the vertical rail gains the axis-awareness the sidebar-only predicates missed: correct drag-reorder insertion, active-tab scroll-into-view, floating windows anchored beside rail tabs, connector redraws on rail scroll, server-seeded orientation applied on first load, a pre-paint stamp so vertical mode no longer flashes through the header strip, and a 12px session-name default matching the sidebar's historical size so untouched installs are not restyled. ### Thanks - **@aakhter** built both halves of the new tab experience: the owner-scoped, server-authoritative tab-layout foundation with recipient-safe SSE publication and an unusually deep test suite (#335), and the resizable vertical session rail with accessible pointer/keyboard sizing and careful FitAddon handoff (#334). Fifth and sixth merged PRs, and the layout work also fixed real multi-user ordering leaks along the way. ## 1.21.0 ### Minor Changes - **`codeman tui`: a terminal dashboard for your sessions.** For the times you are in SSH or Termius instead of a browser. The web UI remains the primary surface and bare `codeman` still prints help, so the dashboard itself is strictly additive. Sessions are grouped NEEDS YOU / WORKING / IDLE / RECENT in the same status language as the web tabs and the phone overview, and the states come from the server (hooks, idle confirmation, the approvals inbox) over the existing HTTP/SSE API rather than being screen-scraped. That is what lets the dashboard answer a permission dialog instead of only reporting one. - `↑↓`/`j`/`k` select; `1`-`9`, `[`/`]` and `Tab` switch between sessions - `Enter` attaches and hands the terminal to tmux; **`F1` comes back**, one key, no modifier. Inside the pane a bar across the top carries the session strip and `Alt+1`..`Alt+9` switch without returning to the dashboard first - `Enter` on a RECENT row resumes that conversation; on a session whose pane has died it refuses and offers `r` to resume it in a fresh pane - `y`/`n`/digits answer the selected session's pending permission or question card (the server re-captures the pane first, so a keystroke can never land in the composer) - `p` sends a one-line prompt without attaching, `x` kills (`y` confirms), `n` starts a session and opens straight into it - `/` cross-session search, `g` away digest, `?` help, live preview pane, plan-usage chip in the header, a terminal bell when a new approval arrives - `codeman tui --list` and `codeman tui ` are scriptable fast paths; with no server running it lists panes straight from the instance's tmux socket, attach-only, and upgrades live when the server comes back Narrow terminals (under 72 columns, a phone SSH client) drop the preview and get a single-column layout. `NO_COLOR`, non-UTF-8 glyph fallback and a non-TTY refusal are all handled. Zero new dependencies: hand-rolled ANSI over chalk and commander. User guide: `docs/tui.md`. **Breaking: the `sc` tmux chooser is retired.** `scripts/tmux-chooser.sh` is deleted and `install.sh` no longer creates the `tmux-chooser` symlink or the `sc` alias; it sweeps both up instead, on update and on uninstall. `codeman tui` replaces it and does the job better: `sc` numbered its entries globally but only accepted a single `[1-9]` keypress, so sessions 10+ were listed and could not be selected, and it inferred nothing about what an agent was doing. The alias cleanup is marker-owned, matching the exact line the installer wrote, so a user's own `alias sc=` for another tool is untouched. **CLI polish that came with it.** - New shared style kit (`src/cli-style.ts`) used across the CLI: semantic palette, glyphs, width-aware table, spinner, confirm. - `codeman doctor` is colorized and its table is measured, so the "Antigravity CLI" label no longer pushes its row out of column. `--json` output is unchanged. - `codeman web` no longer prints its "running at" line twice, and the server's non-loopback security warning is painted like the CLI's (chalk degrades off a TTY, so journald and `web.log` stay free of escape codes). - Spinners on the silent up-to-30s waits in `codeman web -d`, `codeman web --stop` and `codeman service install`. - `codeman reset` asks a real y/N confirmation on a TTY; non-interactive callers keep the old `--force` refusal. - `codeman list` and `codeman session list` share one renderer instead of drifting copies. - `codeman attach` is described correctly in the README (it shows an attachment card for a local file). - `test/cli-commands.test.ts` now derives its inventory from the real commander program instead of a hand-written fixture that had drifted. **Internal.** New `tmux -L` callers resolve the socket through `resolveTmuxSocketName()`, now exported from `config/instance.ts`, so a second process can never point a beta instance at prod's panes. CLAUDE.md and `docs/architecture-invariants.md` both record the rule. ### Thanks The TUI went through seven rounds of beta testing over PuTTY/SSH by **@Ark0N**, which is where the way out of an attach, the session strip, the preview repaint handling and the glyph set all came from. ## 1.20.1 ### Patch Changes - Terminal input and scrollback fixes (PRs #327, #331): - IME punctuation preserved (#327): keyCode 229 / `Process` key events are now delegated to xterm's CompositionHelper instead of being suppressed, so an active Chinese IME committing numbers and full-width punctuation (,。!? and friends) reaches the terminal correctly. The CJK input field sends the browser's committed text instead of guessing from `KeyboardEvent.key`, and the redundant Android orphan-input fallback is removed so xterm is the single input owner. - Shell history replay bounded (#331): selecting a Shell session loads a bounded 1 MiB tail instead of replaying the entire multi-megabyte tmux scrollback on xterm's main thread; full history stays available via the explicit "Load full history" action. tmux history limits now apply correctly on both legacy tmux (global default set in the same command queue before pane creation) and tmux 3.7+ (per-pane targeting that never resizes or trims unrelated live panes). Also adds `Server-Timing` and `[TERMINAL-PERF]` timing stages for terminal loads, fixes `scrollToLastNonEmptyLine` double-counting scrollback rows, and keeps live output ordered behind snapshot replays. ### Thanks - @dignfei for both fixes: the IME punctuation root-cause fix (#327) and the bounded shell history replay with the tmux history-limit correctness work (#331). ## 1.20.0 ### Minor Changes - Response viewer for OpenCode, Gemini, Antigravity and Pi sessions (#326). External CLIs render their own TUIs, so the viewer used to come up empty for them; a new transcript parser (`response-viewer-transcript.ts`) reconstructs the conversation from the pane text instead, and the `?context=full` view now tags every block with a role so prompts render as "You" and agent output as the assistant. The divider normalizer was rewritten as a linear scan after review found catastrophic backtracking on agent-controlled input (minutes of stall on a long dash run), with an equivalence corpus pinning the old accept set. CLIs installed via nvm or Homebrew are now found when Codeman runs as a service (#329). A shared resolver falls back to a login-shell probe when the direct PATH lookup misses, so systemd and LaunchAgent installs no longer report every CLI as missing. Review hardening on top: a failed resolution is negative-cached with doubling backoff instead of re-spawning a login shell on every request, all probes pass `killSignal: 'SIGKILL'` (interactive bash shrugs off SIGTERM, and a blocking `.bash_profile` could have hung the server indefinitely), the resolvers are inert under vitest again so test suites cannot execute binaries found on the dev box, and the improved not-found guidance is wired into both the session-create errors and the per-CLI status endpoints. `GET /api/system/repo-status` reports branch, upstream, ahead/behind and remote reachability for git-clone installs (#328). Review hardening: the git network calls moved off the synchronous path onto a single-flight 45s cache (one slow remote could previously freeze the whole server for up to a minute per request), remote URLs and git stderr are credential-redacted before they leave the server, the spawns use the same non-interactive git env as the clone path, and a local-branch upstream no longer parses into garbage. Auto Copy for the terminal (#325, opt-in, per-device): a finished selection (mouse drag, double or triple click, or a phone long-press) lands on the clipboard by itself, so select-then-copy becomes select. Alongside it, hand-encoded tap reports are now gated on the server-observed `cliMouseTracking` state, so a pane that has fallen back to a plain shell no longer receives `[<0;88;20M` junk on tap. The Ralph loop no longer stops polling after two ticks (#330): the reschedule guard read a stale timer handle that the timer callback never cleared, so the loop silently died while its status stayed `running`. The handle is now nulled as the callback's first statement, and a regression test pins the bug. The red "needs you" tab alert clears when a dialog is answered in the terminal instead of surviving until the end of the turn: the post-hook re-capture could erase the parsed dialog options that the staleness sweep relies on (`applyCapture` is now add-only for options), and a delayed staleness pass now runs while a page is open. The unreachable `copyTerminal()` was removed, closing out #322. ### Thanks - @aakhter contributed the external-CLI response viewer (#326), the repo-status endpoint (#328), the login-shell CLI resolution (#329) and the Ralph reschedule fix (#330) - @rounakdatta reported the mobile copy gap (#322) closed out in this release ## 1.19.7 ### Patch Changes - Mobile catches up: links open from a tap, terminal text can be selected and copied, long prompts stay visible while you type. Plus Files panel search, a bundled Nerd Font symbols fallback, and a per-device terminal font setting. - **Terminal and chat links work on phones** (#321): tapping a URL or file path in terminal output now opens it (new tab, file preview, or log viewer), resolved through the same provider desktop hover uses, so tap and click can never disagree about what is a link. Dialog rows and the composer keep their existing meaning. Response-viewer links open in a new tab with `rel="noopener noreferrer"` instead of navigating the dashboard away. Wrapped links open whole: the logical-line reconstruction now stitches hard wraps through the indent their continuation carries, which also fixes desktop hover-click truncating wrapped URLs. - **Terminal text can be copied on touch devices** (#321): long-press selects the token under the finger, drag or tap the other end to extend, and a small bar offers Copy, Line (the whole logical line, wraps included) and dismiss. Copy works on plain-HTTP installs too. Three guards keep the keyboard down and the selection alive through the browser's own long-press handling. - **A long prompt stays visible on phones** (#321): the local-echo overlay grows upward once it would run past the last visible row (a prompt taller than the screen keeps its tail, where the cursor is), and the keyboard-driven padding shrink can no longer reclaim the space the fixed toolbar and accessory bar stand in. - **Files panel search** (#324): `GET /api/sessions/:id/files?q=...` answers a flat match list (name or path substring, `*`/`?` globs), recursing past non-matching directories with its own match cap on top of the existing bounds; without `q` the response is byte-identical to before. Glob queries are matched without regex so a pathological pattern cannot stall the server. - **Nerd Font prompt glyphs out of the box, custom terminal font** (#320): a bundled icons-only Symbols Nerd Font Mono fallback renders powerlevel10k/starship/oh-my-posh glyphs on every device with no font install, and App Settings gains a per-device terminal font family that is prepended to the built-in stack. ### Thanks Three contributor PRs in one release: thanks to @rounakdatta (#321), @aakhter (#324) and @comzine (#320). - 8a54b33: Clear every production-reachable npm advisory, and fix a service-worker caching regression the upgrade exposed. `npm audit` reported 20 advisories, but 16 were devDependencies-only (Remotion, Puppeteer, postcss, the eslint/tsx toolchain) and never reached anyone installing the package. Four reached production and are now resolved: - **`@fastify/static` 9.1.3 to 10.1.3** — GHSA-8pvw-jcv7-9cmj, authorization bypass via non-canonical URL paths. The advisory covers `<=10.1.1`, so the entire 9.x line is affected and the fix only exists on 10.x. - **`find-my-way` 9.6.0 to 9.8.0** — GHSA-c96f-x56v-gq3h (HTTP/2 DDoS). Not exploitable here since Codeman does not enable HTTP/2, fixed anyway. - **`fast-uri` 3.1.2 to 3.1.5** — GHSA-v2hh-gcrm-f6hx, host confusion via a literal backslash authority delimiter. - **`brace-expansion` to 5.0.9 / 1.1.18** — GHSA-3jxr-9vmj-r5cp, exponential-time expansion DoS. The last three were transitive and only needed a lockfile re-resolve; no `overrides` were added. The `@fastify/static` major changes the `setHeaders` callback's first argument from a Node `ServerResponse` to a `FastifyReply`, which required two fixes: - `res.setHeader()` became `reply.header()`. A v9-style body throws `TypeError: res.setHeader is not a function` from inside the plugin on every static request. - **That change also flips precedence, silently.** The callback used to write to the raw response and be overwritten by the route's staged reply headers; it now writes to the reply and wins instead. That handed `/sw.js` a year of `immutable` in place of the `no-cache, no-store` its route sets, which would pin a service worker on every client with no server-side way to recover. A route that already set `Cache-Control` now keeps it. `ws` also appears in `npm audit` but production is already on 8.21.0, outside the vulnerable range; the only affected copy is bundled under `@remotion/renderer` and is dev-only. Adds `test/static-cache-headers.test.ts`, which drives a real server and covers the caching contract that had no test at all, and moves the floors in `test/dependency-security.test.ts` up to the patched versions. ## 1.19.6 ### Patch Changes - Wiki user manual, a phone tab tap-zone fix, per-parent lineage colours, and two robustness fixes. - **Wiki**: `docs/wiki/` is now a 30-page user manual (installation, quick start, the dashboard, agent CLIs, remote/Docker cases, hooks, security, HTTP API, troubleshooting and more), published to the GitHub wiki by a sync workflow on every push that touches it. - **Phone tabs**: on a narrow phone the active tab's geometric centre could land on its gear icon, so a thumb aiming at the tab opened Session Options instead of switching. The active tab's name now reserves a minimum width, and a static test recomputes the clearance from the stylesheet so widening the icons fails there rather than on a phone. - **Lineage lines**: the arcs between a tab and the tabs it spawned are now coloured per SPAWNING tab, so every arc leaving one tab shares a colour and the strip reads as "these came from w1, those from w2". A child that spawns in turn gets its own colour, so a chain changes colour at each generation. - **File access**: `validateSessionFilePath()` now canonicalizes the workspace as well as the candidate path before comparing them. Resolving only the candidate made a workspace reached through a symlink (`/tmp` on macOS, symlinked project dirs, bind-mounted case paths) report a spurious escape and refuse every read and write in that session. Escapes are still refused. - **Respawn**: a cycle step that is stopped mid-write no longer revives the state machine. `stop()` could land during the `await` on the kickstart / update / clear / init write, after which the controller set itself back to a waiting state and kept running. ### Thanks - @aakhter for the symlink-safe workspace confinement fix (#314) and the respawn stop-race fix (#315). - 98e37bf: Session List Layout gains a third option, "Left sidebar", whose rows carry the same per-session detail the home screen shows. The sidebar previously had one row style: a name and a folder. That is the whole story a tab can tell, but a docked column is not a tab strip — it has width to spare and a row per session either way, and the information that was missing is exactly the information the desktop home rail and the phone overview already put on screen. So the new option lifts it onto the rows: when the session was first created, how long it has been in the state it is in, and a status pill naming that state. - The old "Left sidebar" is now **"Left sidebar simple"** and is unchanged, down to the byte — the stored value stays `sidebar`, so anyone already using it keeps exactly the layout they chose. The new option is `sidebar-rich`. - Both sidebar values are the SAME layout and both set `data-session-list="sidebar"`; row detail rides on a separate `data-sidebar-detail` attribute. That is deliberate: every `isSessionSidebarActive()` call site and every `html[data-session-list="sidebar"]` rule in styles.css and mobile.css keeps matching both, untouched. - Which state a session is in, and which stamp measures it, come from `_mobileOverviewState()` / `_mobileOverviewSince()` rather than being re-derived — the sidebar, the home rail and the phone overview cannot disagree about what "working" means. A working row is measured from the turn's last Enter, not from its last repaint, so a running turn reads `working 12m` instead of `0m`. - The stamps refresh in place on a 20s clock instead of re-rendering: a rebuild would restart every load spinner and alert animation in the list, twice a minute. The clock only runs while rich rows are on screen. - The column widens to 300px for the extra line, and the collapsed 44px rail and the handheld drawer are explicitly held back from that width. - 947ff6f: `npm test` is now the CI gate and is safe to run bare; the suites it cannot run each got their own command. `npm test` ran the everything-config, which fails ~87 tests on a clean master on any machine without chromium, a free port and per-machine PNG baselines. That made the repo's most obvious command useless as a pass/fail signal, and the docs had accumulated "never run bare `npm test`" warnings in four files to work around it. It now runs `config/vitest.ci.config.ts` — exactly what CI runs — so local green means CI green. - New: `test:browser` (5 Playwright files), `test:perf` (2 wall-clock benchmarks), `test:all` (the old everything-behaviour, kept reachable). `test:ci` and `test:mobile` are unchanged; `test:watch` and `test:coverage` follow `test` onto the gate's config. - The exclusion list moved to `config/test-suites.ts`, with the reason each suite cannot run in CI. Every config derives from it, so the gate's excludes and the runners' includes cannot drift. - That drift was a silent hole, not a tidiness problem: a file excluded from CI and added to no runner is tested by NOTHING, and every command stays green, because vitest counts "no files matched" as success. `test/test-suite-partition.test.ts` now fails if any test file is reachable by no runner or by two. - ⚠️ A file filter must match its runner: `npm test -- test/mobile/keyboard.test.ts` matches nothing and exits green having run zero tests, because the gate excludes that path. Use `npm run test:mobile -- `. Documented in CLAUDE.md, and the one place that recommended the old form was corrected. - Docs synced: CLAUDE.md, AGENTS.md, .github/CONTRIBUTING.md, both READMEs, and two ci.yml comments that claimed only `test/mobile/**` was excluded (it is three suites, and 5 Playwright files rather than 3). ## 1.19.5 ### Patch Changes - Closing the session you are looking at now always moves you to the next tab. The delete request and its own `session_deleted` broadcast raced each other: the close path selected the next tab, while the broadcast handler cleared the active session and showed the home screen, and whichever ran first decided what you saw. On one build, closing a tab either switched sessions or dumped you on the welcome screen depending on timing. The close now owns that handoff from beginning to end, and the broadcast handler stays out of the way for a close started in that tab. A session deleted from somewhere else still returns you to the home screen, which is the honest answer when what you were looking at was taken away. The next tab is also picked from sessions that still exist, so a stale entry in the tab order can no longer name a tab that is already gone. ## 1.19.4 ### Patch Changes - Only a human opening a session clears its yellow "waiting for input" tab alert. 1.19.2 made that clear durable and cross-device, which also meant the app itself could spend it: restoring your last session on page load, a popped-out window opening its target, and the fallback to another tab after you close the active one all counted as "I checked it", so a yellow tab could clear itself before you ever saw it. Those three app-driven selections are now marked and skip the acknowledgement, so the alert survives until you actually open the session. Everything a human does still clears it, on every surface: tapping a tab, tapping a row on the phone home screen, the keyboard tab shortcuts, and submitting a prompt into the session. The flag defaults to user-initiated, so a selection path nobody marked keeps acknowledging rather than leaving an alert nothing can clear. ## 1.19.3 ### Patch Changes - Red "needs you" tab alerts now follow the dialog instead of the keyboard. Typing in the terminal no longer clears a red alert. It used to clear every pending alert on the device you typed on, but a permission or question dialog ignores keystrokes that are not one of its options, so the dialog was still open and still blocking: the other devices stayed red and a reload brought the red back on the first one. Input now spends the yellow idle alert only, and it does that through the server-side acknowledgement added in 1.19.2, so the clear is durable and reaches every device. A dialog answered in the terminal now clears by itself. Claude Code fires no "permission answered" hook, so the item stayed pending until the whole turn ended, and any page load in between re-armed a red alert for a dialog that was long gone. Listing approvals now re-captures the pane and resolves items whose dialog is no longer on screen, using the same conservative check the answer path already uses: only an item whose original frame parsed numbered options can be dropped this way, so an unreadable capture keeps the alert rather than losing a live one. Measured against a real AskUserQuestion dialog: the stale item cleared 5 seconds ahead of the stop hook that used to be the only signal, while a dialog still on screen survived 11 consecutive listings over 55 seconds untouched. ## 1.19.2 ### Patch Changes - Yellow "waiting for input" tab alerts now stay cleared once you have checked them, on every device. Viewing a session used to clear its idle alert in that browser's memory only. The server-side approval store still held the prompt, so the next page load seeded the alert straight back and a tab you had already checked went yellow again, while your other devices never heard about the click at all. Opening a session now acknowledges its pending idle prompt server-side (`POST /api/approvals/session/:sessionId/viewed`, a new `acknowledgedAt` field on approval items, broadcast as `approval:updated`), so the clear survives reloads and reaches every connected client. Acknowledgement is deliberately not resolution: the prompt is still unanswered, so the item stays in the Approvals Inbox, stays answerable, and stays available as Read My Mind context, it just stops arming the tab alert. Permission and question dialogs are never acknowledged this way, since looking at a dialog does not answer it, so the red "needs you" alert survives being viewed. Clicking the tab you are already on now clears the alert as well; that path returned early before, so an alert armed on the active tab could not be cleared by clicking at all. ## 1.19.1 ### Patch Changes - Follow-up hardening from the 1.19.0 reviews, across all three of that release's areas (#309, #310, #311). Home screens: the activity ordering introduced in 1.19.0 now stays truthful. Hook events push a session state broadcast, so a blocked session ranks by a fresh stamp instead of whatever the page loaded with; a working row with no recorded submit shows the same stamp it sorts by; Alt+1..9 resolves through the live sessions the tabs actually paint, so a stale id in the saved order can no longer shift every number off its target; and the "most recently quiet" ordering survives restarts, since recovery now restores each session's previous activity stamp from state.json instead of restamping everything at boot (previously every deploy flattened the ordering to tab order). Files and sidebar: playable media extensions are pinned to the attachment registry by a parity test, so an in-workspace .m4a/.flac/.opus opens the preview player instead of the log viewer; /etc paths no longer render as links that can only 403; the sidebar session count counts the rows actually on screen (web tabs included, filtered rows excluded) and follows the filter box; connectors re-anchor on incremental renders in sidebar layout; and ~/.claude.json plus ~/.claude/settings(.local).json are blocked from file serving, home-anchored only, so case-level .claude files stay viewable. Workspace hooks: the install-vs-refresh decision is one shared core that every claude create path routes through, so the workspaceHooksEnabled setting now also applies to cron jobs, legacy scheduled runs, and plan-orchestrator one-shots; a shell session in a docker case no longer authors a hooks block; the boot sweep no longer resurrects a deleted workspace as an empty directory; and the statusLine exporter got the same remote-attach and cwd-fallback guards as the hooks install. ## 1.19.0 ### Minor Changes - c01edcb: Add an optional collapsible left session sidebar as an alternative to the header tab strip. With many concurrent sessions the horizontal strip wraps into several rows and stops being scannable. The new layout puts the session list in a vertical `