@pellux/goodvibes-tui 0.27.0 → 0.28.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 (101) hide show
  1. package/CHANGELOG.md +23 -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/eval.ts +4 -3
  9. package/src/input/handler-feed-routes.ts +61 -4
  10. package/src/input/handler-feed.ts +13 -0
  11. package/src/input/handler-picker-routes.ts +18 -13
  12. package/src/input/handler-ui-state.ts +4 -0
  13. package/src/input/keybindings.ts +5 -0
  14. package/src/input/model-picker.ts +5 -0
  15. package/src/input/panel-integration-actions.ts +10 -0
  16. package/src/main.ts +0 -1
  17. package/src/panels/agent-inspector-panel.ts +18 -13
  18. package/src/panels/agent-inspector-shared.ts +3 -3
  19. package/src/panels/agent-logs-panel.ts +22 -53
  20. package/src/panels/approval-panel.ts +146 -95
  21. package/src/panels/automation-control-panel.ts +24 -1
  22. package/src/panels/base-panel.ts +26 -2
  23. package/src/panels/builtin/development.ts +1 -1
  24. package/src/panels/cockpit-panel.ts +24 -5
  25. package/src/panels/cockpit-read-model.ts +2 -1
  26. package/src/panels/communication-panel.ts +108 -43
  27. package/src/panels/context-visualizer-panel.ts +25 -1
  28. package/src/panels/control-plane-panel.ts +27 -1
  29. package/src/panels/cost-tracker-panel.ts +82 -54
  30. package/src/panels/debug-panel.ts +51 -16
  31. package/src/panels/diff-panel.ts +59 -30
  32. package/src/panels/docs-panel.ts +19 -3
  33. package/src/panels/eval-panel.ts +130 -81
  34. package/src/panels/expandable-list-panel.ts +189 -0
  35. package/src/panels/file-explorer-panel.ts +40 -41
  36. package/src/panels/file-preview-panel.ts +9 -2
  37. package/src/panels/forensics-panel.ts +27 -13
  38. package/src/panels/git-panel.ts +58 -70
  39. package/src/panels/hooks-panel.ts +36 -2
  40. package/src/panels/incident-review-panel.ts +31 -10
  41. package/src/panels/intelligence-panel.ts +152 -71
  42. package/src/panels/knowledge-graph-panel.ts +89 -27
  43. package/src/panels/local-auth-panel.ts +17 -11
  44. package/src/panels/marketplace-panel.ts +31 -12
  45. package/src/panels/memory-panel.ts +5 -4
  46. package/src/panels/ops-control-panel.ts +69 -7
  47. package/src/panels/ops-strategy-panel.ts +61 -43
  48. package/src/panels/orchestration-panel.ts +34 -4
  49. package/src/panels/panel-list-panel.ts +33 -8
  50. package/src/panels/panel-manager.ts +23 -4
  51. package/src/panels/plan-dashboard-panel.ts +183 -66
  52. package/src/panels/plugins-panel.ts +48 -10
  53. package/src/panels/policy-panel.ts +60 -23
  54. package/src/panels/polish-core.ts +157 -0
  55. package/src/panels/polish-tables.ts +258 -0
  56. package/src/panels/polish.ts +44 -145
  57. package/src/panels/project-planning-panel.ts +22 -0
  58. package/src/panels/provider-accounts-panel.ts +55 -18
  59. package/src/panels/provider-health-panel.ts +65 -23
  60. package/src/panels/provider-stats-panel.ts +47 -22
  61. package/src/panels/qr-panel.ts +23 -11
  62. package/src/panels/remote-panel.ts +12 -5
  63. package/src/panels/routes-panel.ts +27 -5
  64. package/src/panels/sandbox-panel.ts +62 -27
  65. package/src/panels/schedule-panel.ts +40 -17
  66. package/src/panels/scrollable-list-panel.ts +124 -10
  67. package/src/panels/security-panel.ts +72 -20
  68. package/src/panels/services-panel.ts +68 -22
  69. package/src/panels/session-browser-panel.ts +29 -5
  70. package/src/panels/session-maintenance.ts +2 -2
  71. package/src/panels/settings-sync-panel.ts +30 -15
  72. package/src/panels/subscription-panel.ts +21 -3
  73. package/src/panels/symbol-outline-panel.ts +40 -47
  74. package/src/panels/system-messages-panel.ts +60 -27
  75. package/src/panels/tasks-panel.ts +34 -7
  76. package/src/panels/thinking-panel.ts +26 -25
  77. package/src/panels/token-budget-panel.ts +49 -38
  78. package/src/panels/tool-inspector-panel.ts +27 -21
  79. package/src/panels/types.ts +25 -0
  80. package/src/panels/watchers-panel.ts +25 -5
  81. package/src/panels/work-plan-panel.ts +125 -34
  82. package/src/panels/worktree-panel.ts +65 -20
  83. package/src/panels/wrfc-panel-format.ts +133 -0
  84. package/src/panels/wrfc-panel.ts +31 -127
  85. package/src/renderer/compaction-preview.ts +2 -1
  86. package/src/renderer/compositor.ts +46 -43
  87. package/src/renderer/model-picker-overlay.ts +9 -4
  88. package/src/renderer/model-workspace.ts +2 -3
  89. package/src/renderer/panel-composite.ts +7 -20
  90. package/src/renderer/panel-workspace-bar.ts +14 -8
  91. package/src/renderer/progress.ts +3 -2
  92. package/src/renderer/tab-strip.ts +148 -34
  93. package/src/renderer/ui-factory.ts +4 -6
  94. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  95. package/src/runtime/ui/model-picker/types.ts +4 -0
  96. package/src/utils/format-duration.ts +55 -0
  97. package/src/utils/format-number.ts +71 -0
  98. package/src/version.ts +1 -1
  99. package/src/panels/panel-picker.ts +0 -106
  100. package/src/renderer/panel-picker-overlay.ts +0 -202
  101. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,5 +1,6 @@
