/** * @fileoverview Orchestrator loop panel — plan-based autonomous execution UI. * Shows orchestrator state, plan phases, task progress, and verification results. * Provides controls for start, approve, reject, pause, resume, stop, skip, retry. * * @mixin Extends CodemanApp.prototype via Object.assign * @dependency app.js (CodemanApp class, this.orchestratorState) * @dependency constants.js (SSE_EVENTS, escapeHtml) * @loadorder 9.5 of 16 — loaded after ralph-panel.js, before settings-ui.js */ // ═══════════════════════════════════════════════════════════════ // State color/label mappings // ═══════════════════════════════════════════════════════════════ const ORCH_STATE_COLORS = { idle: '#6b7280', planning: '#f59e0b', approval: '#8b5cf6', executing: '#3b82f6', verifying: '#06b6d4', replanning: '#f97316', completed: '#22c55e', failed: '#ef4444', paused: '#9ca3af', }; const ORCH_PHASE_STATUS_ICONS = { pending: '\u25cb', // ○ executing: '\u25d4', // ◔ passed: '\u2713', // ✓ failed: '\u2717', // ✗ skipped: '\u2192', // → }; Object.assign(CodemanApp.prototype, { // ═══════════════════════════════════════════════════════════════ // SSE Event Handlers // ═══════════════════════════════════════════════════════════════ _onOrchestratorStateChanged(data) { if (!this.orchestratorState) this.orchestratorState = {}; this.orchestratorState.state = data.state; if (data.state === 'planning') { this.orchestratorState.planProgress = []; this.showOrchestratorPanel(); } this.renderOrchestratorPanel(); }, _onOrchestratorPlanProgress(data) { if (!this.orchestratorState) this.orchestratorState = {}; if (!this.orchestratorState.planProgress) this.orchestratorState.planProgress = []; this.orchestratorState.planProgress.push({ phase: data.phase, detail: data.detail, time: Date.now() }); this.renderOrchestratorPanel(); }, _onOrchestratorPlanReady(data) { if (!this.orchestratorState) this.orchestratorState = {}; this.orchestratorState.plan = data.plan; this.orchestratorState.state = 'approval'; this.showOrchestratorPanel(); this.renderOrchestratorPanel(); }, _onOrchestratorPhaseStarted(data) { if (!this.orchestratorState) return; this._updateOrchestratorPhase(data.phase); this.renderOrchestratorPanel(); }, _onOrchestratorPhaseCompleted(data) { if (!this.orchestratorState) return; this._updateOrchestratorPhase(data.phase); this.renderOrchestratorPanel(); }, _onOrchestratorPhaseFailed(data) { if (!this.orchestratorState) return; this._updateOrchestratorPhase(data.phase); this.renderOrchestratorPanel(); }, _onOrchestratorVerification(data) { if (!this.orchestratorState) return; // Store verification result on the phase if (this.orchestratorState.plan) { const phase = this.orchestratorState.plan.phases.find(p => p.id === data.phaseId); if (phase) phase._lastVerification = data.result; } this.renderOrchestratorPanel(); }, _onOrchestratorTaskAssigned(data) { if (!this.orchestratorState) return; this._updateOrchestratorTask(data.task); this.renderOrchestratorPanel(); }, _onOrchestratorTaskCompleted(data) { if (!this.orchestratorState) return; this._updateOrchestratorTask(data.task); this.renderOrchestratorPanel(); }, _onOrchestratorTaskFailed(data) { if (!this.orchestratorState) return; this._updateOrchestratorTask(data.task); this.renderOrchestratorPanel(); }, _onOrchestratorCompleted(data) { if (!this.orchestratorState) this.orchestratorState = {}; this.orchestratorState.state = 'completed'; this.orchestratorState.stats = data.stats; this.renderOrchestratorPanel(); }, _onOrchestratorError(data) { if (!this.orchestratorState) this.orchestratorState = {}; this.orchestratorState.state = 'failed'; this.orchestratorState.lastError = data.error; this.renderOrchestratorPanel(); }, // ═══════════════════════════════════════════════════════════════ // Internal helpers // ═══════════════════════════════════════════════════════════════ _updateOrchestratorPhase(updatedPhase) { if (!this.orchestratorState?.plan) return; const idx = this.orchestratorState.plan.phases.findIndex(p => p.id === updatedPhase.id); if (idx >= 0) this.orchestratorState.plan.phases[idx] = updatedPhase; }, _updateOrchestratorTask(updatedTask) { if (!this.orchestratorState?.plan) return; for (const phase of this.orchestratorState.plan.phases) { const idx = phase.tasks.findIndex(t => t.id === updatedTask.id); if (idx >= 0) { phase.tasks[idx] = updatedTask; return; } } }, // ═══════════════════════════════════════════════════════════════ // Panel visibility // ═══════════════════════════════════════════════════════════════ showOrchestratorPanel() { this.orchestratorPanelVisible = true; const panel = document.getElementById('orchestratorPanel'); if (panel) panel.style.display = ''; this.renderOrchestratorPanel(); }, closeOrchestratorPanel() { this.orchestratorPanelVisible = false; const panel = document.getElementById('orchestratorPanel'); if (panel) panel.style.display = 'none'; }, toggleOrchestratorPanel() { if (this.orchestratorPanelVisible) { this.closeOrchestratorPanel(); } else { this.showOrchestratorPanel(); } }, // ═══════════════════════════════════════════════════════════════ // API calls // ═══════════════════════════════════════════════════════════════ async orchestratorStart(goal, config) { try { const res = await fetch('/api/orchestrator/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ goal, config }), }); const data = await res.json(); if (data.ok) { this.orchestratorState = { state: 'planning', plan: null }; this.showOrchestratorPanel(); this.renderOrchestratorPanel(); } return data; } catch (err) { console.error('[Orchestrator] Start failed:', err); } }, async orchestratorApprove() { try { await fetch('/api/orchestrator/approve', { method: 'POST' }); } catch (err) { console.error('[Orchestrator] Approve failed:', err); } }, async orchestratorReject(feedback) { try { await fetch('/api/orchestrator/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ feedback }), }); } catch (err) { console.error('[Orchestrator] Reject failed:', err); } }, async orchestratorPause() { try { await fetch('/api/orchestrator/pause', { method: 'POST' }); } catch (err) { console.error('[Orchestrator] Pause failed:', err); } }, async orchestratorResume() { try { await fetch('/api/orchestrator/resume', { method: 'POST' }); } catch (err) { console.error('[Orchestrator] Resume failed:', err); } }, async orchestratorStop() { try { await fetch('/api/orchestrator/stop', { method: 'POST' }); this.orchestratorState = { state: 'idle' }; this.renderOrchestratorPanel(); } catch (err) { console.error('[Orchestrator] Stop failed:', err); } }, async orchestratorSkipPhase(phaseId) { try { await fetch(`/api/orchestrator/phase/${phaseId}/skip`, { method: 'POST' }); } catch (err) { console.error('[Orchestrator] Skip failed:', err); } }, async orchestratorRetryPhase(phaseId) { try { await fetch(`/api/orchestrator/phase/${phaseId}/retry`, { method: 'POST' }); } catch (err) { console.error('[Orchestrator] Retry failed:', err); } }, async refreshOrchestratorStatus() { try { const res = await fetch('/api/orchestrator/status'); const data = await res.json(); if (data.ok) { this.orchestratorState = data; this.renderOrchestratorPanel(); } } catch (err) { console.error('[Orchestrator] Status fetch failed:', err); } }, // ═══════════════════════════════════════════════════════════════ // Rendering // ═══════════════════════════════════════════════════════════════ renderOrchestratorPanel() { const panel = document.getElementById('orchestratorPanel'); if (!panel) return; const state = this.orchestratorState?.state || 'idle'; const plan = this.orchestratorState?.plan; // Update badge const badge = document.getElementById('orchestratorStateBadge'); if (badge) { badge.textContent = state; badge.style.background = ORCH_STATE_COLORS[state] || '#6b7280'; } // Update action buttons const actions = document.getElementById('orchestratorActions'); if (actions) { actions.innerHTML = this._renderOrchestratorActions(state); } // Update body const body = document.getElementById('orchestratorBody'); if (body) { body.innerHTML = this._renderOrchestratorBody(state, plan); } }, _renderOrchestratorActions(state) { const btn = (label, onclick, cls = '') => ``; switch (state) { case 'idle': case 'completed': case 'failed': return btn('New Goal', 'app.promptOrchestratorGoal()', 'orch-btn-primary'); case 'planning': return btn('Cancel', 'app.orchestratorStop()', 'orch-btn-danger'); case 'approval': return [ btn('Approve', 'app.orchestratorApprove()', 'orch-btn-primary'), btn('Reject', 'app.promptOrchestratorReject()', 'orch-btn-warn'), btn('Cancel', 'app.orchestratorStop()', 'orch-btn-danger'), ].join(''); case 'executing': case 'verifying': case 'replanning': return [ btn('Pause', 'app.orchestratorPause()'), btn('Stop', 'app.orchestratorStop()', 'orch-btn-danger'), ].join(''); case 'paused': return [ btn('Resume', 'app.orchestratorResume()', 'orch-btn-primary'), btn('Stop', 'app.orchestratorStop()', 'orch-btn-danger'), ].join(''); default: return ''; } }, _renderOrchestratorBody(state, plan) { if (state === 'idle' && !plan) { return '