@pellux/goodvibes-tui 0.28.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 (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  118. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,5 +1,5 @@
1
- import type { Stats } from 'node:fs';
2
- import { promises as fsPromises, readFileSync, statSync } from 'node:fs';
1
+ import type { Stats, FSWatcher } from 'node:fs';
2
+ import { promises as fsPromises, readFileSync, statSync, watch } from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import type { Line, Cell } from '../types/grid.ts';
5
5
  import { createStyledCell, createEmptyLine } from '../types/grid.ts';
@@ -20,12 +20,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
20
20
 
21
21
  const MAX_FILE_SIZE = 100 * 1024; // 100 KB
22
22
  const BG = '#0d0d0d';
23
- const HEADER_BG = '#1e1e1e';
24
- const HEADER_FG = '#d4d4d4';
25
- const HEADER_ACCENT = '#4ec9b0';
26
23
  const LINE_NUM_FG = '238';
27
- const WARNING_FG = '#f44747';
28
- const EMPTY_FG = '244';
29
24
 
30
25
  // ─── Language Detection (from file extension) ─────────────────────────────────
31
26
 
@@ -64,8 +59,31 @@ export class FilePreviewPanel extends BasePanel {
64
59
  /** Per-file scroll position memory: path -> scrollOffset */
65
60
  private readonly scrollMemory = new Map<string, number>();
66
61
 
62
+ /**
63
+ * 1-based line number to visibly highlight, set by goToLine() (e.g. a
64
+ * symbol-outline jump). Cleared when a new file is opened.
65
+ */
66
+ private highlightedLine: number | null = null;
67
+
68
+ /**
69
+ * Monotonic counter bumped every time fileLines is replaced. Combined with
70
+ * filePath/width/highlightedLine, forms the cache key for the built
71
+ * preview lines so render() doesn't rebuild the whole file every frame.
72
+ */
73
+ private contentVersion = 0;
74
+ private cachedPreviewLines: { key: string; lines: Line[] } | null = null;
75
+
76
+ /** Set by the 'r' key; consumed by handlePanelIntegrationAction on the next
77
+ * dispatch, which awaits the reload before re-syncing the symbol outline
78
+ * (panel-integration-actions.ts owns the panelManager reference this
79
+ * panel doesn't have). */
80
+ private pendingReload = false;
81
+
82
+ /** Optional live-refresh watcher on the currently open file. */
83
+ private watcher: FSWatcher | null = null;
84
+
67
85
  constructor() {
68
- super('preview', 'Preview', 'P', 'development');
86
+ super('preview', 'Preview', '', 'development');
69
87
  }
70
88
 
71
89
  // ─── Public API ─────────────────────────────────────────────────────────────
@@ -83,6 +101,7 @@ export class FilePreviewPanel extends BasePanel {
83
101
  this.filePath = filePath;
84
102
  this.oversized = false;
85
103
  this.fenceTag = extToFenceTag(filePath);
104
+ this.highlightedLine = null;
86
105
 
87
106
  // Restore scroll position for this file, or start at top
88
107
  this.scrollOffset = this.scrollMemory.get(filePath) ?? 0;
@@ -101,10 +120,53 @@ export class FilePreviewPanel extends BasePanel {
101
120
  } catch {
102
121
  this.fileLines = [`(cannot open: ${filePath})`];
103
122
  }
123
+ this.contentVersion++;
104
124
 
125
+ this._startWatching(filePath);
105
126
  void this._loadFileAsync(filePath);
106
127
  }
107
128
 
129
+ /**
130
+ * Re-read the current file from disk (the 'r' reload key). Returns the
131
+ * pending load's promise so callers can chain follow-up work (e.g.
132
+ * re-syncing the symbol outline) after it settles, or null if there is no
133
+ * open file or no reload was queued via the 'r' key.
134
+ */
135
+ consumePendingReload(): Promise<void> | null {
136
+ if (!this.pendingReload) return null;
137
+ this.pendingReload = false;
138
+ if (this.filePath === null) return null;
139
+ return this._loadFileAsync(this.filePath);
140
+ }
141
+
142
+ /**
143
+ * Start (or restart) an optional fs.watch auto-refresh on the given file.
144
+ * Best-effort: some filesystems/platforms don't support watch, so failures
145
+ * are swallowed and auto-refresh is simply unavailable for this file — the
146
+ * explicit 'r' reload key always still works.
147
+ */
148
+ private _startWatching(filePath: string): void {
149
+ this._stopWatching();
150
+ try {
151
+ const watcher = watch(filePath, { persistent: false }, (eventType) => {
152
+ if (eventType !== 'change' && eventType !== 'rename') return;
153
+ if (this.filePath !== filePath) return; // stale watcher from a since-replaced file
154
+ void this._loadFileAsync(filePath);
155
+ });
156
+ watcher.on('error', () => this._stopWatching());
157
+ this.watcher = watcher;
158
+ } catch {
159
+ this.watcher = null;
160
+ }
161
+ }
162
+
163
+ private _stopWatching(): void {
164
+ if (this.watcher) {
165
+ try { this.watcher.close(); } catch { /* already closed */ }
166
+ this.watcher = null;
167
+ }
168
+ }
169
+
108
170
  private async _loadFileAsync(filePath: string): Promise<void> {
109
171
  try {
110
172
  await this.withLoading('Loading…', async () => {
@@ -148,6 +210,7 @@ export class FilePreviewPanel extends BasePanel {
148
210
  } catch (err) {
149
211
  this.setError(summarizeError(err));
150
212
  }
213
+ this.contentVersion++;
151
214
  this.markDirty();
152
215
  }
153
216
 
@@ -162,7 +225,9 @@ export class FilePreviewPanel extends BasePanel {
162
225
 
163
226
  goToLine(line: number): void {
164
227
  if (!Number.isFinite(line)) return;
165
- this.scrollOffset = Math.max(0, Math.min(Math.floor(line) - 1, Math.max(0, this.fileLines.length - 1)));
228
+ const target = Math.max(0, Math.min(Math.floor(line) - 1, Math.max(0, this.fileLines.length - 1)));
229
+ this.scrollOffset = target;
230
+ this.highlightedLine = target + 1;
166
231
  this.markDirty();
167
232
  }
168
233
 
@@ -183,6 +248,11 @@ export class FilePreviewPanel extends BasePanel {
183
248
  }
184
249
  }
185
250
 
251
+ override onDestroy(): void {
252
+ this._stopWatching();
253
+ super.onDestroy();
254
+ }
255
+
186
256
  // ─── Input handling ──────────────────────────────────────────────────────────
187
257
 
188
258
  handleInput(key: string): boolean {
@@ -193,6 +263,12 @@ export class FilePreviewPanel extends BasePanel {
193
263
  case 'pagedown': return this.scrollPage(1);
194
264
  case 'home': return this.scrollTo(0);
195
265
  case 'end': return this.scrollTo(Infinity);
266
+ case 'r':
267
+ if (this.filePath === null) return false;
268
+ this.pendingReload = true;
269
+ return true;
270
+ case 'd':
271
+ return this.filePath !== null;
196
272
  default: return false;
197
273
  }
198
274
  }
@@ -280,24 +356,42 @@ export class FilePreviewPanel extends BasePanel {
280
356
  { keys: 'Home/End', label: 'top/bottom' },
281
357
  ], DEFAULT_PANEL_PALETTE),
282
358
  ];
283
- const fullCode = this.fileLines.join('\n');
284
- const hlLines = this.fenceTag
285
- ? this.syntaxHighlighter.highlight(fullCode, this.fenceTag)
286
- : null;
287
-
288
- const lineNumW = String(this.fileLines.length).length;
289
- const contentX = lineNumW + 2; // "NNN | "
290
- const previewLines: Line[] = [];
291
- for (let fileIdx = 0; fileIdx < this.fileLines.length; fileIdx++) {
292
-
293
- const rawLine = this.fileLines[fileIdx];
294
- const tokens: SyntaxToken[] =
295
- hlLines && fileIdx < hlLines.length && hlLines[fileIdx].length > 0
296
- ? (hlLines[fileIdx] as SyntaxToken[])
297
- : [{ text: rawLine, fg: '' }];
298
-
299
- previewLines.push(this.renderCodeLine(fileIdx, lineNumW, contentX, tokens, width));
359
+ // Line-build cache keyed on (filePath, contentVersion, width, highlightedLine):
360
+ // rebuilding every line on every frame showed up as an O(file-length)
361
+ // cost per render even when nothing changed (e.g. while idly scrolling
362
+ // within the same file). Only re-tokenize/re-render when the underlying
363
+ // content, panel width, or highlighted row actually changes.
364
+ const cacheKey = `${this.filePath ?? ''}${this.contentVersion}${width}${this.highlightedLine ?? ''}`;
365
+ let previewLines: Line[];
366
+ if (this.cachedPreviewLines && this.cachedPreviewLines.key === cacheKey) {
367
+ previewLines = this.cachedPreviewLines.lines;
368
+ } else {
369
+ const fullCode = this.fileLines.join('\n');
370
+ const hlLines = this.fenceTag
371
+ ? this.syntaxHighlighter.highlight(fullCode, this.fenceTag)
372
+ : null;
373
+
374
+ const lineNumW = String(this.fileLines.length).length;
375
+ const contentX = lineNumW + 2; // "NNN | "
376
+ const built: Line[] = [];
377
+ for (let fileIdx = 0; fileIdx < this.fileLines.length; fileIdx++) {
378
+
379
+ const rawLine = this.fileLines[fileIdx];
380
+ const tokens: SyntaxToken[] =
381
+ hlLines && fileIdx < hlLines.length && hlLines[fileIdx].length > 0
382
+ ? (hlLines[fileIdx] as SyntaxToken[])
383
+ : [{ text: rawLine, fg: '' }];
384
+
385
+ const highlighted = this.highlightedLine !== null && fileIdx === this.highlightedLine - 1;
386
+ built.push(this.renderCodeLine(fileIdx, lineNumW, contentX, tokens, width, highlighted));
387
+ }
388
+ previewLines = built;
389
+ this.cachedPreviewLines = { key: cacheKey, lines: previewLines };
300
390
  }
391
+ // Once a goToLine target is set, track it with a selectedIndex so the
392
+ // scrollable-section window keeps that row visible (with guard rows)
393
+ // the same way list-cursor panels do; plain free-scroll (no active
394
+ // highlight) keeps using the scrollOffset-only window it always has.
301
395
  const previewSection = resolveScrollablePanelSection(width, height, {
302
396
  intro,
303
397
  footerLines,
@@ -307,6 +401,7 @@ export class FilePreviewPanel extends BasePanel {
307
401
  title: 'Preview',
308
402
  scrollableLines: previewLines,
309
403
  scrollOffset: this.scrollOffset,
404
+ selectedIndex: this.highlightedLine !== null ? this.highlightedLine - 1 : undefined,
310
405
  minRows: 8,
311
406
  },
312
407
  });
@@ -347,9 +442,14 @@ export class FilePreviewPanel extends BasePanel {
347
442
  contentX: number,
348
443
  tokens: SyntaxToken[],
349
444
  width: number,
445
+ highlighted = false,
350
446
  ): Line {
447
+ // The goToLine target row (e.g. a symbol-outline jump) renders on the
448
+ // theme's selection background instead of the plain code background so
449
+ // it's visibly distinguishable from the rest of the file.
450
+ const bg = highlighted ? DEFAULT_PANEL_PALETTE.selectBg : BG;
351
451
  const line: Cell[] = new Array(width).fill(null).map(() =>
352
- createStyledCell(' ', { bg: BG }),
452
+ createStyledCell(' ', { bg }),
353
453
  );
354
454
 
355
455
  // Line number gutter
@@ -357,12 +457,12 @@ export class FilePreviewPanel extends BasePanel {
357
457
  let cx = 0;
358
458
  for (const ch of lineNum) {
359
459
  if (cx >= lineNumW) break;
360
- line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
460
+ line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg, dim: true });
361
461
  }
362
462
  // Separator " | "
363
- line[cx++] = createStyledCell(' ', { bg: BG });
364
- line[cx++] = createStyledCell('│', { fg: LINE_NUM_FG, bg: BG, dim: true });
365
- line[cx++] = createStyledCell(' ', { bg: BG });
463
+ line[cx++] = createStyledCell(' ', { bg });
464
+ line[cx++] = createStyledCell('│', { fg: LINE_NUM_FG, bg, dim: true });
465
+ line[cx++] = createStyledCell(' ', { bg });
366
466
 
367
467
  // Syntax tokens
368
468
  for (const token of tokens) {
@@ -373,7 +473,7 @@ export class FilePreviewPanel extends BasePanel {
373
473
  const cw = getDisplayWidth(ch);
374
474
  line[cx] = createStyledCell(ch, {
375
475
  fg: token.fg || '',
376
- bg: BG,
476
+ bg,
377
477
  bold: token.bold,
378
478
  italic: token.italic,
379
479
  });
@@ -385,31 +485,6 @@ export class FilePreviewPanel extends BasePanel {
385
485
  return line;
386
486
  }
387
487
 
388
- private renderEmpty(width: number, height: number, message: string): Line[] {
389
- const lines: Line[] = [];
390
- const msgLine = createEmptyLine(width);
391
- const isWarning = message.startsWith('File too large');
392
- const fg = isWarning ? WARNING_FG : EMPTY_FG;
393
- let cx = 2;
394
- for (const ch of message) {
395
- if (cx >= width - 1) break;
396
- msgLine[cx++] = createStyledCell(ch, { fg, bg: BG });
397
- }
398
- lines.push(msgLine);
399
- for (let i = 1; i < height; i++) {
400
- lines.push(this.renderBgLine(width));
401
- }
402
- return lines;
403
- }
404
-
405
- private renderBgLine(width: number): Line {
406
- const line = createEmptyLine(width);
407
- for (let x = 0; x < width; x++) {
408
- line[x] = createStyledCell(' ', { bg: BG });
409
- }
410
- return line;
411
- }
412
-
413
488
  private scroll(delta: number): boolean {
414
489
  const before = this.scrollOffset;
415
490
  this.scrollOffset = Math.max(0, this.scrollOffset + delta);