From 7767b16d4f2432dbabef996094b385a1f10f8318 Mon Sep 17 00:00:00 2001 From: Michael Grundberg Date: Sun, 13 Sep 2026 12:15:28 +0200 Subject: [PATCH 1/2] 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 Claude's registry entry 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. Changing the color in a custom Claude theme moved nothing on screen. Claude now exports COLORTERM=truecolor and unsets NO_COLOR, matching codex, gemini, antigravity, pi, grok and omp. CLAUDECODE stays unset, because Claude reads it as a signal that it is running nested inside itself. Both the tmux session and the attach client read this one registry entry, so they cannot disagree. PR #3 introduced the unset 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 already sets terminal-overrides ",*:Tc" on its own tmux server, so 24-bit color reaches the browser today for every CLI that asks for it. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/fix-claude-truecolor-in-panes.md | 21 +++++++++++++++++++++ docs/architecture-invariants.md | 2 +- src/config/cli-registry/stock.ts | 13 +++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-claude-truecolor-in-panes.md diff --git a/.changeset/fix-claude-truecolor-in-panes.md b/.changeset/fix-claude-truecolor-in-panes.md new file mode 100644 index 000000000..71ea5ab1f --- /dev/null +++ b/.changeset/fix-claude-truecolor-in-panes.md @@ -0,0 +1,21 @@ +--- +"aicodeman": patch +--- + +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 now exports `COLORTERM=truecolor` and unsets `NO_COLOR`, which is what codex, gemini, +antigravity, pi, grok and omp already do. `CLAUDECODE` stays unset, because Claude reads it +as a signal that it is running nested inside itself. + +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 six CLIs that ask for it. diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index 238ccb0a7..8b22ae882 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -20,7 +20,7 @@ Implementation detail extracted from `CLAUDE.md` so that file stays small enough ### External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP) -**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini' || 'antigravity' || 'pi' || 'grok' || 'deepseek'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). ⚠️ **Work detection left this gate in #385** and is now per-CLI `capabilities.workDetect` data (`promptGlyph` + `workingLine`), because gating it on the mode left every Codex session reporting `idle` for its entire life; a CLI declaring neither falls back to Claude's pair, which is logic-identical to the pre-registry behaviour. All seven modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode ` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Codex AND Gemini export `COLORTERM=truecolor` + unset `NO_COLOR` (other modes unset `COLORTERM`); Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Antigravity specifics: command built by `buildAntigravityCommand()` (`--model`, `--conversation ` resume, `--dangerously-skip-permissions` from the `antigravityConfig` payload); availability via `GET /api/antigravity/status` — routes fail with `OPERATION_FAILED` + install hint (`curl -fsSL https://antigravity.google/cli/install.sh | bash`) when missing. Unlike the other three it is NOT an npm package (standalone binary, `~/.local/bin/agy`), which is why `docker/agent.Dockerfile` installs it with its own `--dir /usr/local/bin` step rather than in the `npm install -g` line, and why it does NOT join `isAltScreenStripMode()`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Agents & CLIs → Codex; Respawn/Ralph options are Claude-only, so session options open on the Session tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM). Grok specifics: command built by `buildGrokCommand()` (`--always-approve` from `grokConfig.alwaysApprove` — grok's `bypassPermissions` permission mode, deny rules still apply; `--model`; `--resume ` / `--continue`, id-regexed so grok's resume-by-TITLE feature can never put an arbitrary string on the spawn line); availability via `GET /api/grok/status`, which carries `version` because the resolver version-probes candidates (`grok` has npm squatters, e.g. @vibe-kit/grok-cli — `GROK_VERSION_REGEX` is shared with the dependency registry so doctor and run mode agree). Like antigravity it is a standalone binary (xAI installer → `~/.grok/bin`, symlinked into `~/.local/bin`), so `docker/agent.Dockerfile` installs it in its own step (copy to `/usr/local/bin`, drop root's `~/.grok` in the same layer) and it stays OUT of `isAltScreenStripMode()` (fullscreen alt-screen TUI with mouse support — the opencode case, not the Ink case). Env allowlist: `GROK_*` plus the vendor namespace `XAI_*` (`XAI_API_KEY` is grok's documented headless auth var — the same narrow-vendor-namespace reasoning as `GOOGLE_*` for gemini). Docker cred seeding is per-file (`auth.json`, `config.toml`, `pager.toml` from `~/.grok` — the dir also holds `sessions/`, `memory/`, and the ~160MB binary under `downloads/`). Grok tests: `test/grok-mode.test.ts`, `test/grok-cli-resolver.test.ts`. +**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini' || 'antigravity' || 'pi' || 'grok' || 'deepseek'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). ⚠️ **Work detection left this gate in #385** and is now per-CLI `capabilities.workDetect` data (`promptGlyph` + `workingLine`), because gating it on the mode left every Codex session reporting `idle` for its entire life; a CLI declaring neither falls back to Claude's pair, which is logic-identical to the pre-registry behaviour. All seven modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode ` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Every CLI except `shell` and `opencode` exports `COLORTERM=truecolor` and unsets `NO_COLOR`. tmux gives each pane `TERM=screen`, which supports-color reads as 16 colors, so a CLI that inherits no `COLORTERM` quantizes every RGB color it draws down to the basic palette and loses each dark background to `ESC[40m`, the terminal's own black. Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Antigravity specifics: command built by `buildAntigravityCommand()` (`--model`, `--conversation ` resume, `--dangerously-skip-permissions` from the `antigravityConfig` payload); availability via `GET /api/antigravity/status` — routes fail with `OPERATION_FAILED` + install hint (`curl -fsSL https://antigravity.google/cli/install.sh | bash`) when missing. Unlike the other three it is NOT an npm package (standalone binary, `~/.local/bin/agy`), which is why `docker/agent.Dockerfile` installs it with its own `--dir /usr/local/bin` step rather than in the `npm install -g` line, and why it does NOT join `isAltScreenStripMode()`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Agents & CLIs → Codex; Respawn/Ralph options are Claude-only, so session options open on the Session tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM). Grok specifics: command built by `buildGrokCommand()` (`--always-approve` from `grokConfig.alwaysApprove` — grok's `bypassPermissions` permission mode, deny rules still apply; `--model`; `--resume ` / `--continue`, id-regexed so grok's resume-by-TITLE feature can never put an arbitrary string on the spawn line); availability via `GET /api/grok/status`, which carries `version` because the resolver version-probes candidates (`grok` has npm squatters, e.g. @vibe-kit/grok-cli — `GROK_VERSION_REGEX` is shared with the dependency registry so doctor and run mode agree). Like antigravity it is a standalone binary (xAI installer → `~/.grok/bin`, symlinked into `~/.local/bin`), so `docker/agent.Dockerfile` installs it in its own step (copy to `/usr/local/bin`, drop root's `~/.grok` in the same layer) and it stays OUT of `isAltScreenStripMode()` (fullscreen alt-screen TUI with mouse support — the opencode case, not the Ink case). Env allowlist: `GROK_*` plus the vendor namespace `XAI_*` (`XAI_API_KEY` is grok's documented headless auth var — the same narrow-vendor-namespace reasoning as `GOOGLE_*` for gemini). Docker cred seeding is per-file (`auth.json`, `config.toml`, `pager.toml` from `~/.grok` — the dir also holds `sessions/`, `memory/`, and the ~160MB binary under `downloads/`). Grok tests: `test/grok-mode.test.ts`, `test/grok-cli-resolver.test.ts`. **DeepSeek Harness (`dsh`) specifics** — the mode that breaks three of the assumptions the six above share, so read this before changing anything about it. diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index e75428dae..a03d47689 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -174,8 +174,17 @@ const CLAUDE: CliEntry = { legacyConfigAliases: { resumeId: 'resumeSessionId' }, }, env: { - exports: [], - unset: ['CLAUDECODE', 'COLORTERM'], + // Claude joins the truecolor list with COLORTERM=truecolor and NO_COLOR unset. + // tmux hands the pane TERM=screen, which supports-color reads as 16 colors, and + // Claude then quantizes every RGB color its theme asks for down to that palette. + // Each dark background lands on ESC[40m, the terminal's own black, so the block + // Claude draws behind the user's own messages renders invisible. PR #3 unset + // COLORTERM here against xterm.js#484, which xterm.js had already closed in 2019, + // and Codeman now ships @xterm/xterm 6 and sets `terminal-overrides *:Tc` itself. + // CLAUDECODE stays unset, because Claude reads it as a signal that it is running + // nested inside itself. + exports: [{ name: 'COLORTERM', value: 'truecolor' }], + unset: ['CLAUDECODE', 'NO_COLOR'], tmuxSetenvKeys: [], dockerExecEnvNames: [], allowedPrefixes: ['CLAUDE_CODE_'], From dae2ac580f2a169f6648e4d22c575f09f64fd11e Mon Sep 17 00:00:00 2001 From: Michael Grundberg Date: Sun, 13 Sep 2026 14:42:30 +0200 Subject: [PATCH 2/2] fix(terminal): read the colour env from the registry on every local spawn path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildClaudeEnv(), the direct-PTY fallback taken when mux creation fails, now reads getCli('claude').env and applies its unset and exports lists. It used to delete COLORTERM and CLAUDECODE from a hand-maintained list of its own, which left it contradicting the registry entry that the tmux pane and the attach client both read. An engine value needing a mux name has nothing to resolve against on this path, so it is skipped rather than guessed. Claude no longer unsets NO_COLOR. The invisible-background bug does not need it, and unsetting it overrides a preference the user set deliberately, so a user who exports NO_COLOR globally keeps monochrome panes. The other seven truecolor CLIs still unset it; that inconsistency is intentional and the comment on the entry says so. The invariants doc gains a Terminal colour env paragraph under Session launch modes, where a reader looking up Claude will find it — the previous sentence sat under a heading that lists only the non-Claude CLIs. It now says the lists are the stock catalog and a clis.json override replaces them wholesale, and that the declarations reach the tmux pane, its attach client and the direct PTY but not a remote pane, whose command carries no env exports at all. Docker hands COLORTERM=truecolor to every mode, including the two the registry says must unset it. The changeset named six peer CLIs and there are seven: deepseek also exports truecolor. A test beside the existing OpenCode assertion pins the new behaviour, so a future registry edit cannot make the backgrounds vanish again in silence. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/fix-claude-truecolor-in-panes.md | 14 ++++++++++---- docs/architecture-invariants.md | 4 +++- src/config/cli-registry/stock.ts | 6 ++++-- src/session-cli-builder.ts | 21 +++++++++++++++++++-- test/tmux-manager.test.ts | 7 +++++++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.changeset/fix-claude-truecolor-in-panes.md b/.changeset/fix-claude-truecolor-in-panes.md index 71ea5ab1f..593de379a 100644 --- a/.changeset/fix-claude-truecolor-in-panes.md +++ b/.changeset/fix-claude-truecolor-in-panes.md @@ -11,11 +11,17 @@ therefore quantized every RGB color its theme asked for down to the basic palett `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 now exports `COLORTERM=truecolor` and unsets `NO_COLOR`, which is what codex, gemini, -antigravity, pi, grok and omp already do. `CLAUDECODE` stays unset, because Claude reads it -as a signal that it is running nested inside itself. +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. + +`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. 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 six CLIs that ask for it. +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 8b22ae882..031b38f02 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -18,9 +18,11 @@ 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. + ### External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP) -**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini' || 'antigravity' || 'pi' || 'grok' || 'deepseek'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). ⚠️ **Work detection left this gate in #385** and is now per-CLI `capabilities.workDetect` data (`promptGlyph` + `workingLine`), because gating it on the mode left every Codex session reporting `idle` for its entire life; a CLI declaring neither falls back to Claude's pair, which is logic-identical to the pre-registry behaviour. All seven modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode ` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Every CLI except `shell` and `opencode` exports `COLORTERM=truecolor` and unsets `NO_COLOR`. tmux gives each pane `TERM=screen`, which supports-color reads as 16 colors, so a CLI that inherits no `COLORTERM` quantizes every RGB color it draws down to the basic palette and loses each dark background to `ESC[40m`, the terminal's own black. Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Antigravity specifics: command built by `buildAntigravityCommand()` (`--model`, `--conversation ` resume, `--dangerously-skip-permissions` from the `antigravityConfig` payload); availability via `GET /api/antigravity/status` — routes fail with `OPERATION_FAILED` + install hint (`curl -fsSL https://antigravity.google/cli/install.sh | bash`) when missing. Unlike the other three it is NOT an npm package (standalone binary, `~/.local/bin/agy`), which is why `docker/agent.Dockerfile` installs it with its own `--dir /usr/local/bin` step rather than in the `npm install -g` line, and why it does NOT join `isAltScreenStripMode()`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Agents & CLIs → Codex; Respawn/Ralph options are Claude-only, so session options open on the Session tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM). Grok specifics: command built by `buildGrokCommand()` (`--always-approve` from `grokConfig.alwaysApprove` — grok's `bypassPermissions` permission mode, deny rules still apply; `--model`; `--resume ` / `--continue`, id-regexed so grok's resume-by-TITLE feature can never put an arbitrary string on the spawn line); availability via `GET /api/grok/status`, which carries `version` because the resolver version-probes candidates (`grok` has npm squatters, e.g. @vibe-kit/grok-cli — `GROK_VERSION_REGEX` is shared with the dependency registry so doctor and run mode agree). Like antigravity it is a standalone binary (xAI installer → `~/.grok/bin`, symlinked into `~/.local/bin`), so `docker/agent.Dockerfile` installs it in its own step (copy to `/usr/local/bin`, drop root's `~/.grok` in the same layer) and it stays OUT of `isAltScreenStripMode()` (fullscreen alt-screen TUI with mouse support — the opencode case, not the Ink case). Env allowlist: `GROK_*` plus the vendor namespace `XAI_*` (`XAI_API_KEY` is grok's documented headless auth var — the same narrow-vendor-namespace reasoning as `GOOGLE_*` for gemini). Docker cred seeding is per-file (`auth.json`, `config.toml`, `pager.toml` from `~/.grok` — the dir also holds `sessions/`, `memory/`, and the ~160MB binary under `downloads/`). Grok tests: `test/grok-mode.test.ts`, `test/grok-cli-resolver.test.ts`. +**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini' || 'antigravity' || 'pi' || 'grok' || 'deepseek'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). ⚠️ **Work detection left this gate in #385** and is now per-CLI `capabilities.workDetect` data (`promptGlyph` + `workingLine`), because gating it on the mode left every Codex session reporting `idle` for its entire life; a CLI declaring neither falls back to Claude's pair, which is logic-identical to the pre-registry behaviour. All seven modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode ` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Codex, Gemini, Antigravity, Pi, Grok, DeepSeek and OMP export `COLORTERM=truecolor` and unset `NO_COLOR`; `opencode` unsets `COLORTERM`. **Terminal colour env** under Session launch modes covers Claude and says which panes those declarations actually reach. Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Antigravity specifics: command built by `buildAntigravityCommand()` (`--model`, `--conversation ` resume, `--dangerously-skip-permissions` from the `antigravityConfig` payload); availability via `GET /api/antigravity/status` — routes fail with `OPERATION_FAILED` + install hint (`curl -fsSL https://antigravity.google/cli/install.sh | bash`) when missing. Unlike the other three it is NOT an npm package (standalone binary, `~/.local/bin/agy`), which is why `docker/agent.Dockerfile` installs it with its own `--dir /usr/local/bin` step rather than in the `npm install -g` line, and why it does NOT join `isAltScreenStripMode()`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Agents & CLIs → Codex; Respawn/Ralph options are Claude-only, so session options open on the Session tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM). Grok specifics: command built by `buildGrokCommand()` (`--always-approve` from `grokConfig.alwaysApprove` — grok's `bypassPermissions` permission mode, deny rules still apply; `--model`; `--resume ` / `--continue`, id-regexed so grok's resume-by-TITLE feature can never put an arbitrary string on the spawn line); availability via `GET /api/grok/status`, which carries `version` because the resolver version-probes candidates (`grok` has npm squatters, e.g. @vibe-kit/grok-cli — `GROK_VERSION_REGEX` is shared with the dependency registry so doctor and run mode agree). Like antigravity it is a standalone binary (xAI installer → `~/.grok/bin`, symlinked into `~/.local/bin`), so `docker/agent.Dockerfile` installs it in its own step (copy to `/usr/local/bin`, drop root's `~/.grok` in the same layer) and it stays OUT of `isAltScreenStripMode()` (fullscreen alt-screen TUI with mouse support — the opencode case, not the Ink case). Env allowlist: `GROK_*` plus the vendor namespace `XAI_*` (`XAI_API_KEY` is grok's documented headless auth var — the same narrow-vendor-namespace reasoning as `GOOGLE_*` for gemini). Docker cred seeding is per-file (`auth.json`, `config.toml`, `pager.toml` from `~/.grok` — the dir also holds `sessions/`, `memory/`, and the ~160MB binary under `downloads/`). Grok tests: `test/grok-mode.test.ts`, `test/grok-cli-resolver.test.ts`. **DeepSeek Harness (`dsh`) specifics** — the mode that breaks three of the assumptions the six above share, so read this before changing anything about it. diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index a03d47689..52471f7ca 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -174,17 +174,19 @@ const CLAUDE: CliEntry = { legacyConfigAliases: { resumeId: 'resumeSessionId' }, }, env: { - // Claude joins the truecolor list with COLORTERM=truecolor and NO_COLOR unset. + // Claude asks for truecolor, like every CLI here except `shell` and `opencode`. // tmux hands the pane TERM=screen, which supports-color reads as 16 colors, and // Claude then quantizes every RGB color its theme asks for down to that palette. // Each dark background lands on ESC[40m, the terminal's own black, so the block // Claude draws behind the user's own messages renders invisible. PR #3 unset // COLORTERM here against xterm.js#484, which xterm.js had already closed in 2019, // and Codeman now ships @xterm/xterm 6 and sets `terminal-overrides *:Tc` itself. + // The other truecolor 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. exports: [{ name: 'COLORTERM', value: 'truecolor' }], - unset: ['CLAUDECODE', 'NO_COLOR'], + unset: ['CLAUDECODE'], tmuxSetenvKeys: [], dockerExecEnvNames: [], allowedPrefixes: ['CLAUDE_CODE_'], diff --git a/src/session-cli-builder.ts b/src/session-cli-builder.ts index 482c8105e..904e1dd5c 100644 --- a/src/session-cli-builder.ts +++ b/src/session-cli-builder.ts @@ -13,6 +13,7 @@ import { isEffortLevel } from './types.js'; import { getAugmentedPath } from './utils/index.js'; import { compareVersions } from './utils/dependency-checker.js'; import { dataPath } from './config/instance.js'; +import { getCli } from './config/cli-registry/registry.js'; /** * Build Claude CLI permission flags based on the configured mode. @@ -181,10 +182,26 @@ export function buildClaudeEnv(sessionId: string): Record { expect(callBuildEnvExports('opencode')).toContain('unset COLORTERM'); }); + // Claude renders its themed backgrounds as RGB. Without this the pane inherits + // tmux's TERM=screen, supports-color reads 16 colors, and every dark background + // quantizes to ESC[40m — the terminal's own black — so the block goes invisible. + it('exports truecolor for Claude sessions', () => { + expect(callBuildEnvExports('claude')).toContain('export COLORTERM=truecolor'); + }); + it('exports the server-stamped CODEMAN_API_URL verbatim', () => { const original = process.env.CODEMAN_API_URL; process.env.CODEMAN_API_URL = 'https://127.0.0.1:3199';