@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- 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,14 +5,14 @@
|
|
|
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';
|
|
9
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
10
11
|
import type { Line } from '../types/grid.ts';
|
|
11
|
-
import type { KeyName } from './types.ts';
|
|
12
|
+
import type { KeyName, PanelIntegrationContext } from './types.ts';
|
|
12
13
|
import { createEmptyLine } from '../types/grid.ts';
|
|
13
14
|
import {
|
|
14
15
|
buildAlignedRow,
|
|
15
|
-
buildEmptyState,
|
|
16
16
|
buildKeyboardHints,
|
|
17
17
|
buildMeterLine,
|
|
18
18
|
buildPanelLine,
|
|
@@ -30,9 +30,14 @@ import type {
|
|
|
30
30
|
EvalResult,
|
|
31
31
|
EvalGateResult,
|
|
32
32
|
EvalDimension,
|
|
33
|
+
EvalBaseline,
|
|
34
|
+
RegressionEntry,
|
|
33
35
|
} from '@/runtime/index.ts';
|
|
36
|
+
import { loadBaseline } from '@/runtime/index.ts';
|
|
34
37
|
import { formatShortDuration } from '../utils/format-duration.ts';
|
|
35
38
|
|
|
39
|
+
const BASELINE_PATH = '.goodvibes/eval/baseline.json';
|
|
40
|
+
|
|
36
41
|
/**
|
|
37
42
|
* Holds the latest eval run state for display in EvalPanel.
|
|
38
43
|
* Created externally, injected into the panel.
|
|
@@ -87,24 +92,18 @@ export class EvalRegistry {
|
|
|
87
92
|
|
|
88
93
|
// ── Colour palette (no inline hex in render bodies) ──────────────────────────
|
|
89
94
|
|
|
95
|
+
// Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
|
|
96
|
+
// value/selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
90
97
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
91
|
-
|
|
92
|
-
headerBg: '#1e293b',
|
|
93
|
-
cyan: '#38bdf8',
|
|
94
|
-
green: '#22c55e',
|
|
95
|
-
yellow: '#eab308',
|
|
96
|
-
red: '#ef4444',
|
|
97
|
-
white: '#cbd5e1',
|
|
98
|
-
selected: '#f1f5f9',
|
|
99
|
-
selectBg: '#0f172a',
|
|
98
|
+
selected: '#f1f5f9', // emphasized row/text highlight, brighter than value
|
|
100
99
|
});
|
|
101
100
|
|
|
102
101
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
103
102
|
|
|
104
103
|
function scoreColor(score: number): string {
|
|
105
|
-
if (score >= 80) return C.
|
|
106
|
-
if (score >= 60) return C.
|
|
107
|
-
return C.
|
|
104
|
+
if (score >= 80) return C.good;
|
|
105
|
+
if (score >= 60) return C.warn;
|
|
106
|
+
return C.bad;
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
function fmtTime(ms: number): string {
|
|
@@ -117,19 +116,24 @@ const DIMENSION_ORDER: EvalDimension[] = ['safety', 'quality', 'latency', 'cost'
|
|
|
117
116
|
|
|
118
117
|
export class EvalPanel extends BasePanel {
|
|
119
118
|
private readonly _registry: EvalRegistry;
|
|
119
|
+
private readonly _projectRoot: string;
|
|
120
120
|
private _mode: 'list' | 'detail' = 'list';
|
|
121
121
|
private _selectedSuiteIdx = 0;
|
|
122
122
|
private _selectedScenarioIdx = 0;
|
|
123
123
|
private _scrollOffset = 0;
|
|
124
124
|
private _unsub: (() => void) | null = null;
|
|
125
|
+
private _baseline: EvalBaseline | null = null;
|
|
126
|
+
private _baselineLoading = false;
|
|
125
127
|
|
|
126
|
-
public constructor(registry: EvalRegistry) {
|
|
127
|
-
super('eval', 'Eval', '
|
|
128
|
+
public constructor(registry: EvalRegistry, projectRoot = '.') {
|
|
129
|
+
super('eval', 'Eval', '▮', 'incidents-diagnostics');
|
|
128
130
|
this._registry = registry;
|
|
131
|
+
this._projectRoot = projectRoot;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
public override onActivate(): void {
|
|
132
135
|
this._unsub = this._registry.subscribe(() => this.markDirty());
|
|
136
|
+
this._loadBaselineIfNeeded();
|
|
133
137
|
this.markDirty();
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -138,6 +142,65 @@ export class EvalPanel extends BasePanel {
|
|
|
138
142
|
this._unsub = null;
|
|
139
143
|
}
|
|
140
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
|
+
|
|
141
204
|
public handleInput(key: KeyName): boolean {
|
|
142
205
|
const suites = this._registry.getSuiteResults();
|
|
143
206
|
|
|
@@ -148,7 +211,8 @@ export class EvalPanel extends BasePanel {
|
|
|
148
211
|
return true;
|
|
149
212
|
}
|
|
150
213
|
if (key === 'down' || key === 'j') {
|
|
151
|
-
|
|
214
|
+
const len = this._listLength(suites);
|
|
215
|
+
this._selectedSuiteIdx = Math.min(Math.max(0, len - 1), this._selectedSuiteIdx + 1);
|
|
152
216
|
this.markDirty();
|
|
153
217
|
return true;
|
|
154
218
|
}
|
|
@@ -195,13 +259,37 @@ export class EvalPanel extends BasePanel {
|
|
|
195
259
|
return true;
|
|
196
260
|
}
|
|
197
261
|
if (key === 'pagedown') {
|
|
198
|
-
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);
|
|
199
265
|
this.markDirty();
|
|
200
266
|
return true;
|
|
201
267
|
}
|
|
202
268
|
return false;
|
|
203
269
|
}
|
|
204
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
|
+
|
|
205
293
|
public render(width: number, height: number): Line[] {
|
|
206
294
|
this.needsRender = false;
|
|
207
295
|
const suites = this._registry.getSuiteResults();
|
|
@@ -209,6 +297,12 @@ export class EvalPanel extends BasePanel {
|
|
|
209
297
|
const intro = 'Evaluation harness runs, gates, scenario scorecards, and regression indicators for model and product validation.';
|
|
210
298
|
|
|
211
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
|
+
|
|
212
306
|
const workspace = buildPanelWorkspace(width, height, {
|
|
213
307
|
title: 'Eval Harness',
|
|
214
308
|
intro,
|
|
@@ -216,15 +310,13 @@ export class EvalPanel extends BasePanel {
|
|
|
216
310
|
title: 'Status',
|
|
217
311
|
lines: [
|
|
218
312
|
this._statusLine(width),
|
|
219
|
-
|
|
220
|
-
width,
|
|
221
|
-
' No results yet.',
|
|
222
|
-
'Run an eval suite to populate this workspace with suite scores, gate results, and per-scenario detail.',
|
|
223
|
-
[{ command: '/eval run <suite>', summary: 'start a suite such as core-performance, safety-baseline, or cost-tokens' }],
|
|
224
|
-
C,
|
|
225
|
-
),
|
|
313
|
+
buildPanelLine(width, [[' No results yet — no baseline on disk.', C.dim]]),
|
|
226
314
|
],
|
|
227
315
|
}],
|
|
316
|
+
footerLines: [buildKeyboardHints(width, [
|
|
317
|
+
{ keys: 'r', label: 'run selected suite' },
|
|
318
|
+
{ keys: 'R', label: 'run all suites' },
|
|
319
|
+
], C)],
|
|
228
320
|
palette: C,
|
|
229
321
|
});
|
|
230
322
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
@@ -237,7 +329,7 @@ export class EvalPanel extends BasePanel {
|
|
|
237
329
|
} else {
|
|
238
330
|
const suite = suites[this._selectedSuiteIdx];
|
|
239
331
|
if (suite) {
|
|
240
|
-
this._renderDetail(lines, suite, width, height, intro);
|
|
332
|
+
this._renderDetail(lines, suite, gates, width, height, intro);
|
|
241
333
|
}
|
|
242
334
|
}
|
|
243
335
|
|
|
@@ -254,11 +346,11 @@ export class EvalPanel extends BasePanel {
|
|
|
254
346
|
const failed = suites.length - passed;
|
|
255
347
|
return buildPanelLine(width, [
|
|
256
348
|
[' state ', C.label],
|
|
257
|
-
[running ? 'running' : 'idle', running ? C.
|
|
349
|
+
[running ? 'running' : 'idle', running ? C.warn : C.dim],
|
|
258
350
|
[' passed ', C.label],
|
|
259
|
-
[String(passed), passed > 0 ? C.
|
|
351
|
+
[String(passed), passed > 0 ? C.good : C.dim],
|
|
260
352
|
[' failed ', C.label],
|
|
261
|
-
[String(failed), failed > 0 ? C.
|
|
353
|
+
[String(failed), failed > 0 ? C.bad : C.dim],
|
|
262
354
|
[' last ', C.label],
|
|
263
355
|
[lastRun ? new Date(lastRun).toLocaleTimeString() : 'n/a', C.dim],
|
|
264
356
|
]);
|
|
@@ -280,14 +372,14 @@ export class EvalPanel extends BasePanel {
|
|
|
280
372
|
const selected = idx === this._selectedSuiteIdx;
|
|
281
373
|
const gate = gateMap.get(suite.suite);
|
|
282
374
|
const gateStr = gate ? (gate.passed ? 'ok' : 'FAIL') : '-';
|
|
283
|
-
const gateColor = gate ? (gate.passed ? C.
|
|
375
|
+
const gateColor = gate ? (gate.passed ? C.good : C.bad) : C.dim;
|
|
284
376
|
const durationMs = suite.finishedAt - suite.startedAt;
|
|
285
377
|
return {
|
|
286
378
|
selected,
|
|
287
379
|
cells: [
|
|
288
|
-
{ text: suite.suite, fg: selected ? C.selected : C.
|
|
380
|
+
{ text: suite.suite, fg: selected ? C.selected : C.value, bold: selected },
|
|
289
381
|
{ text: suite.meanScore.toFixed(1), fg: scoreColor(suite.meanScore) },
|
|
290
|
-
{ text: suite.passed ? 'PASS' : 'FAIL', fg: suite.passed ? C.
|
|
382
|
+
{ text: suite.passed ? 'PASS' : 'FAIL', fg: suite.passed ? C.good : C.bad },
|
|
291
383
|
{ text: gateStr, fg: gateColor },
|
|
292
384
|
{ text: fmtTime(durationMs), fg: C.dim },
|
|
293
385
|
],
|
|
@@ -308,9 +400,9 @@ export class EvalPanel extends BasePanel {
|
|
|
308
400
|
const passCount = selectedSuite.results.filter((r) => r.scorecard.passed).length;
|
|
309
401
|
detailLines.push(buildPanelLine(width, [
|
|
310
402
|
[' selected ', C.label],
|
|
311
|
-
[selectedSuite.suite, C.
|
|
403
|
+
[selectedSuite.suite, C.info],
|
|
312
404
|
[' scenarios ', C.label],
|
|
313
|
-
[`${passCount}/${selectedSuite.results.length} passed`, passCount === selectedSuite.results.length ? C.
|
|
405
|
+
[`${passCount}/${selectedSuite.results.length} passed`, passCount === selectedSuite.results.length ? C.good : C.warn],
|
|
314
406
|
]));
|
|
315
407
|
const meterW = Math.max(10, Math.min(30, width - 24));
|
|
316
408
|
detailLines.push(buildMeterLine(width, Math.round((selectedSuite.meanScore / 100) * meterW), meterW, {
|
|
@@ -318,12 +410,27 @@ export class EvalPanel extends BasePanel {
|
|
|
318
410
|
empty: C.empty,
|
|
319
411
|
label: C.label,
|
|
320
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
|
+
}
|
|
321
426
|
}
|
|
322
427
|
|
|
323
428
|
const footer = buildKeyboardHints(width, [
|
|
324
429
|
{ keys: `${this._selectedSuiteIdx + 1}/${suites.length}`, label: 'suite' },
|
|
325
430
|
{ keys: '↑/↓', label: 'navigate' },
|
|
326
431
|
{ keys: 'Enter', label: 'open detail' },
|
|
432
|
+
{ keys: 'r', label: 'run selected' },
|
|
433
|
+
{ keys: 'R', label: 'run all' },
|
|
327
434
|
], C);
|
|
328
435
|
|
|
329
436
|
lines.push(...buildPanelWorkspace(width, height, {
|
|
@@ -344,29 +451,39 @@ export class EvalPanel extends BasePanel {
|
|
|
344
451
|
private _renderDetail(
|
|
345
452
|
lines: Line[],
|
|
346
453
|
suite: EvalSuiteResult,
|
|
454
|
+
gates: EvalGateResult[],
|
|
347
455
|
width: number,
|
|
348
456
|
height: number,
|
|
349
457
|
intro: string,
|
|
350
458
|
): void {
|
|
351
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
|
+
|
|
352
463
|
const headerLines: Line[] = [
|
|
353
464
|
this._statusLine(width),
|
|
354
465
|
buildPanelLine(width, [
|
|
355
466
|
[' suite ', C.label],
|
|
356
|
-
[suite.suite, C.
|
|
467
|
+
[suite.suite, C.info],
|
|
357
468
|
[' mean ', C.label],
|
|
358
469
|
[suite.meanScore.toFixed(1), scoreColor(suite.meanScore)],
|
|
359
470
|
[' ', C.label],
|
|
360
|
-
[suite.passed ? 'PASS' : 'FAIL', suite.passed ? C.
|
|
471
|
+
[suite.passed ? 'PASS' : 'FAIL', suite.passed ? C.good : C.bad],
|
|
361
472
|
[' scenarios ', C.label],
|
|
362
|
-
[`${passCount}/${suite.results.length} passed`, passCount === suite.results.length ? C.
|
|
473
|
+
[`${passCount}/${suite.results.length} passed`, passCount === suite.results.length ? C.good : C.warn],
|
|
363
474
|
]),
|
|
364
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
|
+
}
|
|
365
482
|
|
|
366
483
|
const allDetailLines: Line[] = [];
|
|
367
484
|
suite.results.forEach((result, idx) => {
|
|
368
485
|
const selected = idx === this._selectedScenarioIdx;
|
|
369
|
-
this._renderScenarioBlock(allDetailLines, result, selected, width);
|
|
486
|
+
this._renderScenarioBlock(allDetailLines, result, selected, width, regressionMap.get(result.scenario.id));
|
|
370
487
|
});
|
|
371
488
|
|
|
372
489
|
const footer = buildKeyboardHints(width, [
|
|
@@ -404,15 +521,18 @@ export class EvalPanel extends BasePanel {
|
|
|
404
521
|
result: EvalResult,
|
|
405
522
|
selected: boolean,
|
|
406
523
|
width: number,
|
|
524
|
+
regression: RegressionEntry | undefined,
|
|
407
525
|
): void {
|
|
408
526
|
const sc = result.scorecard;
|
|
409
527
|
const scoreC = scoreColor(sc.compositeScore);
|
|
410
|
-
const passC = sc.passed ? C.
|
|
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);
|
|
411
531
|
|
|
412
532
|
lines.push(buildAlignedRow(
|
|
413
533
|
width,
|
|
414
534
|
[
|
|
415
|
-
{ text:
|
|
535
|
+
{ text: nameText, fg: nameColor, bold: selected },
|
|
416
536
|
{ text: sc.compositeScore.toFixed(1), fg: scoreC },
|
|
417
537
|
{ text: sc.passed ? 'PASS' : 'FAIL', fg: passC },
|
|
418
538
|
],
|
|
@@ -436,14 +556,72 @@ export class EvalPanel extends BasePanel {
|
|
|
436
556
|
}, { prefix: ` ${dim.padEnd(9)} `, suffix: ` ${d.score.toFixed(0).padStart(3)}/100 ` }));
|
|
437
557
|
}
|
|
438
558
|
|
|
559
|
+
if (regression) {
|
|
560
|
+
lines.push(buildPanelLine(width, [
|
|
561
|
+
[' ▼ regression vs baseline ', C.bad],
|
|
562
|
+
[`${regression.baselineScore.toFixed(1)} -> ${regression.freshScore.toFixed(1)} (${regression.delta >= 0 ? '+' : ''}${regression.delta.toFixed(1)})`, C.bad],
|
|
563
|
+
]));
|
|
564
|
+
}
|
|
565
|
+
|
|
439
566
|
if (sc.notes && sc.notes.length > 0) {
|
|
440
567
|
for (const note of sc.notes) {
|
|
441
568
|
lines.push(buildPanelLine(width, [
|
|
442
|
-
[' ! ', C.
|
|
443
|
-
[truncateDisplay(note, Math.max(8, width - 6)), C.
|
|
569
|
+
[' ! ', C.warn],
|
|
570
|
+
[truncateDisplay(note, Math.max(8, width - 6)), C.warn],
|
|
444
571
|
]));
|
|
445
572
|
}
|
|
446
573
|
}
|
|
447
574
|
}
|
|
448
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
|
+
}
|
|
449
627
|
}
|