@pellux/goodvibes-tui 1.0.0 → 1.7.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -5,11 +5,11 @@
5
5
  import type { Line } from '../types/grid.ts';
6
6
  import { createStyledCell, createEmptyLine } from '../types/grid.ts';
7
7
  import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
8
+ import { GitService } from '@pellux/goodvibes-sdk/platform/git';
8
9
  import { BasePanel } from './base-panel.ts';
9
10
  import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
10
- import { FilePreviewPanel } from './file-preview-panel.ts';
11
- import type { PanelIntegrationContext } from './types.ts';
12
11
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
12
+ import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
13
13
  import {
14
14
  buildBodyText,
15
15
  buildEmptyState,
@@ -166,20 +166,49 @@ function parseDiff(raw: string): ParsedLine[] {
166
166
  // Split a full `git diff` output into per-file entries
167
167
  // ---------------------------------------------------------------------------
168
168
 
169
+ /**
170
+ * Synthetic placeholder entries ('(error)', '(no changes)', '(no staged
171
+ * changes)', '(not a git repo)') are not real diffs — parenthesized so they
172
+ * can be styled distinctly in the tab/status bar instead of reading as a fake
173
+ * diff of a real file.
174
+ */
175
+ function isPlaceholderPath(filePath: string): boolean {
176
+ return filePath.startsWith('(') && filePath.endsWith(')');
177
+ }
178
+
179
+ /**
180
+ * Extract the file path from one `diff --git`-delimited chunk. Tries, in
181
+ * order: the standard two-sided header (`diff --git a/x b/y`, bare or
182
+ * quoted), the combined/merge-conflict header (`diff --cc <path>` /
183
+ * `diff --combined <path>`, which carries no a/b prefixes), then falls back
184
+ * to the `+++`/`---` file lines (present across every variant, uniformly) —
185
+ * skipping a `/dev/null` side (deleted/new file). 'unknown' is reserved for
186
+ * genuinely unparseable input.
187
+ */
188
+ function extractDiffFilePath(trimmed: string): string {
189
+ const quotedGit = trimmed.match(/^diff --git "a\/(.+)" "b\/(.+)"$/m);
190
+ if (quotedGit) return quotedGit[2]!;
191
+ const plainGit = trimmed.match(/^diff --git a\/.+? b\/(.+)$/m);
192
+ if (plainGit) return plainGit[1]!;
193
+ const combined = trimmed.match(/^diff --(?:cc|combined) "?([^"\n]+?)"?$/m);
194
+ if (combined) return combined[1]!;
195
+ const plus = trimmed.match(/^\+\+\+ (?:b\/)?"?([^"\n]+?)"?\s*$/m);
196
+ if (plus && plus[1] !== '/dev/null') return plus[1]!;
197
+ const minus = trimmed.match(/^--- (?:a\/)?"?([^"\n]+?)"?\s*$/m);
198
+ if (minus && minus[1] !== '/dev/null') return minus[1]!;
199
+ return 'unknown';
200
+ }
201
+
169
202
  function splitIntoDiffEntries(raw: string): DiffEntry[] {
170
203
  const entries: DiffEntry[] = [];
171
- // Split on "diff --git" lines
172
- const chunks = raw.split(/(?=^diff --git )/m);
204
+ // Split on "diff --git" / "diff --cc" / "diff --combined" lines
205
+ const chunks = raw.split(/(?=^diff --git |^diff --cc |^diff --combined )/m);
173
206
  for (const chunk of chunks) {
174
207
  const trimmed = chunk.trim();
175
208
  if (!trimmed) continue;
176
209
 
177
- // Extract file path from "diff --git a/foo b/foo"
178
- const match = trimmed.match(/^diff --git a\/.+? b\/(.+)$/m);
179
- const filePath = match ? match[1]! : 'unknown';
180
-
181
210
  entries.push({
182
- filePath,
211
+ filePath: extractDiffFilePath(trimmed),
183
212
  raw: chunk,
184
213
  lines: parseDiff(chunk),
185
214
  });
@@ -232,10 +261,16 @@ export class DiffPanel extends BasePanel {
232
261
  private selectedFile = 0;
233
262
  private scrollOffset = 0;
234
263
 
235
- /** Set by the 'o' key; consumed by handlePanelIntegrationAction on the next dispatch. */
236
- private pendingOpenPreview = false;
264
+ /** One-line confirmation for the 'w'/'h'/'s' self-load hotkeys otherwise a
265
+ * reload that produces identical content is visually indistinguishable
266
+ * from the keypress doing nothing at all. */
267
+ private hotkeyStatus: string | null = null;
237
268
 
238
- constructor(workingDirectory: string) {
269
+ public readonly confirmOverlay = new PanelConfirmOverlay(() => this.markDirty()); // armed by a command handler (e.g. /rewind); see panel-confirm-overlay.ts
270
+ constructor(
271
+ workingDirectory: string,
272
+ private readonly requestRender: () => void = () => {},
273
+ ) {
239
274
  super('diff', 'Diff', 'D', 'development');
240
275
  this.workingDirectory = workingDirectory;
241
276
  }
@@ -271,12 +306,33 @@ export class DiffPanel extends BasePanel {
271
306
  this.markDirty();
272
307
  }
273
308
 
309
+ /**
310
+ * Defensive gate mirroring GitPanel's own notGitRepo messaging — short-
311
+ * circuits before any of this panel's self-load git spawns run, instead of
312
+ * letting git fail per-subcommand with an inconsistent error shape.
313
+ */
314
+ private showNotGitRepo(): void {
315
+ this.showDiff('(not a git repo)', '@@ -0,0 +0,0 @@\n Not a git repository here.');
316
+ }
317
+
274
318
  /** Run `git diff` against specific files and populate entries. */
275
319
  async showFileDiffs(files: string[], ref?: string): Promise<void> {
320
+ if (!GitService.isGitRepo(this.workingDirectory)) {
321
+ this.showNotGitRepo();
322
+ return;
323
+ }
276
324
  const args = ['diff', ...(ref ? [ref] : []), '--', ...files];
277
- const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', cwd: this.workingDirectory });
278
- const raw = await new Response(proc.stdout).text();
279
- await proc.exited;
325
+ const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
326
+ const [raw, errText] = await Promise.all([
327
+ new Response(proc.stdout).text(),
328
+ new Response(proc.stderr).text(),
329
+ ]);
330
+ const exitCode = await proc.exited;
331
+ if (exitCode !== 0) {
332
+ const errorText = errText.trim() || 'git diff failed';
333
+ this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
334
+ return;
335
+ }
280
336
  this.loadRawDiff(raw);
281
337
  this.enrichSemanticDiffs(files, ref ?? 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
282
338
  }
@@ -287,6 +343,10 @@ export class DiffPanel extends BasePanel {
287
343
  * enriches every loaded file with a semantic-diff summary in the background.
288
344
  */
289
345
  async showGitDiff(ref?: string): Promise<void> {
346
+ if (!GitService.isGitRepo(this.workingDirectory)) {
347
+ this.showNotGitRepo();
348
+ return;
349
+ }
290
350
  const args = ['diff', ...(ref ? [ref] : [])];
291
351
  const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
292
352
  const [raw, errText] = await Promise.all([
@@ -325,6 +385,10 @@ export class DiffPanel extends BasePanel {
325
385
  * `/diff staged` command handler.
326
386
  */
327
387
  async showStagedDiff(): Promise<void> {
388
+ if (!GitService.isGitRepo(this.workingDirectory)) {
389
+ this.showNotGitRepo();
390
+ return;
391
+ }
328
392
  const proc = Bun.spawn(['/bin/sh', '-c', 'git diff --cached'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
329
393
  const [raw, errText] = await Promise.all([
330
394
  new Response(proc.stdout).text(),
@@ -353,7 +417,7 @@ export class DiffPanel extends BasePanel {
353
417
  if (files.length === 0) return;
354
418
  const { computeSemanticDiff, formatSemanticDiffSummary } = await import('../renderer/semantic-diff.ts');
355
419
  const { join, relative: pathRelative } = await import('path');
356
- const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', cwd: this.workingDirectory });
420
+ const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
357
421
  await repoRootProc.exited;
358
422
  const repoRoot = (await new Response(repoRootProc.stdout).text()).trim() || this.workingDirectory;
359
423
  await Promise.allSettled(
@@ -418,7 +482,24 @@ export class DiffPanel extends BasePanel {
418
482
  // Input
419
483
  // -------------------------------------------------------------------------
420
484
 
485
+ /**
486
+ * Wraps a self-load hotkey ('w'/'h'/'s') with visible before/after status
487
+ * text in the status bar — otherwise a reload whose content is unchanged
488
+ * from what's already shown is indistinguishable from the keypress having
489
+ * done nothing at all (contrast with `/diff working`'s ctx.print() calls).
490
+ */
491
+ private runHotkeyReload(label: string, load: () => Promise<void>): void {
492
+ this.hotkeyStatus = `Loading ${label} diff…`;
493
+ this.markDirty();
494
+ void load().then(() => {
495
+ this.hotkeyStatus = `Reloaded ${label} diff.`;
496
+ this.markDirty();
497
+ this.requestRender();
498
+ });
499
+ }
500
+
421
501
  handleInput(key: string): boolean {
502
+ if (this.confirmOverlay.handleInput(key)) return true; // confirm takes priority (git-panel.ts:284-303 precedent)
422
503
  switch (key) {
423
504
  case 'up': this.scrollUp(); return true;
424
505
  case 'down': this.scrollDown(); return true;
@@ -433,51 +514,20 @@ export class DiffPanel extends BasePanel {
433
514
  case 'backtab': this.prevFile(); return true;
434
515
  case 'pageup': this.scrollPageUp(); return true;
435
516
  case 'pagedown': this.scrollPageDown(); return true;
436
- case 'w': void this.showGitDiff(); return true;
437
- case 'h': void this.showGitDiff('HEAD'); return true;
438
- case 's': void this.showStagedDiff(); return true;
439
- case 'o': {
440
- if (!this.currentEntry()) return false;
441
- this.pendingOpenPreview = true;
442
- return true;
443
- }
517
+ case 'w': this.runHotkeyReload('working tree', () => this.showGitDiff()); return true;
518
+ case 'h': this.runHotkeyReload('HEAD', () => this.showGitDiff('HEAD')); return true;
519
+ case 's': this.runHotkeyReload('staged', () => this.showStagedDiff()); return true;
444
520
  default: return false;
445
521
  }
446
522
  }
447
523
 
448
- /**
449
- * Cross-panel integration hook 'o' opens the currently selected file in
450
- * the preview panel via the same open/focus bridge FileExplorerPanel uses
451
- * (src/input/panel-integration-actions.ts), without this panel needing to
452
- * know about PanelManager pane/focus mechanics beyond what ctx exposes.
453
- */
454
- handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
455
- if (!this.pendingOpenPreview) return false;
456
- this.pendingOpenPreview = false;
457
- const entry = this.currentEntry();
458
- if (!entry) return false;
459
-
460
- const pm = ctx.panelManager;
461
- let previewPanel = pm.getPanel('preview');
462
- if (previewPanel instanceof FilePreviewPanel) {
463
- const pane = pm.getPaneOf('preview');
464
- pm.activateById('preview');
465
- if (pane) pm.focusPane(pane);
466
- } else {
467
- const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
468
- ? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
469
- : 'bottom';
470
- const opened = pm.open('preview', targetPane);
471
- pm.show();
472
- pm.focusPane(targetPane);
473
- previewPanel = opened instanceof FilePreviewPanel ? opened : null;
474
- }
475
- if (previewPanel instanceof FilePreviewPanel) {
476
- previewPanel.openFile(entry.filePath);
477
- return true;
478
- }
479
- return false;
480
- }
524
+ // W6.1 (the purge): 'o' used to open the currently selected file in the
525
+ // preview panel via a staged pendingOpenPreview flag + a
526
+ // handlePanelIntegrationAction cross-panel hook (the same bridge
527
+ // FileExplorerPanel used). 'preview' is DELETE-disposition with no
528
+ // successor surface (no file-picker-overlay preview to repoint to
529
+ // either — verified), so the key and the hook were removed rather than
530
+ // repointed; diff no longer has an 'o' action.
481
531
 
482
532
  private scrollUp(): void {
483
533
  if (this.scrollOffset > 0) {
@@ -535,6 +585,8 @@ export class DiffPanel extends BasePanel {
535
585
  return this.trackedRender(() => {
536
586
  if (height <= 0 || width <= 0) return [];
537
587
 
588
+ const confirmLines = this.confirmOverlay.renderLines(width);
589
+ if (confirmLines) return buildPanelWorkspace(width, height, { title: 'Diff Workspace', palette: COLOR, sections: [{ title: 'Confirmation', lines: confirmLines }] });
538
590
  if (this.entries.length === 0) {
539
591
  return buildPanelWorkspace(width, height, {
540
592
  title: 'Diff Workspace',
@@ -623,11 +675,15 @@ export class DiffPanel extends BasePanel {
623
675
  for (let i = 0; i < this.entries.length; i++) {
624
676
  const entry = this.entries[i]!;
625
677
  const active = i === this.selectedFile;
678
+ const placeholder = isPlaceholderPath(entry.filePath);
626
679
  const stat = diffStat(entry);
627
- const fg = active ? COLOR.tabActive : COLOR.tabInactive;
680
+ // Placeholder entries ('(error)', '(no changes)', ...) get a distinct
681
+ // warn color regardless of active/inactive — they are not a real diff
682
+ // of a real file and must not read as one.
683
+ const fg = placeholder ? COLOR.warn : (active ? COLOR.tabActive : COLOR.tabInactive);
628
684
  const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
629
685
  // Active file gets a leading marker; every tab shows +adds/-dels at a glance.
630
- const marker = active ? '▸ ' : ' ';
686
+ const marker = active ? '▸ ' : (placeholder ? '⚠ ' : ' ');
631
687
  const label = `${marker}${basename(entry.filePath)} `;
632
688
  for (const ch of label) push(ch, fg, bg, active);
633
689
  if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
@@ -650,11 +706,15 @@ export class DiffPanel extends BasePanel {
650
706
  return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
651
707
  }
652
708
  const stat = diffStat(entry);
709
+ const placeholder = isPlaceholderPath(entry.filePath);
653
710
  // Keep the file path display-width-aware so a long/wide path can't overflow.
654
711
  const pathBudget = Math.max(8, Math.floor(width / 2));
655
712
  const fileInfo = truncateDisplay(entry.filePath, pathBudget);
656
713
  const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
657
- { text: ` ${fileInfo} `, fg: COLOR.filename, bg: COLOR.statusBar, bold: true },
714
+ // Placeholder states ('(error)', '(no changes)', ...) are not a real
715
+ // diff of a real file — a distinct warn color keeps them from being
716
+ // mistaken for one.
717
+ { text: ` ${fileInfo} `, fg: placeholder ? COLOR.warn : COLOR.filename, bg: COLOR.statusBar, bold: true },
658
718
  { text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
659
719
  { text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
660
720
  { text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
@@ -666,6 +726,9 @@ export class DiffPanel extends BasePanel {
666
726
  if (entry.semanticSummary) {
667
727
  segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
668
728
  }
729
+ if (this.hotkeyStatus) {
730
+ segments.push({ text: ` ${this.hotkeyStatus}`, fg: COLOR.info, bg: COLOR.statusBar, bold: true });
731
+ }
669
732
  return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
670
733
  }
671
734
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * EvalRegistry — holds the latest evaluation harness run state.
3
+ *
4
+ * W6.1 (the purge): extracted out of eval-panel.ts. 'eval' was
5
+ * DELETE-disposition (no surviving human surface — the evaluation harness
6
+ * is driven and reviewed via the `/eval` CLI command, input/commands/eval.ts)
7
+ * but this registry is still a live consumer of `/eval run` output, so it
8
+ * survives as a standalone read-model rather than being deleted with the
9
+ * view. See .goodvibes/audit/2026-07-04-wave6-briefs.json (W6.1).
10
+ */
11
+
12
+ import type { EvalSuiteResult, EvalGateResult } from '@/runtime/index.ts';
13
+
14
+ export class EvalRegistry {
15
+ private _suiteResults: EvalSuiteResult[] = [];
16
+ private _gateResults: EvalGateResult[] = [];
17
+ private _running = false;
18
+ private _lastRunAt: number | null = null;
19
+ private readonly _subscribers = new Set<() => void>();
20
+
21
+ push(result: EvalSuiteResult): void {
22
+ const idx = this._suiteResults.findIndex((r) => r.suite === result.suite);
23
+ if (idx >= 0) {
24
+ this._suiteResults[idx] = result;
25
+ } else {
26
+ this._suiteResults.push(result);
27
+ }
28
+ this._lastRunAt = Date.now();
29
+ this._notify();
30
+ }
31
+
32
+ pushGate(gate: EvalGateResult): void {
33
+ const idx = this._gateResults.findIndex((g) => g.suite === gate.suite);
34
+ if (idx >= 0) {
35
+ this._gateResults[idx] = gate;
36
+ } else {
37
+ this._gateResults.push(gate);
38
+ }
39
+ this._notify();
40
+ }
41
+
42
+ setRunning(running: boolean): void {
43
+ this._running = running;
44
+ this._notify();
45
+ }
46
+
47
+ isRunning(): boolean { return this._running; }
48
+ getLastRunAt(): number | null { return this._lastRunAt; }
49
+ getSuiteResults(): EvalSuiteResult[] { return this._suiteResults; }
50
+ getGateResults(): EvalGateResult[] { return this._gateResults; }
51
+
52
+ subscribe(cb: () => void): () => void {
53
+ this._subscribers.add(cb);
54
+ return () => this._subscribers.delete(cb);
55
+ }
56
+
57
+ private _notify(): void {
58
+ for (const cb of this._subscribers) cb();
59
+ }
60
+ }