erosolar-cli 2.1.282 → 2.1.284
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/unifiedOrchestrator.d.ts +108 -5
- package/dist/core/unifiedOrchestrator.d.ts.map +1 -1
- package/dist/core/unifiedOrchestrator.js +319 -23
- package/dist/core/unifiedOrchestrator.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +1 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +57 -8
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/tools/tao/rl.d.ts.map +1 -1
- package/dist/tools/tao/rl.js +2 -1
- package/dist/tools/tao/rl.js.map +1 -1
- package/dist/ui/orchestration/OrchestrationUIBridge.d.ts +199 -0
- package/dist/ui/orchestration/OrchestrationUIBridge.d.ts.map +1 -0
- package/dist/ui/orchestration/OrchestrationUIBridge.js +681 -0
- package/dist/ui/orchestration/OrchestrationUIBridge.js.map +1 -0
- package/dist/ui/orchestration/StatusOrchestrator.d.ts +33 -0
- package/dist/ui/orchestration/StatusOrchestrator.d.ts.map +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.js +136 -6
- package/dist/ui/orchestration/StatusOrchestrator.js.map +1 -1
- package/dist/ui/orchestration/index.d.ts +18 -0
- package/dist/ui/orchestration/index.d.ts.map +1 -0
- package/dist/ui/orchestration/index.js +21 -0
- package/dist/ui/orchestration/index.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrchestrationUIBridge - Claude Code / Codex CLI Style UI Rendering
|
|
3
|
+
*
|
|
4
|
+
* This module provides real-time integration between the UnifiedOrchestrator and the UI system,
|
|
5
|
+
* rendering output in the Claude Code / Codex CLI visual style:
|
|
6
|
+
*
|
|
7
|
+
* Visual Style:
|
|
8
|
+
* - ⏺ [tool_name] description (tool start, with spinner during execution)
|
|
9
|
+
* - ⎿ output/result (tool result, indented under action)
|
|
10
|
+
* - ✓ success indicator (green checkmark on completion)
|
|
11
|
+
* - ✗ error indicator (red X on failure)
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* - Subscribes to orchestrator events (tool:*, phase:*, finding:*)
|
|
15
|
+
* - Renders each event type with appropriate visual treatment
|
|
16
|
+
* - Uses UIUpdateCoordinator for serialized, non-interleaved output
|
|
17
|
+
* - Uses StatusOrchestrator for status line management
|
|
18
|
+
*
|
|
19
|
+
* Key Features:
|
|
20
|
+
* - Real-time tool execution feedback with spinners
|
|
21
|
+
* - Progressive output streaming
|
|
22
|
+
* - Findings displayed inline as discovered
|
|
23
|
+
* - Status line shows current operation
|
|
24
|
+
* - Clean separation between execution and rendering
|
|
25
|
+
*/
|
|
26
|
+
import { EventEmitter } from 'node:events';
|
|
27
|
+
import { UnifiedOrchestrator, } from '../../core/unifiedOrchestrator.js';
|
|
28
|
+
import { UIUpdateCoordinator } from './UIUpdateCoordinator.js';
|
|
29
|
+
import { StatusOrchestrator } from './StatusOrchestrator.js';
|
|
30
|
+
import { theme } from '../theme.js';
|
|
31
|
+
// Claude Code / Codex CLI style icons
|
|
32
|
+
const ICONS = {
|
|
33
|
+
action: '⏺', // Tool action indicator
|
|
34
|
+
subaction: '⎿', // Result/subaction indicator
|
|
35
|
+
success: '✓', // Success checkmark
|
|
36
|
+
error: '✗', // Error X
|
|
37
|
+
warning: '⚠', // Warning indicator
|
|
38
|
+
info: 'ℹ', // Info indicator
|
|
39
|
+
spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], // Spinner frames
|
|
40
|
+
bullet: '•', // Bullet point
|
|
41
|
+
arrow: '→', // Arrow indicator
|
|
42
|
+
};
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// OrchestrationUIBridge
|
|
45
|
+
// ============================================================================
|
|
46
|
+
export class OrchestrationUIBridge extends EventEmitter {
|
|
47
|
+
display;
|
|
48
|
+
updateCoordinator;
|
|
49
|
+
statusOrchestrator;
|
|
50
|
+
config;
|
|
51
|
+
orchestrator = null;
|
|
52
|
+
currentState;
|
|
53
|
+
startTime = 0;
|
|
54
|
+
commandResults = [];
|
|
55
|
+
findings = [];
|
|
56
|
+
heartbeatId = null;
|
|
57
|
+
disposed = false;
|
|
58
|
+
// Claude Code-style active tool tracking
|
|
59
|
+
activeTools = new Map();
|
|
60
|
+
spinnerInterval = null;
|
|
61
|
+
orchestratorUnsubscribe = null;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
super();
|
|
64
|
+
this.display = config.display;
|
|
65
|
+
this.updateCoordinator = config.updateCoordinator ?? new UIUpdateCoordinator('idle');
|
|
66
|
+
this.statusOrchestrator = config.statusOrchestrator ?? new StatusOrchestrator();
|
|
67
|
+
this.config = {
|
|
68
|
+
showRealTimeOutput: config.showRealTimeOutput ?? true,
|
|
69
|
+
showProgressBar: config.showProgressBar ?? true,
|
|
70
|
+
maxVisibleFindings: config.maxVisibleFindings ?? 5,
|
|
71
|
+
verboseMode: config.verboseMode ?? false,
|
|
72
|
+
};
|
|
73
|
+
this.currentState = this.createInitialState();
|
|
74
|
+
this.setupStatusListeners();
|
|
75
|
+
}
|
|
76
|
+
// --------------------------------------------------------------------------
|
|
77
|
+
// Public API
|
|
78
|
+
// --------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Execute an orchestration operation with Claude Code-style real-time UI updates.
|
|
81
|
+
*
|
|
82
|
+
* Renders tool executions as:
|
|
83
|
+
* ⏺ [tool_name] description
|
|
84
|
+
* ⎿ output line 1
|
|
85
|
+
* ⎿ output line 2
|
|
86
|
+
* ✓ tool_name (duration)
|
|
87
|
+
*/
|
|
88
|
+
async execute(objective, options) {
|
|
89
|
+
if (this.disposed) {
|
|
90
|
+
throw new Error('OrchestrationUIBridge has been disposed');
|
|
91
|
+
}
|
|
92
|
+
this.reset();
|
|
93
|
+
this.startTime = Date.now();
|
|
94
|
+
this.orchestrator = new UnifiedOrchestrator(options?.target);
|
|
95
|
+
try {
|
|
96
|
+
this.setPhase('initializing');
|
|
97
|
+
this.updateCoordinator.setMode('processing');
|
|
98
|
+
// Subscribe to orchestrator events for Claude Code-style rendering
|
|
99
|
+
this.subscribeToOrchestratorEvents();
|
|
100
|
+
this.startHeartbeat();
|
|
101
|
+
this.showStartBanner(objective);
|
|
102
|
+
this.setPhase('analyzing');
|
|
103
|
+
this.updateStatus(`Analyzing: ${this.truncate(objective, 50)}`);
|
|
104
|
+
this.setPhase('executing');
|
|
105
|
+
const report = await this.executeWithProgress(objective, options);
|
|
106
|
+
this.setPhase('reporting');
|
|
107
|
+
this.displayReport(report);
|
|
108
|
+
this.setPhase('complete');
|
|
109
|
+
return report;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.setPhase('error');
|
|
113
|
+
this.handleError(error);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.unsubscribeFromOrchestratorEvents();
|
|
118
|
+
this.stopHeartbeat();
|
|
119
|
+
this.stopSpinner();
|
|
120
|
+
this.updateCoordinator.setMode('idle');
|
|
121
|
+
this.orchestrator = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Subscribe to orchestrator events for real-time Claude Code-style rendering.
|
|
126
|
+
*/
|
|
127
|
+
subscribeToOrchestratorEvents() {
|
|
128
|
+
if (!this.orchestrator)
|
|
129
|
+
return;
|
|
130
|
+
this.orchestratorUnsubscribe = this.orchestrator.onEvent((event) => {
|
|
131
|
+
this.handleOrchestratorEvent(event);
|
|
132
|
+
});
|
|
133
|
+
// Start spinner for active tools
|
|
134
|
+
this.startSpinner();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Unsubscribe from orchestrator events.
|
|
138
|
+
*/
|
|
139
|
+
unsubscribeFromOrchestratorEvents() {
|
|
140
|
+
if (this.orchestratorUnsubscribe) {
|
|
141
|
+
this.orchestratorUnsubscribe();
|
|
142
|
+
this.orchestratorUnsubscribe = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle an orchestrator event and render it in Claude Code style.
|
|
147
|
+
*/
|
|
148
|
+
handleOrchestratorEvent(event) {
|
|
149
|
+
switch (event.type) {
|
|
150
|
+
case 'tool:start':
|
|
151
|
+
this.renderToolStart(event.data);
|
|
152
|
+
break;
|
|
153
|
+
case 'tool:progress':
|
|
154
|
+
this.renderToolProgress(event.data);
|
|
155
|
+
break;
|
|
156
|
+
case 'tool:complete':
|
|
157
|
+
case 'tool:error':
|
|
158
|
+
this.renderToolComplete(event.data);
|
|
159
|
+
break;
|
|
160
|
+
case 'phase:change':
|
|
161
|
+
this.renderPhaseChange(event.data);
|
|
162
|
+
break;
|
|
163
|
+
case 'finding:detected':
|
|
164
|
+
this.renderFinding(event.data);
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
// Legacy events are handled for backward compatibility
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Render tool start in Claude Code style:
|
|
173
|
+
* ⏺ [tool_name] description
|
|
174
|
+
*/
|
|
175
|
+
renderToolStart(event) {
|
|
176
|
+
this.activeTools.set(event.toolId, {
|
|
177
|
+
startEvent: event,
|
|
178
|
+
startTime: Date.now(),
|
|
179
|
+
spinnerFrame: 0,
|
|
180
|
+
});
|
|
181
|
+
const line = `${ICONS.action} ${theme.tool?.(`[${event.toolName}]`) ?? `[${event.toolName}]`} ${event.description}`;
|
|
182
|
+
this.enqueueUIUpdate('tool', () => {
|
|
183
|
+
this.display.writeRaw(`${line}\n`);
|
|
184
|
+
}, `tool-start-${event.toolId}`);
|
|
185
|
+
// Update status line
|
|
186
|
+
this.updateStatus(event.description);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Render tool progress (streaming output) in Claude Code style:
|
|
190
|
+
* ⎿ output line
|
|
191
|
+
*/
|
|
192
|
+
renderToolProgress(event) {
|
|
193
|
+
if (!event.output?.trim())
|
|
194
|
+
return;
|
|
195
|
+
const lines = event.output.trim().split('\n').slice(0, 5); // Limit to 5 lines
|
|
196
|
+
const formatted = lines.map(line => ` ${ICONS.subaction} ${theme.ui?.muted?.(this.truncate(line, 80)) ?? this.truncate(line, 80)}`).join('\n');
|
|
197
|
+
this.enqueueUIUpdate('tool', () => {
|
|
198
|
+
this.display.writeRaw(`${formatted}\n`);
|
|
199
|
+
}, `tool-progress-${event.toolId}-${Date.now()}`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Render tool completion in Claude Code style:
|
|
203
|
+
* ✓ tool_name (1.2s) or ✗ tool_name - error
|
|
204
|
+
*/
|
|
205
|
+
renderToolComplete(event) {
|
|
206
|
+
this.activeTools.delete(event.toolId);
|
|
207
|
+
const duration = event.duration < 1000
|
|
208
|
+
? `${event.duration}ms`
|
|
209
|
+
: `${(event.duration / 1000).toFixed(1)}s`;
|
|
210
|
+
let line;
|
|
211
|
+
if (event.success) {
|
|
212
|
+
line = ` ${theme.success?.(ICONS.success) ?? ICONS.success} ${theme.tool?.(`${event.toolName}`) ?? event.toolName} ${theme.ui?.muted?.(`(${duration})`) ?? `(${duration})`}`;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const errorMsg = event.output?.split('\n')[0]?.slice(0, 60) || 'Unknown error';
|
|
216
|
+
line = ` ${theme.error?.(ICONS.error) ?? ICONS.error} ${theme.tool?.(`${event.toolName}`) ?? event.toolName} ${theme.error?.(` - ${errorMsg}`) ?? ` - ${errorMsg}`}`;
|
|
217
|
+
}
|
|
218
|
+
this.enqueueUIUpdate('tool', () => {
|
|
219
|
+
this.display.writeRaw(`${line}\n`);
|
|
220
|
+
}, `tool-complete-${event.toolId}`);
|
|
221
|
+
// Show output preview for verbose mode
|
|
222
|
+
if (this.config.verboseMode && event.output?.trim()) {
|
|
223
|
+
const outputLines = event.output.trim().split('\n').slice(0, 3);
|
|
224
|
+
const outputFormatted = outputLines.map(l => ` ${ICONS.subaction} ${theme.ui?.muted?.(this.truncate(l, 70)) ?? this.truncate(l, 70)}`).join('\n');
|
|
225
|
+
this.enqueueUIUpdate('tool', () => {
|
|
226
|
+
this.display.writeRaw(`${outputFormatted}\n`);
|
|
227
|
+
}, `tool-output-${event.toolId}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Render phase change notification.
|
|
232
|
+
*/
|
|
233
|
+
renderPhaseChange(event) {
|
|
234
|
+
// Only show major phase transitions
|
|
235
|
+
const majorPhases = ['executing', 'reporting', 'complete'];
|
|
236
|
+
if (!majorPhases.includes(event.phase))
|
|
237
|
+
return;
|
|
238
|
+
const phaseLabels = {
|
|
239
|
+
executing: 'Executing tasks...',
|
|
240
|
+
reporting: 'Generating report...',
|
|
241
|
+
complete: 'Complete',
|
|
242
|
+
};
|
|
243
|
+
const label = phaseLabels[event.phase] || event.phase;
|
|
244
|
+
const line = `\n${theme.secondary?.(`${ICONS.info} ${label}`) ?? `${ICONS.info} ${label}`}`;
|
|
245
|
+
this.enqueueUIUpdate('prompt', () => {
|
|
246
|
+
this.display.writeRaw(`${line}\n`);
|
|
247
|
+
}, `phase-${event.phase}`);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Render a finding in Claude Code style:
|
|
251
|
+
* ⚠ [SEVERITY] title
|
|
252
|
+
* Recommendation: ...
|
|
253
|
+
*/
|
|
254
|
+
renderFinding(finding) {
|
|
255
|
+
const severityColors = {
|
|
256
|
+
critical: theme.error ?? ((t) => t),
|
|
257
|
+
high: theme.error ?? ((t) => t),
|
|
258
|
+
medium: theme.warning ?? ((t) => t),
|
|
259
|
+
low: theme.ui?.muted ?? ((t) => t),
|
|
260
|
+
info: theme.ui?.muted ?? ((t) => t),
|
|
261
|
+
};
|
|
262
|
+
const color = severityColors[finding.severity] || ((t) => t);
|
|
263
|
+
const icon = finding.severity === 'critical' || finding.severity === 'high' ? ICONS.error : ICONS.warning;
|
|
264
|
+
const lines = [
|
|
265
|
+
`${color(`${icon} [${finding.severity.toUpperCase()}]`)} ${finding.title}`,
|
|
266
|
+
];
|
|
267
|
+
if (finding.recommendation) {
|
|
268
|
+
lines.push(` ${ICONS.subaction} ${theme.ui?.muted?.(`Recommendation: ${finding.recommendation}`) ?? `Recommendation: ${finding.recommendation}`}`);
|
|
269
|
+
}
|
|
270
|
+
this.enqueueUIUpdate('tool', () => {
|
|
271
|
+
this.display.writeRaw(`${lines.join('\n')}\n`);
|
|
272
|
+
}, `finding-${Date.now()}`);
|
|
273
|
+
// Also emit as event for external listeners
|
|
274
|
+
this.emitEvent('finding', finding);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Start the spinner animation for active tools.
|
|
278
|
+
*/
|
|
279
|
+
startSpinner() {
|
|
280
|
+
if (this.spinnerInterval)
|
|
281
|
+
return;
|
|
282
|
+
this.spinnerInterval = setInterval(() => {
|
|
283
|
+
for (const [, tool] of this.activeTools) {
|
|
284
|
+
tool.spinnerFrame = (tool.spinnerFrame + 1) % ICONS.spinner.length;
|
|
285
|
+
}
|
|
286
|
+
}, 80);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stop the spinner animation.
|
|
290
|
+
*/
|
|
291
|
+
stopSpinner() {
|
|
292
|
+
if (this.spinnerInterval) {
|
|
293
|
+
clearInterval(this.spinnerInterval);
|
|
294
|
+
this.spinnerInterval = null;
|
|
295
|
+
}
|
|
296
|
+
this.activeTools.clear();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Execute a specific command with UI feedback.
|
|
300
|
+
*/
|
|
301
|
+
async executeCommand(command, timeout) {
|
|
302
|
+
if (!this.orchestrator) {
|
|
303
|
+
this.orchestrator = new UnifiedOrchestrator();
|
|
304
|
+
}
|
|
305
|
+
this.currentState.currentCommand = command;
|
|
306
|
+
this.updateStatus(`Running: ${this.truncate(command, 40)}`);
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
const result = this.orchestrator.exec(command, timeout);
|
|
309
|
+
const duration = Date.now() - startTime;
|
|
310
|
+
this.commandResults.push(result);
|
|
311
|
+
this.currentState.currentCommand = null;
|
|
312
|
+
// Analyze for findings
|
|
313
|
+
const newFindings = this.orchestrator.analyze(result.output, command);
|
|
314
|
+
this.findings.push(...newFindings);
|
|
315
|
+
// Emit events
|
|
316
|
+
this.emitEvent('command', { command, result, duration });
|
|
317
|
+
for (const finding of newFindings) {
|
|
318
|
+
this.emitEvent('finding', finding);
|
|
319
|
+
}
|
|
320
|
+
// Update UI
|
|
321
|
+
if (this.config.showRealTimeOutput) {
|
|
322
|
+
this.displayCommandResult(command, result);
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get current orchestration state.
|
|
328
|
+
*/
|
|
329
|
+
getState() {
|
|
330
|
+
return { ...this.currentState };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check if orchestration is currently running.
|
|
334
|
+
*/
|
|
335
|
+
isRunning() {
|
|
336
|
+
return this.currentState.isRunning;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Dispose the bridge and clean up resources.
|
|
340
|
+
*/
|
|
341
|
+
dispose() {
|
|
342
|
+
if (this.disposed)
|
|
343
|
+
return;
|
|
344
|
+
this.disposed = true;
|
|
345
|
+
this.stopHeartbeat();
|
|
346
|
+
this.statusOrchestrator.dispose();
|
|
347
|
+
this.updateCoordinator.dispose();
|
|
348
|
+
this.removeAllListeners();
|
|
349
|
+
}
|
|
350
|
+
// --------------------------------------------------------------------------
|
|
351
|
+
// Execution with Progress
|
|
352
|
+
// --------------------------------------------------------------------------
|
|
353
|
+
async executeWithProgress(objective, options) {
|
|
354
|
+
if (!this.orchestrator) {
|
|
355
|
+
throw new Error('Orchestrator not initialized');
|
|
356
|
+
}
|
|
357
|
+
// Use the orchestrator's execute method but intercept progress
|
|
358
|
+
const report = await this.orchestrator.execute({
|
|
359
|
+
objective,
|
|
360
|
+
...options,
|
|
361
|
+
});
|
|
362
|
+
// Collect results and findings
|
|
363
|
+
this.commandResults = this.orchestrator.getResults();
|
|
364
|
+
this.findings = this.orchestrator.getFindings();
|
|
365
|
+
// Update state
|
|
366
|
+
this.currentState.findingsCount = this.findings.length;
|
|
367
|
+
this.currentState.errorsCount = this.commandResults.filter(r => !r.success).length;
|
|
368
|
+
this.currentState.progress = 100;
|
|
369
|
+
return report;
|
|
370
|
+
}
|
|
371
|
+
// --------------------------------------------------------------------------
|
|
372
|
+
// UI Updates
|
|
373
|
+
// --------------------------------------------------------------------------
|
|
374
|
+
/**
|
|
375
|
+
* Show start banner in Claude Code style - minimal and clean.
|
|
376
|
+
*/
|
|
377
|
+
showStartBanner(objective) {
|
|
378
|
+
const lines = [
|
|
379
|
+
'',
|
|
380
|
+
`${ICONS.action} ${theme.bold?.('Assistant') ?? 'Assistant'}`,
|
|
381
|
+
'',
|
|
382
|
+
`${theme.secondary?.('Objective:') ?? 'Objective:'} ${objective}`,
|
|
383
|
+
'',
|
|
384
|
+
];
|
|
385
|
+
this.enqueueUIUpdate('prompt', () => {
|
|
386
|
+
this.display.writeRaw(lines.join('\n'));
|
|
387
|
+
}, 'orchestration-banner');
|
|
388
|
+
}
|
|
389
|
+
displayCommandResult(command, result) {
|
|
390
|
+
const status = result.success
|
|
391
|
+
? theme.success?.('✓') ?? '✓'
|
|
392
|
+
: theme.error?.('✗') ?? '✗';
|
|
393
|
+
const duration = result.duration < 1000
|
|
394
|
+
? `${result.duration}ms`
|
|
395
|
+
: `${(result.duration / 1000).toFixed(1)}s`;
|
|
396
|
+
const line = ` ${status} ${this.truncate(command, 60)} ${theme.ui?.muted?.(`(${duration})`) ?? `(${duration})`}`;
|
|
397
|
+
this.enqueueUIUpdate('tool', () => {
|
|
398
|
+
this.display.writeRaw(`${line}\n`);
|
|
399
|
+
}, `cmd-${Date.now()}`);
|
|
400
|
+
// Show errors inline
|
|
401
|
+
if (!result.success && result.error) {
|
|
402
|
+
const errorLine = ` ${theme.error?.(result.error) ?? result.error}`;
|
|
403
|
+
this.enqueueUIUpdate('tool', () => {
|
|
404
|
+
this.display.writeRaw(`${errorLine}\n`);
|
|
405
|
+
}, `cmd-err-${Date.now()}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Display final report in Claude Code style:
|
|
410
|
+
*
|
|
411
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
412
|
+
* ✓ Operation Complete
|
|
413
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
414
|
+
*
|
|
415
|
+
* Status: ✓ Success Duration: 2.3s Commands: 5/5 succeeded
|
|
416
|
+
*
|
|
417
|
+
* Findings
|
|
418
|
+
* • [CRITICAL] Potential secrets in code
|
|
419
|
+
* ⎿ Recommendation: Use environment variables
|
|
420
|
+
*
|
|
421
|
+
* Failed Commands
|
|
422
|
+
* ✗ npm run lint - Command failed
|
|
423
|
+
*
|
|
424
|
+
* Summary
|
|
425
|
+
* 5/5 succeeded, 2 findings (1 critical)
|
|
426
|
+
*/
|
|
427
|
+
displayReport(report) {
|
|
428
|
+
const lines = [''];
|
|
429
|
+
// Visual separator
|
|
430
|
+
const separator = theme.gradient?.primary?.('━'.repeat(50)) ?? '━'.repeat(50);
|
|
431
|
+
lines.push(separator);
|
|
432
|
+
// Header with status
|
|
433
|
+
const statusIcon = report.success
|
|
434
|
+
? theme.success?.(ICONS.success) ?? ICONS.success
|
|
435
|
+
: theme.warning?.(ICONS.warning) ?? ICONS.warning;
|
|
436
|
+
const statusText = report.success ? 'Operation Complete' : 'Operation Incomplete';
|
|
437
|
+
lines.push(`${statusIcon} ${theme.bold?.(statusText) ?? statusText}`);
|
|
438
|
+
lines.push(separator);
|
|
439
|
+
lines.push('');
|
|
440
|
+
// Metrics line - compact Claude Code style
|
|
441
|
+
const duration = report.duration < 1000
|
|
442
|
+
? `${report.duration}ms`
|
|
443
|
+
: `${(report.duration / 1000).toFixed(1)}s`;
|
|
444
|
+
const successCount = report.results.filter(r => r.success).length;
|
|
445
|
+
const failedCount = report.results.length - successCount;
|
|
446
|
+
const criticalCount = report.findings.filter(f => f.severity === 'critical').length;
|
|
447
|
+
const statusBadge = report.success
|
|
448
|
+
? theme.success?.(`${ICONS.success} Success`) ?? `${ICONS.success} Success`
|
|
449
|
+
: theme.warning?.(`${ICONS.warning} Issues Found`) ?? `${ICONS.warning} Issues Found`;
|
|
450
|
+
const metricsLine = [
|
|
451
|
+
`Status: ${statusBadge}`,
|
|
452
|
+
`Duration: ${theme.ui?.muted?.(duration) ?? duration}`,
|
|
453
|
+
`Commands: ${successCount}/${report.results.length} succeeded`,
|
|
454
|
+
].join(' ');
|
|
455
|
+
lines.push(metricsLine);
|
|
456
|
+
lines.push('');
|
|
457
|
+
// Findings section (Claude Code style bullet list)
|
|
458
|
+
if (report.findings.length > 0) {
|
|
459
|
+
lines.push(theme.bold?.('Findings') ?? 'Findings');
|
|
460
|
+
for (const finding of report.findings.slice(0, this.config.maxVisibleFindings)) {
|
|
461
|
+
const severityColor = this.getSeverityColor(finding.severity);
|
|
462
|
+
const icon = finding.severity === 'critical' || finding.severity === 'high' ? ICONS.error : ICONS.bullet;
|
|
463
|
+
lines.push(` ${icon} ${severityColor(`[${finding.severity.toUpperCase()}]`)} ${finding.title}`);
|
|
464
|
+
if (finding.recommendation) {
|
|
465
|
+
lines.push(` ${ICONS.subaction} ${theme.ui?.muted?.(`Recommendation: ${finding.recommendation}`) ?? `Recommendation: ${finding.recommendation}`}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (report.findings.length > this.config.maxVisibleFindings) {
|
|
469
|
+
lines.push(` ${theme.ui?.muted?.(`... and ${report.findings.length - this.config.maxVisibleFindings} more`) ?? `... and ${report.findings.length - this.config.maxVisibleFindings} more`}`);
|
|
470
|
+
}
|
|
471
|
+
lines.push('');
|
|
472
|
+
}
|
|
473
|
+
// Failed commands (Claude Code style)
|
|
474
|
+
const failed = report.results.filter(r => !r.success);
|
|
475
|
+
if (failed.length > 0) {
|
|
476
|
+
lines.push(theme.bold?.('Failed Commands') ?? 'Failed Commands');
|
|
477
|
+
for (const result of failed.slice(0, 5)) {
|
|
478
|
+
const cmd = this.truncate(result.command ?? 'unknown', 50);
|
|
479
|
+
const errorMsg = result.error ? ` - ${this.truncate(result.error, 30)}` : '';
|
|
480
|
+
lines.push(` ${theme.error?.(ICONS.error) ?? ICONS.error} ${cmd}${theme.error?.(errorMsg) ?? errorMsg}`);
|
|
481
|
+
}
|
|
482
|
+
if (failed.length > 5) {
|
|
483
|
+
lines.push(` ${theme.ui?.muted?.(`... and ${failed.length - 5} more`) ?? `... and ${failed.length - 5} more`}`);
|
|
484
|
+
}
|
|
485
|
+
lines.push('');
|
|
486
|
+
}
|
|
487
|
+
// Summary line
|
|
488
|
+
lines.push(theme.bold?.('Summary') ?? 'Summary');
|
|
489
|
+
const summaryParts = [`${successCount}/${report.results.length} succeeded`];
|
|
490
|
+
if (report.findings.length > 0) {
|
|
491
|
+
summaryParts.push(`${report.findings.length} finding${report.findings.length === 1 ? '' : 's'}${criticalCount > 0 ? ` (${criticalCount} critical)` : ''}`);
|
|
492
|
+
}
|
|
493
|
+
if (failedCount > 0) {
|
|
494
|
+
summaryParts.push(`${failedCount} failed`);
|
|
495
|
+
}
|
|
496
|
+
lines.push(` ${summaryParts.join(', ')}`);
|
|
497
|
+
lines.push('');
|
|
498
|
+
this.enqueueUIUpdate('prompt', () => {
|
|
499
|
+
this.display.writeRaw(lines.join('\n'));
|
|
500
|
+
}, 'orchestration-report');
|
|
501
|
+
}
|
|
502
|
+
handleError(error) {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
this.currentState.errorsCount++;
|
|
505
|
+
this.enqueueUIUpdate('prompt', () => {
|
|
506
|
+
this.display.showError('Orchestration Error', error);
|
|
507
|
+
}, 'orchestration-error');
|
|
508
|
+
this.emitEvent('error', { error: message });
|
|
509
|
+
}
|
|
510
|
+
// --------------------------------------------------------------------------
|
|
511
|
+
// Status Management
|
|
512
|
+
// --------------------------------------------------------------------------
|
|
513
|
+
setPhase(phase) {
|
|
514
|
+
this.currentState.phase = phase;
|
|
515
|
+
this.currentState.isRunning = phase !== 'complete' && phase !== 'error';
|
|
516
|
+
this.currentState.elapsedMs = Date.now() - this.startTime;
|
|
517
|
+
const phaseDescriptions = {
|
|
518
|
+
initializing: 'Initializing orchestration...',
|
|
519
|
+
analyzing: 'Analyzing objective...',
|
|
520
|
+
executing: 'Executing commands...',
|
|
521
|
+
reporting: 'Generating report...',
|
|
522
|
+
complete: 'Orchestration complete',
|
|
523
|
+
error: 'Orchestration error',
|
|
524
|
+
};
|
|
525
|
+
this.updateStatus(phaseDescriptions[phase]);
|
|
526
|
+
this.emitEvent('progress', this.getProgress());
|
|
527
|
+
}
|
|
528
|
+
updateStatus(status) {
|
|
529
|
+
this.statusOrchestrator.setBaseStatus({
|
|
530
|
+
text: status,
|
|
531
|
+
tone: 'info',
|
|
532
|
+
startedAt: this.startTime,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
getProgress() {
|
|
536
|
+
return {
|
|
537
|
+
phase: this.currentState.phase,
|
|
538
|
+
currentCommand: this.currentState.currentCommand ?? undefined,
|
|
539
|
+
commandIndex: this.commandResults.length,
|
|
540
|
+
totalCommands: this.commandResults.length,
|
|
541
|
+
elapsedMs: Date.now() - this.startTime,
|
|
542
|
+
findings: [...this.findings],
|
|
543
|
+
errors: this.commandResults.filter(r => !r.success).map(r => r.error ?? 'Unknown error'),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// --------------------------------------------------------------------------
|
|
547
|
+
// Heartbeat & Status Updates
|
|
548
|
+
// --------------------------------------------------------------------------
|
|
549
|
+
startHeartbeat() {
|
|
550
|
+
if (this.heartbeatId)
|
|
551
|
+
return;
|
|
552
|
+
this.heartbeatId = `orchestration-heartbeat-${Date.now()}`;
|
|
553
|
+
this.updateCoordinator.startHeartbeat(this.heartbeatId, {
|
|
554
|
+
intervalMs: 1000,
|
|
555
|
+
run: () => {
|
|
556
|
+
if (this.currentState.isRunning) {
|
|
557
|
+
this.updateElapsedTime();
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
mode: ['processing', 'tooling'],
|
|
561
|
+
immediate: false,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
stopHeartbeat() {
|
|
565
|
+
if (this.heartbeatId) {
|
|
566
|
+
this.updateCoordinator.stopHeartbeat(this.heartbeatId);
|
|
567
|
+
this.heartbeatId = null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
updateElapsedTime() {
|
|
571
|
+
this.currentState.elapsedMs = Date.now() - this.startTime;
|
|
572
|
+
const elapsed = this.formatElapsed(this.currentState.elapsedMs);
|
|
573
|
+
const statusText = this.currentState.currentCommand
|
|
574
|
+
? `Running: ${this.truncate(this.currentState.currentCommand, 40)} (${elapsed})`
|
|
575
|
+
: `Orchestrating... (${elapsed})`;
|
|
576
|
+
this.statusOrchestrator.setBaseStatus({
|
|
577
|
+
text: statusText,
|
|
578
|
+
tone: 'info',
|
|
579
|
+
startedAt: this.startTime,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// --------------------------------------------------------------------------
|
|
583
|
+
// Event Handling
|
|
584
|
+
// --------------------------------------------------------------------------
|
|
585
|
+
setupStatusListeners() {
|
|
586
|
+
this.statusOrchestrator.subscribe((event) => {
|
|
587
|
+
if (event.type.startsWith('tool.')) {
|
|
588
|
+
this.handleToolStatusEvent(event.type, event.data);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
handleToolStatusEvent(type, status) {
|
|
593
|
+
// Bridge tool status to orchestration progress
|
|
594
|
+
if (type === 'tool.start') {
|
|
595
|
+
this.currentState.currentCommand = status.description;
|
|
596
|
+
}
|
|
597
|
+
else if (type === 'tool.complete' || type === 'tool.error') {
|
|
598
|
+
this.currentState.currentCommand = null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
emitEvent(type, data) {
|
|
602
|
+
const event = {
|
|
603
|
+
type,
|
|
604
|
+
timestamp: Date.now(),
|
|
605
|
+
data,
|
|
606
|
+
};
|
|
607
|
+
this.emit(type, event);
|
|
608
|
+
this.emit('event', event);
|
|
609
|
+
}
|
|
610
|
+
// --------------------------------------------------------------------------
|
|
611
|
+
// UI Update Coordination
|
|
612
|
+
// --------------------------------------------------------------------------
|
|
613
|
+
enqueueUIUpdate(lane, run, coalesceKey) {
|
|
614
|
+
this.updateCoordinator.enqueue({
|
|
615
|
+
lane,
|
|
616
|
+
run,
|
|
617
|
+
description: 'orchestration-update',
|
|
618
|
+
priority: 'normal',
|
|
619
|
+
coalesceKey,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// --------------------------------------------------------------------------
|
|
623
|
+
// Helpers
|
|
624
|
+
// --------------------------------------------------------------------------
|
|
625
|
+
createInitialState() {
|
|
626
|
+
return {
|
|
627
|
+
isRunning: false,
|
|
628
|
+
phase: 'initializing',
|
|
629
|
+
progress: 0,
|
|
630
|
+
currentCommand: null,
|
|
631
|
+
findingsCount: 0,
|
|
632
|
+
errorsCount: 0,
|
|
633
|
+
elapsedMs: 0,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
reset() {
|
|
637
|
+
this.currentState = this.createInitialState();
|
|
638
|
+
this.commandResults = [];
|
|
639
|
+
this.findings = [];
|
|
640
|
+
this.startTime = 0;
|
|
641
|
+
}
|
|
642
|
+
truncate(text, maxLength) {
|
|
643
|
+
if (text.length <= maxLength)
|
|
644
|
+
return text;
|
|
645
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
646
|
+
}
|
|
647
|
+
formatElapsed(ms) {
|
|
648
|
+
const seconds = Math.floor(ms / 1000);
|
|
649
|
+
if (seconds < 60)
|
|
650
|
+
return `${seconds}s`;
|
|
651
|
+
const minutes = Math.floor(seconds / 60);
|
|
652
|
+
const remainingSeconds = seconds % 60;
|
|
653
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
654
|
+
}
|
|
655
|
+
getSeverityColor(severity) {
|
|
656
|
+
switch (severity) {
|
|
657
|
+
case 'critical':
|
|
658
|
+
case 'high':
|
|
659
|
+
return theme.error ?? ((t) => t);
|
|
660
|
+
case 'medium':
|
|
661
|
+
return theme.warning ?? ((t) => t);
|
|
662
|
+
case 'low':
|
|
663
|
+
case 'info':
|
|
664
|
+
default:
|
|
665
|
+
return theme.ui?.muted ?? ((t) => t);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// ============================================================================
|
|
670
|
+
// Factory Function
|
|
671
|
+
// ============================================================================
|
|
672
|
+
/**
|
|
673
|
+
* Create an OrchestrationUIBridge with default configuration.
|
|
674
|
+
*/
|
|
675
|
+
export function createOrchestrationUIBridge(display, options) {
|
|
676
|
+
return new OrchestrationUIBridge({
|
|
677
|
+
display,
|
|
678
|
+
...options,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=OrchestrationUIBridge.js.map
|