diff --git a/CLAUDE.md b/CLAUDE.md index 887530bb..1da127de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,6 +95,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Task | Command | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Terminal dashboard | `codeman tui` (`--list` prints the numbered list and exits, `codeman tui ` attaches to row n; both short-circuit before any screen setup). Needs a TTY; without a server it starts attach-only. `docs/tui.md` | | Dev with TLS | `npx tsx src/index.ts web --https` | | Override window title hostname | `npx tsx src/index.ts web --title-hostname ` (default: `os.hostname()` — `codeman:` is used for tab title, title-flash, and OS desktop notification prefix) | | Bind a non-loopback host | `npx tsx src/index.ts web --host 0.0.0.0` (or `-H`; env `CODEMAN_HOST`; default `127.0.0.1`). Without `CODEMAN_PASSWORD` it **starts but warns loudly** — see Common Gotchas + `docs/security-architecture.md` | @@ -145,7 +146,8 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Domain | Key files | Notes | | ---------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| **Entry** | `src/index.ts`, `src/cli.ts`, `daemon-control`, `service-installer`, `config/service-names` | The last three back `web -d` / `service install` | +| **Entry** | `src/index.ts`, `src/cli.ts`, `daemon-control`, `service-installer`, `config/service-names`, `cli-style` | The last three back `web -d` / `service install`; `cli-style` is the shared palette/table/spinner/confirm kit | +| **TUI** | `src/tui/`: `tui-app` ★ + `tui-client` (the only IO) over a pure core (`-model`, `-layout`, `-render`, `-keys`, `-ansi`, `-composer`, `-approvals`, `-digest`, `-sse`, `-types`) | `codeman tui`, a CLIENT of the server, never a second brain. Design doc: `docs/tui-plan.md`; user guide `docs/tui.md` | | **Session** | `src/session.ts` ★, `session-manager`, `session-auto-ops`, `session-cli-builder`, `session-task-cache`, `session-order` (pure), `session-pty-exit-breaker`, `usage-limit-patterns`, `usage-telemetry`; `src/services/unified-session-service.ts` | Pure/unit-tested helpers are split out of `session.ts` on purpose | | **Mux** | `src/mux-interface.ts`, `src/mux-factory.ts`, `src/tmux-manager.ts` ★ | | | **Respawn** | `src/respawn-controller.ts` ★ + 4 helpers (`-adaptive-timing`, `-health`, `-metrics`, `-patterns`) | Read `docs/respawn-state-machine.md` first | diff --git a/README.md b/README.md index 56488ce5..8d9070a0 100644 --- a/README.md +++ b/README.md @@ -658,6 +658,22 @@ These run for **every** request — before auth, even on the default no-password --- +## Terminal UI (`codeman tui`) + +A full-screen dashboard for your sessions, in the terminal. Same states as the web UI, because it is a client of the same server: + +```bash +codeman tui # the dashboard +codeman tui --list # numbered session list, then exit (scriptable) +codeman tui 2 # attach straight to session 2 of that list +``` + +Sessions are grouped **NEEDS YOU → WORKING → IDLE → RECENT**, longest-waiting first. `↑↓`/`j`/`k` select, `Enter` attaches into the tmux pane (`Ctrl+B D` to come back), `1`-`9` jump-attach. `y`/`n`/digit answer a pending permission dialog right from the list, `p` sends a one-line prompt, `n` starts a session, `x` kills one after a typed confirmation, `/` searches, `g` shows the away digest, `?` is help, `q` quits. Below 72 columns it drops the preview pane and becomes a single-column list, so it stays usable in Termius on a phone. With no server running it still starts in attach-only degraded mode. + +The web UI remains the primary surface; see **[docs/tui.md](docs/tui.md)** for the full guide. + +--- + ## SSH Alternative (`sc`) If you prefer SSH (Termius, Blink, etc.), the `sc` command is a thumb-friendly session chooser: @@ -894,6 +910,8 @@ codeman session list # list sessions codeman session logs # tail output codeman task add "fix the failing test" # (t) queue a task codeman attach # show an attachment card for a local file +codeman tui --list # numbered session list (plain text when piped) +codeman tui 3 # attach to session 3 of that list ``` ### Hooks (events flowing _back_ to Codeman) diff --git a/docs/tui-plan.md b/docs/tui-plan.md index d42a5aa9..3af9fe4c 100644 --- a/docs/tui-plan.md +++ b/docs/tui-plan.md @@ -1,6 +1,11 @@ # Codeman TUI Rework Plan -Status: PROPOSED (research done, nothing implemented). Owner review needed on the open questions at the bottom. +Status: **phases 0-2 implemented** on `feat/tui`; phases 3-4 remain follow-ups. The user guide is [`docs/tui.md`](tui.md); this document stays the design record. + +- Phase 0: `src/cli-style.ts` (palette, glyphs, `heading`/`kv`/`table`/`spinner`/`confirm`) plus the mechanical fixes of §5, and `test/cli-commands.test.ts` now derives its inventory from the real commander `program` instead of parsing a fixture. +- Phases 1-2: `src/tui/`. `tui-app.ts` (main loop, attach handoff, verbs) and `tui-client.ts` (API, SSE, degraded enumeration) are the only IO; `tui-model`, `tui-layout`, `tui-render`, `tui-keys`, `tui-ansi`, `tui-composer`, `tui-approvals`, `tui-digest`, `tui-sse` and `tui-types` are pure and unit-tested, with an E2E suite driving the real binary under node-pty. +- Deferred with the rest of phase 3: `r` (resume a RECENT row) is not wired up, so the help overlay does not advertise it. +- Not started: phase 3 (mouse, `--pick` popup switcher, opt-in attach status line, OSC 9) and phase 4 (retiring the bash choosers). The goal: replace Codeman's scattered terminal surfaces with one first-class TUI, `codeman tui`, that gives SSH/terminal users the same at-a-glance awareness the web UI gives browsers. The reference point is herdr (herdr.dev), the trending Rust "agent multiplexer" whose defining feature is a live agent-state sidebar. Codeman can match and beat that sidebar in the terminal because the states herdr infers from screen-scraping heuristics are states our server already computes from hooks, pane probing, and the approvals inbox. diff --git a/docs/tui.md b/docs/tui.md new file mode 100644 index 00000000..97a39321 --- /dev/null +++ b/docs/tui.md @@ -0,0 +1,238 @@ +# Terminal UI (`codeman tui`) + +`codeman tui` is a full-screen dashboard for your Codeman sessions, in the terminal. +It shows every session grouped by whether it needs you, lets you answer a permission +dialog or send a prompt without switching anywhere, and puts you inside a session's +tmux pane with one keystroke. + +It is **additional, not a replacement**: the web UI stays the primary surface and +gets every feature first. The TUI exists for the terminal workflow (SSH, Termius, +a tmux window you keep open all day), and it is a *client* of the running server, +so the two surfaces can never disagree about what a session is doing. It is also +not a multiplexer: tmux still owns every pane, and attaching hands the terminal to +tmux rather than proxying bytes. + +## Starting it + +```bash +codeman tui # the dashboard +codeman tui --list # print the numbered session list and exit +codeman tui 2 # attach straight to session 2 of that list +``` + +The two fast paths are the scriptable ones (they are the `sc -l` / `sc 2` shapes). +Neither sets up a screen, so both are as quick as the one API call they make, and +`--list` prints plain text when piped, so it composes with `grep`/`awk`. + +What it needs: + +| Needs | What you get | +| --- | --- | +| **Full features** | A running Codeman server (states, approvals, preview, prompts, search, digest). The TUI finds it the way `codeman attach` does: `CODEMAN_API_URL`, else loopback on `CODEMAN_PORT` for this `CODEMAN_INSTANCE`. The self-signed certificate an `--https` install generates is accepted, as it is everywhere else in the CLI. | +| **Server down** | It still starts, in **degraded mode**: sessions are enumerated straight from `tmux -L codeman` plus a read-only peek at `state.json`, and attach is the only verb. See [Troubleshooting](#troubleshooting). | +| **A terminal** | `codeman tui` refuses to run when stdin/stdout are not a TTY, and says to use `--list` instead. A cron job or a pipe therefore fails loudly rather than emitting escape codes into a log. | + +## What it looks like + +A real frame at 100x30 (`NO_COLOR`, trailing blank rows trimmed). The selected +session has a pending permission dialog, so the preview pane leads with the card: + +``` + codeman ⚠ 2 tnode · v1.19.0 · 5 sessions · 5h 32% · wk 61% ? help q quit + NEEDS YOU ─────────────────────────│ w4-api-refactor · claude · /home/you/dev/api · blocked + 1 w6-docs ✋ 11m│ ⚠ requests: Bash(git push origin main) +▶ 2 w4-api-refactor ⚠ 2m│ 1. Yes + WORKING ───────────────────────────│ 2. Yes, and do not ask again + 3 w1-codeman ∗ 1h│ 3. No, tell Claude what to do + 4 w2-gallery ∗ 15m│ y approve · n deny · digit chooses + IDLE ──────────────────────────────│ + 5 w3-promo shell ○ 2h│ > refactor the api routes onto the shared port interface + RECENT ────────────────────────────│ + 6 api-hotfix ✔ 3d│ Read src/web/ports/session-port.ts (48 lines) + │ Read src/api/routes.ts (312 lines) + │ Edit src/api/routes.ts + │ 1 -import { SessionManager } from "../session-manager.js"; + │ 2 +import type { SessionPort } from "../web/ports/session- + │ + │ Bash(npm run typecheck) + │ └ tsc --noEmit: no errors + │ + │ ✻ Actualizing… (2m 14s · ↓ 12.3k tokens) + ↑↓ select · ⏎ attach · y approve · n deny · 1-9 option · p prompt · x kill · / search · g digest · +``` + +- **Header**: the machine, the server version, how many sessions are live, and the + plan-usage chip (the same statusLine telemetry that feeds the web chip, when the + server has a snapshot). A `⚠ n` badge counts pending approvals. +- **Sidebar**: every session, grouped and numbered. +- **Preview**: a live tail of the selected session, its own colors preserved, with + the parsed dialog card on top when that session is blocked. +- **Footer**: only the keys that work right now. `n` reads `n new` normally and + `n deny` when the selected session has a dialog, because it cannot be both. + +The same world through `--list`: + +``` + 1 waiting w6-docs /home/you/dev/docs + 2 blocked w4-api-refactor /home/you/dev/api + 3 working w1-codeman /home/you/dev/codeman + 4 working w2-gallery /home/you/dev/gallery + 5 idle w3-promo /home/you/dev/promo + 6 done api-hotfix /home/you/dev/api +``` + +The numbers are the same on both surfaces, so `codeman tui --list` then +`codeman tui 4` is one thought. + +## The four groups + +Groups are always in this order, and a session is in exactly one of them: + +| Group | Glyph | Means | Comes from | +| --- | --- | --- | --- | +| **NEEDS YOU** | `⚠` | A permission or question dialog is blocking the agent | The approvals inbox (`permission_prompt` hooks, with the on-screen options parsed) | +| | `✋` | Waiting for your next instruction, or errored | `idle_prompt`, or an errored session (equally something only a human clears) | +| **WORKING** | `✻` animating | A turn is running | The same working classification the web dashboard uses | +| **IDLE** | `○` | Live, but sitting there | | +| **RECENT** | `✔` | A past session from the unified list | History rows, no live pane | + +Ordering inside a group is "the one that has waited longest, first": blocked +sessions sort by how long the dialog has been up, working sessions by when their +turn started (the pane's last Enter, since a working pane repaints every second +and would otherwise always look freshly started), and quiet ones by last activity. +That is the ordering the web home screens already use. + +The cursor sticks to a **session**, not a row number, so a session that jumps to +NEEDS YOU does not drag your selection with it. The number beside each row is what +`1-9` and `codeman tui ` mean, and it is renumbered on every re-sort. + +When a new dialog appears, the terminal bell rings once, for that dialog only: the +same item announced twice does not ring twice. + +## Keymap + +| Key | Does | +| --- | --- | +| `↑` `↓` or `j` `k` | Move the cursor. PageUp/PageDown jump five rows. | +| `Enter` | Attach to the selected session (see [Attaching](#attaching)) | +| `1`-`9` | Jump to that row and attach. When a dialog is on screen, a digit answers it instead (see below). | +| `y` | Approve the selected session's dialog | +| `n` | Deny it, or **start a new session** when there is no dialog | +| `p` | Send one line to the selected session without attaching | +| `x` | Kill the selected session, with a typed confirmation | +| `/` | Search sessions, events and files | +| `g` | Away digest: what happened while you were gone | +| `?` | Help overlay | +| `Esc` | Close whatever overlay is open | +| `q` or `Ctrl+C` | Quit, restoring the screen you started with | + +Inside the `p` composer and the `/` query: `←` `→` `Home` `End` `Delete` +`Backspace` plus `Ctrl+A` / `Ctrl+E` / `Ctrl+U` / `Ctrl+W`, `Enter` to send or open, +`Esc` (or `Ctrl+C`) to cancel. In the kill confirmation you retype the session name; +anything else cancels. In the `n` pickers, type to filter, `Enter` chooses. + +Verbs that need the server (`y`/`n`/`p`/`x`/`/`/`g`) say so in degraded mode +instead of failing silently; `Enter` and `1-9` keep working. + +### `p` sends exactly one line + +The composer is a single line by design, ending in a carriage return: that is the +input contract every Codeman path follows, because multi-line text breaks the +agent's own composer. Pasted newlines become spaces rather than being rejected, so +a paste cannot silently run a different command than the one you read. + +## Answering approvals + +This is the thing the terminal could not do before. Select a blocked session and: + +- `y` approves. +- `n` picks the parsed "No" option, or sends Esc when the dialog did not parse one. +- A digit picks that numbered option, **but only a digit the dialog actually + offers**. A digit with no matching option falls through to the list's own + jump-and-attach binding, so it can never be typed at whatever has focus. + +The answer goes through `POST /api/approvals/:id/answer`, which **re-captures the +pane before it types anything**. If the dialog is no longer on screen (you answered +it in tmux a moment ago, or the agent moved on), the server refuses with a 409 and +the TUI says `that dialog is no longer on screen` rather than pressing a key into a +live composer. The answer is scoped to the options the server parsed off the actual +frame, never to a guess. + +An idle prompt (`✋`) is not a dialog: there is nothing to approve, so `p` is the +reply path and the footer says `p reply` instead of `p prompt`. + +## Attaching + +`Enter` suspends the dashboard (main screen back, cooked mode back) and hands the +terminal to tmux with `stdio: inherit`. Colors, mouse and paste are tmux's, at full +fidelity. Detach with **`Ctrl+B D`** (tmux's default prefix, which Codeman does not +change for local sessions) and the dashboard comes back and refreshes. + +Three cases: + +| Where you are | What happens | +| --- | --- | +| Not in tmux | `tmux -L codeman attach-session` | +| Already in tmux on Codeman's socket | `switch-client`, so you do not nest | +| In tmux on a **different** socket | Refused, with an explanation: detach first (`Ctrl+B D`), then run `codeman tui` again | + +A RECENT row and a direct-PTY session have no pane to attach to, and say so. + +`x` never bulk-kills: it kills one session, only after you retype its name, never a +history row, and never the session the TUI itself is running in. + +## Over SSH, and on a phone + +The TUI is an ordinary terminal program with no local dependencies beyond tmux, so +`ssh box` then `codeman tui` works exactly like running it locally. There is no +separate remote mode. + +Below 72 columns (Termius, an iPhone in portrait) the preview pane is dropped and +rows take two lines each: the same constraint the `sc` chooser was built around, +now with a cursor, live states and the answer/prompt/kill verbs. The switch is +width-driven at draw time, so unfolding a foldable or resizing a window re-lays out +immediately; there is no mode flag to set. + +## Troubleshooting + +**"The Codeman server rejected these credentials."** The server has +`CODEMAN_PASSWORD` set. Export `CODEMAN_PASSWORD` (and `CODEMAN_USERNAME` if it is +not `admin`), or put them in the data dir's `.env` (`~/.codeman/.env`), which is +where `codeman attach` already reads them from. + +**`server not running: attach only`** in a yellow banner. Nothing answered on the +expected port, so the TUI fell back to enumerating tmux. You get names and attach; +you do not get states, approvals or previews, because those only exist on the +server. Start the server (`codeman web -d`, or `systemctl --user start codeman-web`) +and the banner clears on its own: the TUI keeps re-probing. + +**It found the wrong server, or none.** Discovery is instance-scoped. A beta +instance (`CODEMAN_INSTANCE=beta`) has its own data dir *and* its own tmux socket, +so its TUI sees only its own sessions. Set `CODEMAN_PORT` or `CODEMAN_API_URL` +explicitly when you run more than one. + +**"this terminal is already inside tmux on socket ..."** You are in a tmux session +on a socket that is not Codeman's, so attaching would nest two multiplexers whose +prefix keys collide. Detach (`Ctrl+B D`) and run `codeman tui` from outside. + +**Boxes and glyphs render as garbage.** The TUI picks a glyph tier from the +environment: no `TERM` (or `dumb`), or a non-UTF-8 locale, gets the ASCII set +(`[!] [w] [*] [-]`, `+`/`-`/`|` frames). Force it either way with +`CODEMAN_TUI_GLYPHS=ascii|unicode|nerd`. + +**Colors.** Standard `NO_COLOR` / `FORCE_COLOR` handling (chalk's, the same as the +rest of the CLI). Under `NO_COLOR` the frame is cursor addressing and text only, +and the preview's own colors are stripped too, so a session's output cannot repaint +the dashboard. + +**It refuses to open at all**, saying it needs an interactive terminal. stdout or +stdin is not a TTY. That is the guard: use `codeman tui --list`. + +## Related + +- [`docs/tui-plan.md`](tui-plan.md): the design record. Why hand-rolled ANSI, why a + client and not a second brain, and what is deliberately deferred. +- [`docs/approvals-inbox-plan.md`](approvals-inbox-plan.md): where the parsed + dialogs and the answer endpoint come from. +- [`docs/remote-sessions.md`](remote-sessions.md): remote-SSH cases, which the TUI + lists like any other session.