@pellux/goodvibes-tui 0.27.0 → 0.29.0
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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
package/src/panels/eval-panel.ts
CHANGED
|
@@ -5,16 +5,22 @@
|
|
|
5
5
|
* indicators. Wired with an EvalRegistry that holds the latest run results.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { join } from 'node:path';
|
|
8
9
|
import { BasePanel } from './base-panel.ts';
|
|
10
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
9
11
|
import type { Line } from '../types/grid.ts';
|
|
10
|
-
import type { KeyName } from './types.ts';
|
|
12
|
+
import type { KeyName, PanelIntegrationContext } from './types.ts';
|
|
11
13
|
import { createEmptyLine } from '../types/grid.ts';
|
|
12
14
|
import {
|
|
13
|
-
|
|
15
|
+
buildAlignedRow,
|
|
16
|
+
buildKeyboardHints,
|
|
17
|
+
buildMeterLine,
|
|
14
18
|
buildPanelLine,
|
|
15
19
|
buildPanelWorkspace,
|
|
20
|
+
buildTable,
|
|
16
21
|
resolveScrollablePanelSection,
|
|
17
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
|
+
extendPalette,
|
|
18
24
|
} from './polish.ts';
|
|
19
25
|
|
|
20
26
|
// ── EvalRegistry ─────────────────────────────────────────────────────────────
|
|
@@ -24,7 +30,13 @@ import type {
|
|
|
24
30
|
EvalResult,
|
|
25
31
|
EvalGateResult,
|
|
26
32
|
EvalDimension,
|
|
33
|
+
EvalBaseline,
|
|
34
|
+
RegressionEntry,
|
|
27
35
|
} from '@/runtime/index.ts';
|
|
36
|
+
import { loadBaseline } from '@/runtime/index.ts';
|
|
37
|
+
import { formatShortDuration } from '../utils/format-duration.ts';
|
|
38
|
+
|
|
39
|
+
const BASELINE_PATH = '.goodvibes/eval/baseline.json';
|
|
28
40
|
|
|
29
41
|
/**
|
|
30
42
|
* Holds the latest eval run state for display in EvalPanel.
|
|
@@ -78,36 +90,24 @@ export class EvalRegistry {
|
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
// ── Colour palette (hex
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
green: '#22c55e',
|
|
89
|
-
yellow: '#eab308',
|
|
90
|
-
red: '#ef4444',
|
|
91
|
-
dim: '#4b5563',
|
|
92
|
-
label: '#64748b',
|
|
93
|
-
value: '#e2e8f0',
|
|
94
|
-
selected: '#f1f5f9',
|
|
95
|
-
sep: '#1e293b',
|
|
96
|
-
white: '#cbd5e1',
|
|
97
|
-
selectBg: '#0f172a',
|
|
98
|
-
} as const;
|
|
93
|
+
// ── Colour palette (no inline hex in render bodies) ──────────────────────────
|
|
94
|
+
|
|
95
|
+
// Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
|
|
96
|
+
// value/selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
97
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
98
|
+
selected: '#f1f5f9', // emphasized row/text highlight, brighter than value
|
|
99
|
+
});
|
|
99
100
|
|
|
100
101
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
101
102
|
|
|
102
103
|
function scoreColor(score: number): string {
|
|
103
|
-
if (score >= 80) return C.
|
|
104
|
-
if (score >= 60) return C.
|
|
105
|
-
return C.
|
|
104
|
+
if (score >= 80) return C.good;
|
|
105
|
+
if (score >= 60) return C.warn;
|
|
106
|
+
return C.bad;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function fmtTime(ms: number): string {
|
|
109
|
-
|
|
110
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
110
|
+
return formatShortDuration(ms);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const DIMENSION_ORDER: EvalDimension[] = ['safety', 'quality', 'latency', 'cost', 'recovery'];
|
|
@@ -116,19 +116,24 @@ const DIMENSION_ORDER: EvalDimension[] = ['safety', 'quality', 'latency', 'cost'
|
|
|
116
116
|
|
|
117
117
|
export class EvalPanel extends BasePanel {
|
|
118
118
|
private readonly _registry: EvalRegistry;
|
|
119
|
+
private readonly _projectRoot: string;
|
|
119
120
|
private _mode: 'list' | 'detail' = 'list';
|
|
120
121
|
private _selectedSuiteIdx = 0;
|
|
121
122
|
private _selectedScenarioIdx = 0;
|
|
122
123
|
private _scrollOffset = 0;
|
|
123
124
|
private _unsub: (() => void) | null = null;
|
|
125
|
+
private _baseline: EvalBaseline | null = null;
|
|
126
|
+
private _baselineLoading = false;
|
|
124
127
|
|
|
125
|
-
public constructor(registry: EvalRegistry) {
|
|
126
|
-
super('eval', 'Eval', '
|
|
128
|
+
public constructor(registry: EvalRegistry, projectRoot = '.') {
|
|
129
|
+
super('eval', 'Eval', '▮', 'incidents-diagnostics');
|
|
127
130
|
this._registry = registry;
|
|
131
|
+
this._projectRoot = projectRoot;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
public override onActivate(): void {
|
|
131
135
|
this._unsub = this._registry.subscribe(() => this.markDirty());
|
|
136
|
+
this._loadBaselineIfNeeded();
|
|
132
137
|
this.markDirty();
|
|
133
138
|
}
|
|
134
139
|
|
|
@@ -137,6 +142,65 @@ export class EvalPanel extends BasePanel {
|
|
|
137
142
|
this._unsub = null;
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Seed the panel with last-known scores from the on-disk baseline so a
|
|
147
|
+
* fresh session shows real data instead of a "run a suite" signpost.
|
|
148
|
+
* Only loaded once per activation; a live suite run supersedes it.
|
|
149
|
+
*
|
|
150
|
+
* Resolved as an absolute path under _projectRoot before handing it to
|
|
151
|
+
* loadBaseline() — the SDK helper resolves a relative path against the
|
|
152
|
+
* process cwd (not the projectRoot argument), so joining here is what
|
|
153
|
+
* actually makes the constructor's projectRoot meaningful.
|
|
154
|
+
*/
|
|
155
|
+
private _loadBaselineIfNeeded(): void {
|
|
156
|
+
if (this._baseline || this._baselineLoading) return;
|
|
157
|
+
this._baselineLoading = true;
|
|
158
|
+
void loadBaseline(join(this._projectRoot, BASELINE_PATH), this._projectRoot)
|
|
159
|
+
.then((baseline) => {
|
|
160
|
+
this._baseline = baseline ?? null;
|
|
161
|
+
})
|
|
162
|
+
.catch(() => {
|
|
163
|
+
this._baseline = null;
|
|
164
|
+
})
|
|
165
|
+
.finally(() => {
|
|
166
|
+
this._baselineLoading = false;
|
|
167
|
+
this.markDirty();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Number of selectable rows in list mode: live suite results, or baseline suites when none have run yet. */
|
|
172
|
+
private _listLength(suites: EvalSuiteResult[]): number {
|
|
173
|
+
if (suites.length > 0) return suites.length;
|
|
174
|
+
return this._baseline ? Object.keys(this._baseline.suites).length : 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Name of the suite currently selected in list mode, from live results or the baseline. */
|
|
178
|
+
private _selectedSuiteName(suites: EvalSuiteResult[]): string | undefined {
|
|
179
|
+
if (suites.length > 0) return suites[this._selectedSuiteIdx]?.suite;
|
|
180
|
+
if (!this._baseline) return undefined;
|
|
181
|
+
return Object.keys(this._baseline.suites)[this._selectedSuiteIdx];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Total scenario/detail line count for the given suite, used to clamp pagedown scrolling. */
|
|
185
|
+
private _detailLineCount(suite: EvalSuiteResult): number {
|
|
186
|
+
const gates = this._registry.getGateResults();
|
|
187
|
+
const gate = gates.find((g) => g.suite === suite.suite);
|
|
188
|
+
const regressedIds = new Set((gate?.regressions ?? []).map((r) => r.scenarioId));
|
|
189
|
+
|
|
190
|
+
let count = 0;
|
|
191
|
+
suite.results.forEach((result, idx) => {
|
|
192
|
+
count += 1;
|
|
193
|
+
if (idx === this._selectedScenarioIdx) {
|
|
194
|
+
count += DIMENSION_ORDER.filter((dim) =>
|
|
195
|
+
result.scorecard.dimensions.some((d) => d.dimension === dim),
|
|
196
|
+
).length;
|
|
197
|
+
if (regressedIds.has(result.scenario.id)) count += 1;
|
|
198
|
+
count += result.scorecard.notes?.length ?? 0;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return count;
|
|
202
|
+
}
|
|
203
|
+
|
|
140
204
|
public handleInput(key: KeyName): boolean {
|
|
141
205
|
const suites = this._registry.getSuiteResults();
|
|
142
206
|
|
|
@@ -147,7 +211,8 @@ export class EvalPanel extends BasePanel {
|
|
|
147
211
|
return true;
|
|
148
212
|
}
|
|
149
213
|
if (key === 'down' || key === 'j') {
|
|
150
|
-
|
|
214
|
+
const len = this._listLength(suites);
|
|
215
|
+
this._selectedSuiteIdx = Math.min(Math.max(0, len - 1), this._selectedSuiteIdx + 1);
|
|
151
216
|
this.markDirty();
|
|
152
217
|
return true;
|
|
153
218
|
}
|
|
@@ -194,45 +259,64 @@ export class EvalPanel extends BasePanel {
|
|
|
194
259
|
return true;
|
|
195
260
|
}
|
|
196
261
|
if (key === 'pagedown') {
|
|
197
|
-
this.
|
|
262
|
+
const suite = suites[this._selectedSuiteIdx];
|
|
263
|
+
const maxOffset = suite ? Math.max(0, this._detailLineCount(suite) - 1) : 0;
|
|
264
|
+
this._scrollOffset = Math.min(maxOffset, this._scrollOffset + 5);
|
|
198
265
|
this.markDirty();
|
|
199
266
|
return true;
|
|
200
267
|
}
|
|
201
268
|
return false;
|
|
202
269
|
}
|
|
203
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Cross-panel integration hook: r runs the selected suite, R runs all
|
|
273
|
+
* suites, both dispatched through the real /eval command executor rather
|
|
274
|
+
* than a printed signpost. registry.setRunning()/push() (already wired in
|
|
275
|
+
* src/input/commands/eval.ts) animate the in-panel state as the run proceeds.
|
|
276
|
+
*/
|
|
277
|
+
public handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
278
|
+
if (key !== 'r' && key !== 'R') return false;
|
|
279
|
+
if (!ctx.executeCommand) return false;
|
|
280
|
+
|
|
281
|
+
if (key === 'R') {
|
|
282
|
+
void ctx.executeCommand('eval', ['run', 'all']).catch(() => {});
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const suites = this._registry.getSuiteResults();
|
|
287
|
+
const suiteName = this._selectedSuiteName(suites);
|
|
288
|
+
if (!suiteName) return false;
|
|
289
|
+
void ctx.executeCommand('eval', ['run', suiteName]).catch(() => {});
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
204
293
|
public render(width: number, height: number): Line[] {
|
|
205
294
|
this.needsRender = false;
|
|
206
295
|
const suites = this._registry.getSuiteResults();
|
|
207
296
|
const gates = this._registry.getGateResults();
|
|
208
297
|
const intro = 'Evaluation harness runs, gates, scenario scorecards, and regression indicators for model and product validation.';
|
|
209
298
|
|
|
210
|
-
const running = this._registry.isRunning();
|
|
211
|
-
const lastRun = this._registry.getLastRunAt();
|
|
212
|
-
const summaryLine = buildPanelLine(width, [
|
|
213
|
-
[' state: ', C.label],
|
|
214
|
-
[running ? 'running' : 'idle', running ? C.yellow : C.dim],
|
|
215
|
-
[' last: ', C.label],
|
|
216
|
-
[lastRun ? new Date(lastRun).toLocaleTimeString() : 'n/a', C.dim],
|
|
217
|
-
]);
|
|
218
|
-
|
|
219
299
|
if (suites.length === 0) {
|
|
300
|
+
if (this._baseline && Object.keys(this._baseline.suites).length > 0) {
|
|
301
|
+
const workspace = this._renderBaselineList(this._baseline, width, height, intro);
|
|
302
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
303
|
+
return workspace;
|
|
304
|
+
}
|
|
305
|
+
|
|
220
306
|
const workspace = buildPanelWorkspace(width, height, {
|
|
221
307
|
title: 'Eval Harness',
|
|
222
308
|
intro,
|
|
223
309
|
sections: [{
|
|
224
310
|
title: 'Status',
|
|
225
311
|
lines: [
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
width,
|
|
229
|
-
' No results yet.',
|
|
230
|
-
'Run an eval suite to populate this workspace with suite scores, gate results, and per-scenario detail.',
|
|
231
|
-
[{ command: '/eval run <suite>', summary: 'start a suite such as core-performance, safety-baseline, or cost-tokens' }],
|
|
232
|
-
C,
|
|
233
|
-
),
|
|
312
|
+
this._statusLine(width),
|
|
313
|
+
buildPanelLine(width, [[' No results yet — no baseline on disk.', C.dim]]),
|
|
234
314
|
],
|
|
235
315
|
}],
|
|
316
|
+
footerLines: [buildKeyboardHints(width, [
|
|
317
|
+
{ keys: 'r', label: 'run selected suite' },
|
|
318
|
+
{ keys: 'R', label: 'run all suites' },
|
|
319
|
+
], C)],
|
|
236
320
|
palette: C,
|
|
237
321
|
});
|
|
238
322
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
@@ -241,17 +325,37 @@ export class EvalPanel extends BasePanel {
|
|
|
241
325
|
|
|
242
326
|
const lines: Line[] = [];
|
|
243
327
|
if (this._mode === 'list') {
|
|
244
|
-
this._renderList(lines, suites, gates, width, height, intro
|
|
328
|
+
this._renderList(lines, suites, gates, width, height, intro);
|
|
245
329
|
} else {
|
|
246
330
|
const suite = suites[this._selectedSuiteIdx];
|
|
247
331
|
if (suite) {
|
|
248
|
-
this._renderDetail(lines, suite, width, height, intro
|
|
332
|
+
this._renderDetail(lines, suite, gates, width, height, intro);
|
|
249
333
|
}
|
|
250
334
|
}
|
|
251
335
|
|
|
252
336
|
return lines;
|
|
253
337
|
}
|
|
254
338
|
|
|
339
|
+
// ── Shared summary line ───────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
private _statusLine(width: number): Line {
|
|
342
|
+
const running = this._registry.isRunning();
|
|
343
|
+
const lastRun = this._registry.getLastRunAt();
|
|
344
|
+
const suites = this._registry.getSuiteResults();
|
|
345
|
+
const passed = suites.filter((s) => s.passed).length;
|
|
346
|
+
const failed = suites.length - passed;
|
|
347
|
+
return buildPanelLine(width, [
|
|
348
|
+
[' state ', C.label],
|
|
349
|
+
[running ? 'running' : 'idle', running ? C.warn : C.dim],
|
|
350
|
+
[' passed ', C.label],
|
|
351
|
+
[String(passed), passed > 0 ? C.good : C.dim],
|
|
352
|
+
[' failed ', C.label],
|
|
353
|
+
[String(failed), failed > 0 ? C.bad : C.dim],
|
|
354
|
+
[' last ', C.label],
|
|
355
|
+
[lastRun ? new Date(lastRun).toLocaleTimeString() : 'n/a', C.dim],
|
|
356
|
+
]);
|
|
357
|
+
}
|
|
358
|
+
|
|
255
359
|
// ── List view ────────────────────────────────────────────────────────────────
|
|
256
360
|
|
|
257
361
|
private _renderList(
|
|
@@ -259,49 +363,85 @@ export class EvalPanel extends BasePanel {
|
|
|
259
363
|
suites: EvalSuiteResult[],
|
|
260
364
|
gates: EvalGateResult[],
|
|
261
365
|
width: number,
|
|
262
|
-
|
|
366
|
+
height: number,
|
|
263
367
|
intro: string,
|
|
264
|
-
summaryLine: Line,
|
|
265
368
|
): void {
|
|
266
369
|
const gateMap = new Map(gates.map((g) => [g.suite, g]));
|
|
267
|
-
const sectionLines: Line[] = [
|
|
268
|
-
summaryLine,
|
|
269
|
-
buildPanelLine(width, [
|
|
270
|
-
['Suite'.padEnd(28), C.header],
|
|
271
|
-
['Score'.padEnd(8), C.header],
|
|
272
|
-
['Pass'.padEnd(6), C.header],
|
|
273
|
-
['Gate'.padEnd(6), C.header],
|
|
274
|
-
['Duration', C.header],
|
|
275
|
-
]),
|
|
276
|
-
];
|
|
277
370
|
|
|
278
|
-
suites.
|
|
371
|
+
const tableRows = suites.map((suite, idx) => {
|
|
279
372
|
const selected = idx === this._selectedSuiteIdx;
|
|
280
373
|
const gate = gateMap.get(suite.suite);
|
|
281
374
|
const gateStr = gate ? (gate.passed ? 'ok' : 'FAIL') : '-';
|
|
282
|
-
const gateColor = gate ? (gate.passed ? C.
|
|
375
|
+
const gateColor = gate ? (gate.passed ? C.good : C.bad) : C.dim;
|
|
283
376
|
const durationMs = suite.finishedAt - suite.startedAt;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
[(suite.passed ? 'PASS' : 'FAIL').padEnd(6), passC, bg],
|
|
295
|
-
[gateStr.padEnd(6), gateColor, bg],
|
|
296
|
-
[fmtTime(durationMs), C.dim, bg],
|
|
297
|
-
]));
|
|
377
|
+
return {
|
|
378
|
+
selected,
|
|
379
|
+
cells: [
|
|
380
|
+
{ text: suite.suite, fg: selected ? C.selected : C.value, bold: selected },
|
|
381
|
+
{ text: suite.meanScore.toFixed(1), fg: scoreColor(suite.meanScore) },
|
|
382
|
+
{ text: suite.passed ? 'PASS' : 'FAIL', fg: suite.passed ? C.good : C.bad },
|
|
383
|
+
{ text: gateStr, fg: gateColor },
|
|
384
|
+
{ text: fmtTime(durationMs), fg: C.dim },
|
|
385
|
+
],
|
|
386
|
+
};
|
|
298
387
|
});
|
|
299
388
|
|
|
300
|
-
|
|
301
|
-
|
|
389
|
+
const tableLines = buildTable(width, [
|
|
390
|
+
{ label: 'Suite' },
|
|
391
|
+
{ label: 'Score', width: 7, align: 'right' },
|
|
392
|
+
{ label: 'Pass', width: 6 },
|
|
393
|
+
{ label: 'Gate', width: 6 },
|
|
394
|
+
{ label: 'Duration', width: 10, align: 'right' },
|
|
395
|
+
], tableRows, C, { selectedBg: C.selectBg });
|
|
396
|
+
|
|
397
|
+
const selectedSuite = suites[this._selectedSuiteIdx];
|
|
398
|
+
const detailLines: Line[] = [];
|
|
399
|
+
if (selectedSuite) {
|
|
400
|
+
const passCount = selectedSuite.results.filter((r) => r.scorecard.passed).length;
|
|
401
|
+
detailLines.push(buildPanelLine(width, [
|
|
402
|
+
[' selected ', C.label],
|
|
403
|
+
[selectedSuite.suite, C.info],
|
|
404
|
+
[' scenarios ', C.label],
|
|
405
|
+
[`${passCount}/${selectedSuite.results.length} passed`, passCount === selectedSuite.results.length ? C.good : C.warn],
|
|
406
|
+
]));
|
|
407
|
+
const meterW = Math.max(10, Math.min(30, width - 24));
|
|
408
|
+
detailLines.push(buildMeterLine(width, Math.round((selectedSuite.meanScore / 100) * meterW), meterW, {
|
|
409
|
+
filled: scoreColor(selectedSuite.meanScore),
|
|
410
|
+
empty: C.empty,
|
|
411
|
+
label: C.label,
|
|
412
|
+
}, { prefix: ' mean ', suffix: ` ${selectedSuite.meanScore.toFixed(1)}/100 ` }));
|
|
413
|
+
|
|
414
|
+
const gate = gateMap.get(selectedSuite.suite);
|
|
415
|
+
if (gate && gate.regressions.length > 0) {
|
|
416
|
+
const worst = [...gate.regressions].sort((a, b) => a.delta - b.delta)[0];
|
|
417
|
+
const segments: Array<[string, string]> = [
|
|
418
|
+
[' regressions ', C.label],
|
|
419
|
+
[`${gate.regressions.length}`, C.bad],
|
|
420
|
+
];
|
|
421
|
+
if (worst) {
|
|
422
|
+
segments.push([' worst ', C.label], [`${worst.scenarioName} ${worst.delta.toFixed(1)}`, C.bad]);
|
|
423
|
+
}
|
|
424
|
+
detailLines.push(buildPanelLine(width, segments));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const footer = buildKeyboardHints(width, [
|
|
429
|
+
{ keys: `${this._selectedSuiteIdx + 1}/${suites.length}`, label: 'suite' },
|
|
430
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
431
|
+
{ keys: 'Enter', label: 'open detail' },
|
|
432
|
+
{ keys: 'r', label: 'run selected' },
|
|
433
|
+
{ keys: 'R', label: 'run all' },
|
|
434
|
+
], C);
|
|
435
|
+
|
|
436
|
+
lines.push(...buildPanelWorkspace(width, height, {
|
|
302
437
|
title: 'Eval Harness',
|
|
303
438
|
intro,
|
|
304
|
-
sections: [
|
|
439
|
+
sections: [
|
|
440
|
+
{ lines: [this._statusLine(width)] },
|
|
441
|
+
{ title: 'Suites', lines: tableLines },
|
|
442
|
+
...(detailLines.length > 0 ? [{ title: 'Selected Suite', lines: detailLines }] : []),
|
|
443
|
+
],
|
|
444
|
+
footerLines: [footer],
|
|
305
445
|
palette: C,
|
|
306
446
|
}));
|
|
307
447
|
}
|
|
@@ -311,45 +451,67 @@ export class EvalPanel extends BasePanel {
|
|
|
311
451
|
private _renderDetail(
|
|
312
452
|
lines: Line[],
|
|
313
453
|
suite: EvalSuiteResult,
|
|
454
|
+
gates: EvalGateResult[],
|
|
314
455
|
width: number,
|
|
315
456
|
height: number,
|
|
316
457
|
intro: string,
|
|
317
|
-
summaryLine: Line,
|
|
318
458
|
): void {
|
|
319
|
-
const
|
|
320
|
-
|
|
459
|
+
const passCount = suite.results.filter((r) => r.scorecard.passed).length;
|
|
460
|
+
const gate = gates.find((g) => g.suite === suite.suite);
|
|
461
|
+
const regressionMap = new Map((gate?.regressions ?? []).map((r) => [r.scenarioId, r]));
|
|
462
|
+
|
|
463
|
+
const headerLines: Line[] = [
|
|
464
|
+
this._statusLine(width),
|
|
321
465
|
buildPanelLine(width, [
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
466
|
+
[' suite ', C.label],
|
|
467
|
+
[suite.suite, C.info],
|
|
468
|
+
[' mean ', C.label],
|
|
469
|
+
[suite.meanScore.toFixed(1), scoreColor(suite.meanScore)],
|
|
470
|
+
[' ', C.label],
|
|
471
|
+
[suite.passed ? 'PASS' : 'FAIL', suite.passed ? C.good : C.bad],
|
|
472
|
+
[' scenarios ', C.label],
|
|
473
|
+
[`${passCount}/${suite.results.length} passed`, passCount === suite.results.length ? C.good : C.warn],
|
|
327
474
|
]),
|
|
328
475
|
];
|
|
476
|
+
if (gate && gate.regressions.length > 0) {
|
|
477
|
+
headerLines.push(buildPanelLine(width, [
|
|
478
|
+
[' regressions ', C.label],
|
|
479
|
+
[`${gate.regressions.length} scenario(s) below baseline`, C.bad],
|
|
480
|
+
]));
|
|
481
|
+
}
|
|
329
482
|
|
|
330
483
|
const allDetailLines: Line[] = [];
|
|
331
484
|
suite.results.forEach((result, idx) => {
|
|
332
485
|
const selected = idx === this._selectedScenarioIdx;
|
|
333
|
-
this._renderScenarioBlock(allDetailLines, result, selected, width);
|
|
486
|
+
this._renderScenarioBlock(allDetailLines, result, selected, width, regressionMap.get(result.scenario.id));
|
|
334
487
|
});
|
|
335
488
|
|
|
489
|
+
const footer = buildKeyboardHints(width, [
|
|
490
|
+
{ keys: `${this._selectedScenarioIdx + 1}/${suite.results.length}`, label: 'scenario' },
|
|
491
|
+
{ keys: 'Esc', label: 'back' },
|
|
492
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
493
|
+
{ keys: 'PgUp/PgDn', label: 'scroll' },
|
|
494
|
+
], C);
|
|
495
|
+
|
|
336
496
|
const detailSection = resolveScrollablePanelSection(width, height, {
|
|
337
497
|
intro,
|
|
498
|
+
footerLines: [footer],
|
|
338
499
|
palette: C,
|
|
339
|
-
beforeSections: [{ title:
|
|
500
|
+
beforeSections: [{ title: `Scenario Detail · ${suite.suite}`, lines: headerLines }],
|
|
340
501
|
section: {
|
|
341
502
|
scrollableLines: allDetailLines,
|
|
342
503
|
scrollOffset: this._scrollOffset,
|
|
343
504
|
minRows: 1,
|
|
505
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
344
506
|
},
|
|
345
507
|
});
|
|
346
508
|
this._scrollOffset = detailSection.scrollOffset;
|
|
347
|
-
|
|
348
|
-
sectionLines.push(buildPanelLine(width, [[' Esc/q: back j/k: scenario PgUp/PgDn: scroll', C.dim]]));
|
|
509
|
+
|
|
349
510
|
lines.push(...buildPanelWorkspace(width, height, {
|
|
350
511
|
title: 'Eval Harness',
|
|
351
512
|
intro,
|
|
352
|
-
sections: [{ title:
|
|
513
|
+
sections: [{ title: `Scenario Detail · ${suite.suite}`, lines: [...headerLines, ...detailSection.section.lines] }],
|
|
514
|
+
footerLines: [footer],
|
|
353
515
|
palette: C,
|
|
354
516
|
}));
|
|
355
517
|
}
|
|
@@ -359,42 +521,107 @@ export class EvalPanel extends BasePanel {
|
|
|
359
521
|
result: EvalResult,
|
|
360
522
|
selected: boolean,
|
|
361
523
|
width: number,
|
|
524
|
+
regression: RegressionEntry | undefined,
|
|
362
525
|
): void {
|
|
363
526
|
const sc = result.scorecard;
|
|
364
|
-
const prefix = selected ? '▸ ' : ' ';
|
|
365
|
-
const nameColor = selected ? C.selected : C.white;
|
|
366
527
|
const scoreC = scoreColor(sc.compositeScore);
|
|
367
|
-
const passC = sc.passed ? C.
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
[
|
|
374
|
-
|
|
375
|
-
|
|
528
|
+
const passC = sc.passed ? C.good : C.bad;
|
|
529
|
+
const nameText = regression ? `▼ ${result.scenario.name}` : result.scenario.name;
|
|
530
|
+
const nameColor = selected ? C.selected : (regression ? C.bad : C.value);
|
|
531
|
+
|
|
532
|
+
lines.push(buildAlignedRow(
|
|
533
|
+
width,
|
|
534
|
+
[
|
|
535
|
+
{ text: nameText, fg: nameColor, bold: selected },
|
|
536
|
+
{ text: sc.compositeScore.toFixed(1), fg: scoreC },
|
|
537
|
+
{ text: sc.passed ? 'PASS' : 'FAIL', fg: passC },
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
{ width: Math.max(8, width - 16) },
|
|
541
|
+
{ width: 6, align: 'right' },
|
|
542
|
+
{ width: 5 },
|
|
543
|
+
],
|
|
544
|
+
{ gap: 1, selected, selectedBg: C.selectBg, marker: '▸' },
|
|
545
|
+
));
|
|
376
546
|
|
|
377
547
|
if (selected) {
|
|
378
548
|
for (const dim of DIMENSION_ORDER) {
|
|
379
549
|
const d = sc.dimensions.find((x) => x.dimension === dim);
|
|
380
550
|
if (!d) continue;
|
|
381
551
|
const filled = Math.round(d.score / 10);
|
|
382
|
-
|
|
552
|
+
lines.push(buildMeterLine(width, filled, 10, {
|
|
553
|
+
filled: scoreColor(d.score),
|
|
554
|
+
empty: C.empty,
|
|
555
|
+
label: C.label,
|
|
556
|
+
}, { prefix: ` ${dim.padEnd(9)} `, suffix: ` ${d.score.toFixed(0).padStart(3)}/100 ` }));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (regression) {
|
|
383
560
|
lines.push(buildPanelLine(width, [
|
|
384
|
-
['
|
|
385
|
-
[
|
|
386
|
-
[` ${d.score.toFixed(0).padStart(3)}/100`, C.value],
|
|
561
|
+
[' ▼ regression vs baseline ', C.bad],
|
|
562
|
+
[`${regression.baselineScore.toFixed(1)} -> ${regression.freshScore.toFixed(1)} (${regression.delta >= 0 ? '+' : ''}${regression.delta.toFixed(1)})`, C.bad],
|
|
387
563
|
]));
|
|
388
564
|
}
|
|
389
565
|
|
|
390
566
|
if (sc.notes && sc.notes.length > 0) {
|
|
391
567
|
for (const note of sc.notes) {
|
|
392
568
|
lines.push(buildPanelLine(width, [
|
|
393
|
-
[' ! ', C.
|
|
394
|
-
[note.
|
|
569
|
+
[' ! ', C.warn],
|
|
570
|
+
[truncateDisplay(note, Math.max(8, width - 6)), C.warn],
|
|
395
571
|
]));
|
|
396
572
|
}
|
|
397
573
|
}
|
|
398
574
|
}
|
|
399
575
|
}
|
|
576
|
+
|
|
577
|
+
// ── Baseline-seeded list (no live suite results yet) ─────────────────────────
|
|
578
|
+
|
|
579
|
+
private _renderBaselineList(
|
|
580
|
+
baseline: EvalBaseline,
|
|
581
|
+
width: number,
|
|
582
|
+
height: number,
|
|
583
|
+
intro: string,
|
|
584
|
+
): Line[] {
|
|
585
|
+
const entries = Object.entries(baseline.suites);
|
|
586
|
+
|
|
587
|
+
const tableRows = entries.map(([name, summary], idx) => {
|
|
588
|
+
const selected = idx === this._selectedSuiteIdx;
|
|
589
|
+
const scenarioCount = Object.keys(summary.scenarioScores).length;
|
|
590
|
+
return {
|
|
591
|
+
selected,
|
|
592
|
+
cells: [
|
|
593
|
+
{ text: name, fg: selected ? C.selected : C.value, bold: selected },
|
|
594
|
+
{ text: summary.meanScore.toFixed(1), fg: scoreColor(summary.meanScore) },
|
|
595
|
+
{ text: `${scenarioCount}`, fg: C.dim },
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const tableLines = buildTable(width, [
|
|
601
|
+
{ label: 'Suite' },
|
|
602
|
+
{ label: 'Score', width: 7, align: 'right' },
|
|
603
|
+
{ label: 'Scenarios', width: 10, align: 'right' },
|
|
604
|
+
], tableRows, C, { selectedBg: C.selectBg });
|
|
605
|
+
|
|
606
|
+
const footer = buildKeyboardHints(width, [
|
|
607
|
+
{ keys: `${entries.length > 0 ? this._selectedSuiteIdx + 1 : 0}/${entries.length}`, label: 'suite' },
|
|
608
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
609
|
+
{ keys: 'r', label: 'run selected' },
|
|
610
|
+
{ keys: 'R', label: 'run all' },
|
|
611
|
+
], C);
|
|
612
|
+
|
|
613
|
+
return buildPanelWorkspace(width, height, {
|
|
614
|
+
title: 'Eval Harness',
|
|
615
|
+
intro,
|
|
616
|
+
sections: [
|
|
617
|
+
{ lines: [this._statusLine(width)] },
|
|
618
|
+
{
|
|
619
|
+
title: `Baseline · ${baseline.label} (captured ${new Date(baseline.capturedAt).toLocaleString()})`,
|
|
620
|
+
lines: tableLines,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
footerLines: [footer],
|
|
624
|
+
palette: C,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
400
627
|
}
|