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.
@@ -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