feat(grok): add Grok Build (xAI) as a seventh CLI run mode

SessionMode gains 'grok', a first-class backend alongside Claude Code,
shell, OpenCode, Codex, Gemini, Antigravity and Pi: its own PTY, tmux
session, charcoal tab identity ('gk' badge), welcome button, run-mode
entry, cron agentType, Docker and remote-SSH command defaults, and
clone-repo Brain option. Flag surface verified live against grok 1.0.5.

Grok mixes two existing shapes and the wiring follows from that:

- Codex-shaped on permissions: the bypass switch is GrokConfig.alwaysApprove
  (--always-approve, grok's bypassPermissions mode; config-level deny rules
  still apply on top). The Run button sends it true, like runAntigravity(),
  and clampExternalCliBypassForOwner() puts grok in the only-if-sent branch:
  a bare grok spawn is grok's own ask-mode default, which is already safe,
  so only a sent config needs the flag forced off. Cron needs nothing for
  the same reason.
- OpenCode-shaped on rendering: grok is a fullscreen alternate-screen TUI
  with mouse support, so it stays OUT of isAltScreenStripMode() and lands
  on the narrow tmux-attach strip and the 'buffer' local-echo fallthrough
  (unmeasured against an authenticated composer; documented fallback is the
  'off' branch).
- Pi-shaped on resolution: 'grok' has npm squatters (@vibe-kit/grok-cli
  also installs a grok bin), so grok-cli-resolver.ts version-probes every
  candidate (grok --version, killSignal SIGKILL, VITEST-gated) and
  GET /api/grok/status surfaces path AND version; GROK_VERSION_REGEX is
  shared with the dependency registry so doctor and run mode cannot drift.

Env allowlist gains GROK_* plus the XAI_* vendor namespace (XAI_API_KEY is
grok's documented headless auth var), the same narrow-vendor reasoning as
GOOGLE_* for gemini. Resume is id-regexed on purpose: grok's own --resume
also matches session titles, which are arbitrary user strings that must
never reach the bash -c spawn line.

Docker: grok is not on npm, so the agent image installs it in its own step
(xAI's installer has no --dir override; the binary is copied to
/usr/local/bin and root's ~/.grok dropped in the same layer), and
credentials are seeded per-file (auth.json, config.toml, pager.toml; the
dir also holds sessions/, memory/ and the ~160MB binary). Remote SSH routes
through the login-shell wrapper like the other agent CLIs.

Verified end to end on an isolated CODEMAN_INSTANCE with grok 1.0.5
installed: /api/grok/status resolves and reports the probed version,
quick-start spawns a pane whose command line ends in 'grok
--always-approve', the real TUI renders (OAuth device screen on an
unauthenticated box), and grokConfig round-trips through state.json.
Docs: docs/grok-integration.md (user guide) + docs/grok-integration-plan.md
(decisions, verification record, follow-ups).

Tests: test/grok-mode.test.ts, test/grok-cli-resolver.test.ts, plus
extended clamp/system-routes/render-index-html/run-mode-ui/mobile-overview/
local-echo-gating coverage. npm test (the CI gate) green: 5910 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-23 08:39:03 +02:00
parent 88bb98de43
commit 3f8c8e99d1
55 changed files with 1474 additions and 119 deletions
+53 -1
View File
@@ -347,7 +347,8 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.history-view-all-btn,
.session-tab .tab-mode.gemini,
.session-tab .tab-mode.antigravity,
.session-tab .tab-mode.pi
.session-tab .tab-mode.pi,
.session-tab .tab-mode.grok
) {
color: var(--accent-d);
}
@@ -2246,6 +2247,11 @@ body.solo-mode .btn-lifecycle-log {
color: #f472b6;
}
.session-tab .tab-mode.grok {
background: rgba(212, 212, 216, 0.18);
color: #d4d4d8;
}
/* Timer Banner - Compact */
.timer-banner {
display: flex;
@@ -3610,6 +3616,23 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
transform: translateY(-1px);
}
/* Grok (xAI): monochrome charcoal identity, matching .btn-toolbar.btn-run.mode-grok
and .run-mode-dot.grok so the welcome action reads as the same backend. */
.welcome-btn-grok {
background: linear-gradient(135deg, #131316 0%, #27272a 55%, #3f3f46 100%);
border-color: rgba(212, 212, 216, 0.4);
color: #f4f4f5;
box-shadow: 0 2px 8px rgba(212, 212, 216, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.welcome-btn-grok:hover {
background: linear-gradient(135deg, #1f1f23 0%, #3f3f46 55%, #52525b 100%);
box-shadow: 0 4px 20px rgba(212, 212, 216, 0.22), 0 0 40px rgba(161, 161, 170, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(228, 228, 231, 0.5);
color: #fafafa;
transform: translateY(-1px);
}
.welcome-btn-gemini {
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
border-color: rgba(96, 165, 250, 0.4);
@@ -4684,6 +4707,25 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
color: #fff1f7;
}
/* Grok mode colors. Same cascade note as pi above: this base-sheet pair only
renders on the `og` skin — the nested `html:not([data-skin="og"])` block
re-declares `.btn-toolbar.btn-run` at a HIGHER specificity, so grok also
carries a rule inside that block (search `.btn-toolbar.btn-run.mode-grok`). */
.btn-toolbar.btn-run.mode-grok,
.btn-toolbar.btn-run-gear.mode-grok {
background: linear-gradient(135deg, #131316 0%, #27272a 55%, #3f3f46 100%);
border-color: rgba(212, 212, 216, 0.5);
color: #f4f4f5;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.btn-toolbar.btn-run.mode-grok:hover,
.btn-toolbar.btn-run-gear.mode-grok:hover {
background: linear-gradient(135deg, #1f1f23 0%, #3f3f46 55%, #52525b 100%);
box-shadow: 0 0 12px rgba(212, 212, 216, 0.28), 0 2px 8px rgba(63, 63, 70, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(228, 228, 231, 0.6);
color: #fafafa;
}
/* Dropdown menu */
.run-mode-menu {
display: none;
@@ -4767,6 +4809,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.run-mode-dot.gemini { background: #8ab4f8; }
.run-mode-dot.antigravity { background: #22d3ee; }
.run-mode-dot.pi { background: #f472b6; }
.run-mode-dot.grok { background: #a1a1aa; }
.run-mode-dot.shell { background: #94a3b8; }
/* Phone-only Enter button (see index.html). Hidden by default at every width;
@@ -14102,6 +14145,15 @@ html:not([data-skin="og"]) {
color: #fff1f7;
}
.btn-toolbar.btn-run.mode-pi:hover { box-shadow: 0 0 14px -2px rgba(244, 114, 182, 0.45); }
/* Grok keeps its charcoal identity on the non-og skins. Same specificity trap
as pi above: without this rule the generic `.btn-toolbar.btn-run` in this
nested block wins and grok renders as generic claude blue. */
.btn-toolbar.btn-run.mode-grok {
background: linear-gradient(135deg, #27272a, #52525b);
border-color: #18181b;
color: #fafafa;
}
.btn-toolbar.btn-run.mode-grok:hover { box-shadow: 0 0 14px -2px rgba(161, 161, 170, 0.5); }
.btn-toolbar.btn-run-gear {
background: var(--accent-d);
border-color: var(--accent);