@pellux/goodvibes-agent 0.1.1 → 0.1.3
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/CHANGELOG.md +14 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management-commands.ts +3 -1
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +33 -3
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +6 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/input/submission-router.ts +1 -1
- package/src/main.ts +2 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +8 -4
- package/src/runtime/bootstrap-shell.ts +5 -2
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
package/src/panels/wrfc-panel.ts
DELETED
|
@@ -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
|
-
}
|