chore(web): remove frontend code and styles nothing reaches

The frontend JS is outside ESLint and knip, so this pass used an AST scan
(every method, function and top-level constant in src/web/public, counted
against identifiers, strings and index.html) plus a selector scan of both
stylesheets. Each hit was confirmed by text search before it went.

JavaScript
- The ten run<Cli>() one-line wrappers and the quickStart() alias: run()
  has dispatched to _runCliMode(mode) since the launcher consolidation and
  nothing called the wrappers except tests, which now call the launcher.
- The Agent Teams panel code that lost its event listeners in February:
  the team tasks panel, the teammate pane window, rebuildTeammateMap and
  the three maps that nothing filled any more.
- Unreferenced methods: redockSession, refreshOrchestratorStatus,
  toggleSubagentPanel, minimizeSubagentWindow,
  openAllActiveSubagentWindows, pickBetterPath, getTodoIcon, deletePreset,
  closeRunSummary, saveSessionOptions, getAgentParentSessionId,
  _isUltracodeAgentMinimized.
- Six unused constants and a few unused locals (an unused frame-time
  computation in the touch scroll loop among them).

CSS
- 62 classes and one id that no markup, script or test names: the old
  settings-item / settings-section layout, the pre-redesign respawn and
  context-settings rows, the existing-wizard card and others. 116 rules
  and about 770 lines. A dead class inside an :is() list only loses that
  alternative; the rule stays.

