diff --git a/CLAUDE.md b/CLAUDE.md index 0a552ae8..137812ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,6 +77,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Build the docker agent image | `node scripts/build-agent-image.mjs --no-cache` (builds `codeman/agent:base` from `docker/agent.Dockerfile`; prerequisite for Docker cases; `--engine`/`--image`). ⚠ **Always `--no-cache`** — a plain rebuild re-uses the cached `npm install -g` layer and silently keeps the CLIs frozen at their original versions, which once shipped a BROKEN codex while reporting success. See `docs/docker-cases.md` | | Browser-test exclusion check | `npm run check:browser-excludes` (`scripts/check-browser-test-excludes.mjs`; runs in CI, <1s). Fails if a test importing playwright/puppeteer is still collected by `config/vitest.ci.config.ts`; add it to `BROWSER_TEST_GLOBS` in `config/test-suites.ts` | | Dead-code report | `npm run knip` (`config/knip.json`; fetched with `npx`, so it needs the network and is not a CI step). It reports nothing on a clean tree: an export, file or dependency it lists is new dead code, or a manual script that needs an `entry`. Tests count as users, and `src/web/public/` is outside it (plain scripts with no imports). A deliberate keep carries `@knipignore` in its JSDoc | +| Frontend dead-code check | `npm run check:frontend-dead` (`scripts/check-frontend-dead-code.mjs`, a few seconds, not a CI step). `src/web/public/` is plain scripts that knip cannot follow, so this is its counterpart there: it fails on a function, method or top-level binding that nothing references (identifiers, strings, `index.html`, the gesture overlay's source, tests) and on a CSS class or id that no markup or script can produce. `--production` also lists what only tests reference. A deliberate keep goes in the script's `ALLOW` map with its reason | | Pre-push hook | Installed by `npm install` (`scripts/git-hooks.mjs`, via postinstall): runs the static CI checks (~10-40s) before `git push`. Skip once: `CODEMAN_SKIP_PREPUSH=1 git push`. Skips itself with a notice when HEAD is not the pushed commit or the tree has uncommitted changes the checks would read. Marker-owned, so a hand-written `pre-push` is never overwritten; installs ONLY into the repo's own `/hooks` (worktree-safe; a `core.hooksPath` elsewhere, e.g. a global one, is left alone) | | Production logs | `journalctl --user -u codeman-web -f` | | Detached server | `codeman web -d` (`--status`, `--stop`; pidfile+log at `dataPath('web.pid'/'web.log')`). ⚠ Refuses to start a 2nd server on one data dir — see Instance isolation | diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index d58d3655..e17a089b 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -1123,7 +1123,7 @@ Tests: `test/tab-rail-search.test.ts` (gate) and `test/tab-rail-search.browser.t **Adding a skin means touching four places, and each one fails silently on its own**: (1) the `html[data-skin="…"]` token block in `styles.css`; (2) the xterm ANSI palette in `CODEMAN_XTERM_THEMES` (terminal-ui.js) — a missing entry falls back to `daylight-blue`, so the terminal simply stays the wrong color; (3) the pre-paint allowlist array in `index.html` — a skin missing there is coerced to `daylight-blue` on every reload, which reads as "my theme keeps resetting"; (4) the `` popups, date pickers and scrollbars as dark widgets on a light page. xterm `minimumContrastRatio: 4.5` (set at construction in terminal-ui.js **and** panels-ui.js for teammate terminals, and re-applied in `applyTerminalSkin()`), because CLIs emit colors chosen for dark backgrounds; dark skins keep `1` to avoid the per-cell contrast work. And `applyTerminalSkin()` must call `this._localEchoOverlay?.refreshFont()` — the zero-lag overlay **caches** the terminal fg/bg at construction, so without the refresh typed-but-not-yet-flushed text keeps the previous theme's dark backing after a live skin switch. +**Light skins carry three extra obligations.** `color-scheme: light` on the `html[data-skin]` block, or the UA renders native ` - ${item.priority ? `${item.priority}` : ''} - ${escapeHtml(item.content)} - `; - fragment.appendChild(row); - }); - list.appendChild(fragment); - }, - - togglePlanItem(index) { - const plan = this.ralphWizardConfig.generatedPlan; - if (plan && plan[index]) { - plan[index].enabled = !plan[index].enabled; - this.renderPlanChecklist(); - } - }, - - async cancelPlanGeneration() { - this.stopPlanGeneration(); - this.planGenerationStopped = true; // Ignore future SSE events - - // Call the cancel API to stop server-side processing - if (this.activePlanOrchestratorId) { - try { - console.log('[Cancel] Sending cancel request for', this.activePlanOrchestratorId); - await fetch('/api/cancel-plan-generation', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ orchestratorId: this.activePlanOrchestratorId }), - }); - this.activePlanOrchestratorId = null; - } catch (err) { - console.error('[Cancel] Failed to cancel plan generation:', err); - } - } - - this.showToast('Plan generation stopped', 'info'); - - // Allow user to proceed without a plan by clicking Next - this.ralphWizardConfig.skipPlanGeneration = true; - - // Show stopped state with clear visual indicator - const errorEl = document.getElementById('planGenerationError'); - const msgEl = document.getElementById('planErrorMsg'); - const stoppedIndicator = document.getElementById('planStoppedIndicator'); - - // Show the stopped indicator, hide error message since this was intentional - if (stoppedIndicator) stoppedIndicator.style.display = 'flex'; - if (msgEl) msgEl.textContent = ''; - - // Hide spinner immediately and show stopped state - document.getElementById('planGenerationLoading')?.classList.add('hidden'); - errorEl?.classList.remove('hidden'); - - // Close all plan subagent windows - this.closePlanSubagentWindows(); - }, - - // ═══════════════════════════════════════════════════════════════ - // Plan Subagent Windows - // ═══════════════════════════════════════════════════════════════ - - handlePlanSubagentEvent(event) { - if (this.planGenerationStopped) return; - - const { type, agentId, agentType, model, status, detail, itemCount, durationMs, error } = event; - - if (type === 'started') { - this.createPlanSubagentWindow(agentId, agentType, model, detail); - } else if (type === 'completed' || type === 'failed') { - this.updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error); - } else if (type === 'progress') { - const windowData = this.planSubagents.get(agentId); - if (windowData?.element) { - const detailEl = windowData.element.querySelector('.plan-subagent-detail'); - if (detailEl) detailEl.textContent = detail || ''; - } - } - - this.renderMonitorPlanAgents(); - }, - - /** - * Position a plan subagent window to the left or right of the wizard. - * Research goes left, planner goes right. - */ - _positionPlanSubagentWindow(agentType) { - const wizardModal = document.getElementById('ralphWizardModal'); - const wizardContent = wizardModal?.querySelector('.modal-content'); - const wizardRect = wizardContent?.getBoundingClientRect(); - const gap = 20; - const windowWidth = 280; - - if (!wizardRect) { - const offset = this.planSubagents.size * 30; - return { x: window.innerWidth - windowWidth - 50 + offset, y: 120 + offset }; - } - - const side = agentType === 'research' ? 'left' : 'right'; - let x = side === 'left' - ? wizardRect.left - windowWidth - gap - : wizardRect.right + gap; - x = Math.max(10, Math.min(x, window.innerWidth - windowWidth - 10)); - const y = Math.max(60, Math.min(wizardRect.top, window.innerHeight - 120)); - - return { x, y }; - }, - - createPlanSubagentWindow(agentId, agentType, model, detail) { - if (this.planSubagents.has(agentId)) return; - - const { x, y } = this._positionPlanSubagentWindow(agentType); - - const win = document.createElement('div'); - win.className = 'plan-subagent-window'; - win.id = `plan-subagent-${agentId}`; - win.style.left = `${x}px`; - win.style.top = `${y}px`; - win.style.zIndex = ++this.planSubagentWindowZIndex; - - const typeLabels = { research: 'Research Agent', planner: 'Planner' }; - const typeIcons = { research: '🔬', planner: '📋' }; - - win.innerHTML = ` -
- - ${typeIcons[agentType] || '🤖'} - ${typeLabels[agentType] || escapeHtml(agentType)} - - ${model} -
-
-
- - Running... -
-
${detail || ''}
-
- `; - - document.body.appendChild(win); - - const header = win.querySelector('.plan-subagent-header'); - const dragListeners = this.makePlanSubagentDraggable(win, header); - - this.planSubagents.set(agentId, { - agentId, - type: agentType, - model, - status: 'running', - startTime: Date.now(), - element: win, - dragListeners, - }); - }, - - makePlanSubagentDraggable(win, handle) { - let isDragging = false; - let startX, startY, startLeft, startTop; - - const mousedownHandler = (e) => { - isDragging = true; - startX = e.clientX; - startY = e.clientY; - startLeft = parseInt(win.style.left) || 0; - startTop = parseInt(win.style.top) || 0; - win.style.zIndex = ++this.planSubagentWindowZIndex; - e.preventDefault(); - }; - - const moveHandler = (e) => { - if (!isDragging) return; - const newLeft = Math.max(10, Math.min(startLeft + (e.clientX - startX), window.innerWidth - 290)); - const newTop = Math.max(10, Math.min(startTop + (e.clientY - startY), window.innerHeight - 110)); - win.style.left = `${newLeft}px`; - win.style.top = `${newTop}px`; - }; - - const upHandler = () => { isDragging = false; }; - - handle.addEventListener('mousedown', mousedownHandler); - document.addEventListener('mousemove', moveHandler); - document.addEventListener('mouseup', upHandler); - - return { mousedown: mousedownHandler, move: moveHandler, up: upHandler }; - }, - - updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error) { - const windowData = this.planSubagents.get(agentId); - if (!windowData?.element) return; - - const win = windowData.element; - const statusEl = win.querySelector('.plan-subagent-status'); - const statusTextEl = win.querySelector('.plan-subagent-status-text'); - const spinnerEl = win.querySelector('.plan-subagent-spinner'); - const detailEl = win.querySelector('.plan-subagent-detail'); - - windowData.status = status; - windowData.itemCount = itemCount; - - if (status === 'completed') { - statusEl?.classList.remove('running'); - statusEl?.classList.add('completed'); - if (spinnerEl) spinnerEl.style.display = 'none'; - if (statusTextEl) statusTextEl.textContent = `Done (${itemCount || 0} items)`; - if (detailEl && durationMs) detailEl.textContent = `${(durationMs / 1000).toFixed(1)}s`; - } else if (status === 'failed' || status === 'cancelled') { - statusEl?.classList.remove('running'); - statusEl?.classList.add('failed'); - if (spinnerEl) spinnerEl.style.display = 'none'; - if (statusTextEl) statusTextEl.textContent = status === 'cancelled' ? 'Cancelled' : 'Failed'; - if (detailEl) detailEl.textContent = error || ''; - } - }, - - closePlanSubagentWindows() { - for (const [, windowData] of this.planSubagents) { - if (windowData.dragListeners) { - document.removeEventListener('mousemove', windowData.dragListeners.move); - document.removeEventListener('mouseup', windowData.dragListeners.up); - } - if (windowData.element) { - windowData.element.remove(); - } - } - this.planSubagents.clear(); - this.renderMonitorPlanAgents(); - }, - - regeneratePlan() { - this.ralphWizardConfig.generatedPlan = null; - this.ralphWizardConfig.planGenerated = false; - this.generatePlan(); - }, - - generateFixPlanContent(items) { - // Group items by priority - const p0Items = items.filter(i => i.priority === 'P0'); - const p1Items = items.filter(i => i.priority === 'P1'); - const p2Items = items.filter(i => i.priority === 'P2'); - const noPriorityItems = items.filter(i => !i.priority); - - let content = '# Implementation Plan\n\n'; - content += `Generated: ${new Date().toISOString().slice(0, 10)}\n\n`; - - if (p0Items.length > 0) { - content += '## Critical Path (P0)\n\n'; - p0Items.forEach(item => { - content += `- [ ] ${item.content}\n`; - }); - content += '\n'; - } - - if (p1Items.length > 0) { - content += '## Standard (P1)\n\n'; - p1Items.forEach(item => { - content += `- [ ] ${item.content}\n`; - }); - content += '\n'; - } - - if (p2Items.length > 0) { - content += '## Nice-to-Have (P2)\n\n'; - p2Items.forEach(item => { - content += `- [ ] ${item.content}\n`; - }); - content += '\n'; - } - - if (noPriorityItems.length > 0) { - content += '## Tasks\n\n'; - noPriorityItems.forEach(item => { - content += `- [ ] ${item.content}\n`; - }); - content += '\n'; - } - - return content; - }, - - async startRalphLoop() { - const config = this.ralphWizardConfig; - config.enableRespawn = document.getElementById('ralphEnableRespawn')?.checked ?? false; - this.closeRalphWizard(); - - const enabledItems = config.generatedPlan?.filter(i => i.enabled); - - try { - const ralphGlobalSettings = this.loadAppSettingsFromStorage(); - const envOverrides = this.buildEnvOverrides(this.getCaseSettings(config.caseName), ralphGlobalSettings); - const effort = this.getEffortSetting(ralphGlobalSettings); - const advisorModel = this.getAdvisorSetting(ralphGlobalSettings); - const res = await fetch('/api/ralph-loop/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - caseName: config.caseName, - taskDescription: config.taskDescription, - completionPhrase: config.completionPhrase, - maxIterations: config.maxIterations || null, - enableRespawn: config.enableRespawn, - planItems: enabledItems?.length ? enabledItems : undefined, - ...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}), - ...(effort ? { effort } : {}), - ...(advisorModel ? { advisorModel } : {}), - }), - }); - const data = await res.json(); - if (!data.success) { - this.showToast(data.error || 'Failed to start', 'error'); - return; - } - this.ralphClosedSessions.delete(data.data.sessionId); - await this.selectSession(data.data.sessionId); - this.showToast(`Ralph Loop started in ${config.caseName}`, 'success'); - } catch (err) { - console.error('Failed to start Ralph loop:', err); - this.showToast('Failed to start Ralph loop: ' + err.message, 'error'); - } - }, - -}); diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index a8c68e6e..09a350bc 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -8,7 +8,7 @@ * @dependency app.js (CodemanApp class, this.sessions, this.cases, this.activeSessionId) * @dependency constants.js (escapeHtml) * @dependency mobile-handlers.js (MobileDetection) - * @loadorder 12 of 15 — loaded after panels-ui.js, before ralph-wizard.js + * @loadorder 12 of 15 — loaded after panels-ui.js, before api-client.js */ /** diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 123b6a48..4c802d2a 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -11335,47 +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)); } -/* Wizard connection lines - different color to show plan generation link */ -.connection-line.wizard-connection { - stroke: #60a5fa; - stroke-width: 3; - /* Dark outline for contrast against blurred backgrounds */ - filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)) - drop-shadow(0 0 5px #60a5fa) - drop-shadow(0 0 10px #60a5fa); - stroke-dasharray: 8 4; - animation: wizard-pulse 1.5s ease-in-out infinite; -} - -@keyframes wizard-pulse { - 0%, 100% { opacity: 0.75; } - 50% { opacity: 1; } -} - -/* Plan subagent to regular subagent connection lines (Opus → Haiku) */ -.connection-line.plan-to-subagent-line { - stroke: #3b82f6; - stroke-width: 3; - /* Dark outline for contrast, vibrant blue glow */ - filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)) - drop-shadow(0 0 4px rgba(59, 130, 246, 0.8)) - drop-shadow(0 0 8px rgba(59, 130, 246, 0.5)); - stroke-dasharray: 5 3; - animation: plan-subagent-pulse 1.2s ease-in-out infinite; -} - -.connection-line.plan-to-subagent-line:hover { - stroke-width: 3.5; - filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)) - drop-shadow(0 0 6px rgba(59, 130, 246, 1)) - drop-shadow(0 0 12px rgba(59, 130, 246, 0.7)); -} - -@keyframes plan-subagent-pulse { - 0%, 100% { opacity: 0.8; } - 50% { opacity: 1; } -} - /* ===== Session lineage lines (tab → tab it spawned, session-lineage.js) ===== One quiet orthogonal tree per spawning tab, every family always drawn and routed through the gaps between tab rows (computeLineageTree in constants.js). @@ -12623,565 +12582,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat cursor: pointer; } -/* ========== Ralph Loop Wizard ========== */ - -/* Wizard Progress Indicator */ -.wizard-progress { - display: flex; - justify-content: center; - gap: 1.5rem; - padding: 0.5rem 0.75rem; - background: var(--bg-dark); - border-bottom: 1px solid var(--border); -} - -.wizard-step { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--text-muted); - font-size: 0.8rem; - transition: color 0.2s; -} - -.wizard-step.active { - color: var(--accent); -} - -.wizard-step.completed { - color: var(--green); -} - -.wizard-step-number { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 50%; - font-size: 0.7rem; - font-weight: 600; -} - -.wizard-step.active .wizard-step-number { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -.wizard-step.completed .wizard-step-number { - background: var(--green); - border-color: var(--green); - color: #fff; -} - -.wizard-step-label { - font-weight: 500; -} - -/* Wizard Pages */ -.wizard-page { - min-height: 180px; -} - -.wizard-page.hidden { - display: none; -} - -/* Wizard Footer */ -.wizard-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - border-top: 1px solid var(--border); - background: var(--bg-card); -} - -.wizard-footer-right { - display: flex; - gap: 0.5rem; -} - -/* Wizard Modal - compact layout */ -.modal-wizard { - width: 480px; - max-width: 95vw; - max-height: 70vh; - display: flex; - flex-direction: column; -} - -/* Draggable wizard state */ -.modal-wizard.draggable { - position: absolute; - transform: none; - cursor: default; -} - -.modal-wizard .modal-body { - overflow-y: auto; - flex: 1; - min-height: 0; -} - -.modal-wizard .wizard-footer { - flex-shrink: 0; -} - -.wizard-page-compact { - min-height: auto; - padding: 0.5rem 0; -} - -.wizard-step-intro { - text-align: center; - margin-bottom: 0.75rem; - color: var(--text-dim); - font-size: 0.9rem; -} - -.wizard-step-intro p { - margin: 0; -} - -/* Wizard inline row for compact layout */ -.wizard-inline-row { - display: flex; - gap: 1rem; - margin-top: 0.75rem; -} - -.wizard-inline-field { - flex: 1; -} - -.wizard-inline-field-small { - flex: 0 0 auto; -} - -.wizard-inline-field label { - display: block; - font-size: 0.8rem; - color: var(--text-dim); - margin-bottom: 0.25rem; -} - -.iteration-presets-compact { - display: flex; - gap: 0.25rem; -} - -.iteration-presets-compact .iteration-preset-btn { - padding: 0.35rem 0.6rem; - font-size: 0.75rem; - min-width: 36px; -} - -/* Plan Generation Step */ -.plan-section { - padding: 1rem; - text-align: center; -} - -.plan-actions { - display: flex; - gap: 0.5rem; - justify-content: center; -} - -/* Existing Plan Badge (in case selector) */ -.existing-plan-badge { - font-size: 0.7rem; - background: var(--accent); - color: white; - padding: 0.1rem 0.4rem; - border-radius: 3px; - margin-left: 0.5rem; - font-weight: normal; -} - -/* Existing Plan Card (Step 2) */ -.existing-plan-card { - background: var(--bg-input); - border: 1px solid var(--accent); - border-radius: 6px; - padding: 1rem; - text-align: left; -} - -.existing-plan-header { - display: flex; - align-items: center; - gap: 0.5rem; - font-weight: 500; - margin-bottom: 0.75rem; -} - -.existing-plan-icon { - font-size: 1.2rem; -} - -.existing-plan-stats { - display: flex; - gap: 1rem; - margin-bottom: 1rem; - font-size: 0.85rem; -} - -.existing-plan-stats .stat { - display: flex; - align-items: center; - gap: 0.25rem; -} - -.existing-plan-stats .stat.pending { - color: var(--yellow); -} - -.existing-plan-stats .stat.completed { - color: var(--green); -} - -.existing-plan-stats .stat.total { - color: var(--text-muted); -} - -.existing-plan-actions { - display: flex; - gap: 0.5rem; -} - -/* Plan Loading State */ -.plan-loading-state { - padding: 1.5rem 1rem; - text-align: center; -} - -.plan-loading-content { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-bottom: 0.75rem; -} - -.plan-spinner { - width: 28px; - height: 28px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 1s linear infinite; - flex-shrink: 0; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.plan-loading-text { - display: flex; - flex-direction: column; - align-items: flex-start; - text-align: left; -} - -.plan-loading-title { - font-weight: 500; - color: var(--text); -} - -.plan-loading-time { - font-size: 0.8rem; - color: var(--text-muted); - font-family: 'SF Mono', Monaco, monospace; -} - -.plan-loading-hint { - color: var(--text-dim); - font-size: 0.85rem; - margin: 0 0 1rem 0; -} - -.plan-error-msg { - color: var(--red); - margin-bottom: 1rem; -} - -.plan-option-group { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.plan-option-group label { - font-size: 0.8rem; - color: var(--text-dim); -} - -.plan-option-group select { - padding: 0.3rem 0.5rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text); - font-size: 0.8rem; -} - -.plan-detail-btns { - display: flex; - gap: 0.25rem; -} - -.plan-detail-btn { - padding: 0.3rem 0.6rem; - font-size: 0.75rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text-dim); - cursor: pointer; - transition: all 0.15s; -} - -.plan-detail-btn:hover { - border-color: var(--border-light); -} - -.plan-detail-btn.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -/* Plan Editor */ -.plan-editor-header { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-bottom: 0.75rem; - font-weight: 500; - font-size: 0.9rem; -} - -.plan-stats { - font-weight: normal; - font-size: 0.8rem; - color: var(--text-muted); - margin-left: 0.5rem; -} - -.plan-editor-actions { - display: flex; - gap: 0.35rem; - flex-wrap: wrap; - align-items: center; -} - -.plan-items-list { - display: flex; - flex-direction: column; - gap: 0.35rem; - max-height: 280px; - overflow-y: auto; - margin-bottom: 0.5rem; -} - -.plan-loading-actions { - display: flex; - gap: 0.5rem; - justify-content: center; -} - -.plan-autostart-toggle { - margin-top: 1.25rem; - padding-top: 1rem; - border-top: 1px solid var(--border); -} - -.plan-autostart-label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - justify-content: center; -} - -.plan-autostart-label input[type="checkbox"] { - width: 18px; - height: 18px; - accent-color: var(--accent); - cursor: pointer; -} - -.plan-autostart-text { - font-size: 0.85rem; - color: var(--text); -} - -.plan-autostart-label:hover .plan-autostart-text { - color: var(--accent); -} - -/* Existing wizard files section */ - -.plan-cancel-btn { - color: var(--red); -} - -/* Plan Stopped State */ -.plan-stopped-indicator { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - margin-bottom: 1rem; - padding: 0.75rem; - background: var(--surface); - border-radius: 6px; - border: 1px solid var(--border); -} - -.plan-stopped-icon { - color: var(--red); - font-size: 1rem; -} - -.plan-stopped-text { - font-weight: 500; - color: var(--text); -} - -.plan-skip-hint { - font-size: 0.75rem; - color: var(--text-muted); - text-align: center; - margin-top: 0.75rem; -} - -.plan-skip-hint strong { - color: var(--text-dim); -} - -/* ========== Plan Subagent Windows (Visible Opus Agents) ========== */ -.plan-subagent-window { - position: fixed; - width: 280px; - background: var(--bg-card); - border: 1px solid var(--accent); - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.2); - overflow: hidden; - animation: planSubagentSpawn 0.3s ease-out; -} - -@keyframes planSubagentSpawn { - from { - opacity: 0; - transform: scale(0.8); - } - to { - opacity: 1; - transform: scale(1); - } -} - -.plan-subagent-header { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(96, 165, 250, 0.1)); - border-bottom: 1px solid var(--border); - cursor: grab; - user-select: none; -} - -.plan-subagent-header:active { - cursor: grabbing; -} - -.plan-subagent-icon { - font-size: 1rem; -} - -.plan-subagent-title { - flex: 1; - font-weight: 500; - font-size: 0.8rem; - color: var(--text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.plan-subagent-model { - font-size: 0.65rem; - padding: 0.15rem 0.4rem; - background: #7c3aed; - color: white; - border-radius: 3px; - text-transform: uppercase; - font-weight: 600; -} - -.plan-subagent-body { - padding: 0.75rem; -} - -.plan-subagent-status { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.35rem; -} - -.plan-subagent-status.running .plan-subagent-spinner { - width: 14px; - height: 14px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -.plan-subagent-status.completed .plan-subagent-status-text { - color: var(--green); -} - -.plan-subagent-status.failed .plan-subagent-status-text { - color: var(--red); -} - -.plan-subagent-status-text { - font-size: 0.8rem; - font-weight: 500; -} - -.plan-subagent-detail { - font-size: 0.7rem; - color: var(--text-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Clickable status text */ -.plan-subagent-status-text.clickable { - cursor: pointer; - text-decoration: underline; - text-decoration-style: dotted; - text-underline-offset: 2px; -} - -.plan-subagent-status-text.clickable:hover { - text-decoration-style: solid; - color: var(--accent); -} - /* Ralph panel version info */ .ralph-version-info { display: flex; @@ -13212,129 +12612,6 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat text-decoration: underline; } -/* Textarea with AI Assist button */ -.textarea-with-assist { - position: relative; -} - -.textarea-with-assist textarea { - width: 100%; - padding: 0.6rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text); - font-size: 0.85rem; - font-family: 'SF Mono', Monaco, 'Courier New', monospace; - resize: vertical; - min-height: 80px; -} - -.textarea-with-assist textarea:focus { - outline: none; - border-color: var(--accent); -} - -/* Iteration Presets */ - -.iteration-preset-btn { - padding: 0.5rem 1rem; - font-size: 0.8rem; - font-weight: 500; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text-dim); - cursor: pointer; - transition: all 0.15s; - min-width: 50px; -} - -.iteration-preset-btn:hover { - border-color: var(--border-light); - color: var(--text); -} - -.iteration-preset-btn.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -/* Case Select Inline */ -.case-select-inline { - display: flex; - gap: 0.5rem; -} - -.case-select-inline select { - flex: 1; -} - -/* Ralph Prompt Preview */ -.ralph-preview-section { - margin-bottom: 1rem; -} - -.ralph-preview-section > label { - display: block; - font-size: 0.7rem; - color: var(--text-dim); - margin-bottom: 0.35rem; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.ralph-prompt-preview { - background: var(--bg-dark); - border: 1px solid var(--border); - border-radius: 4px; - padding: 0.5rem 0.75rem; - font-family: 'SF Mono', Monaco, 'Courier New', monospace; - font-size: 0.75rem; - color: var(--text-dim); - max-height: 120px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-word; -} - -.ralph-prompt-preview .preview-highlight { - color: var(--accent); - font-weight: 500; -} - -/* Config Summary */ -.ralph-config-summary { - background: var(--bg-input); - border-radius: 4px; - padding: 0.75rem; - margin-bottom: 1rem; -} - -.config-summary-row { - display: flex; - justify-content: space-between; - padding: 0.35rem 0; - font-size: 0.8rem; -} - -.config-summary-row:not(:last-child) { - border-bottom: 1px solid var(--border); -} - -.config-label { - color: var(--text-dim); -} - -.config-summary-row code { - background: var(--bg-dark); - padding: 0.15rem 0.4rem; - border-radius: 3px; - font-size: 0.75rem; - color: var(--text); -} - /* Advanced Options */ .advanced-options { margin-top: 1rem; diff --git a/src/web/public/subagent-windows.js b/src/web/public/subagent-windows.js index d39b6c37..2bc759e6 100644 --- a/src/web/public/subagent-windows.js +++ b/src/web/public/subagent-windows.js @@ -243,11 +243,6 @@ Object.assign(CodemanApp.prototype, { const svg = document.getElementById('connectionLines'); if (!svg) return; - // Check if Ralph wizard modal is open - const wizardModal = document.getElementById('ralphWizardModal'); - const wizardOpen = wizardModal?.classList.contains('active'); - const wizardContent = wizardOpen ? wizardModal.querySelector('.modal-content') : null; - // Collect visible regular subagent windows const visibleSubagentWindows = []; for (const [agentId, windowInfo] of this.subagentWindows) { @@ -257,11 +252,6 @@ Object.assign(CodemanApp.prototype, { visibleSubagentWindows.push({ agentId, windowInfo, win }); } - // Get plan subagent windows as array for distribution - const planSubagentArray = Array.from(this.planSubagents.entries()) - .filter(([, data]) => data.element) - .map(([id, data]) => ({ id, ...data })); - // === PHASE 1: Batch all layout reads (getBoundingClientRect) === // Reading layout properties forces the browser to calculate layout. // By batching all reads before any writes, we avoid repeated forced reflows. @@ -272,36 +262,14 @@ Object.assign(CodemanApp.prototype, { rects.set('sub:' + agentId, win.getBoundingClientRect()); } - // Read all plan subagent rects - for (const planAgent of planSubagentArray) { - rects.set('plan:' + planAgent.id, planAgent.element.getBoundingClientRect()); - } - - // Read wizard rect (if open) - let wizardRect = null; - if (wizardOpen && wizardContent) { - wizardRect = wizardContent.getBoundingClientRect(); - } - - // Read tab rects for normal mode (only tabs that are actually needed). + // Read tab rects (only tabs that are actually needed). // Only a painted row is cached: a row a search or filter hid has no line // (_paintedSessionTab), rather than one drawn from the viewport's corner. - if (!wizardOpen) { - for (const { agentId } of visibleSubagentWindows) { - const parentSessionId = this.subagentParentMap.get(agentId); - if (!parentSessionId || rects.has('tab:' + parentSessionId)) continue; - const tab = this._paintedSessionTab(parentSessionId); - if (tab) rects.set('tab:' + parentSessionId, tab.getBoundingClientRect()); - } - } - - // Read plan window rects for wizard-to-plan lines - if (wizardOpen && wizardContent && this.planSubagents.size > 0 && !this.planAgentsMinimized) { - for (const [agentId, windowData] of this.planSubagents) { - if (!windowData.element) continue; - const key = 'planwin:' + agentId; - if (!rects.has(key)) rects.set(key, windowData.element.getBoundingClientRect()); - } + for (const { agentId } of visibleSubagentWindows) { + const parentSessionId = this.subagentParentMap.get(agentId); + if (!parentSessionId || rects.has('tab:' + parentSessionId)) continue; + const tab = this._paintedSessionTab(parentSessionId); + if (tab) rects.set('tab:' + parentSessionId, tab.getBoundingClientRect()); } // === PHASE 2: DOM writes using cached rects (no more layout reads) === @@ -310,149 +278,34 @@ Object.assign(CodemanApp.prototype, { for (const { agentId } of visibleSubagentWindows) { const winRect = rects.get('sub:' + agentId); - // If wizard is open with plan subagents, connect regular subagents to plan subagent windows - if (wizardOpen && wizardContent && planSubagentArray.length > 0) { - // Find the nearest plan subagent window to connect to - let nearestPlanAgent = null; - let nearestDistance = Infinity; + // Connect the agent window to its parent TAB. + // Use the PERSISTENT subagentParentMap as the ONLY source of truth + const parentSessionId = this.subagentParentMap.get(agentId); - for (const planAgent of planSubagentArray) { - const planRect = rects.get('plan:' + planAgent.id); - const planCenterX = planRect.left + planRect.width / 2; - const planCenterY = planRect.top + planRect.height / 2; - const winCenterX = winRect.left + winRect.width / 2; - const winCenterY = winRect.top + winRect.height / 2; - const distance = Math.hypot(planCenterX - winCenterX, planCenterY - winCenterY); - - if (distance < nearestDistance) { - nearestDistance = distance; - nearestPlanAgent = planAgent; - } - } - - if (nearestPlanAgent) { - const planRect = rects.get('plan:' + nearestPlanAgent.id); - - // Draw line from plan subagent window to regular subagent window - let x1, y1, x2, y2; - const planCenterX = planRect.left + planRect.width / 2; - const winCenterX = winRect.left + winRect.width / 2; - - if (winCenterX < planCenterX) { - x1 = planRect.left; - y1 = planRect.top + planRect.height / 2; - x2 = winRect.right; - y2 = winRect.top + winRect.height / 2; - } else { - x1 = planRect.right; - y1 = planRect.top + planRect.height / 2; - x2 = winRect.left; - y2 = winRect.top + winRect.height / 2; - } - - const midX = (x1 + x2) / 2; - const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`; - - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', path); - line.setAttribute('class', 'connection-line plan-to-subagent-line'); - line.setAttribute('data-agent-id', agentId); - line.setAttribute('data-plan-agent-id', nearestPlanAgent.id); - svg.appendChild(line); - } - } else if (wizardOpen && wizardContent) { - // Wizard open but no plan subagents - connect directly to wizard - const winCenterX = winRect.left + winRect.width / 2; - const wizardCenterX = wizardRect.left + wizardRect.width / 2; - - let x1, y1, x2, y2; - - if (winCenterX < wizardCenterX) { - x1 = wizardRect.left; - y1 = wizardRect.top + wizardRect.height / 2; - x2 = winRect.right; - y2 = winRect.top + winRect.height / 2; - } else { - x1 = wizardRect.right; - y1 = wizardRect.top + wizardRect.height / 2; - x2 = winRect.left; - y2 = winRect.top + winRect.height / 2; - } - - const midX = (x1 + x2) / 2; - const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`; - - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', path); - line.setAttribute('class', 'connection-line wizard-connection'); - line.setAttribute('data-agent-id', agentId); - svg.appendChild(line); - } else { - // NORMAL MODE: Connect agent window to its parent TAB - // Use the PERSISTENT subagentParentMap as the ONLY source of truth - const parentSessionId = this.subagentParentMap.get(agentId); - - if (!parentSessionId) { - // No parent known yet - skip this agent - continue; - } - - const tabRect = rects.get('tab:' + parentSessionId); - if (!tabRect) { - // Tab not painted (session closed, collapsed group, or hidden by a search) - continue; - } - - // Draw a curved line from the tab to the window. Header strip: tab - // bottom-center → window top-center (vertical). Sidebar: tab right-edge → - // window left-edge (horizontal), otherwise the curve loops backwards - // underneath the sidebar. _tabAnchor/_tabConnectorPath live in app.js. - const anchor = this._tabAnchor(tabRect); - const path = this._tabConnectorPath(anchor, winRect); - - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', path); - line.setAttribute('class', 'connection-line'); - line.setAttribute('data-agent-id', agentId); - line.setAttribute('data-parent-tab', parentSessionId); - svg.appendChild(line); + if (!parentSessionId) { + // No parent known yet - skip this agent + continue; } - } - // Draw lines from wizard to plan subagent windows (Opus agents during plan generation) - // Skip if agents are minimized to tab - if (wizardOpen && wizardContent && this.planSubagents.size > 0 && !this.planAgentsMinimized) { - for (const [agentId] of this.planSubagents) { - const winRect = rects.get('planwin:' + agentId); - if (!winRect) continue; - - // Determine which side of wizard the window is on - const winCenterX = winRect.left + winRect.width / 2; - const wizardCenterX = wizardRect.left + wizardRect.width / 2; - - let x1, y1, x2, y2; - - if (winCenterX < wizardCenterX) { - x1 = wizardRect.left; - y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50); - x2 = winRect.right; - y2 = winRect.top + winRect.height / 2; - } else { - x1 = wizardRect.right; - y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50); - x2 = winRect.left; - y2 = winRect.top + winRect.height / 2; - } - - const midX = (x1 + x2) / 2; - const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`; - - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', path); - line.setAttribute('class', 'connection-line wizard-connection plan-subagent-line'); - line.setAttribute('data-plan-agent-id', agentId); - svg.appendChild(line); + const tabRect = rects.get('tab:' + parentSessionId); + if (!tabRect) { + // Tab not painted (session closed, collapsed group, or hidden by a search) + continue; } + + // Draw a curved line from the tab to the window. Header strip: tab + // bottom-center → window top-center (vertical). Sidebar: tab right-edge → + // window left-edge (horizontal), otherwise the curve loops backwards + // underneath the sidebar. _tabAnchor/_tabConnectorPath live in app.js. + const anchor = this._tabAnchor(tabRect); + const path = this._tabConnectorPath(anchor, winRect); + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + line.setAttribute('d', path); + line.setAttribute('class', 'connection-line'); + line.setAttribute('data-agent-id', agentId); + line.setAttribute('data-parent-tab', parentSessionId); + svg.appendChild(line); } // Ultracode floating run windows → parent tab (additional layer, ultracode-windows.js). @@ -475,83 +328,6 @@ Object.assign(CodemanApp.prototype, { this._applyLineEntrances?.(svg); }, - // ═══════════════════════════════════════════════════════════════ - // Lazy Terminal Lifecycle - // ═══════════════════════════════════════════════════════════════ - // - // Teammate terminal windows use xterm.js Terminal instances that consume - // ~75KB of DOM memory each. With 50 agents minimized, that's ~3.75MB of - // invisible terminals. To avoid this, we dispose the Terminal when a - // window is minimized and lazily re-create it when restored. - // - // Flow: - // minimize → _disposeTeammateTerminalForMinimize() → sets _lazyTerminal flag - // restore → _restoreTeammateTerminalFromLazy() → re-creates Terminal - // create (hidden/minimized) → skip initTeammateTerminal, set _lazyTerminal - // - // The tmux pane buffer is re-fetched from the API on restore. Regular - // (non-teammate) subagent windows use activity HTML and are unaffected - // by this optimization. - - /** - * Dispose a teammate terminal when its window is minimized. - * Saves pane metadata so the terminal can be re-created on restore. - * No-op if the window has no teammate terminal. - */ - _disposeTeammateTerminalForMinimize(agentId) { - const termData = this.teammateTerminals.get(agentId); - if (!termData) return; // Not a teammate terminal window - - const windowData = this.subagentWindows.get(agentId); - - // Save pane metadata needed to re-create the terminal on restore - if (windowData) { - windowData._lazyTerminal = true; - windowData._lazyPaneTarget = termData.paneTarget; - windowData._lazySessionId = termData.sessionId; - } - - // Dispose the resize observer - if (termData.resizeObserver) { - termData.resizeObserver.disconnect(); - } - - // Dispose the xterm.js Terminal instance (frees DOM nodes and internal buffers) - if (termData.terminal) { - try { - termData.terminal.dispose(); - } catch {} - } - - // Remove from teammateTerminals map so renderSubagentWindowContent won't skip this window - // (the activity HTML can serve as a lightweight placeholder while minimized) - this.teammateTerminals.delete(agentId); - }, - - /** - * Re-create a teammate terminal when its window is restored from minimized state. - * Fetches the current pane buffer from the API (tmux is the source of truth). - * No-op if the window doesn't have the _lazyTerminal flag. - */ - _restoreTeammateTerminalFromLazy(agentId) { - const windowData = this.subagentWindows.get(agentId); - if (!windowData || !windowData._lazyTerminal) return; - - const paneTarget = windowData._lazyPaneTarget; - const sessionId = windowData._lazySessionId; - - // Clear lazy state - windowData._lazyTerminal = false; - windowData._lazyPaneTarget = null; - windowData._lazySessionId = null; - - if (!paneTarget || !sessionId) return; - - // Re-create the terminal using the same initTeammateTerminal flow - const paneInfo = { paneTarget, sessionId }; - this.initTeammateTerminal(agentId, paneInfo, windowData.element); - }, - // ═══════════════════════════════════════════════════════════════ // Subagent Floating Windows // ═══════════════════════════════════════════════════════════════ @@ -627,43 +403,9 @@ Object.assign(CodemanApp.prototype, { finalY = topStart + visibleCount * (mobileCardHeight + mobileCardGap); } } else { - // Check if Ralph wizard modal is open - if so, position windows on the sides - const wizardModal = document.getElementById('ralphWizardModal'); - const wizardOpen = wizardModal?.classList.contains('active'); - - let startX, startY, maxCols; - - if (wizardOpen) { - // Wizard is ~720px wide, centered. Position windows on left/right sides - const wizardWidth = 720; - const centerX = viewportWidth / 2; - const wizardLeft = centerX - wizardWidth / 2; - const wizardRight = centerX + wizardWidth / 2; - - // Alternate between left and right sides of the wizard - const leftSideSpace = wizardLeft - 20; - const rightSideSpace = viewportWidth - wizardRight - 20; - - if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) { - // Even windows go to the right - startX = wizardRight + 20; - maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1; - } else if (leftSideSpace >= windowWidth) { - // Odd windows go to the left - startX = Math.max(10, wizardLeft - windowWidth - 20); - maxCols = 1; // Usually only room for 1 column on left - } else { - // Not enough side space, use right side - startX = wizardRight + 20; - maxCols = 1; - } - startY = 80; // Start higher when wizard is open - } else { - // Normal positioning - startX = 50; - startY = WINDOW_INITIAL_TOP_PX; - maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1; - } + const startX = 50; + const startY = WINDOW_INITIAL_TOP_PX; + const maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1; const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1; const col = windowCount % maxCols; @@ -793,24 +535,7 @@ Object.assign(CodemanApp.prototype, { win.style.display = 'none'; } - // Render content — check if this teammate has a tmux pane - const paneInfo = teammateInfo ? this.teammatePanesByName.get(teammateInfo.name) : null; - if (paneInfo) { - if (shouldHide) { - // Window starts hidden — defer terminal creation until visible (lazy init). - // Saves ~75KB of DOM memory per hidden teammate terminal window. - const windowEntry = this.subagentWindows.get(agentId); - if (windowEntry) { - windowEntry._lazyTerminal = true; - windowEntry._lazyPaneTarget = paneInfo.paneTarget; - windowEntry._lazySessionId = paneInfo.sessionId; - } - } else { - this.initTeammateTerminal(agentId, paneInfo, win); - } - } else { - this.renderSubagentWindowContent(agentId); - } + this.renderSubagentWindowContent(agentId); // Focus on click win.addEventListener('mousedown', () => { @@ -882,10 +607,6 @@ Object.assign(CodemanApp.prototype, { this.setAgentParentSessionId(agentId, parentSessionId); } - // Dispose teammate terminal on minimize to free DOM/memory (~75KB per instance). - // The terminal will be lazily re-created on restore via initTeammateTerminal(). - this._disposeTeammateTerminalForMinimize(agentId); - // Always minimize to tab windowData.element.style.display = 'none'; windowData.minimized = true; @@ -956,18 +677,6 @@ Object.assign(CodemanApp.prototype, { } this.subagentWindows.clear(); - // Clean up all teammate terminals - for (const [, termData] of this.teammateTerminals) { - if (termData.resizeObserver) termData.resizeObserver.disconnect(); - if (termData.terminal) { - try { - termData.terminal.dispose(); - } catch {} - } - } - this.teammateTerminals.clear(); - this.teammatePanesByName.clear(); - // Clean up all log viewer windows with their EventSources and drag listeners for (const data of this.logViewerWindows.values()) { if (data.eventSource) { @@ -986,20 +695,6 @@ Object.assign(CodemanApp.prototype, { } this.logViewerWindows.clear(); - // Clean up plan subagent windows (wizard agents) - if (this.planSubagents) { - for (const windowData of this.planSubagents.values()) { - if (windowData.dragListeners) { - document.removeEventListener('mousemove', windowData.dragListeners.move); - document.removeEventListener('mouseup', windowData.dragListeners.up); - } - if (windowData.element) { - windowData.element.remove(); - } - } - this.planSubagents.clear(); - } - // Clean up all image popup windows with their drag listeners for (const popupData of this.imagePopups.values()) { if (popupData.dragListeners) { @@ -1018,25 +713,7 @@ Object.assign(CodemanApp.prototype, { // Clean up ultracode floating run windows (re-seeded from data.workflowRuns on reconnect) if (typeof this.removeAllUltracodeWindows === 'function') this.removeAllUltracodeWindows(); - // Clear orphaned plan generation state - this.activePlanOrchestratorId = null; - this._planProgressHandler = null; - this.planGenerationStopped = true; - if (this.planGenerationAbortController) { - this.planGenerationAbortController.abort(); - this.planGenerationAbortController = null; - } - - // Clean up wizard-specific timers (leak fix: not cleared on SSE reconnect) - if (this.wizardMinimizedTimer) { - clearInterval(this.wizardMinimizedTimer); - this.wizardMinimizedTimer = null; - } - - // Clean up wizard drag listeners (leak fix: document-level handlers) - if (typeof this.cleanupWizardDragging === 'function') this.cleanupWizardDragging(); - - // Deactivate focus trap if wizard was open (leak fix: keydown listener) + // Deactivate a focus trap a modal left active (leak fix: keydown listener) if (this.activeFocusTrap) { this.activeFocusTrap.deactivate(); this.activeFocusTrap = null; @@ -1045,9 +722,6 @@ Object.assign(CodemanApp.prototype, { // Clear minimized agents tracking this.minimizedSubagents.clear(); - // Update monitor panel - this.renderMonitorPlanAgents(); - // Update connection lines (should be empty now) this.updateConnectionLines(); }, @@ -1084,12 +758,6 @@ Object.assign(CodemanApp.prototype, { } windowData.minimized = false; - // Lazily re-create teammate terminal if it was disposed on minimize. - // Only re-create when the window is actually becoming visible. - if (shouldShow && windowData._lazyTerminal) { - this._restoreTeammateTerminalFromLazy(agentId); - } - this.updateConnectionLines(); // Restack all visible mobile windows so restored ones don't overlap this.relayoutMobileSubagentWindows(); diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index d3838c57..eab9a73e 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -428,8 +428,8 @@ // Per-skin xterm.js palettes. The 'daylight-blue' object equals the legacy hardcoded // theme, so default behavior is unchanged. Shared at module scope and exported on the - // global so both terminal-ui.js (main terminal) and panels-ui.js (teammate terminals, - // a separate IIFE) can read the current skin's palette. + // global so the main terminal and every other module that builds a terminal (tiles, + // the split pane) can read the current skin's palette. // // ⚠️ The selection key is `selectionBackground`, xterm's name for it since v5 (#360). // An ITheme is a plain object handed straight to xterm, so an unknown key is not an @@ -6585,16 +6585,6 @@ Object.assign(CodemanApp.prototype, { this._refitAfterCellSizeChange(); this._localEchoOverlay?.refreshFont(); this._predictiveEcho?.refreshFont(); - for (const [, entry] of this.teammateTerminals || []) { - if (!entry?.terminal) continue; - entry.terminal.options.fontWeight = fontWeight; - entry.terminal.options.fontWeightBold = fontWeightBold; - try { - entry.fitAddon?.fit(); - } catch { - /* pane not laid out yet — its own resize observer refits it */ - } - } this._forEachTile?.((tile) => { tile.terminal.options.fontWeight = fontWeight; tile.terminal.options.fontWeightBold = fontWeightBold; @@ -7158,7 +7148,7 @@ Object.assign(CodemanApp.prototype, { }, 100); }, - // Re-theme all live xterm terminals (main + teammate) to the given skin's palette. + // Re-theme all live xterm terminals (main pane and tiles) to the given skin's palette. // Uses the xterm v5+ live setter (full object assignment triggers a repaint for both // DOM and WebGL renderers) plus a belt-and-suspenders refresh(). applyTerminalSkin(skin) { @@ -7176,17 +7166,6 @@ Object.assign(CodemanApp.prototype, { this.terminal.refresh(0, this.terminal.rows - 1); } catch {} } - if (this.teammateTerminals) { - for (const [, entry] of this.teammateTerminals) { - if (entry && entry.terminal) { - entry.terminal.options.minimumContrastRatio = minimumContrastRatio; - entry.terminal.options.theme = { ...theme }; - try { - entry.terminal.refresh(0, entry.terminal.rows - 1); - } catch {} - } - } - } this._forEachTile?.((tile) => { tile.terminal.options.minimumContrastRatio = minimumContrastRatio; tile.terminal.options.theme = { ...theme }; diff --git a/test/operation-lightspeed.test.ts b/test/operation-lightspeed.test.ts index f954a5ca..d5d885cd 100644 --- a/test/operation-lightspeed.test.ts +++ b/test/operation-lightspeed.test.ts @@ -7,7 +7,6 @@ * only session:terminal is filtered by subscription) * - Tab switching: terminal buffer loading, session creation + switch * - Terminal data cap / backpressure recovery - * - Lazy teammate terminal lifecycle * - SSE padding strategy (only latency-sensitive events) */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; diff --git a/test/skin-themes.test.ts b/test/skin-themes.test.ts index b1f21b20..2f55836b 100644 --- a/test/skin-themes.test.ts +++ b/test/skin-themes.test.ts @@ -138,23 +138,19 @@ describe('Codeman light skins', () => { it('switches live terminals between light and dark contrast policies', () => { const main = { options: {} as Record, rows: 24, refresh: vi.fn() }; - const teammate = { options: {} as Record, rows: 12, refresh: vi.fn() }; const refreshFont = vi.fn(); const app = { terminal: main, - teammateTerminals: new Map([['agent-1', { terminal: teammate }]]), _localEchoOverlay: { refreshFont }, }; terminal.mixin.applyTerminalSkin.call(app, 'paper-gray'); expect(main.options.minimumContrastRatio).toBe(4.5); - expect(teammate.options.minimumContrastRatio).toBe(4.5); expect((main.options.theme as Record).background).toBe('#f6f8fa'); expect(refreshFont).toHaveBeenCalledTimes(1); terminal.mixin.applyTerminalSkin.call(app, 'daylight-blue'); expect(main.options.minimumContrastRatio).toBe(1); - expect(teammate.options.minimumContrastRatio).toBe(1); expect((main.options.theme as Record).background).toBe('#161b23'); expect(refreshFont).toHaveBeenCalledTimes(2); }); diff --git a/test/tab-rail-search.browser.test.ts b/test/tab-rail-search.browser.test.ts index 978e7a9c..256f12b3 100644 --- a/test/tab-rail-search.browser.test.ts +++ b/test/tab-rail-search.browser.test.ts @@ -277,7 +277,6 @@ describe('vertical rail session search in Chromium', () => { ['ag1', { element: document.getElementById('subWin'), minimized: false, hidden: false }], ]); app.subagentParentMap = new Map([['ag1', 'alpha']]); - app.planSubagents = new Map(); }); try { const anchor = await rowAnchor(); diff --git a/test/tab-rail-search.test.ts b/test/tab-rail-search.test.ts index 493bda36..122d56d3 100644 --- a/test/tab-rail-search.test.ts +++ b/test/tab-rail-search.test.ts @@ -801,7 +801,6 @@ describe('floating windows whose parent row the search hid', () => { ['ag1', { element: document.getElementById('subWin'), minimized: false, hidden: false }], ]); app.subagentParentMap = new Map([['ag1', 'review']]); - app.planSubagents = new Map(); app.ultracodeWindows = new Map([ ['run1', { element: document.getElementById('runWin'), parentSessionId: 'review' }], ]); diff --git a/test/terminal-font-weight.test.ts b/test/terminal-font-weight.test.ts index ee872011..2ed70b64 100644 --- a/test/terminal-font-weight.test.ts +++ b/test/terminal-font-weight.test.ts @@ -11,11 +11,9 @@ * * Three things are pinned here because each fails silently: * - * - A live save reaches the echo overlays and the Agent Teams panes. Both - * cache the weight (the overlays paint it into their spans, the panes read - * their options at construction), so without the propagation the characters - * being typed, or a pane left open across the save, keep the old weight - * beside a repainted terminal. + * - A live save reaches the echo overlays. They cache the weight and paint + * it into their spans, so without the propagation the characters being + * typed keep the old weight beside a repainted terminal. * - An unchanged save is a no-op, so opening and closing App Settings does not * churn the terminal. * - The bundled face is declared over its full axis. The `@font-face` @@ -65,19 +63,11 @@ function fakeTerminal(options: Record = {}) { function makeApp( opts: { - teammates?: number; terminal?: ReturnType | null; splitPane?: { terminal: ReturnType; fit: () => void } | null; } = {} ) { const fit = vi.fn(); - const teammateFits: ReturnType[] = []; - const teammateTerminals = new Map; fitAddon: unknown }>(); - for (let i = 0; i < (opts.teammates ?? 0); i++) { - const teammateFit = vi.fn(); - teammateFits.push(teammateFit); - teammateTerminals.set(`agent-${i}`, { terminal: fakeTerminal(), fitAddon: { fit: teammateFit } }); - } const app = { applyTerminalFontWeights: mixin.applyTerminalFontWeights, // Secondary panes (the split pane's second terminal) are reached through @@ -99,12 +89,11 @@ function makeApp( _awaitTerminalFont: vi.fn(() => Promise.resolve()), terminal: opts.terminal === undefined ? fakeTerminal() : opts.terminal, fitAddon: { fit }, - teammateTerminals, _localEchoOverlay: { refreshFont: vi.fn() }, _predictiveEcho: { refreshFont: vi.fn() }, _terminalFontReady: null as unknown, }; - return { app, fit, teammateFits, teammateTerminals }; + return { app, fit }; } describe('applyTerminalFontWeights', () => { @@ -152,21 +141,6 @@ describe('applyTerminalFontWeights', () => { expect(app._predictiveEcho.refreshFont).toHaveBeenCalledTimes(1); }); - it('reaches Agent Teams panes, which read their options at construction', () => { - const { app, teammateTerminals, teammateFits } = makeApp({ teammates: 2 }); - - (app as unknown as { applyTerminalFontWeights: (s: unknown) => void }).applyTerminalFontWeights({ - terminalFontWeight: '300', - terminalFontWeightBold: '800', - }); - - for (const [, entry] of teammateTerminals) { - expect(entry.terminal.options.fontWeight).toBe(300); - expect(entry.terminal.options.fontWeightBold).toBe(800); - } - for (const teammateFit of teammateFits) expect(teammateFit).toHaveBeenCalled(); - }); - it('restores xterm’s own defaults when the setting is cleared', () => { const { app } = makeApp({ terminal: fakeTerminal({ fontWeight: 300, fontWeightBold: 800 }) });