diff --git a/.changeset/7ab53c88.md b/.changeset/7ab53c88.md new file mode 100644 index 00000000..7679bcf4 --- /dev/null +++ b/.changeset/7ab53c88.md @@ -0,0 +1,21 @@ +--- +"aicodeman": patch +--- + +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. diff --git a/.changeset/fix-claude-truecolor-in-panes.md b/.changeset/fix-claude-truecolor-in-panes.md index 593de379..32ff6988 100644 --- a/.changeset/fix-claude-truecolor-in-panes.md +++ b/.changeset/fix-claude-truecolor-in-panes.md @@ -4,24 +4,36 @@ 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 inside a Codeman -pane that block was invisible. tmux hands each pane `TERM=screen`, which supports-color reads -as 16 colors, and the registry entry for Claude deleted `COLORTERM` on top of that. Claude -therefore quantized every RGB color its theme asked for down to the basic palette, where -`rgb(55, 55, 55)` and every other dark background becomes `ESC[40m` — the terminal's own -black. A custom Claude theme could change the color and nothing on screen moved. +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. -Claude now exports `COLORTERM=truecolor`, which is what codex, gemini, antigravity, pi, grok, -deepseek and omp already do. Those seven 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. +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. A remote pane still exports nothing — `buildRemoteLaunchCommand()` -never carried these declarations — so an SSH-remote Claude session keeps the old rendering. +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 — +`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. 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. +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. diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index 031b38f0..e03f883e 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -18,7 +18,7 @@ Implementation detail extracted from `CLAUDE.md` so that file stays small enough ## Session launch modes -**Terminal colour env** — the stock registry decides each CLI's colour vars. Claude, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek and OMP export `COLORTERM=truecolor`; `shell` and `opencode` unset it. All of those except Claude also unset `NO_COLOR`, so a user who exports `NO_COLOR` globally keeps monochrome Claude panes. The variable matters because tmux gives each pane `TERM=screen`, which supports-color reads as 16 colors: a CLI inheriting no `COLORTERM` quantizes every RGB color it draws down to the basic palette, and each dark background lands on `ESC[40m`, the terminal's own black. ⚠️ These declarations reach the local tmux pane via `buildEnvExports()`, its attach client via `cliExportsTruecolor()`, and the direct-PTY fallback via `buildClaudeEnv()` — they do NOT reach a remote pane, which `buildRemoteLaunchCommand()` builds with no env exports at all. A Docker pane takes `COLORTERM=truecolor` from the hardcoded `envCreate`/`execEnv` in `tmux-manager.ts`, which apply to every mode including the two the registry says must unset it. A `~/.codeman/clis.json` override replaces these arrays wholesale (`deepMerge`), so a custom entry can drop either list. +**Terminal colour env** — the stock registry decides each CLI's colour vars. Claude, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek and OMP export `COLORTERM=truecolor`; `shell` and `opencode` unset it. All of those except Claude also unset `NO_COLOR`, so a user who exports `NO_COLOR` globally keeps monochrome Claude panes. The variable matters because a CLI inheriting no `COLORTERM` quantizes every RGB color it draws down to whatever palette `TERM` alone implies, and the pane's `TERM` is not a constant. ⚠️ Codeman sets no `default-terminal` and passes tmux no `-f`, so it is tmux's default (`tmux-256color` since 3.2) unless the user's own `~/.tmux.conf` says otherwise — which Codeman's server DOES read. At `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 named. Where `TERM` resolves to a 16-color entry instead (tmux older than 3.2, or a conf setting `default-terminal screen`) every dark background collapses to `ESC[40m`, the terminal's own black, and the block disappears entirely — which is why the same Claude theme looks different on two machines, and why a bug report here is worth pairing with the reporter's `tmux -V` and their pane's real `TERM`. ⚠️ These declarations reach the local tmux pane via `buildEnvExports()`, its attach client via `cliExportsTruecolor()`, and the direct-PTY fallback via `buildClaudeEnv()` — they do NOT reach a remote pane, which `buildRemoteLaunchCommand()` builds with no env exports at all. A Docker pane takes `COLORTERM=truecolor` from the hardcoded `envCreate`/`execEnv` in `tmux-manager.ts`, which apply to every mode including the two the registry says must unset it. A `~/.codeman/clis.json` override replaces these arrays wholesale (`deepMerge`), so a custom entry can drop either list — which is why both consumers apply the entry BEFORE Codeman's own variables rather than after: `buildEnvExports()` emits `...cliEnv` ahead of `export CODEMAN_MUX=1`, and `buildClaudeEnv()` assigns `PATH`/`TERM`/`CODEMAN_*` after its unset/export loop. Reversed, a config-supplied `unset` naming `CODEMAN_HOOK_SECRET_FILE` would strip it on one path and not the other. ### External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP) diff --git a/src/session-cli-builder.ts b/src/session-cli-builder.ts index 904e1dd5..f4807a23 100644 --- a/src/session-cli-builder.ts +++ b/src/session-cli-builder.ts @@ -170,21 +170,17 @@ export function buildClaudeEnv(sessionId: string): Record