@pellux/goodvibes-agent 0.1.2 → 0.1.4

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +12 -1
  4. package/docs/README.md +2 -0
  5. package/docs/getting-started.md +19 -1
  6. package/docs/release-and-publishing.md +3 -1
  7. package/package.json +10 -1
  8. package/src/agent/persona-registry.ts +379 -0
  9. package/src/agent/skill-registry.ts +360 -0
  10. package/src/audio/spoken-turn-model-routing.ts +2 -1
  11. package/src/cli/agent-knowledge-command.ts +46 -10
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/config/surface.ts +1 -0
  14. package/src/input/agent-workspace.ts +32 -2
  15. package/src/input/command-registry.ts +4 -1
  16. package/src/input/commands/agent-skills-runtime.ts +216 -0
  17. package/src/input/commands/knowledge.ts +18 -18
  18. package/src/input/commands/personas-runtime.ts +219 -0
  19. package/src/input/commands/skills-runtime.ts +7 -2
  20. package/src/input/commands.ts +4 -0
  21. package/src/input/panel-integration-actions.ts +0 -52
  22. package/src/main.ts +2 -1
  23. package/src/panels/builtin/session.ts +4 -3
  24. package/src/panels/index.ts +0 -5
  25. package/src/panels/orchestration-panel.ts +4 -5
  26. package/src/panels/qr-panel.ts +3 -2
  27. package/src/panels/tasks-panel.ts +4 -4
  28. package/src/renderer/agent-workspace.ts +2 -0
  29. package/src/runtime/bootstrap-command-context.ts +3 -0
  30. package/src/runtime/bootstrap-command-parts.ts +6 -2
  31. package/src/runtime/bootstrap-core.ts +9 -5
  32. package/src/runtime/bootstrap-shell.ts +3 -1
  33. package/src/runtime/bootstrap.ts +10 -2
  34. package/src/runtime/cloudflare-control-plane.ts +2 -1
  35. package/src/runtime/services.ts +3 -3
  36. package/src/version.ts +1 -1
  37. package/src/daemon/cli.ts +0 -55
  38. package/src/daemon/safe-serve.ts +0 -61
  39. package/src/panels/diff-panel.ts +0 -520
  40. package/src/panels/file-explorer-panel.ts +0 -584
  41. package/src/panels/file-preview-panel.ts +0 -434
  42. package/src/panels/git-panel.ts +0 -638
  43. package/src/panels/sandbox-panel.ts +0 -283
  44. package/src/panels/symbol-outline-panel.ts +0 -486
  45. package/src/panels/worktree-panel.ts +0 -182
  46. package/src/panels/wrfc-panel.ts +0 -609
