@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -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
- header: '#94a3b8',
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.green;
106
- if (score >= 60) return C.yellow;
107
- return C.red;
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', 'V', 'monitoring');
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
- 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);
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._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);
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
- ...buildEmptyState(
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.yellow : C.dim],
349
+ [running ? 'running' : 'idle', running ? C.warn : C.dim],
258
350
  [' passed ', C.label],
259
- [String(passed), passed > 0 ? C.green : C.dim],
351
+ [String(passed), passed > 0 ? C.good : C.dim],
260
352
  [' failed ', C.label],
261
- [String(failed), failed > 0 ? C.red : C.dim],
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.green : C.red) : C.dim;
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.white, bold: selected },
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.green : C.red },
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.cyan],
403
+ [selectedSuite.suite, C.info],
312
404
  [' scenarios ', C.label],
313
- [`${passCount}/${selectedSuite.results.length} passed`, passCount === selectedSuite.results.length ? C.green : C.yellow],
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.cyan],
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.green : C.red],
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.green : C.yellow],
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.green : C.red;
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: result.scenario.name, fg: selected ? C.selected : C.white, bold: selected },
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.yellow],
443
- [truncateDisplay(note, Math.max(8, width - 6)), C.yellow],
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
  }