Checked in a browser against an isolated instance: no console errors on
desktop or phone, a shell session runs, and 96 screenshots (desktop,
tablet and phone; settings, session options, add case, panels) are
pixel-identical before and after, apart from clock times.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-13 14:18:13 +01:00
1 parent f604deba3e
commit 6fb8b27768
17 files changed
+48 -1298

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ Pure helpers unit-tested in `test/base-path.test.ts` + `test/webview-proxy.test.
### External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP)
**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP, Copilot, jcode)**: `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()` (`--no-daemon` behind the `noDaemonFlag` gate at 0.162.0, measured against codex's own version, local launches only; `--model`, `--config model_reasoning_effort=<level>` from `reasoningEffort`, `--config sandbox_workspace_write.network_access=true` from `networkAccess` (the **Allow network access** app setting `codexNetworkAccess`, a launch default and a privileged param; ⚠️ with it on, a command in codex's sandbox can call Codeman's API and start a session outside that sandbox, password or not), `--config tui.animations=<bool>` from `animations`, `resume <id>`, `--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 <default|auto_edit|yolo|plan>` 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 <id>` 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 <id>` / `--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 `isAltScreenLine truncated
**External CLI modes (OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP, Copilot, jcode)**: `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()` (`--no-daemon` behind the `noDaemonFlag` gate at 0.162.0, measured against codex's own version, local launches only; `--model`, `--config model_reasoning_effort=<level>` from `reasoningEffort`, `--config sandbox_workspace_write.network_access=true` from `networkAccess` (the **Allow network access** app setting `codexNetworkAccess`, a launch default and a privileged param; ⚠️ with it on, a command in codex's sandbox can call Codeman's API and start a session outside that sandbox, password or not), `--config tui.animations=<bool>` from `animations`, `resume <id>`, `--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 <default|auto_edit|yolo|plan>` 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 <id>` 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 → `run()` → the shared `_runCliMode(mode)` 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 <id>` / `--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 OUTLine truncated
**DeepSeek Harness (`dsh`) specifics** — the mode that breaks three of the assumptions the six above share, so read this before changing anything about it.
-16
View File
@@ -863,11 +863,6 @@ class CodemanApp {
// Once set, never recalculated. Persisted to localStorage and server.
this.subagentParentMap = new Map();
// Agent Teams tracking
this.teams = new Map(); // Map<teamName, TeamConfig>
this.teamTasks = new Map(); // Map<teamName, TeamTask[]>
this.teammateMap = new Map(); // Map<agentId-prefix, {name, color, teamName}> for quick lookup
// Teammate tmux pane terminals (Agent Teams feature)
this.teammatePanesByName = new Map(); // Map<name, { paneTarget, sessionId, color }>
this.teammateTerminals = new Map(); // Map<agentId, { terminal, fitAddon, paneTarget, sessionId, resizeObserver }>
@@ -1819,15 +1814,6 @@ class CodemanApp {
return true;
}
/** Re-dock a session: close its window (which re-docks via its unload
* announcement) and clear dashboard state now. */
redockSession(id) {
const win = this.detachedWindows.get(id);
if (win && !win.closed) { try { win.close(); } catch {} }
this._postWindowMessage({ type: 'close-request', id });
this._redock(id);
}
/** Clear all dashboard-side detached state/timers for a session, and take its
* sizing back: the popup owned the pane while it was open, so the dashboard's
* record of it is stale and the session it is showing needs re-measuring.
@@ -4989,8 +4975,6 @@ class CodemanApp {
this._xtermSnapshots?.clear();
}
this.projectInsights.clear();
this.teams.clear();
this.teamTasks.clear();
// Clear all idle timers to prevent stale timers from firing
for (const timer of this.idleTimers.values()) {
clearTimeout(timer);
+2 -8
View File
@@ -112,8 +112,7 @@ const MOBILE_RESIZE_RETRY_MS = 30000; // Small-viewport resize re-send whi
const AUTO_CLOSE_NOTIFICATION_MS = 8000; // Auto-close browser notifications
const DEFAULT_TOAST_DURATION_MS = 3000; // How long a corner toast stays by default
const MIN_NOTIFICATION_DURATION_MS = 1000; // Shortest configurable toast / browser-notification time
const MAX_NOTIFICATION_DURATION_MS = 300000; // Longest configurable toast / browser-notification time
const THROTTLE_DELAY_MS = 100; // General UI throttle delay
const MAX_NOTIFICATION_DURATION_MS = 300000; // General UI throttle delay
const TERMINAL_CHUNK_SIZE = 32 * 1024; // 32KB chunks for terminal buffer loading
const TERMINAL_TAIL_SIZE = 1024 * 1024; // 1MB tail for initial load (more scrollback on tab switch)
// A shell's automatic loads (tab select, drop recovery, a tile's load) read
@@ -131,8 +130,7 @@ const TERMINAL_TAIL_SIZE = 1024 * 1024; // 1MB tail for initial load (more s
const SHELL_LOAD_HISTORY_LINES = 10000;
const SHELL_LOAD_BASE_QUERY = `full=1&tail=${TERMINAL_TAIL_SIZE}&alt=recording`;
const SHELL_LOAD_QUERY = `${SHELL_LOAD_BASE_QUERY}&lines=${SHELL_LOAD_HISTORY_LINES}`;
const SYNC_WAIT_TIMEOUT_MS = 50; // Wait timeout for terminal sync
const STATS_POLLING_INTERVAL_MS = 2000; // System stats polling
const SYNC_WAIT_TIMEOUT_MS = 50; // System stats polling
const TUI_REDRAW_SETTLE_MS = 400; // Grace for a TUI to redraw after a real resize, before fetching its buffer
// Z-index base values for layered floating windows
@@ -143,10 +141,6 @@ const ZINDEX_IMAGE_POPUP_BASE = 3000;
// Subagent/floating window layout
const WINDOW_INITIAL_TOP_PX = 120;
const WINDOW_CASCADE_OFFSET_PX = 30;
const WINDOW_MIN_WIDTH_PX = 200;
const WINDOW_MIN_HEIGHT_PX = 200;
const WINDOW_DEFAULT_WIDTH_PX = 300;
// WebGL renderer auto-fallback thresholds.
// _installWebGLLongTaskGuard() observes longtask entries and disables WebGL
-158
View File
@@ -226,15 +226,6 @@ html.mobile-init .file-browser-panel {
min-height: 56px;
}
/* Settings grid stays 2-col on tablet but tighter */
.settings-grid {
gap: 0.4rem 0.75rem;
}
.settings-item {
font-size: 0.7rem;
}
/* Event type grid - slightly tighter */
.event-type-grid {
grid-template-columns: 1fr 40px 40px 40px;
@@ -1774,11 +1765,6 @@ html.mobile-init .file-browser-panel {
min-height: 0; /* Allow flex item to shrink below content size */
}
/* Show CLI info bar on mobile (Claude version/model) */
.cli-info-bar {
display: block;
}
/* Mobile terminal — JS touch handler (terminal.scrollLines()) drives
scrollback because xterm.js DOM renderer doesn't populate xterm-viewport's
scroll area. touch-action:none lets our JS handler own the gesture. */
@@ -2095,27 +2081,6 @@ html.mobile-init .file-browser-panel {
/* ---- Session Options Modal: Respawn Tab Mobile Optimizations ---- */
/* Respawn header — keep horizontal, compact */
.respawn-header {
flex-direction: row;
gap: 0.4rem;
align-items: center;
}
/* Enable/Stop buttons — compact */
.respawn-header .respawn-actions {
display: flex;
gap: 0.3rem;
flex-shrink: 0;
}
.respawn-header .respawn-actions .btn-toolbar {
min-height: 28px !important;
font-size: 0.7rem !important;
padding: 0.2rem 0.5rem !important;
border-radius: 5px;
}
/* Duration preset buttons — one compact row */
.duration-presets {
display: flex;
@@ -2148,28 +2113,6 @@ html.mobile-init .file-browser-panel {
padding: 0.1rem 0.3rem;
}
/* Preset selector row — full-width dropdown, buttons below */
.preset-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.25rem;
}
.preset-selector select {
grid-column: 1 / -1;
min-height: 32px;
font-size: 16px; /* Prevents iOS zoom */
border-radius: 5px;
padding: 0.2rem 0.4rem;
}
.preset-selector .btn {
min-height: 32px;
font-size: 0.65rem;
border-radius: 5px;
padding: 0.2rem 0.4rem;
}
/* Textareas — prevent iOS zoom with 16px font, compact height */
#sessionOptionsModal textarea {
font-size: 16px;
@@ -2204,21 +2147,6 @@ html.mobile-init .file-browser-panel {
height: 18px;
}
/* Respawn-loop box title matches the auto-resume title; its cycle-step
checkboxes match the step labels */
#sessionOptionsModal .respawn-loop-title {
font-size: 0.75rem;
}
#sessionOptionsModal .respawn-loop-box .checkbox-inline {
font-size: 0.65rem;
}
/* Respawn options row — stack if needed */
#sessionOptionsModal .respawn-options-row {
gap: 0.5rem;
}
/* Form section headers — compact on mobile */
#sessionOptionsModal .form-section-header {
margin-top: 0.75rem;
@@ -2244,30 +2172,6 @@ html.mobile-init .file-browser-panel {
/* ---- Context Tab: Mobile Optimizations ---- */
/* Context settings cards — tighter padding */
.context-setting {
padding: 0.625rem;
}
/* Stack context setting header vertically on narrow screens */
.context-setting-header {
flex-wrap: wrap;
gap: 0.4rem;
}
.context-setting-header .input-suffix-sm input {
width: 60px;
font-size: 16px; /* Prevents iOS zoom */
min-height: 36px;
}
.context-setting input[type="text"] {
font-size: 16px; /* Prevents iOS zoom */
min-height: 36px;
padding: 0.4rem 0.6rem;
border-radius: 6px;
}
/* Color picker — larger swatches for touch */
.color-picker {
gap: 8px;
@@ -2285,27 +2189,8 @@ html.mobile-init .file-browser-panel {
gap: 0.4rem;
}
/* Number inputs in context tab — prevent iOS zoom */
.context-settings-grid input[type="number"] {
font-size: 16px;
min-height: 36px;
}
/* ---- Ralph Tab: Mobile Optimizations ---- */
/* Ralph limits grid — 2 columns on mobile */
.ralph-limits-grid {
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.ralph-limits-grid .form-col input[type="number"] {
font-size: 16px; /* Prevents iOS zoom */
min-height: 36px;
padding: 0.4rem 0.5rem;
border-radius: 6px;
}
/* Ralph phrase input — prevent iOS zoom */
#modalRalphPhrase {
font-size: 16px;
@@ -2313,18 +2198,6 @@ html.mobile-init .file-browser-panel {
border-radius: 6px;
}
/* Save button — full width, proper touch target */
.ralph-config-actions {
margin-top: 1rem;
}
.ralph-config-actions .btn-toolbar {
width: 100%;
min-height: 44px;
font-size: 0.85rem;
border-radius: 8px;
}
/* ---- Summary Tab: Mobile Optimizations ---- */
/* Filter buttons — larger touch targets */
@@ -2587,31 +2460,6 @@ html.mobile-init .file-browser-panel {
border-radius: 10px;
}
/* Settings grid - single column on phones */
.settings-grid {
grid-template-columns: 1fr;
gap: 0.35rem;
}
.settings-grid-3col {
grid-template-columns: 1fr 1fr 1fr;
}
.settings-item {
padding: 0.35rem 0.5rem;
font-size: 0.7rem;
}
.settings-item-label {
font-size: 0.7rem;
}
.settings-section-header {
font-size: 0.6rem;
padding: 0.35rem 0 0.2rem 0;
margin-top: 0.5rem;
}
/* Form rows - compact on mobile */
.form-row {
margin-bottom: 0.5rem;
@@ -3560,12 +3408,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
transform: none;
}
/* Show dropdown on tap instead of hover */
.subagent-dropdown-trigger:active + .subagent-dropdown-menu,
.subagent-dropdown-trigger:focus + .subagent-dropdown-menu {
display: block;
}
/* Press feedback for the touch bars. The pressed look is transform + filter,
never a background or a shadow: the skin block (html:not([data-skin="og"]),
specificity 0,2,1) and the per-run-mode colors own the background and its
-13
View File
@@ -255,19 +255,6 @@ Object.assign(CodemanApp.prototype, {
}
},
async refreshOrchestratorStatus() {
try {
const res = await fetch('/api/orchestrator/status');
const data = await res.json();
if (data.data?.ok) {
this.orchestratorState = data.data;
this.renderOrchestratorPanel();
}
} catch (err) {
console.error('[Orchestrator] Status fetch failed:', err);
}
},
// ═══════════════════════════════════════════════════════════════
// Rendering
// ═══════════════════════════════════════════════════════════════
+2 -320
View File
@@ -1668,11 +1668,6 @@ Object.assign(CodemanApp.prototype, {
// Subagent Panel (Claude Code Background Agents)
// ═══════════════════════════════════════════════════════════════
// Legacy alias
toggleSubagentPanel() {
this.toggleSubagentsPanel();
},
updateSubagentBadge() {
const badge = this.$('subagentCountBadge');
const activeCount = Array.from(this.subagents.values()).filter(s => s.status === 'active' || s.status === 'idle').length;
@@ -2253,7 +2248,7 @@ Object.assign(CodemanApp.prototype, {
// If cleanupData is true, also remove activity and toolResults data to prevent memory leaks
closeSessionSubagentWindows(sessionId, cleanupData = false) {
const toClose = [];
for (const [agentId, _windowData] of this.subagentWindows) {
for (const agentId of this.subagentWindows.keys()) {
const agent = this.subagents.get(agentId);
// Check both subagent parentSessionId and subagentParentMap
// (standalone pane windows use subagentParentMap, not subagents map)
@@ -2316,19 +2311,6 @@ Object.assign(CodemanApp.prototype, {
}
},
minimizeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (windowData) {
// Dispose teammate terminal on minimize to free DOM/memory (lazy re-creation on restore)
this._disposeTeammateTerminalForMinimize(agentId);
windowData.element.style.display = 'none';
windowData.minimized = true;
this.updateConnectionLines();
}
},
// Debounced wrapper — coalesces rapid subagent events (tool_call, progress,
// message) into a single DOM update per 100ms per agent window.
scheduleSubagentWindowRender(agentId) {
@@ -2500,15 +2482,6 @@ Object.assign(CodemanApp.prototype, {
}
},
// Open windows for all active subagents
openAllActiveSubagentWindows() {
for (const [agentId, agent] of this.subagents) {
if (agent.status === 'active' && !this.subagentWindows.has(agentId)) {
this.openSubagentWindow(agentId);
}
}
},
// ═══════════════════════════════════════════════════════════════
// Agent Teams
@@ -2649,155 +2622,6 @@ Object.assign(CodemanApp.prototype, {
});
},
/** Open a standalone terminal window for a tmux-pane teammate (no subagent entry needed) */
openTeammateTerminalWindow(paneData) {
// Only open if the session has a tab in Codeman
if (!this.sessions.has(paneData.sessionId)) return;
// Use pane target as the unique ID for this window
const windowId = `pane-${paneData.paneTarget}`;
// If window already exists, focus it
if (this.subagentWindows.has(windowId)) {
const existing = this.subagentWindows.get(windowId);
if (existing.hidden) {
existing.element.style.display = 'flex';
existing.hidden = false;
}
existing.element.style.zIndex = ++this.subagentWindowZIndex;
if (existing.minimized) {
this.restoreSubagentWindow(windowId);
}
return;
}
// Calculate position
const windowCount = this.subagentWindows.size;
const windowWidth = 550;
const windowHeight = 400;
const gap = 20;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const startX = 50;
const startY = 120;
const maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1;
const col = windowCount % maxCols;
const row = Math.floor(windowCount / maxCols) % maxRows;
let finalX = startX + col * (windowWidth + gap);
let finalY = startY + row * (windowHeight + gap);
finalX = Math.max(10, Math.min(finalX, viewportWidth - windowWidth - 10));
finalY = Math.max(10, Math.min(finalY, viewportHeight - windowHeight - 10));
// Color badge
const colorClass = paneData.color || 'blue';
// Create window element
const win = document.createElement('div');
win.className = 'subagent-window has-terminal';
win.id = `subagent-window-${windowId}`;
win.style.zIndex = ++this.subagentWindowZIndex;
win.style.left = `${finalX}px`;
win.style.top = `${finalY}px`;
win.style.width = `${windowWidth}px`;
win.style.height = `${windowHeight}px`;
win.innerHTML = `
<div class="subagent-window-header">
<div class="subagent-window-title" title="Teammate terminal: ${escapeHtml(paneData.teammateName)} (pane ${paneData.paneTarget})">
<span class="icon" style="color: var(--team-color-${colorClass}, #339af0)">⬤</span>
<span class="id">${escapeHtml(paneData.teammateName)}</span>
<span class="status running">terminal</span>
</div>
<div class="subagent-window-actions">
<button onclick="app.closeSubagentWindow(${escapeHtml(JSON.stringify(windowId))})" title="Minimize to tab">─</button>
</div>
</div>
<div class="subagent-window-body teammate-terminal-body" id="subagent-window-body-${windowId}">
</div>
`;
document.body.appendChild(win);
// Make draggable
const dragListeners = this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
// Make resizable if method exists
if (typeof this.makeWindowResizable === 'function') {
this.makeWindowResizable(win);
}
// Check visibility based on active session
const settings = this.loadAppSettingsFromStorage();
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
const shouldHide = activeTabOnly && paneData.sessionId !== this.activeSessionId;
// Store reference
this.subagentWindows.set(windowId, {
element: win,
minimized: false,
hidden: shouldHide,
dragListeners,
description: `Teammate: ${paneData.teammateName}`,
});
// Also add to subagentParentMap for tab-based visibility
this.subagentParentMap.set(windowId, paneData.sessionId);
if (shouldHide) {
win.style.display = 'none';
}
// Focus on click
win.addEventListener('mousedown', () => {
win.style.zIndex = ++this.subagentWindowZIndex;
});
// Resize observer for connection lines
const resizeObserver = new ResizeObserver(() => {
this.updateConnectionLines();
});
resizeObserver.observe(win);
this.subagentWindows.get(windowId).resizeObserver = resizeObserver;
// Init the xterm.js terminal (lazy if hidden)
if (shouldHide) {
// Window starts hidden — defer terminal creation until visible (lazy init)
const windowEntry = this.subagentWindows.get(windowId);
if (windowEntry) {
windowEntry._lazyTerminal = true;
windowEntry._lazyPaneTarget = paneData.paneTarget;
windowEntry._lazySessionId = paneData.sessionId;
}
} else {
this.initTeammateTerminal(windowId, paneData, win);
}
// Animate in
requestAnimationFrame(() => {
win.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
win.style.transform = 'scale(1)';
win.style.opacity = '1';
});
},
/** Rebuild the teammate lookup map from all team configs */
rebuildTeammateMap() {
this.teammateMap.clear();
for (const [teamName, team] of this.teams) {
for (const member of team.members) {
if (member.agentType !== 'team-lead') {
// Use name as key prefix for matching subagent descriptions
this.teammateMap.set(member.name, {
name: member.name,
color: member.color || 'blue',
teamName,
agentId: member.agentId,
});
}
}
}
},
/** Check if a subagent is a teammate and return its info */
getTeammateInfo(agent) {
if (!agent?.description) return null;
@@ -2807,7 +2631,7 @@ Object.assign(CodemanApp.prototype, {
const teammateId = match[1];
// Extract name from teammate_id (format: name@teamName)
const name = teammateId.split('@')[0];
return this.teammateMap.get(name) || { name, color: 'blue', teamName: 'unknown' };
return { name, color: 'blue', teamName: 'unknown' };
},
/** Get teammate badge HTML for a subagent */
@@ -2817,117 +2641,6 @@ Object.assign(CodemanApp.prototype, {
return `<span class="teammate-badge teammate-color-${info.color}" title="Team: ${escapeHtml(info.teamName)}">@${escapeHtml(info.name)}</span>`;
},
/** Render the team tasks panel */
renderTeamTasksPanel() {
const panel = document.getElementById('teamTasksPanel');
if (!panel) return;
// Find team for active session
let activeTeam = null;
let activeTeamName = null;
if (this.activeSessionId) {
for (const [name, team] of this.teams) {
if (team.leadSessionId === this.activeSessionId) {
activeTeam = team;
activeTeamName = name;
break;
}
}
}
if (!activeTeam) {
panel.style.display = 'none';
return;
}
// Set initial position and make draggable on first show
const wasHidden = panel.style.display === 'none';
panel.style.display = 'flex';
if (wasHidden && !this.teamTasksDragListeners) {
// Position bottom-right
const panelWidth = 360;
const panelHeight = 300;
panel.style.left = `${Math.max(10, window.innerWidth - panelWidth - 20)}px`;
panel.style.top = `${Math.max(10, window.innerHeight - panelHeight - 70)}px`;
// Make draggable
const header = panel.querySelector('.team-tasks-header');
if (header) {
this.teamTasksDragListeners = this.makeWindowDraggable(panel, header);
}
}
const tasks = this.teamTasks.get(activeTeamName) || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const total = tasks.length;
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
const headerEl = panel.querySelector('.team-tasks-header-text');
if (headerEl) {
const teammateCount = activeTeam.members.filter(m => m.agentType !== 'team-lead').length;
headerEl.textContent = `Team Tasks (${teammateCount} teammates)`;
}
const progressEl = panel.querySelector('.team-tasks-progress-fill');
if (progressEl) {
progressEl.style.width = `${pct}%`;
}
const progressText = panel.querySelector('.team-tasks-progress-text');
if (progressText) {
progressText.textContent = `${completed}/${total}`;
}
const listEl = panel.querySelector('.team-tasks-list');
if (!listEl) return;
if (tasks.length === 0) {
listEl.innerHTML = '<div class="team-task-empty">No tasks yet</div>';
return;
}
const html = tasks.map(task => {
const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '◉' : '○';
const statusClass = task.status.replace('_', '-');
const ownerBadge = task.owner
? `<span class="team-task-owner teammate-color-${this.getTeammateColor(task.owner)}">${escapeHtml(task.owner)}</span>`
: '';
return `<div class="team-task-item ${statusClass}">
<span class="team-task-status">${statusIcon}</span>
<span class="team-task-subject">${escapeHtml(task.subject)}</span>
${ownerBadge}
</div>`;
}).join('');
listEl.innerHTML = html;
},
/** Hide team tasks panel and clean up drag listeners */
hideTeamTasksPanel() {
const panel = document.getElementById('teamTasksPanel');
if (panel) panel.style.display = 'none';
if (this.teamTasksDragListeners) {
document.removeEventListener('mousemove', this.teamTasksDragListeners.move);
document.removeEventListener('mouseup', this.teamTasksDragListeners.up);
if (this.teamTasksDragListeners.touchMove) {
document.removeEventListener('touchmove', this.teamTasksDragListeners.touchMove);
document.removeEventListener('touchend', this.teamTasksDragListeners.up);
document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
}
if (this.teamTasksDragListeners.handle) {
this.teamTasksDragListeners.handle.removeEventListener('mousedown', this.teamTasksDragListeners.handleMouseDown);
this.teamTasksDragListeners.handle.removeEventListener('touchstart', this.teamTasksDragListeners.handleTouchStart);
}
this.teamTasksDragListeners = null;
}
},
/** Get teammate color by name */
getTeammateColor(name) {
const info = this.teammateMap.get(name);
return info?.color || 'blue';
},
// ═══════════════════════════════════════════════════════════════
// Project Insights Panel (Bash Tools with Clickable File Paths)
@@ -3038,37 +2751,6 @@ Object.assign(CodemanApp.prototype, {
return false;
},
/**
* Pick the "better" of two paths that resolve to the same file.
* Prefers paths inside the case folder, longer/more explicit paths, and absolute paths.
*/
pickBetterPath(path1, path2, workingDir) {
// Prefer paths inside the case folder (working directory)
if (workingDir) {
const inWorkDir1 = this.isPathInWorkingDir(path1, workingDir);
const inWorkDir2 = this.isPathInWorkingDir(path2, workingDir);
if (inWorkDir1 && !inWorkDir2) return path1;
if (inWorkDir2 && !inWorkDir1) return path2;
}
// Prefer absolute paths
const abs1 = path1.startsWith('/');
const abs2 = path2.startsWith('/');
if (abs1 && !abs2) return path1;
if (abs2 && !abs1) return path2;
// Both absolute or both relative - prefer longer (more explicit)
if (path1.length !== path2.length) {
return path1.length > path2.length ? path1 : path2;
}
// Prefer paths without ~
if (!path1.includes('~') && path2.includes('~')) return path1;
if (!path2.includes('~') && path1.includes('~')) return path2;
return path1;
},
/**
* Deduplicate file paths across all tools, keeping the "best" version.
* Uses smart equivalence checking:
-5
View File
@@ -953,11 +953,6 @@ Object.assign(CodemanApp.prototype, {
}
},
// Legacy method for backwards compatibility
getTodoIcon(status) {
return this.getRalphTaskIcon(status);
},
// ═══════════════════════════════════════════════════════════════
// Plan Versioning
+1 -28
View File
@@ -731,20 +731,6 @@ Object.assign(CodemanApp.prototype, {
this.showToast(`Saved preset: ${name}`, 'success');
},
deletePreset(presetId) {
const presets = this.loadRespawnPresets();
const preset = presets.find(p => p.id === presetId);
if (!preset || preset.builtIn) {
this.showToast('Cannot delete built-in presets', 'warning');
return;
}
const filtered = presets.filter(p => p.id !== presetId);
this.saveRespawnPresets(filtered);
this.renderPresetDropdown();
this.showToast(`Deleted preset: ${preset.name}`, 'success');
},
// Get respawn config from modal inputs
getModalRespawnConfig() {
const updatePrompt = document.getElementById('modalRespawnPrompt').value;
@@ -947,13 +933,6 @@ Object.assign(CodemanApp.prototype, {
await this.loadRunSummary(sessionId);
},
closeRunSummary() {
this.runSummarySessionId = null;
this.stopRunSummaryAutoRefresh();
// Close session options modal (summary is now a tab in it)
this.closeSessionOptions();
},
async refreshRunSummary() {
const sessionId = this.runSummarySessionId || this.editingSessionId;
if (!sessionId) return;
@@ -1074,7 +1053,7 @@ Object.assign(CodemanApp.prototype, {
renderRunSummary() {
if (!this.runSummaryData) return;
const { stats, events, sessionName, startedAt, lastUpdatedAt } = this.runSummaryData;
const { events, sessionName, startedAt, lastUpdatedAt } = this.runSummaryData;
// Update session info
const duration = lastUpdatedAt - startedAt;
@@ -1175,10 +1154,4 @@ Object.assign(CodemanApp.prototype, {
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
},
saveSessionOptions() {
// Session options are applied immediately via individual controls
// This just closes the modal
this.closeSessionOptions();
},
});
+6 -53
View File
@@ -21,13 +21,12 @@
* own no-id-branching rule exists to prevent server-side.
*
* Deliberately a LOCAL table rather than a server-injected catalogue: several
* unit tests exercise these run*() methods inside a bare `vm.createContext()`
* unit tests exercise the launcher inside a bare `vm.createContext()`
* sandbox with no `window` global at all (see test/run-mode-ui.test.ts) —
* referencing `window` there unguarded would throw, not degrade. `buildConfig`
* returns the CLI's top-level legacy config field for a LOCAL launch, or
* `null` for a CLI that sends none (pi: no bypass flag exists, so there is
* nothing to send — see runPi's own history below for why that must stay
* true).
* nothing to send, and that must stay true).
*/
const RUN_MODE_LAUNCH = {
opencode: {
@@ -677,10 +676,6 @@ Object.assign(CodemanApp.prototype, {
return this._caseSyncChain;
},
async quickStart() {
return this.run();
},
/** Ensure a newly-created session is visible without waiting for the SSE event.
* The POST response and session:created can arrive in either order, so the
* normal idempotent SSE handler remains the single state-upsert path.
@@ -2484,7 +2479,7 @@ Object.assign(CodemanApp.prototype, {
}
if (remoteIds[0]) {
// Don't pre-set activeSessionId — selectSession early-returns when the
// IDs match, skipping the buffer load, tab activation, and focus (see runCodex).
// IDs match, skipping the buffer load, tab activation, and focus (see _runCliMode).
await this.selectSession(remoteIds[0]);
}
this.terminal.focus();
@@ -2536,7 +2531,7 @@ Object.assign(CodemanApp.prototype, {
// Switch to first session. Don't pre-set activeSessionId — selectSession
// early-returns when the IDs match, skipping the buffer load, tab
// activation, and focus (see runCodex), which left the new shell tab
// activation, and focus (see _runCliMode), which left the new shell tab
// created but not shown until the user manually clicked it.
if (sessionIds.length > 0) {
await this.selectSession(sessionIds[0]);
@@ -2601,10 +2596,8 @@ Object.assign(CodemanApp.prototype, {
* Shared launcher for every RUN_MODE_LAUNCH entry (every run mode except
* claude/shell, which have their own flows — claude for its remote/docker
* branching and parallel-create path, shell for needing no CLI probe at
* all). The eight run<Mode>() methods below are thin named wrappers: their
* names stay because index.html's welcome-screen buttons and the run-mode
* menu call them directly by name (`app.runOpenCode()` etc.), and several
* tests assert on that name directly too.
* all). `run()` reaches it for every mode in the table; there is no per-CLI
* wrapper method any more.
*/
async _runCliMode(mode, options = {}) {
const catalogEntry = registryCliById(mode);
@@ -2690,46 +2683,6 @@ Object.assign(CodemanApp.prototype, {
}
},
async runOpenCode() {
return this._runCliMode('opencode');
},
async runCodex() {
return this._runCliMode('codex');
},
async runGemini() {
return this._runCliMode('gemini');
},
async runAntigravity() {
return this._runCliMode('antigravity');
},
async runPi() {
return this._runCliMode('pi');
},
async runOmp() {
return this._runCliMode('omp');
},
async runGrok() {
return this._runCliMode('grok');
},
async runDeepSeek() {
return this._runCliMode('deepseek');
},
async runCopilot() {
return this._runCliMode('copilot');
},
async runJcode() {
return this._runCliMode('jcode');
},
// ═══════════════════════════════════════════════════════════════
// Session Options Modal
-8
View File
@@ -5455,14 +5455,6 @@ Object.assign(CodemanApp.prototype, {
}
},
/**
* Get the parent session ID for an agent from the persistent map.
* This is the ONLY source of truth for connection lines.
*/
getAgentParentSessionId(agentId) {
return this.subagentParentMap.get(agentId) || null;
},
/**
* Set and persist the parent session ID for an agent.
* Once set, this association is PERMANENT and never recalculated.
-612
View File
@@ -329,7 +329,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
box-shadow: var(--elevated-shadow);
}
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(
.settings-item,
.cron-job-row,
#cronModal .form-row-switch.cron-switch-row,
.cron-weekdays label
@@ -338,7 +337,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
border-color: var(--control-border);
}
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(
.settings-item:hover,
.cron-job-row:hover,
.cron-weekdays label:hover
) {
@@ -418,7 +416,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
/* Button focus styles */
button:focus-visible,
.btn-toolbar:focus-visible,
.btn-icon:focus-visible,
.btn-icon-header:focus-visible,
.btn-icon-sm:focus-visible {
outline: 2px solid var(--accent);
@@ -2707,13 +2704,6 @@ html[data-header-stats] .header-system-stats .stat-item[hidden] {
background: rgba(239, 68, 68, 0.2);
}
.tunnel-panel-empty {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
padding: 4px 0;
}
.header-tokens {
font-size: 0.75rem;
font-weight: 500;
@@ -3757,16 +3747,6 @@ html[data-tab-logos='off'] .home-sessions-harness {
stroke-width: 3;
}
.ralph-ring-progress {
fill: none;
stroke: url(#ralphGradient);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 100;
stroke-dashoffset: 100;
transition: stroke-dashoffset 0.5s ease-out;
}
.ralph-ring-progress-mini {
fill: none;
stroke: url(#ralphGradientMini);
@@ -4086,23 +4066,6 @@ html[data-tab-logos='off'] .home-sessions-harness {
align-content: flex-start;
}
.ralph-tasks-compact .ralph-task-card {
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
border-left-width: 2px;
}
.ralph-tasks-compact .ralph-task-icon {
font-size: 0.7rem;
}
.ralph-tasks-compact .ralph-task-content {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Large Progress Ring */
.ralph-ring-svg-large {
width: 100%;
@@ -6405,11 +6368,6 @@ html[data-skin="og"] .welcome-primary:hover {
}
/* Button icon styling */
.btn-toolbar .btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-toolbar.btn-success {
background: linear-gradient(135deg, #16a34a 0%, #22c55e 100%);
@@ -7153,28 +7111,6 @@ html[data-skin="og"] .welcome-primary:hover {
outline: none;
}
.toolbar-input-sm {
padding: 0.3rem 0.4rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.75rem;
width: 40px;
text-align: center;
}
.toolbar-input-sm:focus {
outline: none;
border-color: var(--accent);
}
.toolbar-input-sm::-webkit-inner-spin-button,
.toolbar-input-sm::-webkit-outer-spin-button {
opacity: 1;
height: 16px;
}
#dirDisplay {
max-width: 250px;
overflow: hidden;
@@ -7231,18 +7167,6 @@ html[data-skin="og"] .welcome-primary:hover {
background: var(--bg-hover);
}
.settings-section {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.settings-section:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.form-row {
margin-bottom: 0.75rem;
}
@@ -7296,19 +7220,6 @@ html[data-skin="og"] .welcome-primary:hover {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
}
.form-col {
flex: 1;
}
.form-col label {
display: block;
font-size: 0.7rem;
color: var(--text-dim);
margin-bottom: 0.35rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.input-suffix {
display: flex;
align-items: center;
@@ -7352,46 +7263,6 @@ html[data-skin="og"] .welcome-primary:hover {
color: var(--red);
}
/* Preset selector */
.preset-selector {
display: flex;
gap: 8px;
align-items: center;
}
.preset-selector select {
flex: 1;
min-width: 0;
padding: 0.4rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.75rem;
}
.preset-selector select:focus {
outline: none;
border-color: var(--accent);
}
.preset-selector .btn-sm {
white-space: nowrap;
padding: 4px 8px;
font-size: 0.7rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
cursor: pointer;
transition: all 0.15s;
}
.preset-selector .btn-sm:hover {
background: var(--bg-hover);
border-color: var(--border-light);
}
/* Switch toggle for form rows */
.form-section-header {
font-size: 0.7rem;
@@ -7495,138 +7366,6 @@ html[data-skin="og"] .welcome-primary:hover {
transform: translateX(12px);
}
/* Settings Grid Layout - 2 column compact view */
.settings-grid {
display: grid;
/* minmax(0, 1fr) lets tracks shrink below their min-content so long
labels ellipsis-truncate instead of blowing out the modal width and
forcing a horizontal scrollbar (1fr === minmax(auto, 1fr) would not). */
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem 1rem;
}
.settings-section-header {
grid-column: 1 / -1;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
padding: 0.5rem 0 0.25rem 0;
border-bottom: 1px solid var(--border);
margin-top: 0.75rem;
}
.settings-section-header:first-child {
margin-top: 0;
}
.settings-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.45rem 0.7rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: var(--btn-radius);
font-size: 0.75rem;
transition: background var(--transition-smooth);
}
.settings-item-label {
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.settings-inline-input,
.settings-inline-select {
width: min(190px, 52%);
min-width: 0;
padding: 0.3rem 0.45rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg-input);
color: var(--text);
font: inherit;
}
.settings-inline-input:focus,
.settings-inline-select:focus {
outline: none;
border-color: var(--accent);
}
/* Skin picker: full-width row (not squeezed into a half-width grid cell) with an
auto-width select, so the "Skin" label keeps its space instead of truncating. */
.settings-item-skin {
grid-column: 1 / -1;
gap: 0.75rem;
}
.settings-item-skin .settings-item-label {
flex: 0 0 auto;
}
.settings-item-skin .form-select {
width: auto;
min-width: 220px;
max-width: 320px;
flex: 0 1 auto;
/* Match the field to its settings row: the base .form-select (0.8rem / 0.5rem
padding) renders bigger and taller than the row, and the daylight skins'
Manrope font exaggerates it further. Slim it to the row's 0.75rem text. */
font-size: 0.75rem;
padding-top: 0.3rem;
padding-bottom: 0.3rem;
}
.settings-item-multiline {
align-items: center;
}
.settings-item-text {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.settings-item-desc {
font-size: 0.6rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.settings-item-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
/* Three column variant for notification levels */
.settings-grid-3col {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
}
/* Inline input in settings item */
.settings-item-input {
width: 60px;
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
text-align: right;
}
.settings-item-input:focus {
outline: none;
border-color: var(--accent);
}
/* Status badge in settings */
.settings-status {
font-size: 0.6rem;
@@ -8310,13 +8049,6 @@ html[data-skin="og"] .welcome-primary:hover {
margin-top: 0.125rem;
}
.run-summary-session-info {
font-size: 0.7rem;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
padding: 0.25rem 0;
}
.run-summary-footer {
display: flex;
flex-direction: column;
@@ -8363,19 +8095,6 @@ html[data-skin="og"] .welcome-primary:hover {
}
/* Legacy run summary modal footer */
#runSummaryModal .modal-footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
#runSummaryModal .modal-footer .run-summary-session-info {
font-size: 0.7rem;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
}
.modal-warning-text {
color: var(--text-dim);
@@ -8697,10 +8416,6 @@ html[data-skin="og"] .welcome-primary:hover {
}
/* Session respawn section in options modal */
.session-respawn-section {
padding-top: 0;
margin-top: 0;
}
.session-respawn-status {
display: flex;
@@ -8814,86 +8529,7 @@ html[data-skin="og"] .welcome-primary:hover {
width: 60px;
}
/* Respawn status + Enable/Stop row — sits at the bottom of the Respawn tab,
below the settings it acts on */
.respawn-header {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(34, 197, 94, 0.2);
}
.respawn-header .respawn-actions {
margin: 0;
padding: 0;
border: none;
}
/* Respawn options row - inline checkboxes */
.respawn-options-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 0.25rem;
}
/* Auto-resume on usage limit (token pause control) */
.auto-resume-box {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin: 0.75rem 0;
padding: 0.6rem 0.75rem;
background: rgba(59, 130, 246, 0.07);
border: 1px solid rgba(59, 130, 246, 0.35);
border-radius: 6px;
}
.auto-resume-box .checkbox-inline {
font-size: 0.85rem;
font-weight: 500;
color: var(--text);
}
.auto-resume-box .form-hint {
margin-top: 0;
}
/* Respawn loop box — groups every setting that belongs to the autonomous
respawn cycle (duration, presets, cycle prompts, enable). Green like its
Enable button; a visual sibling of the standalone auto-resume option above */
.respawn-loop-box {
margin: 0.75rem 0;
padding: 0.6rem 0.75rem;
background: rgba(34, 197, 94, 0.05);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 6px;
}
.respawn-loop-title {
font-size: 0.85rem;
font-weight: 500;
color: var(--text);
margin-bottom: 0.3rem;
}
.respawn-loop-hint {
display: block;
margin-top: 0;
margin-bottom: 0.5rem;
}
.respawn-loop-box .form-section-header {
border-bottom-color: rgba(34, 197, 94, 0.2);
}
/* Match the cycle-step checkboxes (2./3.) to the step labels (1./4.) */
.respawn-loop-box .checkbox-inline {
font-size: 0.7rem;
}
.auto-resume-status {
display: none;
@@ -8906,106 +8542,11 @@ html[data-skin="og"] .welcome-primary:hover {
display: block;
}
/* Respawn actions in modal */
.respawn-actions {
display: flex;
gap: 0.5rem;
}
/* The `.modal-tabs` / `.modal-tab-btn` / `.modal-tab-content` chrome is gone:
App Settings, Session Options and Add Case all use the `set-*` surface now.
Its last user was Add Case; see the settings-surface block near the end. */
/* Context Tab Settings Grid */
.context-settings-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.context-setting {
padding: 0.875rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 6px;
}
.context-setting-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.context-setting-header .checkbox-inline span {
font-weight: 500;
}
.context-setting-header .input-suffix-sm {
flex-shrink: 0;
}
.context-setting-header .input-suffix-sm input {
width: 70px;
}
.context-setting input[type="text"] {
width: 100%;
padding: 0.375rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.75rem;
}
.context-setting input[type="text"]:focus {
outline: none;
border-color: var(--accent);
}
.context-setting .form-hint {
margin-top: 0.5rem;
}
/* Ralph Limits Grid */
.ralph-limits-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 0.5rem;
}
@media (max-width: 500px) {
.ralph-limits-grid {
grid-template-columns: 1fr 1fr;
}
}
.ralph-limits-grid .form-col input[type="number"] {
width: 100%;
padding: 0.375rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.8rem;
}
.ralph-limits-grid .form-col .form-hint {
font-size: 0.6rem;
}
/* Ralph Config Actions */
.ralph-config-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border);
}
.modal-header {
display: flex;
@@ -10641,9 +10182,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
/* Header removed - no longer needed */
.subagent-dropdown-header {
display: none;
}
.subagent-dropdown-item {
display: flex;
@@ -10723,13 +10261,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
transition: opacity 0.1s, color 0.1s;
}
.task-empty {
padding: 1rem;
text-align: center;
color: var(--text-muted);
font-size: 0.8rem;
}
/* Task Tree */
.task-tree {
font-size: 0.75rem;
@@ -10810,28 +10341,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
margin-top: 0.25rem;
}
/* Task toggle button in toolbar */
.task-toggle-btn {
position: relative;
}
.task-toggle-btn .badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 14px;
height: 14px;
padding: 0 3px;
background: var(--yellow);
border-radius: 7px;
font-size: 0.6rem;
font-weight: 600;
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
@@ -10843,13 +10352,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
background: var(--border-light);
}
.process-empty {
padding: 1rem;
text-align: center;
color: var(--text-muted);
font-size: 0.8rem;
}
.process-item {
display: flex;
align-items: center;
@@ -11833,11 +11335,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
drop-shadow(0 0 12px rgba(59, 130, 246, 0.7));
}
.connection-line.spawning-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
}
/* Wizard connection lines - different color to show plan generation link */
.connection-line.wizard-connection {
stroke: #60a5fa;
@@ -11971,10 +11468,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.connection-line.lineage-line--working {
animation: lineage-flow 1.1s linear infinite;
}
.lineage-line-dot--working {
animation: lineage-dot-pulse 1.4s ease-in-out infinite;
}
}
/* Two full dash cycles, so the march loops seamlessly. Tied to the working
@@ -12341,10 +11834,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
background: rgba(59, 130, 246, 0.15);
}
.file-tree-item.hidden-by-filter {
display: none;
}
/* Under an expanded folder the scan's file limit stopped short of. Inert: no hover, no click. */
.file-tree-incomplete {
padding: 0.2rem 0.5rem;
@@ -12992,12 +12481,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
line-height: 1.4;
}
.log-viewer-window-body .log-line {
padding: 0.1rem 0;
content-visibility: auto;
contain-intrinsic-size: auto 20px;
}
.log-viewer-window-body .log-error {
color: var(--error);
}
@@ -13544,60 +13027,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
/* Existing wizard files section */
.existing-wizard-files-section {
padding: 1rem;
}
.existing-wizard-card {
background: var(--bg-dark);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 1rem;
}
.existing-wizard-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.existing-wizard-icon {
font-size: 1.2rem;
}
.existing-wizard-title {
font-weight: 600;
color: var(--text);
}
.existing-wizard-desc {
font-size: 0.85rem;
color: var(--text-dim);
margin: 0 0 0.75rem 0;
}
.existing-wizard-agents {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 1rem;
}
.existing-wizard-agent {
background: rgba(96, 165, 250, 0.2);
color: var(--accent);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.existing-wizard-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.plan-cancel-btn {
color: var(--red);
@@ -13740,22 +13169,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
text-overflow: ellipsis;
}
/* Clickable prompt link in plan subagent header */
.plan-subagent-prompt-link {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.15rem 0.3rem;
margin: -0.15rem -0.3rem;
border-radius: 4px;
transition: background 0.15s ease;
}
.plan-subagent-prompt-link:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Clickable status text */
.plan-subagent-status-text.clickable {
cursor: pointer;
@@ -13823,11 +13236,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
/* Iteration Presets */
.iteration-presets {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.iteration-preset-btn {
padding: 0.5rem 1rem;
@@ -16771,20 +16179,6 @@ body.touch-device.cjk-input-visible .main {
display: flex;
}
.attachment-type-badge {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 6px;
background: var(--bg-input);
color: var(--text);
border: 1px solid var(--border-light);
font-size: 0.7rem;
font-weight: 700;
}
.attachment-card-main {
min-width: 0;
}
@@ -17129,7 +16523,6 @@ html:not([data-skin="og"]) {
background: linear-gradient(90deg, var(--banner-bg-a), var(--banner-bg-b));
border-bottom: 1px solid var(--glass-border);
}
.respawn-countdown-timer .respawn-timer-value,
.action-type { color: var(--accent); }
.respawn-timer-progress { background: var(--accent); }
.respawn-action-log { border-left-color: rgba(var(--accent-rgb), 0.18); }
@@ -17145,7 +16538,6 @@ html:not([data-skin="og"]) {
/* Ralph status badges: collapse the off-token Material palette onto the system */
.ralph-status-badge { color: var(--text-dim); }
.ralph-status-dot { box-shadow: none; }
.ralph-info { background: rgba(var(--accent-rgb), 0.05); }
/* ---- Bottom toolbar + neutral buttons ---- */
.toolbar { box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.22), 0 -4px 14px rgba(0, 0, 0, 0.14); }
@@ -17312,10 +16704,6 @@ html:not([data-skin="og"]) {
border: 1px solid var(--control-border);
}
/* ---- Subagent progress ring: emerald, calm glow ---- */
.subagent-ring-progress,
.subagent-ring circle.progress { stroke: var(--accent); }
/* ---- PROGRESS RING (Ralph large): the one place a soft glow lives ---- */
.ralph-ring-fill { filter: drop-shadow(0 0 6px rgba(var(--accent-rgb), 0.45)); }
+4 -16
View File
@@ -939,7 +939,7 @@ Object.assign(CodemanApp.prototype, {
// Clean up ALL floating windows (called during handleInit to prevent memory leaks on reconnect)
cleanupAllFloatingWindows() {
// Clean up all subagent windows with their ResizeObservers and drag listeners
for (const [agentId, windowData] of this.subagentWindows) {
for (const windowData of this.subagentWindows.values()) {
if (windowData.resizeObserver) {
windowData.resizeObserver.disconnect();
}
@@ -969,7 +969,7 @@ Object.assign(CodemanApp.prototype, {
this.teammatePanesByName.clear();
// Clean up all log viewer windows with their EventSources and drag listeners
for (const [windowId, data] of this.logViewerWindows) {
for (const data of this.logViewerWindows.values()) {
if (data.eventSource) {
data.eventSource.close();
}
@@ -988,7 +988,7 @@ Object.assign(CodemanApp.prototype, {
// Clean up plan subagent windows (wizard agents)
if (this.planSubagents) {
for (const [agentId, windowData] of this.planSubagents) {
for (const windowData of this.planSubagents.values()) {
if (windowData.dragListeners) {
document.removeEventListener('mousemove', windowData.dragListeners.move);
document.removeEventListener('mouseup', windowData.dragListeners.up);
@@ -1001,7 +1001,7 @@ Object.assign(CodemanApp.prototype, {
}
// Clean up all image popup windows with their drag listeners
for (const [imageId, popupData] of this.imagePopups) {
for (const popupData of this.imagePopups.values()) {
if (popupData.dragListeners) {
document.removeEventListener('mousemove', popupData.dragListeners.move);
document.removeEventListener('mouseup', popupData.dragListeners.up);
@@ -1042,18 +1042,6 @@ Object.assign(CodemanApp.prototype, {
this.activeFocusTrap = null;
}
// Clean up team tasks panel drag listeners
if (this.teamTasksDragListeners) {
document.removeEventListener('mousemove', this.teamTasksDragListeners.move);
document.removeEventListener('mouseup', this.teamTasksDragListeners.up);
if (this.teamTasksDragListeners.touchMove) {
document.removeEventListener('touchmove', this.teamTasksDragListeners.touchMove);
document.removeEventListener('touchend', this.teamTasksDragListeners.up);
document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
}
this.teamTasksDragListeners = null;
}
// Clear minimized agents tracking
this.minimizedSubagents.clear();
+1 -6
View File
@@ -1256,7 +1256,6 @@ Object.assign(CodemanApp.prototype, {
let touchLastX = 0;
let touchLastY = 0;
let velocity = 0;
let lastTime = 0;
let scrollFrame = null;
let isTouching = false;
// One id per touch contact (touchstart), carried by its touchmoves AND its
@@ -1264,10 +1263,7 @@ Object.assign(CodemanApp.prototype, {
// gesture however slowly the finger moves (pageKeysForGesture).
let touchContactId = 0;
const scrollLoop = (timestamp) => {
const dt = lastTime ? (timestamp - lastTime) / 16.67 : 1;
lastTime = timestamp;
const scrollLoop = () => {
if (!isTouching && Math.abs(velocity) > 0.3) {
// Momentum phase — convert pixel velocity to lines
const lines = Math.round(velocity / cellHeight());
@@ -1389,7 +1385,6 @@ Object.assign(CodemanApp.prototype, {
longPressTimer = null;
this._beginTouchSelection(longPressStartX, longPressStartY);
}, window.CodemanTerminalInput.TOUCH_SELECT_LONG_PRESS_MS);
lastTime = 0;
if (scrollFrame) {
cancelAnimationFrame(scrollFrame);
scrollFrame = null;
-11
View File
@@ -409,17 +409,6 @@ Object.assign(CodemanApp.prototype, {
this.updateConnectionLines();
},
// ── Minimize an agent transcript window into its tab (same idiom as run windows) ──
/** Is this agent transcript currently minimized to a tab? */
_isUltracodeAgentMinimized(agentId) {
if (!this.minimizedUltracodeAgents) return false;
for (const map of this.minimizedUltracodeAgents.values()) {
if (map.has(agentId)) return true;
}
return false;
},
/** Look up a minimized agent's {runId,label} entry (across sessions). */
_getMinimizedUltracodeAgent(agentId) {
if (!this.minimizedUltracodeAgents) return null;
+4 -4
View File
@@ -10,7 +10,7 @@
* reinitializes on a restart.
*
* Uses the same JSDOM + `runScripts: "dangerously"` approach as
* test/custom-model-run-menu-ui.test.ts, extended with the DOM elements runCodex() (the
* test/custom-model-run-menu-ui.test.ts, extended with the DOM elements the codex launch (the
* CLI this was reported against) reads.
*
* Port: none.
@@ -221,7 +221,7 @@ describe('_quickStartWithCustomModelConfirm', () => {
});
});
describe('runCodex(): one-shot custom-model launch (the CLI this was reported against)', () => {
describe('the codex launch: one-shot custom-model launch (the CLI this was reported against)', () => {
it('folds _pendingCustomModelForLaunch into the quick-start body as customModel', async () => {
const { win, app } = bootApp();
(win as unknown as { fetch: typeof fetch }).fetch = (async (url: string, opts?: any) => {
@@ -232,7 +232,7 @@ describe('runCodex(): one-shot custom-model launch (the CLI this was reported ag
}) as unknown as typeof fetch;
app._pendingCustomModelForLaunch = { endpointId: 'llama-box', modelId: 'qwen3' };
await app.runCodex();
await app._runCliMode('codex');
expect(app._lastReportedError).toBeUndefined();
});
@@ -245,7 +245,7 @@ describe('runCodex(): one-shot custom-model launch (the CLI this was reported ag
return { json: async () => ({ success: true, data: { sessionId: 's1' } }) };
}) as unknown as typeof fetch;
await app.runCodex();
await app._runCliMode('codex');
expect(app._lastReportedError).toBeUndefined();
});
});
+4 -6
View File
@@ -70,11 +70,9 @@ describe('OpenCode session initial resize', () => {
// NOT pre-set activeSessionId before calling selectSession. If it did,
// selectSession would early-return and skip sendResize.
//
// PR B2 consolidated runOpenCode() (and 7 siblings) into one shared
// _runCliMode(mode) — runOpenCode is now a one-line wrapper
// (`return this._runCliMode('opencode')`), so inspecting ITS source would
// never see the real launch logic and this check would pass vacuously
// regardless of what _runCliMode actually does. Inspect _runCliMode itself.
// PR B2 consolidated the per-CLI launchers into one shared
// _runCliMode(mode), so that is where the real launch logic lives and
// what this check inspects.
({ context, page } = await freshPage());
await navigateAndWait(page);
@@ -151,7 +149,7 @@ describe('OpenCode session initial resize', () => {
expect(sessionId).toBeTruthy();
// Call selectSession (which is what runOpenCode does after fix)
// Call selectSession (which is what the opencode launch does after fix)
await page.evaluate(async (sid: string) => {
const app = (window as unknown as { app: { selectSession: (id: string) => Promise<void> } }).app;
await app.selectSession(sid);
+23 -33
View File
@@ -160,7 +160,7 @@ describe('Run launch synchronization', () => {
* is active wipes that session's terminal, and _cleanupPreviousSession()
* then serializes the wiped view into its restore snapshot. Asserting on the
* helpers alone cannot see that, so pin the call sites here. This also
* covers run modes added later, which is how runAntigravity was caught.
* covers run modes added later, which is how the antigravity launch was caught.
*/
it('routes every run mode through the ownership helpers, never the terminal directly', () => {
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
@@ -168,15 +168,11 @@ describe('Run launch synchronization', () => {
// Methods live in one Object.assign(prototype, {...}) block at a fixed
// 2-space indent, so `\n },` reliably closes the one we are inside.
//
// `(\w*)` in the param list, not `()`, and the leading `_?`: PR B2
// consolidated the eight run<Mode>() bodies into one shared
// `_runCliMode(mode)`, and the ORIGINAL `\(\)`-only pattern matched every
// one-line wrapper (`async runOpenCode() { return this._runCliMode(...) }`)
// but not `_runCliMode` itself, where the real terminal-ownership logic
// now lives — so this guard could see 8 clean one-liners and stay green
// while the actual bug shipped unseen for all eight external CLIs at
// once. Confirmed live: adding `this.terminal.clear()` to `_runCliMode`
// left this test 32/32 green under the old pattern.
// The leading `_?` matters: PR B2 consolidated the per-CLI run<Mode>()
// bodies into one shared `_runCliMode(mode)`, where the real
// terminal-ownership logic lives, and a `\(\)`-only pattern never matched
// it. Confirmed live: adding `this.terminal.clear()` to `_runCliMode` left
// this test 32/32 green under the old pattern.
const bodies = new Map<string, string>();
// Any parameter list (`(mode, options = {})` included), never `\(\w*\)`
// alone, or a launcher that grew a second parameter silently drops out.
@@ -195,12 +191,6 @@ describe('Run launch synchronization', () => {
'runClaude',
'runShell',
'_runCliMode',
'runOpenCode',
'runCodex',
'runGemini',
'runAntigravity',
'runPi',
'runGrok',
])
);
@@ -773,7 +763,7 @@ describe('Codex quick start settings', () => {
selected.push(id);
};
await app.runCodex();
await app._runCliMode('codex');
expect(requests.find((req) => req.url === '/api/quick-start')?.body).toMatchObject({
caseName: 'codex-case',
@@ -1167,12 +1157,12 @@ describe('toolbar instance count', () => {
});
describe('Gemini quick start', () => {
// Regression guard for the ApiResponse-envelope unwrap in runGemini(): the
// Regression guard for the ApiResponse-envelope unwrap in the gemini launch: the
// status check must read `.data.available` and the quick-start response must
// read `.data.sessionId`. Reading the raw shape (pre-fix) silently bails on
// the status check and never selects the new tab — exactly the two blockers
// caught in PR #134 review.
it('drives runGemini() through the {success,data} envelope and selects the new session', async () => {
it('drives the gemini launch through the {success,data} envelope and selects the new session', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'gemini-case' },
tabCount: { value: '1' },
@@ -1214,7 +1204,7 @@ describe('Gemini quick start', () => {
selected.push(id);
};
await app.runGemini();
await app._runCliMode('gemini');
expect(requests.find((req) => req.url === '/api/quick-start')?.body).toMatchObject({
caseName: 'gemini-case',
@@ -1226,8 +1216,8 @@ describe('Gemini quick start', () => {
});
describe('Antigravity quick start', () => {
// Same envelope-unwrap regression guard as the Gemini block above, for runAntigravity().
it('drives runAntigravity() through the {success,data} envelope and selects the new session', async () => {
// Same envelope-unwrap regression guard as the Gemini block above, for antigravity.
it('drives the antigravity launch through the {success,data} envelope and selects the new session', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'ag-case' },
tabCount: { value: '1' },
@@ -1268,7 +1258,7 @@ describe('Antigravity quick start', () => {
selected.push(id);
};
await app.runAntigravity();
await app._runCliMode('antigravity');
expect(requests.find((req) => req.url === '/api/quick-start')?.body).toMatchObject({
caseName: 'ag-case',
@@ -1280,11 +1270,11 @@ describe('Antigravity quick start', () => {
});
describe('Pi quick start', () => {
// Same envelope-unwrap regression guard as the blocks above, for runPi(), plus the
// Same envelope-unwrap regression guard as the blocks above, for pi, plus the
// rule that makes pi different: it must send NO piConfig. Pi has no permission
// prompts, and `approveProjectTrust` would opt the session into EXECUTING
// repo-supplied TypeScript — never something a Run button decides silently.
it('drives runPi() through the {success,data} envelope and sends no piConfig', async () => {
it('drives the pi launch through the {success,data} envelope and sends no piConfig', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'pi-case' },
tabCount: { value: '1' },
@@ -1327,7 +1317,7 @@ describe('Pi quick start', () => {
selected.push(id);
};
await app.runPi();
await app._runCliMode('pi');
const body = requests.find((req) => req.url === '/api/quick-start')?.body;
expect(body).toMatchObject({ caseName: 'pi-case', mode: 'pi' });
@@ -1359,7 +1349,7 @@ describe('Pi quick start', () => {
const errors: string[] = [];
app._reportSessionLaunchError = (_owns: boolean, msg: string) => errors.push(msg);
await app.runPi();
await app._runCliMode('pi');
expect(requests).toEqual(['/api/pi/status']);
expect(errors[0]).toContain('@earendil-works/pi-coding-agent');
@@ -1367,13 +1357,13 @@ describe('Pi quick start', () => {
});
describe('Grok quick start', () => {
// Same envelope-unwrap regression guard as the blocks above, for runGrok(),
// Same envelope-unwrap regression guard as the blocks above, for grok,
// plus the rule that makes grok the OPPOSITE of pi: the Run button DOES send
// `grokConfig: { alwaysApprove: true }` (grok's bypassPermissions mode), the
// same product decision as runAntigravity's dangerouslySkipPermissions and
// same product decision as antigravity's dangerouslySkipPermissions and
// claude's --dangerously-skip-permissions. The multi-user clamp strips it
// server-side for non-granted owners.
it('drives runGrok() through the {success,data} envelope and sends alwaysApprove', async () => {
it('drives the grok launch through the {success,data} envelope and sends alwaysApprove', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'grok-case' },
tabCount: { value: '1' },
@@ -1419,7 +1409,7 @@ describe('Grok quick start', () => {
selected.push(id);
};
await app.runGrok();
await app._runCliMode('grok');
const body = requests.find((req) => req.url === '/api/quick-start')?.body;
expect(body).toMatchObject({
@@ -1454,7 +1444,7 @@ describe('Grok quick start', () => {
const errors: string[] = [];
app._reportSessionLaunchError = (_owns: boolean, msg: string) => errors.push(msg);
await app.runGrok();
await app._runCliMode('grok');
expect(requests).toEqual(['/api/grok/status']);
expect(errors[0]).toContain('https://x.ai/cli/install.sh');
@@ -1516,7 +1506,7 @@ describe('Grok quick start', () => {
selected.push(id);
};
await app.runGrok();
await app._runCliMode('grok');
const names = requests.filter((r) => r.url === '/api/quick-start').map((r) => r.body.sessionName);
expect(names).toEqual(['w1-grok-case', 'w2-grok-case', 'w3-grok-case']);