feat: add live progress during orchestrator plan generation

New SSE event orchestrator:planProgress streams phase/detail updates
from the planner to the frontend in real-time. The panel now shows
a scrollable log of planning steps instead of just "Generating plan..."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-21 20:03:24 +01:00
co-authored by Claude Opus 4.6
parent 6e64ddd853
commit 7b7cf958c0
7 changed files with 52 additions and 4 deletions
+2 -2
View File
@@ -62,6 +62,7 @@ const POST_PHASE_DELAY_MS = 1000;
export interface OrchestratorLoopEvents {
stateChanged: (state: OrchestratorState, prevState: OrchestratorState) => void;
planProgress: (phase: string, detail: string) => void;
planReady: (plan: OrchestratorPlan) => void;
phaseStarted: (phase: OrchestratorPhase) => void;
phaseCompleted: (phase: OrchestratorPhase) => void;
@@ -143,8 +144,7 @@ export class OrchestratorLoop extends EventEmitter {
try {
const plan = await this.planner.generatePlan(goal, (phase, detail) => {
// Forward planning progress — could emit an event here
console.log(`[Orchestrator] Planning: ${phase} — ${detail}`);
this.emit('planProgress', phase, detail);
});
if (this.currentState() !== 'planning') {
+1
View File
@@ -238,6 +238,7 @@ const _SSE_HANDLER_MAP = [
// Orchestrator loop
[SSE_EVENTS.ORCHESTRATOR_STATE_CHANGED, '_onOrchestratorStateChanged'],
[SSE_EVENTS.ORCHESTRATOR_PLAN_PROGRESS, '_onOrchestratorPlanProgress'],
[SSE_EVENTS.ORCHESTRATOR_PLAN_READY, '_onOrchestratorPlanReady'],
[SSE_EVENTS.ORCHESTRATOR_PHASE_STARTED, '_onOrchestratorPhaseStarted'],
[SSE_EVENTS.ORCHESTRATOR_PHASE_COMPLETED, '_onOrchestratorPhaseCompleted'],
+1
View File
@@ -280,6 +280,7 @@ const SSE_EVENTS = {
// Orchestrator Loop
ORCHESTRATOR_STATE_CHANGED: 'orchestrator:stateChanged',
ORCHESTRATOR_PLAN_PROGRESS: 'orchestrator:planProgress',
ORCHESTRATOR_PLAN_READY: 'orchestrator:planReady',
ORCHESTRATOR_PHASE_STARTED: 'orchestrator:phaseStarted',
ORCHESTRATOR_PHASE_COMPLETED: 'orchestrator:phaseCompleted',
+20 -1
View File
@@ -42,6 +42,17 @@ Object.assign(CodemanApp.prototype, {
_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();
},
@@ -328,7 +339,15 @@ Object.assign(CodemanApp.prototype, {
}
if (state === 'planning') {
return '<div class="orch-planning"><div class="orch-spinner"></div>Generating plan...</div>';
const progress = this.orchestratorState?.planProgress || [];
let progressHtml = '';
if (progress.length > 0) {
const items = progress.map(p =>
`<div class="orch-progress-item"><span class="orch-progress-phase">${escapeHtml(p.phase)}</span> ${escapeHtml(p.detail)}</div>`
).join('');
progressHtml = `<div class="orch-progress-log">${items}</div>`;
}
return `<div class="orch-planning"><div class="orch-spinner"></div>Generating plan...${progressHtml}</div>`;
}
if (!plan) return '';
+22 -1
View File
@@ -7629,11 +7629,32 @@ kbd {
.orch-planning {
display: flex;
align-items: center;
flex-direction: column;
gap: 8px;
color: #f59e0b;
padding: 12px;
}
.orch-planning > .orch-spinner {
display: inline-block;
}
.orch-progress-log {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 0.8em;
color: var(--text-secondary);
max-height: 200px;
overflow-y: auto;
}
.orch-progress-item {
padding: 2px 0;
line-height: 1.3;
}
.orch-progress-phase {
color: #f59e0b;
font-weight: 600;
margin-right: 4px;
}
.orch-spinner {
width: 14px;
+3
View File
@@ -45,6 +45,9 @@ export function registerOrchestratorRoutes(app: FastifyInstance, ctx: Orchestrat
loop.on('stateChanged', (state, prevState) => {
ctx.broadcast(SseEvent.OrchestratorStateChanged, { state, prevState });
});
loop.on('planProgress', (phase, detail) => {
ctx.broadcast(SseEvent.OrchestratorPlanProgress, { phase, detail });
});
loop.on('planReady', (plan) => {
ctx.broadcast(SseEvent.OrchestratorPlanReady, { plan });
});
+3
View File
@@ -296,6 +296,8 @@ export const HookTaskCompleted = 'hook:task_completed' as const;
/** Orchestrator state machine transitioned. */
export const OrchestratorStateChanged = 'orchestrator:stateChanged' as const;
/** Orchestrator plan generation progress update. */
export const OrchestratorPlanProgress = 'orchestrator:planProgress' as const;
/** Orchestrator plan generated and ready for approval. */
export const OrchestratorPlanReady = 'orchestrator:planReady' as const;
/** Orchestrator phase started executing. */
@@ -468,6 +470,7 @@ export const SseEvent = {
// Orchestrator
OrchestratorStateChanged,
OrchestratorPlanProgress,
OrchestratorPlanReady,
OrchestratorPhaseStarted,
OrchestratorPhaseCompleted,