/** * @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 '
No orchestration active. Click "New Goal" to start.
'; } if (state === 'planning') { const progress = this.orchestratorState?.planProgress || []; let progressHtml = ''; if (progress.length > 0) { const items = progress.map(p => `
${escapeHtml(p.phase)} ${escapeHtml(p.detail)}
` ).join(''); progressHtml = `
${items}
`; } return `
Generating plan...${progressHtml}
`; } if (!plan) return ''; const parts = []; // Goal parts.push(`
Goal: ${escapeHtml(plan.goal.slice(0, 200))}
`); // Progress summary const completed = plan.phases.filter(p => p.status === 'passed' || p.status === 'skipped').length; const total = plan.phases.length; const pct = total > 0 ? Math.round((completed / total) * 100) : 0; parts.push(`
${completed}/${total} phases
`); // Phase list parts.push('
'); for (const phase of plan.phases) { parts.push(this._renderOrchestratorPhase(phase, state)); } parts.push('
'); // Stats (if completed/failed) if (state === 'completed' || state === 'failed') { const stats = this.orchestratorState?.stats; if (stats) { parts.push(this._renderOrchestratorStats(stats)); } if (this.orchestratorState?.lastError) { parts.push(`
Error: ${escapeHtml(this.orchestratorState.lastError)}
`); } } return parts.join(''); }, _renderOrchestratorPhase(phase, orchState) { const icon = ORCH_PHASE_STATUS_ICONS[phase.status] || '\u25cb'; const isActive = phase.status === 'executing'; const cls = `orch-phase ${isActive ? 'orch-phase-active' : ''} orch-phase-${phase.status}`; let actions = ''; if (orchState === 'executing' || orchState === 'failed') { if (phase.status === 'pending') { actions += ``; } if (phase.status === 'failed') { actions += ``; } } // Task summary const tasksDone = phase.tasks.filter(t => t.status === 'completed').length; const tasksFailed = phase.tasks.filter(t => t.status === 'failed').length; const tasksTotal = phase.tasks.length; const taskSummary = `${tasksDone}/${tasksTotal}${tasksFailed > 0 ? ` (${tasksFailed} failed)` : ''}`; // Duration let duration = ''; if (phase.durationMs) { const secs = Math.round(phase.durationMs / 1000); duration = secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`; } let html = `
`; html += `
`; html += `${icon}`; html += `${escapeHtml(phase.name)}`; html += `${taskSummary}`; if (duration) html += `${duration}`; if (actions) html += `${actions}`; html += `
`; // Expanded task list for active phase if (isActive || phase.status === 'failed') { html += '
'; for (const task of phase.tasks) { const taskIcon = ORCH_PHASE_STATUS_ICONS[task.status] || '\u25cb'; const taskCls = `orch-task orch-task-${task.status}`; html += `
${taskIcon}`; html += `${escapeHtml(task.prompt.slice(0, 100))}`; if (task.error) html += `${escapeHtml(task.error.slice(0, 80))}`; html += '
'; } html += '
'; } // Verification result if (phase._lastVerification) { const v = phase._lastVerification; const vCls = v.passed ? 'orch-verify-pass' : 'orch-verify-fail'; html += `
${v.passed ? 'Verified' : 'Failed'}: ${escapeHtml(v.summary || '')}
`; } html += '
'; return html; }, _renderOrchestratorStats(stats) { return `
Phases: ${stats.phasesCompleted} done, ${stats.phasesFailed} failed Tasks: ${stats.totalTasksCompleted} done, ${stats.totalTasksFailed} failed ${stats.replanCount > 0 ? `Replans: ${stats.replanCount}` : ''} ${stats.totalDurationMs ? `Duration: ${Math.round(stats.totalDurationMs / 60000)}m` : ''}
`; }, // ═══════════════════════════════════════════════════════════════ // User prompts // ═══════════════════════════════════════════════════════════════ promptOrchestratorGoal() { const goal = prompt('Enter your goal for the orchestrator:'); if (goal && goal.trim()) { this.orchestratorStart(goal.trim()); } }, promptOrchestratorReject() { const feedback = prompt('Feedback on the plan (what should change?):'); if (feedback && feedback.trim()) { this.orchestratorReject(feedback.trim()); } }, });