diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index 87bfa961..d58d3655 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -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=` 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=` from `animations`, `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, which is now `isMuxMouseStripMode`, 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, 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=` 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=` from `animations`, `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 → `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 ` / `--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, which is now `isMuxMouseStripMode`, 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/web/public/app.js b/src/web/public/app.js index d4d51543..5633aa21 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -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 - this.teamTasks = new Map(); // Map - this.teammateMap = new Map(); // Map for quick lookup - // Teammate tmux pane terminals (Agent Teams feature) this.teammatePanesByName = new Map(); // Map this.teammateTerminals = new Map(); // Map @@ -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); diff --git a/src/web/public/constants.js b/src/web/public/constants.js index 8d7446b8..f3f2b2ae 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -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 diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 37e28c4f..ce8532a7 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -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 diff --git a/src/web/public/orchestrator-panel.js b/src/web/public/orchestrator-panel.js index 00e2d644..dea402bb 100644 --- a/src/web/public/orchestrator-panel.js +++ b/src/web/public/orchestrator-panel.js @@ -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 // ═══════════════════════════════════════════════════════════════ diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index 097275df..c50317aa 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -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 = ` -
-
- ⬤ - ${escapeHtml(paneData.teammateName)} - terminal -
-
- -
-
-
-
- `; - - 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 `@${escapeHtml(info.name)}`; }, - /** 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 = '
No tasks yet
'; - return; - } - - const html = tasks.map(task => { - const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '◉' : '○'; - const statusClass = task.status.replace('_', '-'); - const ownerBadge = task.owner - ? `${escapeHtml(task.owner)}` - : ''; - return `
- ${statusIcon} - ${escapeHtml(task.subject)} - ${ownerBadge} -
`; - }).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: diff --git a/src/web/public/ralph-panel.js b/src/web/public/ralph-panel.js index 85f9a5d9..d67d8a81 100644 --- a/src/web/public/ralph-panel.js +++ b/src/web/public/ralph-panel.js @@ -953,11 +953,6 @@ Object.assign(CodemanApp.prototype, { } }, - // Legacy method for backwards compatibility - getTodoIcon(status) { - return this.getRalphTaskIcon(status); - }, - // ═══════════════════════════════════════════════════════════════ // Plan Versioning diff --git a/src/web/public/respawn-ui.js b/src/web/public/respawn-ui.js index 1ba7ce24..631cc0e4 100644 --- a/src/web/public/respawn-ui.js +++ b/src/web/public/respawn-ui.js @@ -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(); - }, }); diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 099863c2..a8c68e6e 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -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() 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 diff --git a/src/web/public/settings-ui.js b/src/web/public/settings-ui.js index bf24a1e5..8cd7a874 100644 --- a/src/web/public/settings-ui.js +++ b/src/web/public/settings-ui.js @@ -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. diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 4e452af7..3be18da6 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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)); } diff --git a/src/web/public/subagent-windows.js b/src/web/public/subagent-windows.js index b0e5f882..d39b6c37 100644 --- a/src/web/public/subagent-windows.js +++ b/src/web/public/subagent-windows.js @@ -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(); diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 65d09766..d3838c57 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -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; diff --git a/src/web/public/ultracode-windows.js b/src/web/public/ultracode-windows.js index 6869ccdc..734da9d6 100644 --- a/src/web/public/ultracode-windows.js +++ b/src/web/public/ultracode-windows.js @@ -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; diff --git a/test/custom-model-one-shot-launch.test.ts b/test/custom-model-one-shot-launch.test.ts index 29225302..423bef62 100644 --- a/test/custom-model-one-shot-launch.test.ts +++ b/test/custom-model-one-shot-launch.test.ts @@ -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(); }); }); diff --git a/test/opencode-resize.test.ts b/test/opencode-resize.test.ts index 6dac0007..1f1b5b29 100644 --- a/test/opencode-resize.test.ts +++ b/test/opencode-resize.test.ts @@ -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 } }).app; await app.selectSession(sid); diff --git a/test/run-mode-ui.test.ts b/test/run-mode-ui.test.ts index 1e833794..2e63e5d6 100644 --- a/test/run-mode-ui.test.ts +++ b/test/run-mode-ui.test.ts @@ -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() 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() + // 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(); // 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 = { 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 = { 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 = { 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 = { 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']);