diff --git a/src/orchestrator-loop.ts b/src/orchestrator-loop.ts index a399b3bd..19def538 100644 --- a/src/orchestrator-loop.ts +++ b/src/orchestrator-loop.ts @@ -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') { diff --git a/src/web/public/app.js b/src/web/public/app.js index f7fb35cd..01537caa 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -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'], diff --git a/src/web/public/constants.js b/src/web/public/constants.js index a26962c0..ef31a045 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -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', diff --git a/src/web/public/orchestrator-panel.js b/src/web/public/orchestrator-panel.js index 53fc74b4..c7f2f4b9 100644 --- a/src/web/public/orchestrator-panel.js +++ b/src/web/public/orchestrator-panel.js @@ -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 '
Generating plan...
'; + 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 ''; diff --git a/src/web/public/styles.css b/src/web/public/styles.css index eb07c3c9..3f996aa8 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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; diff --git a/src/web/routes/orchestrator-routes.ts b/src/web/routes/orchestrator-routes.ts index 019626fb..5d8d686a 100644 --- a/src/web/routes/orchestrator-routes.ts +++ b/src/web/routes/orchestrator-routes.ts @@ -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 }); }); diff --git a/src/web/sse-events.ts b/src/web/sse-events.ts index d6e554e1..5c93e7be 100644 --- a/src/web/sse-events.ts +++ b/src/web/sse-events.ts @@ -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,