1
1
  import { BasePanel } from './base-panel.ts';
2
2
  import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
3
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import type { TurnEvent } from '@/runtime/index.ts';
4
5
  import type { UiEventFeed } from '../runtime/ui-events.ts';
5
6
  import type { Orchestrator } from '../core/orchestrator';
@@ -14,6 +15,8 @@ import {
14
15
  extendPalette,
15
16
  type PanelWorkspaceSection,
16
17
  } from './polish.ts';
18
+ import { abbreviateCount } from '../utils/format-number.ts';
19
+ import { formatLatencyMs } from '../utils/format-duration.ts';
17
20
 
18
21
  // ---------------------------------------------------------------------------
19
22
  // Types
@@ -68,16 +71,11 @@ const LATENCY_BAD_MS = 5_000;
68
71
  // ---------------------------------------------------------------------------
69
72
 
70
73
  function fmtTok(n: number): string {
71
- if (n < 10_000) return String(n);
72
- if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
73
- return (n / 1_000_000).toFixed(2) + 'M';
74
+ return abbreviateCount(n, { guard: 10_000, decimals: 1, mDecimals: 2 });
74
75
  }
75
76
 
76
77
  function fmtMs(ms: number): string {
77
- if (ms <= 0) return 'n/a';
78
- if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
79
- if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
80
- return `${Math.round(ms)}ms`;
78
+ return formatLatencyMs(ms);
81
79
  }
82
80
 
83
81
  function fmtAgo(ts: number): string {
@@ -315,10 +313,21 @@ export class DebugPanel extends BasePanel {
315
313
  }
316
314
  }
317
315
 
316
+ const latestError = this._errors[this._errors.length - 1];
317
+ const footerLines = latestError
318
+ ? [buildStyledPanelLine(width, [
319
+ { text: ' ✕ latest error ', fg: C.bad },
320
+ { text: truncateDisplay(latestError.errorMessage ?? 'unknown error', Math.max(0, width - 18)), fg: C.dim },
321
+ ])]
322
+ : [buildStyledPanelLine(width, [
323
+ { text: ' Live feed — newest calls at the bottom; auto-follows.', fg: C.dim },
324
+ ])];
325
+
318
326
  return buildPanelWorkspace(width, height, {
319
327
  title: ' API Debug',
320
328
  intro: 'Recent provider calls, token deltas, latency, status codes, and error history.',
321
329
  sections,
330
+ footerLines,
322
331
  palette: DEFAULT_PANEL_PALETTE,
323
332
  });
324
333
  }