@@ -1,609 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import type { WrfcChain, WrfcState, QualityGateResult } from '@pellux/goodvibes-sdk/platform/agents';
3
- import type { Constraint, ConstraintFinding } from '@pellux/goodvibes-sdk/platform/agents';
4
- import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
5
- import { BasePanel } from './base-panel.ts';
6
- import type { WorkflowEvent } from '@/runtime/index.ts';
7
- import type { UiEventFeed } from '../runtime/ui-events.ts';
8
- import {
9
- buildPanelLine,
10
- buildPanelWorkspace,
11
- resolveScrollablePanelSection,
12
- DEFAULT_PANEL_PALETTE,
13
- type PanelWorkspaceSection,
14
- buildSelectablePanelLine,
15
- buildStyledPanelLine,
16
- buildEmptyState,
17
- } from './polish.ts';
18
- import { getDisplayWidth } from '../utils/terminal-width.ts';
19
-
20
- // ---------------------------------------------------------------------------
21
- // Colour palette
22
- // ---------------------------------------------------------------------------
23
- const C = {
24
- // states
25
- passed: '#22c55e', // green
26
- failed: '#ef4444', // red
27
- reviewing: '#eab308', // yellow
28
- engineering:'#22d3ee', // cyan
29
- fixing: '#f97316', // orange
30
- pending: '#6b7280', // grey
31
- gating: '#a78bfa', // violet
32
- committing: '#38bdf8', // sky
33
-
34
- // UI chrome
35
- header: '#94a3b8',
36
- headerBold: '#e2e8f0',
37
- dim: '#4b5563',
38
- label: '#64748b',
39
- value: '#cbd5e1',
40
- selected: '#1e40af', // selection bg
41
- selectedFg: '#f8fafc',
42
- border: '#334155',
43
- spark: '#38bdf8',
44
- sparkLow: '#ef4444',
45
- sparkHigh: '#22c55e',
46
- issueCrit: '#ef4444',
47
- issueMaj: '#f97316',
48
- issueMin: '#eab308',
49
- issueSug: '#6b7280',
50
- gatePass: '#22c55e',
51
- gateFail: '#ef4444',
52
- // constraint status
53
- constraintSat: '#22c55e', // green — satisfied
54
- constraintUnsat:'#ef4444', // red — unsatisfied
55
- constraintUnv: '#4b5563', // grey — unverified (no finding yet)
56
- } as const;
57
-
58
- // ---------------------------------------------------------------------------
59
- // Helpers
60
- // ---------------------------------------------------------------------------
61
- const SPARKLINE_CHARS = '._-:=+*#';
62
-
63
- export function sparkline(scores: number[], maxScore = 10): string {
64
- if (scores.length === 0) return '';
65
- return scores
66
- .map(s => {
67
- const ratio = Math.max(0, Math.min(1, s / maxScore));
68
- const idx = Math.round(ratio * (SPARKLINE_CHARS.length - 1));
69
- return SPARKLINE_CHARS[idx];
70
- })
71
- .join('');
72
- }
73
-
74
- export function stateColor(state: WrfcState): string {
75
- switch (state) {
76
- case 'passed': return C.passed;
77
- case 'failed': return C.failed;
78
- case 'reviewing': return C.reviewing;
79
- case 'engineering': return C.engineering;
80
- case 'fixing': return C.fixing;
81
- case 'gating':
82
- case 'awaiting_gates': return C.gating;
83
- case 'committing': return C.committing;
84
- default: return C.pending;
85
- }
86
- }
87
-
88
- export function stateLabel(state: WrfcState): string {
89
- switch (state) {
90
- case 'engineering': return 'ENG';
91
- case 'reviewing': return 'REV';
92
- case 'fixing': return 'FIX';
93
- case 'gating': return 'GATE';
94
- case 'awaiting_gates': return 'WAIT';
95
- case 'committing': return 'COMMIT';
96
- case 'passed': return 'PASS';
97
- case 'failed': return 'FAIL';
98
- default: return 'PEND';
99
- }
100
- }
101
-
102
- function issueColor(severity: string): string {
103
- switch (severity) {
104
- case 'critical': return C.issueCrit;
105
- case 'major': return C.issueMaj;
106
- case 'minor': return C.issueMin;
107
- default: return C.issueSug;
108
- }
109
- }
110
-
111
- function issuePrefix(severity: string): string {
112
- switch (severity) {
113
- case 'critical': return '[CRIT] ';
114
- case 'major': return '[MAJR] ';
115
- case 'minor': return '[MINR] ';
116
- default: return '[SUGG] ';
117
- }
118
- }
119
-
120
- export function truncate(s: string, max: number): string {
121
- if (s.length <= max) return s;
122
- return s.slice(0, max - 3) + '...';
123
- }
124
-
125
- // ---------------------------------------------------------------------------
126
- // Constraint helpers
127
- // ---------------------------------------------------------------------------
128
-
129
- /**
130
- * Returns display tag, foreground colour, and dim flag for a single constraint
131
- * based on whether a reviewer finding exists for it.
132
- */
133
- export function constraintStatusMarker(
134
- constraint: Constraint,
135
- findings: ConstraintFinding[] | undefined,
136
- ): { tag: string; fg: string; dim: boolean } {
137
- const finding = findings?.find(f => f.constraintId === constraint.id);
138
- if (!finding) {
139
- return { tag: '[UNV]', fg: C.constraintUnv, dim: true };
140
- }
141
- if (finding.satisfied) {
142
- return { tag: '[SAT]', fg: C.constraintSat, dim: false };
143
- }
144
- // Unsatisfied — use severity to pick colour and tag text
145
- const sev = finding.severity ?? 'major';
146
- let sevTag: string;
147
- let fg: string;
148
- switch (sev) {
149
- case 'critical': sevTag = '[UNS CRIT]'; fg = C.issueCrit; break;
150
- case 'minor': sevTag = '[UNS MINOR]'; fg = C.issueMin; break;
151
- default: sevTag = '[UNS MAJOR]'; fg = C.issueMaj; break;
152
- }
153
- return { tag: sevTag, fg, dim: false };
154
- }
155
-
156
- // ---------------------------------------------------------------------------
157
- // Panel
158
- // ---------------------------------------------------------------------------
159
- export interface WrfcPanelDeps {
160
- readonly controller: Pick<WrfcController, 'listChains'>;
161
- }
162
-
163
- export class WrfcPanel extends BasePanel {
164
- private chains: WrfcChain[] = [];
165
- private selectedIndex = 0;
166
- private scrollOffset = 0;
167
- private expandedChainIds = new Set<string>();
168
- private unsubscribers: Array<() => void> = [];
169
-
170
- constructor(
171
- private readonly workflowEvents: UiEventFeed<WorkflowEvent>,
172
- private readonly deps: WrfcPanelDeps,
173
- ) {
174
- super('wrfc', 'WRFC', 'W', 'agent');
175
- this.subscribeToEvents();
176
- this.syncFromController();
177
- }
178
-
179
- // -------------------------------------------------------------------------
180
- // Lifecycle
181
- // -------------------------------------------------------------------------
182
- override onActivate(): void {
183
- super.onActivate();
184
- this.syncFromController();
185
- }
186
-
187
- override onDestroy(): void {
188
- for (const unsub of this.unsubscribers) unsub();
189
- this.unsubscribers = [];
190
- }
191
-
192
- // -------------------------------------------------------------------------
193
- // Input
194
- // -------------------------------------------------------------------------
195
- handleInput(key: string): boolean {
196
- switch (key) {
197
- case 'up': this.moveSelection(-1); return true;
198
- case 'down': this.moveSelection(1); return true;
199
- case 'return':
200
- case 'enter': this.toggleExpanded(); return true;
201
- default: return false;
202
- }
203
- }
204
-
205
- // -------------------------------------------------------------------------
206
- // Render
207
- // -------------------------------------------------------------------------
208
- render(width: number, height: number): Line[] {
209
- return this.trackedRender(() => {
210
- const activeCount = this.chains.filter(c => !['passed', 'failed'].includes(c.state)).length;
211
- const passedCount = this.chains.filter(c => c.state === 'passed').length;
212
- const failedCount = this.chains.filter(c => c.state === 'failed').length;
213
-
214
- if (this.chains.length === 0) {
215
- return buildPanelWorkspace(width, height, {
216
- title: ' WRFC Chain Monitor',
217
- intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
218
- sections: [
219
- {
220
- lines: buildEmptyState(
221
- width,
222
- ' No WRFC chains yet',
223
- 'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
224
- [],
225
- DEFAULT_PANEL_PALETTE,
226
- ),
227
- },
228
- ],
229
- palette: DEFAULT_PANEL_PALETTE,
230
- });
231
- }
232
-
233
- const chainLines: Line[] = [];
234
- let selectedLineIndex = 0;
235
- for (let i = 0; i < this.chains.length; i++) {
236
- const chain = this.chains[i];
237
- const isSelected = i === this.selectedIndex;
238
- const isExpanded = this.expandedChainIds.has(chain.id);
239
- const rowBg = isSelected ? C.selected : '';
240
- const rowFg = isSelected ? C.selectedFg : '';
241
-
242
- if (isSelected) selectedLineIndex = chainLines.length;
243
- chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg));
244
-
245
- if (isExpanded) {
246
- chainLines.push(...this.renderChainDetail(chain, width, 12));
247
- }
248
- }
249
-
250
- const selectedChain = this.chains[this.selectedIndex];
251
- const selectedLines: Line[] = selectedChain
252
- ? [
253
- buildPanelLine(width, [
254
- [' State ', DEFAULT_PANEL_PALETTE.label],
255
- [stateLabel(selectedChain.state), stateColor(selectedChain.state)],
256
- [' Task ', DEFAULT_PANEL_PALETTE.label],
257
- [truncate(selectedChain.task, Math.max(8, width - 24)), DEFAULT_PANEL_PALETTE.value],
258
- ]),
259
- buildPanelLine(width, [
260
- [' Reviews ', DEFAULT_PANEL_PALETTE.label],
261
- [String(selectedChain.reviewCycles), DEFAULT_PANEL_PALETTE.value],
262
- [' Fixes ', DEFAULT_PANEL_PALETTE.label],
263
- [String(selectedChain.fixAttempts), DEFAULT_PANEL_PALETTE.value],
264
- [' Scores ', DEFAULT_PANEL_PALETTE.label],
265
- [selectedChain.reviewScores.length > 0 ? selectedChain.reviewScores.map((score) => score.toFixed(0)).join(' -> ') : 'none', DEFAULT_PANEL_PALETTE.info],
266
- ]),
267
- ...(selectedChain.constraints.length > 0 ? [
268
- buildPanelLine(width, (() => {
269
- const total = selectedChain.constraints.length;
270
- const findings = selectedChain.reviewerReport?.constraintFindings;
271
- const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
272
- const satFg = !findings || findings.length === 0
273
- ? DEFAULT_PANEL_PALETTE.dim
274
- : satisfied === total ? C.constraintSat : C.constraintUnsat;
275
- return [
276
- [' Constraints ', DEFAULT_PANEL_PALETTE.label],
277
- [`${satisfied} sat / ${total} total`, satFg],
278
- ] as Array<[string, string]>;
279
- })()),
280
- ] : []),
281
- ]
282
- : [];
283
-
284
- const summarySection: PanelWorkspaceSection = {
285
- title: 'Summary',
286
- lines: [
287
- buildPanelLine(width, [
288
- [' Active ', DEFAULT_PANEL_PALETTE.label],
289
- [String(activeCount), activeCount > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
290
- [' Passed ', DEFAULT_PANEL_PALETTE.label],
291
- [String(passedCount), DEFAULT_PANEL_PALETTE.good],
292
- [' Failed ', DEFAULT_PANEL_PALETTE.label],
293
- [String(failedCount), failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
294
- ]),
295
- ],
296
- };
297
- const selectedSection: PanelWorkspaceSection = {
298
- title: 'Selected',
299
- lines: selectedLines,
300
- };
301
- const chainsSection = resolveScrollablePanelSection(width, height, {
302
- intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
303
- footerLines: [
304
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
305
- ],
306
- palette: DEFAULT_PANEL_PALETTE,
307
- beforeSections: [summarySection],
308
- section: {
309
- title: 'Chains',
310
- scrollableLines: chainLines,
311
- selectedIndex: selectedLineIndex,
312
- scrollOffset: this.scrollOffset,
313
- minRows: 8,
314
- },
315
- afterSections: [selectedSection],
316
- });
317
- this.scrollOffset = chainsSection.scrollOffset;
318
- const sections: PanelWorkspaceSection[] = [summarySection, chainsSection.section, selectedSection];
319
-
320
- return buildPanelWorkspace(width, height, {
321
- title: ' WRFC Chain Monitor',
322
- intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
323
- sections,
324
- footerLines: [
325
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
326
- ],
327
- palette: DEFAULT_PANEL_PALETTE,
328
- });
329
- });
330
- }
331
-
332
- // -------------------------------------------------------------------------
333
- // Rendering helpers
334
- // -------------------------------------------------------------------------
335
- private renderChainRow(
336
- chain: WrfcChain,
337
- width: number,
338
- isSelected: boolean,
339
- isExpanded: boolean,
340
- bg: string,
341
- fg: string,
342
- ): Line[] {
343
- const stateCol = stateColor(chain.state);
344
- const stateTag = ` ${stateLabel(chain.state).padEnd(6)}`;
345
- const arrow = isExpanded ? '▾' : '▸';
346
- const chainIdShort = chain.id.slice(-6);
347
- const prefix = ` ${arrow} [${chainIdShort}] `;
348
- const fixes = chain.fixAttempts > 0 ? ` fix:${chain.fixAttempts}` : '';
349
- const cycles = chain.reviewCycles > 0 ? ` rev:${chain.reviewCycles}` : '';
350
- const latestScore = chain.reviewScores.length > 0
351
- ? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
352
- : '';
353
- // Constraint badge: c:sat/total — only when constraints exist
354
- let constraintBadge = '';
355
- if (chain.constraints.length > 0) {
356
- const total = chain.constraints.length;
357
- const findings = chain.reviewerReport?.constraintFindings;
358
- const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
359
- constraintBadge = ` c:${satisfied}/${total}`;
360
- }
361
- const rightInfo = `${latestScore}${fixes}${cycles}${constraintBadge} `;
362
-
363
- // Compute how much space the task text can use, then check if rightInfo fits.
364
- // If the terminal is narrow and rightInfo would overflow, omit it entirely
365
- // rather than producing corrupted layout.
366
- const usedWithoutTask = getDisplayWidth(prefix) + getDisplayWidth(stateTag) + 1; // prefix + stateTag + space
367
- const taskMax = width - usedWithoutTask - getDisplayWidth(rightInfo);
368
- const taskText = truncate(chain.task, Math.max(8, taskMax));
369
- const usedWidth = usedWithoutTask + getDisplayWidth(taskText);
370
- const remaining = width - usedWidth;
371
-
372
- const segments = [
373
- { text: prefix, fg: isSelected ? fg : C.header },
374
- { text: stateTag, fg: stateCol, bold: true },
375
- { text: ' ', fg: '' },
376
- { text: taskText, fg: isSelected ? fg : C.value },
377
- ];
378
- if (remaining >= rightInfo.length + 1) {
379
- // Right-align rightInfo in the remaining space
380
- // Colour the constraint badge separately when present
381
- if (chain.constraints.length > 0 && !isSelected) {
382
- const total = chain.constraints.length;
383
- const findings = chain.reviewerReport?.constraintFindings;
384
- const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
385
- // Determine badge colour
386
- let badgeFg: string;
387
- if (!findings || findings.length === 0) {
388
- badgeFg = C.constraintUnv;
389
- } else if (satisfied === total) {
390
- badgeFg = C.constraintSat;
391
- } else if (findings.some(f => !f.satisfied)) {
392
- badgeFg = C.constraintUnsat;
393
- } else {
394
- badgeFg = C.reviewing; // some unverified but none failed
395
- }
396
- // Split: everything before the badge, then the badge
397
- const badgeText = ` c:${satisfied}/${total}`;
398
- const beforeBadge = rightInfo.slice(0, rightInfo.length - badgeText.length - 1);
399
- const padding = remaining - rightInfo.length;
400
- segments.push({ text: beforeBadge.padStart(padding + beforeBadge.length), fg: isSelected ? fg : C.label });
401
- segments.push({ text: badgeText, fg: badgeFg });
402
- segments.push({ text: ' ', fg: '' });
403
- } else {
404
- segments.push({ text: rightInfo.padStart(remaining), fg: isSelected ? fg : C.label });
405
- }
406
- }
407
- // else: no room — makeSegmentedLine will pad with spaces to fill width
408
-
409
- const row = buildSelectablePanelLine(width, segments, {
410
- selected: isSelected,
411
- selectedBg: bg,
412
- fillFg: isSelected ? fg : '',
413
- });
414
-
415
- return [row];
416
- }
417
-
418
- private renderChainDetail(chain: WrfcChain, width: number, maxLines: number): Line[] {
419
- const lines: Line[] = [];
420
- const indent = ' ';
421
-
422
- // Score sparkline
423
- if (chain.reviewScores.length > 0) {
424
- const spark = sparkline(chain.reviewScores);
425
- const latestScore = chain.reviewScores[chain.reviewScores.length - 1];
426
- const sparkColor = latestScore >= 8 ? C.sparkHigh : latestScore >= 5 ? C.spark : C.sparkLow;
427
- lines.push(buildStyledPanelLine(width, [
428
- { text: `${indent}Scores `, fg: C.label },
429
- { text: spark, fg: sparkColor },
430
- { text: ` (${chain.reviewScores.map(s => s.toFixed(0)).join(' -> ')})`, fg: C.dim },
431
- ]));
432
- }
433
-
434
- // Fix attempts + review cycles
435
- if (chain.fixAttempts > 0 || chain.reviewCycles > 0) {
436
- lines.push(buildStyledPanelLine(width, [
437
- { text: `${indent}Cycles `, fg: C.label },
438
- { text: `${chain.reviewCycles} review`, fg: C.value },
439
- { text: ' ', fg: '' },
440
- { text: `${chain.fixAttempts} fix`, fg: chain.fixAttempts > 0 ? C.fixing : C.value },
441
- ]));
442
- }
443
-
444
- // Constraints section (between Cycles and Gates)
445
- if (chain.constraints.length > 0 && lines.length < maxLines) {
446
- lines.push(buildStyledPanelLine(width, [{ text: `${indent}Constraints`, fg: C.label }]));
447
- const MAX_CONSTRAINTS = 10;
448
- const findings = chain.reviewerReport?.constraintFindings;
449
- const displayed = chain.constraints.slice(0, MAX_CONSTRAINTS);
450
- for (const constraint of displayed) {
451
- if (lines.length >= maxLines) break;
452
- const marker = constraintStatusMarker(constraint, findings);
453
- const statusTag = marker.tag;
454
- const rowPrefix = `${indent} ${statusTag} `;
455
- const textMax = Math.max(8, width - rowPrefix.length);
456
- const constraintText = truncate(constraint.text, textMax);
457
- lines.push(buildStyledPanelLine(width, [
458
- { text: `${indent} `, fg: C.dim },
459
- { text: statusTag, fg: marker.fg, dim: marker.dim, bold: !marker.dim },
460
- { text: ' ', fg: '' },
461
- { text: constraintText, fg: C.value },
462
- ]));
463
- }
464
- const remaining = chain.constraints.length - MAX_CONSTRAINTS;
465
- if (remaining > 0 && lines.length < maxLines) {
466
- lines.push(buildStyledPanelLine(width, [
467
- { text: `${indent} (+${remaining} more)`, fg: C.dim, dim: true },
468
- ]));
469
- }
470
- }
471
-
472
- // Quality gate results
473
- if (chain.gateResults && chain.gateResults.length > 0) {
474
- lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates`, fg: C.label }]));
475
- for (const gate of chain.gateResults) {
476
- if (lines.length >= maxLines) break;
477
- lines.push(this.renderGateResult(gate, width, indent + ' '));
478
- }
479
- }
480
-
481
- // Gate status (no results yet but state is gating)
482
- if (chain.state === 'gating' || chain.state === 'awaiting_gates') {
483
- if (!chain.gateResults || chain.gateResults.length === 0) {
484
- lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates awaiting...`, fg: C.gating, dim: true }]));
485
- }
486
- }
487
-
488
- // Synthetic issues injected by controller (continuity violations)
489
- if (chain.syntheticIssues && chain.syntheticIssues.length > 0 && lines.length < maxLines) {
490
- lines.push(buildStyledPanelLine(width, [{ text: `${indent}Controller flags`, fg: C.issueCrit, bold: true }]));
491
- for (const synthetic of chain.syntheticIssues) {
492
- if (lines.length >= maxLines) break;
493
- const prefix = `${indent} [CRIT] `;
494
- const descMax = Math.max(8, width - prefix.length);
495
- const desc = truncate(synthetic.description, descMax);
496
- lines.push(buildStyledPanelLine(width, [
497
- { text: prefix, fg: C.issueCrit, bold: true },
498
- { text: desc, fg: C.value },
499
- ]));
500
- }
501
- }
502
-
503
- // Issues from reviewer
504
- const issues = chain.reviewerReport?.issues ?? [];
505
- if (issues.length > 0 && lines.length < maxLines) {
506
- lines.push(buildStyledPanelLine(width, [{ text: `${indent}Issues`, fg: C.label }]));
507
- for (const issue of issues) {
508
- if (lines.length >= maxLines) break;
509
- const prefix = `${indent} ${issuePrefix(issue.severity)}`;
510
- const descMax = width - prefix.length;
511
- const desc = truncate(issue.description, Math.max(8, descMax));
512
- lines.push(buildStyledPanelLine(width, [
513
- { text: prefix, fg: issueColor(issue.severity), bold: issue.severity === 'critical' },
514
- { text: desc, fg: C.value },
515
- ]));
516
- }
517
- }
518
-
519
- // Error
520
- if (chain.error && lines.length < maxLines) {
521
- const errPrefix = `${indent}Error `;
522
- lines.push(buildStyledPanelLine(width, [
523
- { text: errPrefix, fg: C.failed, bold: true },
524
- { text: truncate(chain.error, width - errPrefix.length), fg: C.value },
525
- ]));
526
- }
527
-
528
- // Divider after expanded section
529
- if (lines.length < maxLines) {
530
- lines.push(buildStyledPanelLine(width, [{ text: ' ' + '-'.repeat(Math.max(0, width - 4)) + ' ', fg: C.border, dim: true }]));
531
- }
532
-
533
- return lines.slice(0, maxLines);
534
- }
535
-
536
- private renderGateResult(gate: QualityGateResult, width: number, indent: string): Line {
537
- const icon = gate.passed ? '✓' : '✕';
538
- const iconFg = gate.passed ? C.gatePass : C.gateFail;
539
- const dur = `${gate.durationMs}ms`;
540
- const nameMax = width - indent.length - 2 - dur.length - 4;
541
- const name = truncate(gate.gate, Math.max(8, nameMax));
542
-
543
- return buildStyledPanelLine(width, [
544
- { text: indent, fg: C.dim },
545
- { text: `${icon} `, fg: iconFg, bold: true },
546
- { text: name, fg: gate.passed ? C.value : C.failed },
547
- { text: ` (${dur})`, fg: C.dim },
548
- ]);
549
- }
550
-
551
- // -------------------------------------------------------------------------
552
- // Navigation
553
- // -------------------------------------------------------------------------
554
- private moveSelection(delta: number): void {
555
- if (this.chains.length === 0) return;
556
- this.selectedIndex = Math.max(0, Math.min(this.chains.length - 1, this.selectedIndex + delta));
557
- this.markDirty();
558
- }
559
-
560
- private toggleExpanded(): void {
561
- const chain = this.chains[this.selectedIndex];
562
- if (!chain) return;
563
- if (this.expandedChainIds.has(chain.id)) {
564
- this.expandedChainIds.delete(chain.id);
565
- } else {
566
- this.expandedChainIds.add(chain.id);
567
- }
568
- this.markDirty();
569
- }
570
-
571
- // -------------------------------------------------------------------------
572
- // Event subscriptions
573
- // -------------------------------------------------------------------------
574
- private subscribeToEvents(): void {
575
- const refresh = () => { this.syncFromController(); this.markDirty(); };
576
-
577
- this.unsubscribers.push(
578
- this.workflowEvents.on('WORKFLOW_CHAIN_CREATED', refresh),
579
- this.workflowEvents.on('WORKFLOW_STATE_CHANGED', refresh),
580
- this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED', refresh),
581
- this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED', refresh),
582
- this.workflowEvents.on('WORKFLOW_GATE_RESULT', refresh),
583
- this.workflowEvents.on('WORKFLOW_CHAIN_PASSED', refresh),
584
- this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', refresh),
585
- this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', refresh),
586
- this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', refresh),
587
- this.workflowEvents.on('WORKFLOW_CONSTRAINTS_ENUMERATED', refresh),
588
- );
589
- }
590
-
591
- private syncFromController(): void {
592
- try {
593
- // Sort: active first (by createdAt desc), then completed
594
- const all = this.deps.controller.listChains();
595
- const active = all.filter(c => !['passed', 'failed'].includes(c.state));
596
- const done = all.filter(c => ['passed', 'failed'].includes(c.state));
597
- active.sort((a, b) => b.createdAt - a.createdAt);
598
- done.sort( (a, b) => (b.completedAt ?? 0) - (a.completedAt ?? 0));
599
- this.chains = [...active, ...done];
600
-
601
- // Clamp selection
602
- if (this.chains.length > 0) {
603
- this.selectedIndex = Math.min(this.selectedIndex, this.chains.length - 1);
604
- }
605
- } catch {
606
- // WrfcController not yet initialized — leave chain list empty
607
- }
608
- }
609
- }