mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -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') {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user