@@ -330,16 +339,42 @@ export class DebugPanel extends BasePanel {
330
339
  private _renderSummary(width: number): Line[] {
331
340
  const errCount = this._totalErrors;
332
341
  const okCount = this._totalCalls - this._totalErrors;
333
- return [
342
+ const last = this._calls[this._calls.length - 1];
343
+ const recent = this._calls.slice(-10);
344
+ const avgLat = recent.length > 0
345
+ ? Math.round(recent.reduce((s, c) => s + c.latencyMs, 0) / recent.length)
346
+ : 0;
347
+ const sessionTokens = this._calls.reduce((s, c) => s + c.inputTokens + c.outputTokens, 0);
348
+
349
+ const lines: Line[] = [
334
350
  buildStyledPanelLine(width, [
335
- { text: ' Calls: ', fg: C.label },
351
+ { text: ' Calls ', fg: C.label },
336
352
  { text: String(this._totalCalls), fg: C.value },
337
- { text: ' OK: ', fg: C.label },
353
+ { text: ' OK ', fg: C.label },
338
354
  { text: String(okCount), fg: C.good },
339
- { text: ' Errors: ', fg: C.label },
355
+ { text: ' Errors ', fg: C.label },
340
356
  { text: String(errCount), fg: errCount > 0 ? C.bad : C.dim },
357
+ { text: ' Avg latency ', fg: C.label },
358
+ { text: fmtMs(avgLat), fg: avgLat > 0 ? latColor(avgLat) : C.dim },
359
+ { text: ' Tokens ', fg: C.label },
360
+ { text: fmtTok(sessionTokens), fg: C.value },
341
361
  ]),
342
362
  ];
363
+ // Live status: most recent call (latency / age) or wiring hint.
364
+ if (last) {
365
+ lines.push(buildStyledPanelLine(width, [
366
+ { text: ' Last ', fg: C.label },
367
+ { text: last.status === 'ok' ? '✓ ' : '✕ ', fg: last.status === 'ok' ? C.good : C.bad },
368
+ { text: fitDisplay(last.model, 22), fg: C.value },
369
+ { text: ' ' + fmtMs(last.latencyMs), fg: latColor(last.latencyMs) },
370
+ { text: ' ' + fmtAgo(last.ts), fg: C.dim },
371
+ ]));
372
+ } else if (!this._orchestrator) {
373
+ lines.push(buildStyledPanelLine(width, [
374
+ { text: ' Per-call token deltas need the orchestrator wired (wireOrchestrator).', fg: C.dim },
375
+ ]));
376
+ }
377
+ return lines;
343
378
  }
344
379
 
345
380
  private _renderCallLog(width: number): Line[] {
@@ -355,15 +390,15 @@ export class DebugPanel extends BasePanel {
355
390
  private _callLogHeader(width: number): Line {
356
391
  // Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
357
392
  const header = ' Time S Provider Model In Out Lat';
358
- return this._textLine(header.slice(0, width), C.colHdr, width, { dim: true });
393
+ return this._textLine(truncateDisplay(header, width), C.colHdr, width, { dim: true });
359
394
  }
360
395
 
361
396
  private _callLogRow(e: ApiCallEntry, width: number): Line {
362
397
  const timeStr = fmtAgo(e.ts).padEnd(8);
363
398
  const statusChar = e.status === 'ok' ? '✓' : '✕';
364
399
  const statusFg = e.status === 'ok' ? C.good : C.bad;
365
- const provStr = e.provider.slice(0, 11).padEnd(12);
366
- const modelStr = e.model.slice(0, 19).padEnd(20);
400
+ const provStr = fitDisplay(e.provider, 12);
401
+ const modelStr = fitDisplay(e.model, 20);
367
402
  const inStr = fmtTok(e.inputTokens).padStart(8);
368
403
  const outStr = fmtTok(e.outputTokens).padStart(8);
369
404
  const latStr = fmtMs(e.latencyMs).padStart(8);
@@ -401,9 +436,9 @@ export class DebugPanel extends BasePanel {
401
436
  private _errorRow(e: ApiCallEntry, width: number): Line {
402
437
  const timeStr = fmtAgo(e.ts).padEnd(8);
403
438
  const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
404
- const msgStr = (e.errorMessage ?? 'unknown error').slice(0, width - 12 - codeStr.length);
439
+ const msgStr = truncateDisplay(e.errorMessage ?? 'unknown error', Math.max(0, width - 12 - codeStr.length));
405
440
  const full = ` ${timeStr} ${codeStr}${msgStr}`;
406
- return this._textLine(full.slice(0, width), C.bad, width);
441
+ return this._textLine(truncateDisplay(full, width), C.bad, width);
407
442
  }
408
443
 
409
444
  // -------------------------------------------------------------------------
@@ -4,6 +4,7 @@
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';
8
9
  import {
9
10
  buildBodyText,
@@ -21,14 +22,18 @@ import {
21
22
 
22
23
  const COLOR = {
23
24
  addition: '#00ff88',
25
+ additionBg: '#001a0d',
24
26
  deletion: '#ff4444',
27
+ deletionBg: '#1a0000',
25
28
  hunk: '#88aaff',
29
+ hunkBg: '#0a0a1a',
26
30
  header: '#aaaaaa',
27
31
  lineNum: '#555555',
28
32
  lineNumAdd: '#00aa55',
29
33
  lineNumDel: '#aa2222',
30
34
  filename: '#ffffff',
31
35
  tabActive: '#ffffff',
36
+ tabActiveBg: '#333333',
32
37
  tabInactive: '#666666',
33
38
  tabBg: '#222222',
34
39
  context: '#888888',
@@ -47,6 +52,17 @@ interface DiffEntry {
47
52
  semanticSummary?: string;
48
53
  }
49
54
 
55
+ /** +added / -removed line counts for a parsed diff entry. */
56
+ function diffStat(entry: DiffEntry): { added: number; removed: number } {
57
+ let added = 0;
58
+ let removed = 0;
59
+ for (const line of entry.lines) {
60
+ if (line.kind === 'addition') added++;
61
+ else if (line.kind === 'deletion') removed++;
62
+ }
63
+ return { added, removed };
64
+ }
65
+
50
66
  type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
51
67
 
52
68
  interface ParsedLine {
@@ -147,9 +163,7 @@ function makeLine(
147
163
  const LEFT_W = 5;
148
164
  const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
149
165
  const contentWidth = Math.max(0, width - usedForNums);
150
- const truncated = content.length > contentWidth
151
- ? content.slice(0, contentWidth)
152
- : content;
166
+ const truncated = truncateDisplay(content, contentWidth);
153
167
  return buildStyledPanelLine(width, [
154
168
  { text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
155
169
  { text: ' ', fg: '', bg },
@@ -161,8 +175,7 @@ function makeLine(
161
175
  }
162
176
 
163
177
  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 }]);
178
+ return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
166
179
  }
167
180
 
168
181
  // ---------------------------------------------------------------------------
@@ -448,44 +461,60 @@ export class DiffPanel extends BasePanel {
448
461
 
449
462
  private renderTabBar(width: number): Line {
450
463
  const cells: Line = [];
464
+ const push = (ch: string, fg: string, bg: string, bold = false): void => {
465
+ const cw = getDisplayWidth(ch);
466
+ if (cells.length + cw > width) return;
467
+ cells.push(createStyledCell(ch, { fg, bg, bold }));
468
+ if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
469
+ };
451
470
 
452
471
  for (let i = 0; i < this.entries.length; i++) {
453
472
  const entry = this.entries[i]!;
454
473
  const active = i === this.selectedFile;
455
- const label = ` ${basename(entry.filePath)} `;
474
+ const stat = diffStat(entry);
456
475
  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
- }
476
+ const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
477
+ // Active file gets a leading marker; every tab shows +adds/-dels at a glance.
478
+ const marker = active ? '▸ ' : ' ';
479
+ const label = `${marker}${basename(entry.filePath)} `;
480
+ for (const ch of label) push(ch, fg, bg, active);
481
+ if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
482
+ if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
483
+ if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
484
+ push(' ', fg, bg);
485
+ push('│', COLOR.lineNum, COLOR.tabBg);
467
486
  }
468
487
 
469
- // Fill remaining
470
488
  while (cells.length < width) {
471
489
  cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
472
490
  }
473
-
474
491
  return cells.slice(0, width);
475
492
  }
476
493
 
477
494
  // ── Status bar ───────────────────────────────────────────────────────────
478
495
 
479
496
  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);
497
+ if (!entry) {
498
+ return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
499
+ }
500
+ const stat = diffStat(entry);
501
+ // Keep the file path display-width-aware so a long/wide path can't overflow.
502
+ const pathBudget = Math.max(8, Math.floor(width / 2));
503
+ const fileInfo = truncateDisplay(entry.filePath, pathBudget);
504
+ const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
505
+ { text: ` ${fileInfo} `, fg: COLOR.filename, bg: COLOR.statusBar, bold: true },
506
+ { text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
507
+ { text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
508
+ { text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
509
+ { text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
510
+ { text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
511
+ { text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
512
+ { text: ' Tab next ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
513
+ ];
514
+ if (entry.semanticSummary) {
515
+ segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
516
+ }
517
+ return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
489
518
  }
490
519
 
491
520
  // ── Parsed line ──────────────────────────────────────────────────────────
@@ -496,11 +525,11 @@ export class DiffPanel extends BasePanel {
496
525
 
497
526
  switch (pl.kind) {
498
527
  case 'addition':
499
- return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, '#001a0d', COLOR.lineNumAdd, true);
528
+ return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
500
529
  case 'deletion':
501
- return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, '#1a0000', COLOR.lineNumDel, false);
530
+ return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
502
531
  case 'hunk':
503
- return renderText(width, pl.text, COLOR.hunk, '#0a0a1a', false);
532
+ return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
504
533
  case 'header':
505
534
  return renderText(width, pl.text, COLOR.header, '', false);
506
535
  case 'context':
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { Line } from '../types/grid.ts';
6
6
  import { BasePanel } from './base-panel.ts';
7
- import { buildPanelLine, buildPanelWorkspace, buildSearchInputLine, resolveScrollablePanelSection, extendPalette, DEFAULT_PANEL_PALETTE } from './polish.ts';
7
+ import { buildKeyboardHints, buildPanelLine, buildPanelWorkspace, buildSearchInputLine, resolveScrollablePanelSection, extendPalette, DEFAULT_PANEL_PALETTE } from './polish.ts';
8
8
  import type { ProviderModelCatalogQuery, ToolCatalogQuery } from '../runtime/ui-service-queries.ts';
9
9
  import {
10
10
  getPanelSearchFocusTransition,
@@ -152,9 +152,24 @@ export class DocsPanel extends BasePanel {
152
152
  buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching }),
153
153
  ],
154
154
  } as const;
155
+ // Context-aware footer: while searching, surface only the keys that work in
156
+ // the search field; otherwise surface section + navigation keys.
157
+ const footerLines = [this.searching
158
+ ? buildKeyboardHints(width, [
159
+ { keys: 'type', label: 'filter' },
160
+ { keys: 'Enter/Esc', label: 'apply / exit search' },
161
+ { keys: '↓', label: 'back to list' },
162
+ ], DEFAULT_PANEL_PALETTE)
163
+ : buildKeyboardHints(width, [
164
+ { keys: 't/m/k', label: 'tools / models / shortcuts' },
165
+ { keys: '↑/↓', label: 'navigate' },
166
+ { keys: '/', label: 'search' },
167
+ ], DEFAULT_PANEL_PALETTE)];
168
+
155
169
  const sectionWindow = resolveScrollablePanelSection(width, height, {
156
170
  intro: 'Browse built-in tool docs, available models, and keyboard shortcuts from one shared reference surface.',
157
171
  palette: DEFAULT_PANEL_PALETTE,
172
+ footerLines,
158
173
  beforeSections: [controlsSection],
159
174
  section: {
160
175
  title: sectionLabel,
@@ -176,6 +191,7 @@ export class DocsPanel extends BasePanel {
176
191
  controlsSection,
177
192
  sectionWindow.section.lines.length > 0 ? sectionWindow.section : { title: sectionLabel, lines: [buildPanelLine(width, [[' No matching docs', DEFAULT_PANEL_PALETTE.dim]])] },
178
193
  ],
194
+ footerLines,
179
195
  palette: DEFAULT_PANEL_PALETTE,
180
196
  });
181
197
  }
@@ -207,7 +223,7 @@ export class DocsPanel extends BasePanel {
207
223
  const tools = this.toolRegistry?.list() ?? [];
208
224
  const filtered = q ? tools.filter(t => t.definition.name.toLowerCase().includes(q) || (t.definition.description ?? '').toLowerCase().includes(q)) : tools;
209
225
  if (filtered.length === 0) {
210
- rows.push({ kind: 'empty', text: ' No tools match.', fg: C.dim, bg: '' });
226
+ rows.push({ kind: 'empty', text: q ? ` No tools match "${this.searchQuery}". Press Esc to clear the filter.` : ' Tool registry not wired into this session.', fg: C.dim, bg: '' });
211
227
  } else {
212
228
  rows.push({ kind: 'header', text: ` Tools (${filtered.length})`, fg: C.sectionFg, bg: C.sectionBg, bold: true });
213
229
  for (const tool of filtered) {
@@ -237,7 +253,7 @@ export class DocsPanel extends BasePanel {
237
253
  const models = this.providerRegistry?.listModels() ?? [];
238
254
  const filtered = q ? models.filter(m => m.id.toLowerCase().includes(q) || m.displayName.toLowerCase().includes(q) || m.provider.toLowerCase().includes(q)) : models;
239
255
  if (filtered.length === 0) {
240
- rows.push({ kind: 'empty', text: ' No models match.', fg: C.dim, bg: '' });
256
+ rows.push({ kind: 'empty', text: q ? ` No models match "${this.searchQuery}". Press Esc to clear the filter.` : ' Provider registry not wired into this session.', fg: C.dim, bg: '' });
241
257
  } else {
242
258
  // Group by provider
243
259
  const byProvider = new Map<string, typeof filtered>();