@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -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
- buildEmptyState,
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 fg colours for createStyledCell) ─────────────────────
82
-
83
- const C = {
84
- ...DEFAULT_PANEL_PALETTE,
85
- header: '#94a3b8',
86
- headerBg: '#1e293b',
87
- cyan: '#38bdf8',
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.green;
104
- if (score >= 60) return C.yellow;
105
- return C.red;
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
- if (ms < 1000) return `${ms.toFixed(0)}ms`;
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', 'V', 'monitoring');
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
- this._selectedSuiteIdx = Math.min(suites.length - 1, this._selectedSuiteIdx + 1);
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._scrollOffset += 5;
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
- summaryLine,
227
- ...buildEmptyState(
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, summaryLine);
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, summaryLine);
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
- _height: number,
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.forEach((suite, idx) => {
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.green : C.red) : C.dim;
375
+ const gateColor = gate ? (gate.passed ? C.good : C.bad) : C.dim;
283
376
  const durationMs = suite.finishedAt - suite.startedAt;
284
- const scoreC = scoreColor(suite.meanScore);
285
- const passC = suite.passed ? C.green : C.red;
286
- const nameColor = selected ? C.selected : C.white;
287
- const bg = selected ? C.selectBg : undefined;
288
- const prefix = selected ? '▸ ' : ' ';
289
- const name = suite.suite.slice(0, 24).padEnd(26);
290
-
291
- sectionLines.push(buildPanelLine(width, [
292
- [prefix + name, nameColor, bg],
293
- [suite.meanScore.toFixed(1).padEnd(8), scoreC, bg],
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
- sectionLines.push(buildPanelLine(width, [[' Enter/l: detail j/k: navigate', C.dim]]));
301
- lines.push(...buildPanelWorkspace(width, _height, {
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: [{ title: 'Suites', lines: sectionLines }],
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 sectionLines: Line[] = [
320
- summaryLine,
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
- [`Suite: ${suite.suite}`, C.cyan],
323
- [' mean=', C.label],
324
- [suite.meanScore.toFixed(1), scoreColor(suite.meanScore)],
325
- [' ', C.label],
326
- [suite.passed ? 'PASS' : 'FAIL', suite.passed ? C.green : C.red],
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: 'Scenario Detail', lines: sectionLines }],
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
- sectionLines.push(...detailSection.section.lines);
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: 'Scenario Detail', lines: sectionLines }],
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.green : C.red;
368
- const nameLen = Math.max(1, width - 22);
369
-
370
- lines.push(buildPanelLine(width, [
371
- [prefix + result.scenario.name.slice(0, nameLen).padEnd(nameLen + 2), nameColor, selected ? C.selectBg : undefined],
372
- [sc.compositeScore.toFixed(1).padStart(5), scoreC, selected ? C.selectBg : undefined],
373
- [' ', C.label, selected ? C.selectBg : undefined],
374
- [sc.passed ? 'PASS' : 'FAIL', passC, selected ? C.selectBg : undefined],
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
- const bar = '#'.repeat(filled) + '.'.repeat(10 - filled);
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
- [' ' + dim.padEnd(10) + ' ', C.label],
385
- [bar, scoreColor(d.score)],
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.yellow],
394
- [note.slice(0, width - 6), C.yellow],
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
  }