@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
@@ -4,7 +4,12 @@
4
4
 
5
5
  import type { Line } from '../types/grid.ts';
6
6
  import { createStyledCell, createEmptyLine } from '../types/grid.ts';
7
+ import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
7
8
  import { BasePanel } from './base-panel.ts';
9
+ import { UI_TONES } from '../renderer/ui-primitives.ts';
10
+ import { FilePreviewPanel } from './file-preview-panel.ts';
11
+ import type { PanelIntegrationContext } from './types.ts';
12
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
8
13
  import {
9
14
  buildBodyText,
10
15
  buildEmptyState,
@@ -13,27 +18,63 @@ import {
13
18
  buildStyledPanelLine,
14
19
  type PanelWorkspaceSection,
15
20
  DEFAULT_PANEL_PALETTE,
21
+ extendPalette,
16
22
  } from './polish.ts';
17
23
 
18
24
  // ---------------------------------------------------------------------------
19
- // Colour palette
25
+ // Colour palette — dedicated diff-viewer scheme (like a mini syntax
26
+ // highlighter). Each hex literal is named once and reused for every role
27
+ // that shares it, including the workspace-chrome aliases (info/dim/value/
28
+ // empty) so the raw-hex count never grows. The title band itself is NOT
29
+ // overridden — buildPanelWorkspace always falls back to the canonical
30
+ // DEFAULT_PANEL_PALETTE.headerBg (WO-002: one title band everywhere).
20
31
  // ---------------------------------------------------------------------------
21
32
 
22
- const COLOR = {
23
- addition: '#00ff88',
24
- deletion: '#ff4444',
25
- hunk: '#88aaff',
26
- header: '#aaaaaa',
27
- lineNum: '#555555',
28
- lineNumAdd: '#00aa55',
29
- lineNumDel: '#aa2222',
30
- filename: '#ffffff',
31
- tabActive: '#ffffff',
32
- tabInactive: '#666666',
33
- tabBg: '#222222',
34
- context: '#888888',
35
- statusBar: '#444444',
36
- } as const;
33
+ const HUNK_BLUE = '#88aaff';
34
+ // Context rows and line-number gutter use the shared theme's muted/dim
35
+ // foreground tones rather than dedicated gray hex literals.
36
+ const CONTEXT_GRAY = UI_TONES.fg.muted;
37
+ const FILENAME_WHITE = '#ffffff';
38
+ const ADD_GREEN = '#00ff88';
39
+ const ADD_BG = '#001a0d';
40
+ const DEL_RED = '#ff4444';
41
+ const DEL_BG = '#1a0000';
42
+ const HUNK_BG = '#0a0a1a';
43
+ const MARKER_GRAY = '#aaaaaa';
44
+ const LINE_NUM_GRAY = UI_TONES.fg.dim;
45
+ const LINE_NUM_ADD = '#00aa55';
46
+ const LINE_NUM_DEL = '#aa2222';
47
+ const TAB_ACTIVE_BG = '#333333';
48
+ const TAB_INACTIVE_GRAY = '#666666';
49
+ const TAB_BG = '#222222';
50
+ const STATUS_BAR_BG = '#444444';
51
+
52
+ const COLOR = extendPalette(DEFAULT_PANEL_PALETTE, {
53
+ // Workspace-chrome aliases (title band excluded — no headerBg override)
54
+ info: HUNK_BLUE,
55
+ dim: CONTEXT_GRAY,
56
+ value: FILENAME_WHITE,
57
+ empty: CONTEXT_GRAY,
58
+
59
+ // Domain accents
60
+ addition: ADD_GREEN,
61
+ additionBg: ADD_BG,
62
+ deletion: DEL_RED,
63
+ deletionBg: DEL_BG,
64
+ hunk: HUNK_BLUE,
65
+ hunkBg: HUNK_BG,
66
+ markerText: MARKER_GRAY,
67
+ lineNum: LINE_NUM_GRAY,
68
+ lineNumAdd: LINE_NUM_ADD,
69
+ lineNumDel: LINE_NUM_DEL,
70
+ filename: FILENAME_WHITE,
71
+ tabActive: FILENAME_WHITE,
72
+ tabActiveBg: TAB_ACTIVE_BG,
73
+ tabInactive: TAB_INACTIVE_GRAY,
74
+ tabBg: TAB_BG,
75
+ context: CONTEXT_GRAY,
76
+ statusBar: STATUS_BAR_BG,
77
+ } as const);
37
78
 
38
79
  // ---------------------------------------------------------------------------
39
80
  // Types
@@ -47,6 +88,17 @@ interface DiffEntry {
47
88
  semanticSummary?: string;
48
89
  }
49
90
 
91
+ /** +added / -removed line counts for a parsed diff entry. */
92
+ function diffStat(entry: DiffEntry): { added: number; removed: number } {
93
+ let added = 0;
94
+ let removed = 0;
95
+ for (const line of entry.lines) {
96
+ if (line.kind === 'addition') added++;
97
+ else if (line.kind === 'deletion') removed++;
98
+ }
99
+ return { added, removed };
100
+ }
101
+
50
102
  type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
51
103
 
52
104
  interface ParsedLine {
@@ -147,9 +199,7 @@ function makeLine(
147
199
  const LEFT_W = 5;
148
200
  const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
149
201
  const contentWidth = Math.max(0, width - usedForNums);
150
- const truncated = content.length > contentWidth
151
- ? content.slice(0, contentWidth)
152
- : content;
202
+ const truncated = truncateDisplay(content, contentWidth);
153
203
  return buildStyledPanelLine(width, [
154
204
  { text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
155
205
  { text: ' ', fg: '', bg },
@@ -161,8 +211,7 @@ function makeLine(
161
211
  }
162
212
 
163
213
  function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
164
- const truncated = text.length > width ? text.slice(0, width) : text;
165
- return buildStyledPanelLine(width, [{ text: truncated, fg, bg, bold }]);
214
+ return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
166
215
  }
167
216
 
168
217
  // ---------------------------------------------------------------------------
@@ -177,6 +226,9 @@ export class DiffPanel extends BasePanel {
177
226
  private selectedFile = 0;
178
227
  private scrollOffset = 0;
179
228
 
229
+ /** Set by the 'o' key; consumed by handlePanelIntegrationAction on the next dispatch. */
230
+ private pendingOpenPreview = false;
231
+
180
232
  constructor(workingDirectory: string) {
181
233
  super('diff', 'Diff', 'D', 'development');
182
234
  this.workingDirectory = workingDirectory;
@@ -220,9 +272,14 @@ export class DiffPanel extends BasePanel {
220
272
  const raw = await new Response(proc.stdout).text();
221
273
  await proc.exited;
222
274
  this.loadRawDiff(raw);
275
+ this.enrichSemanticDiffs(files, ref ?? 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
223
276
  }
224
277
 
225
- /** Run `git diff` and populate all changed files. */
278
+ /**
279
+ * Run `git diff` and populate all changed files. Self-load entry point for
280
+ * the 'w' (working tree, no ref) and 'h' (vs HEAD) keys. Automatically
281
+ * enriches every loaded file with a semantic-diff summary in the background.
282
+ */
226
283
  async showGitDiff(ref?: string): Promise<void> {
227
284
  const args = ['diff', ...(ref ? [ref] : [])];
228
285
  const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
@@ -253,6 +310,73 @@ export class DiffPanel extends BasePanel {
253
310
  this.selectedFile = Math.min(this.selectedFile, Math.max(0, this.entries.length - 1));
254
311
  this.scrollOffset = 0;
255
312
  this.markDirty();
313
+ this.enrichSemanticDiffs(newEntries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
314
+ }
315
+
316
+ /**
317
+ * Run `git diff --cached` and populate the staged changes. Self-load entry
318
+ * point for the 's' key — its own diff plumbing, independent of the
319
+ * `/diff staged` command handler.
320
+ */
321
+ async showStagedDiff(): Promise<void> {
322
+ const proc = Bun.spawn(['/bin/sh', '-c', 'git diff --cached'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
323
+ const [raw, errText] = await Promise.all([
324
+ new Response(proc.stdout).text(),
325
+ new Response(proc.stderr).text(),
326
+ ]);
327
+ const exitCode = await proc.exited;
328
+ if (exitCode !== 0) {
329
+ const errorText = errText.trim() || 'git diff --cached failed';
330
+ this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
331
+ return;
332
+ }
333
+ if (!raw.trim()) {
334
+ this.showDiff('(no staged changes)', '@@ -0,0 +0,0 @@\n No staged changes.');
335
+ return;
336
+ }
337
+ this.loadRawDiff(raw);
338
+ this.enrichSemanticDiffs(this.entries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
339
+ }
340
+
341
+ /**
342
+ * Best-effort semantic-diff enrichment for a set of files, run against the
343
+ * before-content at `ref` and the current on-disk after-content. Self-contained
344
+ * diff plumbing — does not depend on the `/diff` command handler.
345
+ */
346
+ private async enrichSemanticDiffs(files: string[], ref: string): Promise<void> {
347
+ if (files.length === 0) return;
348
+ const { computeSemanticDiff, formatSemanticDiffSummary } = await import('../renderer/semantic-diff.ts');
349
+ const { join, relative: pathRelative } = await import('path');
350
+ const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', cwd: this.workingDirectory });
351
+ await repoRootProc.exited;
352
+ const repoRoot = (await new Response(repoRootProc.stdout).text()).trim() || this.workingDirectory;
353
+ await Promise.allSettled(
354
+ files.map(async (filePath) => {
355
+ try {
356
+ const absPath = filePath.startsWith('/') ? filePath : join(this.workingDirectory, filePath);
357
+ const repoRelPath = filePath.startsWith('/') ? pathRelative(repoRoot, filePath) : filePath;
358
+ const [beforeResult, afterResult] = await Promise.allSettled([
359
+ (async () => {
360
+ const proc = Bun.spawn(
361
+ ['git', 'show', `${ref}:${repoRelPath}`],
362
+ { stdout: 'pipe', stderr: 'pipe', cwd: repoRoot },
363
+ );
364
+ const [text, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
365
+ if (exitCode !== 0) throw new Error(`git show failed for ${repoRelPath}`);
366
+ return text;
367
+ })(),
368
+ Bun.file(absPath).text(),
369
+ ]);
370
+ if (beforeResult.status !== 'fulfilled' || afterResult.status !== 'fulfilled') return;
371
+ const semanticDiff = await computeSemanticDiff(filePath, beforeResult.value, afterResult.value);
372
+ if (!semanticDiff) return;
373
+ const summary = formatSemanticDiffSummary(semanticDiff);
374
+ if (summary) this.setSemanticSummary(filePath, summary);
375
+ } catch {
376
+ // best-effort only
377
+ }
378
+ }),
379
+ );
256
380
  }
257
381
 
258
382
  /**
@@ -293,12 +417,62 @@ export class DiffPanel extends BasePanel {
293
417
  case 'up': this.scrollUp(); return true;
294
418
  case 'down': this.scrollDown(); return true;
295
419
  case 'tab': this.nextFile(); return true;
420
+ // Shift+Tab: most terminals send the bare CSI-Z ("backtab") escape
421
+ // sequence rather than a Tab keypress with a shift modifier, and the
422
+ // input tokenizer passes that sequence through unchanged as the
423
+ // logical key name — so it (and the friendlier aliases some terminals/
424
+ // future tokenizer versions may emit) are matched explicitly here.
425
+ case '\x1b[Z':
426
+ case 'shift-tab':
427
+ case 'backtab': this.prevFile(); return true;
296
428
  case 'pageup': this.scrollPageUp(); return true;
297
429
  case 'pagedown': this.scrollPageDown(); return true;
430
+ case 'w': void this.showGitDiff(); return true;
431
+ case 'h': void this.showGitDiff('HEAD'); return true;
432
+ case 's': void this.showStagedDiff(); return true;
433
+ case 'o': {
434
+ if (!this.currentEntry()) return false;
435
+ this.pendingOpenPreview = true;
436
+ return true;
437
+ }
298
438
  default: return false;
299
439
  }
300
440
  }
301
441
 
442
+ /**
443
+ * Cross-panel integration hook — 'o' opens the currently selected file in
444
+ * the preview panel via the same open/focus bridge FileExplorerPanel uses
445
+ * (src/input/panel-integration-actions.ts), without this panel needing to
446
+ * know about PanelManager pane/focus mechanics beyond what ctx exposes.
447
+ */
448
+ handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
449
+ if (!this.pendingOpenPreview) return false;
450
+ this.pendingOpenPreview = false;
451
+ const entry = this.currentEntry();
452
+ if (!entry) return false;
453
+
454
+ const pm = ctx.panelManager;
455
+ let previewPanel = pm.getPanel('preview');
456
+ if (previewPanel instanceof FilePreviewPanel) {
457
+ const pane = pm.getPaneOf('preview');
458
+ pm.activateById('preview');
459
+ if (pane) pm.focusPane(pane);
460
+ } else {
461
+ const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
462
+ ? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
463
+ : 'bottom';
464
+ const opened = pm.open('preview', targetPane);
465
+ pm.show();
466
+ pm.focusPane(targetPane);
467
+ previewPanel = opened instanceof FilePreviewPanel ? opened : null;
468
+ }
469
+ if (previewPanel instanceof FilePreviewPanel) {
470
+ previewPanel.openFile(entry.filePath);
471
+ return true;
472
+ }
473
+ return false;
474
+ }
475
+
302
476
  private scrollUp(): void {
303
477
  if (this.scrollOffset > 0) {
304
478
  this.scrollOffset--;
@@ -336,6 +510,13 @@ export class DiffPanel extends BasePanel {
336
510
  this.markDirty();
337
511
  }
338
512
 
513
+ private prevFile(): void {
514
+ if (this.entries.length === 0) return;
515
+ this.selectedFile = (this.selectedFile - 1 + this.entries.length) % this.entries.length;
516
+ this.scrollOffset = 0;
517
+ this.markDirty();
518
+ }
519
+
339
520
  private currentEntry(): DiffEntry | null {
340
521
  return this.entries[this.selectedFile] ?? null;
341
522
  }
@@ -351,26 +532,20 @@ export class DiffPanel extends BasePanel {
351
532
  if (this.entries.length === 0) {
352
533
  return buildPanelWorkspace(width, height, {
353
534
  title: 'Diff Workspace',
354
- palette: {
355
- ...DEFAULT_PANEL_PALETTE,
356
- info: COLOR.hunk,
357
- dim: COLOR.context,
358
- value: COLOR.filename,
359
- },
535
+ palette: COLOR,
360
536
  sections: [{
361
537
  title: 'Diff',
362
538
  lines: buildEmptyState(
363
539
  width,
364
540
  ' No diff to display.',
365
- 'Load a git diff or select a changed file to populate the workspace.',
366
- [{ command: '/git diff', summary: 'load the current working-tree diff into the diff workspace' }],
367
- {
368
- ...DEFAULT_PANEL_PALETTE,
369
- info: COLOR.hunk,
370
- dim: COLOR.context,
371
- value: COLOR.filename,
372
- empty: COLOR.context,
373
- },
541
+ 'Load a git diff or select a changed file to populate the workspace. w=working h=HEAD s=staged self-load right here.',
542
+ // WO-160: corrected bare /diff loads files changed *this
543
+ // session* (falling back to the full HEAD diff when none are
544
+ // tracked yet), not the working-tree diff the w key already
545
+ // loads in-panel; the old summary text described the wrong
546
+ // command.
547
+ [{ command: '/diff', summary: 'load files changed this session into the diff workspace (falls back to the HEAD diff if none yet)' }],
548
+ COLOR,
374
549
  ),
375
550
  }],
376
551
  });
@@ -383,20 +558,10 @@ export class DiffPanel extends BasePanel {
383
558
 
384
559
  const compact = height <= 12;
385
560
  const summaryLines = entry.semanticSummary
386
- ? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, {
387
- ...DEFAULT_PANEL_PALETTE,
388
- dim: COLOR.context,
389
- value: COLOR.filename,
390
- }, COLOR.context)
561
+ ? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, COLOR, COLOR.context)
391
562
  : [];
392
563
  const previewSection = resolveScrollablePanelSection(width, height, {
393
- palette: {
394
- ...DEFAULT_PANEL_PALETTE,
395
- info: COLOR.hunk,
396
- dim: COLOR.context,
397
- value: COLOR.filename,
398
- headerBg: COLOR.tabBg,
399
- },
564
+ palette: COLOR,
400
565
  footerLines: [this.renderStatusBar(width, entry)],
401
566
  beforeSections: [
402
567
  {
@@ -431,13 +596,7 @@ export class DiffPanel extends BasePanel {
431
596
  ];
432
597
  return buildPanelWorkspace(width, height, {
433
598
  title: 'Diff Workspace',
434
- palette: {
435
- ...DEFAULT_PANEL_PALETTE,
436
- info: COLOR.hunk,
437
- dim: COLOR.context,
438
- value: COLOR.filename,
439
- headerBg: COLOR.tabBg,
440
- },
599
+ palette: COLOR,
441
600
  sections,
442
601
  footerLines: [this.renderStatusBar(width, entry)],
443
602
  });
@@ -448,44 +607,60 @@ export class DiffPanel extends BasePanel {
448
607
 
449
608
  private renderTabBar(width: number): Line {
450
609
  const cells: Line = [];
610
+ const push = (ch: string, fg: string, bg: string, bold = false): void => {
611
+ const cw = getDisplayWidth(ch);
612
+ if (cells.length + cw > width) return;
613
+ cells.push(createStyledCell(ch, { fg, bg, bold }));
614
+ if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
615
+ };
451
616
 
452
617
  for (let i = 0; i < this.entries.length; i++) {
453
618
  const entry = this.entries[i]!;
454
619
  const active = i === this.selectedFile;
455
- const label = ` ${basename(entry.filePath)} `;
620
+ const stat = diffStat(entry);
456
621
  const fg = active ? COLOR.tabActive : COLOR.tabInactive;
457
- const bg = active ? '#333333' : COLOR.tabBg;
458
-
459
- for (const ch of label) {
460
- if (cells.length >= width) break;
461
- cells.push(createStyledCell(ch, { fg, bg, bold: active }));
462
- }
463
-
464
- if (cells.length < width) {
465
- cells.push(createStyledCell('', { fg: COLOR.lineNum, bg: COLOR.tabBg }));
466
- }
622
+ const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
623
+ // Active file gets a leading marker; every tab shows +adds/-dels at a glance.
624
+ const marker = active ? '▸ ' : ' ';
625
+ const label = `${marker}${basename(entry.filePath)} `;
626
+ for (const ch of label) push(ch, fg, bg, active);
627
+ if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
628
+ if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
629
+ if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
630
+ push(' ', fg, bg);
631
+ push('│', COLOR.lineNum, COLOR.tabBg);
467
632
  }
468
633
 
469
- // Fill remaining
470
634
  while (cells.length < width) {
471
635
  cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
472
636
  }
473
-
474
637
  return cells.slice(0, width);
475
638
  }
476
639
 
477
640
  // ── Status bar ───────────────────────────────────────────────────────────
478
641
 
479
642
  private renderStatusBar(width: number, entry: DiffEntry | null): Line {
480
- const fileInfo = entry
481
- ? `${entry.filePath} [${this.selectedFile + 1}/${this.entries.length}]`
482
- : 'No file';
483
- const scroll = entry
484
- ? ` L${this.scrollOffset + 1}/${entry.lines.length} Tab: next file Up/Down: scroll`
485
- : '';
486
- const semantic = entry?.semanticSummary ? ` * ${entry.semanticSummary}` : '';
487
- const text = ` ${fileInfo}${scroll}${semantic}`;
488
- return renderText(width, text, COLOR.tabActive, COLOR.statusBar);
643
+ if (!entry) {
644
+ return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
645
+ }
646
+ const stat = diffStat(entry);
647
+ // Keep the file path display-width-aware so a long/wide path can't overflow.
648
+ const pathBudget = Math.max(8, Math.floor(width / 2));
649
+ const fileInfo = truncateDisplay(entry.filePath, pathBudget);
650
+ const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
651
+ { text: ` ${fileInfo} `, fg: COLOR.filename, bg: COLOR.statusBar, bold: true },
652
+ { text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
653
+ { text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
654
+ { text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
655
+ { text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
656
+ { text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
657
+ { text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
658
+ { text: ' Tab/S-Tab file o open w/h/s load ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
659
+ ];
660
+ if (entry.semanticSummary) {
661
+ segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
662
+ }
663
+ return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
489
664
  }
490
665
 
491
666
  // ── Parsed line ──────────────────────────────────────────────────────────
@@ -496,13 +671,13 @@ export class DiffPanel extends BasePanel {
496
671
 
497
672
  switch (pl.kind) {
498
673
  case 'addition':
499
- return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, '#001a0d', COLOR.lineNumAdd, true);
674
+ return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
500
675
  case 'deletion':
501
- return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, '#1a0000', COLOR.lineNumDel, false);
676
+ return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
502
677
  case 'hunk':
503
- return renderText(width, pl.text, COLOR.hunk, '#0a0a1a', false);
678
+ return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
504
679
  case 'header':
505
- return renderText(width, pl.text, COLOR.header, '', false);
680
+ return renderText(width, pl.text, COLOR.markerText, '', false);
506
681
  case 'context':
507
682
  default:
508
683
  return makeLine(width, left, right, ` ${pl.text}`, COLOR.context, '', COLOR.lineNum, false);