@pellux/goodvibes-tui 0.26.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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -8,10 +8,14 @@ import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
8
8
  import {
9
9
  buildEmptyState,
10
10
  buildGuidanceLine,
11
+ buildKeyboardHints,
12
+ buildMeterLine,
11
13
  buildStyledPanelLine,
14
+ buildTable,
12
15
  buildPanelWorkspace,
13
16
  resolveScrollablePanelSection,
14
17
  DEFAULT_PANEL_PALETTE,
18
+ extendPalette,
15
19
  type PanelWorkspaceSection,
16
20
  } from './polish.ts';
17
21
 
@@ -32,22 +36,16 @@ interface TurnUsage {
32
36
  // Colors
33
37
  // ---------------------------------------------------------------------------
34
38
 
35
- const C = {
36
- title: '#00ffff',
39
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
40
+ // Token data-series colors (input/output/cache) — categorical, no shared equivalent
37
41
  input: '#00ffff',
38
42
  output: '#d000ff',
39
43
  cacheRead: '#00d700',
40
44
  cacheWrite: '#ffaf00',
45
+ // Progress-bar track shade and per-turn table sub-header gray
41
46
  barBg: '236',
42
- label: '244',
43
- value: '252',
44
- sectionHeader: '238',
45
- warnYellow: '#ffaf00',
46
- warnRed: '#ff5f5f',
47
- dim: '240',
48
- good: '#5fd700',
49
47
  turnHeader: '242',
50
- } as const;
48
+ });
51
49
 
52
50
  // Warning thresholds for context window usage
53
51
  const WARN_YELLOW = 0.70;
@@ -99,11 +97,17 @@ export class TokenBudgetPanel extends BasePanel {
99
97
  private sessionReadModel: UiReadModel<UiSessionSnapshot> | null = null;
100
98
  private readonly sessionMemoryStore: SessionMemoryQuery;
101
99
  private readonly configManager: Pick<ConfigManager, 'get'>;
100
+ private readonly requestRender: () => void;
102
101
 
103
- constructor(sessionMemoryStore: SessionMemoryQuery, configManager: Pick<ConfigManager, 'get'>) {
102
+ constructor(
103
+ sessionMemoryStore: SessionMemoryQuery,
104
+ configManager: Pick<ConfigManager, 'get'>,
105
+ requestRender: () => void = () => {},
106
+ ) {
104
107
  super('tokens', 'Tokens', 'T', 'monitoring');
105
108
  this.sessionMemoryStore = sessionMemoryStore;
106
109
  this.configManager = configManager;
110
+ this.requestRender = requestRender;
107
111
  }
108
112
 
109
113
  // ---------------------------------------------------------------------------
@@ -149,7 +153,7 @@ export class TokenBudgetPanel extends BasePanel {
149
153
  this.turnHistory.shift();
150
154
  }
151
155
 
152
- this.refresh();
156
+ this._refreshAndRender();
153
157
  }
154
158
 
155
159
  // ---------------------------------------------------------------------------
@@ -161,11 +165,17 @@ export class TokenBudgetPanel extends BasePanel {
161
165
  this.refresh();
162
166
  // Poll every 2 s while active so the context bar stays current during streaming
163
167
  if (this.refreshTimer !== null) clearInterval(this.refreshTimer);
164
- this.refreshTimer = setInterval(() => this.refresh(), 2_000);
168
+ this.refreshTimer = setInterval(() => this._refreshAndRender(), 2_000);
165
169
  }
166
170
 
167
171
  override onDeactivate(): void {
168
172
  super.onDeactivate();
173
+ // Stop the off-screen poll so a backgrounded Tokens tab does not keep
174
+ // refreshing (and, once requestRender is wired, force a repaint) every 2 s.
175
+ if (this.refreshTimer !== null) {
176
+ clearInterval(this.refreshTimer);
177
+ this.refreshTimer = null;
178
+ }
169
179
  }
170
180
 
171
181
  override onDestroy(): void {
@@ -194,6 +204,15 @@ export class TokenBudgetPanel extends BasePanel {
194
204
  this.markDirty();
195
205
  }
196
206
 
207
+ /**
208
+ * Refresh data and request a repaint. Used by the background poll and on
209
+ * turn completion, where no key event will otherwise drive a render.
210
+ */
211
+ private _refreshAndRender(): void {
212
+ this.refresh();
213
+ this.requestRender();
214
+ }
215
+
197
216
  // ---------------------------------------------------------------------------
198
217
  // Rendering
199
218
  // ---------------------------------------------------------------------------
@@ -221,6 +240,12 @@ export class TokenBudgetPanel extends BasePanel {
221
240
  const priorSections = [...sections];
222
241
  const turnsSection = resolveScrollablePanelSection(width, height, {
223
242
  intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
243
+ footerLines: [
244
+ buildKeyboardHints(width, [
245
+ { keys: '/compact', label: 'compress context' },
246
+ { keys: '/clear', label: 'reset session' },
247
+ ], DEFAULT_PANEL_PALETTE),
248
+ ],
224
249
  palette: DEFAULT_PANEL_PALETTE,
225
250
  beforeSections: priorSections,
226
251
  section: {
@@ -257,6 +282,12 @@ export class TokenBudgetPanel extends BasePanel {
257
282
  title: ' Token Budget',
258
283
  intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
259
284
  sections,
285
+ footerLines: [
286
+ buildKeyboardHints(width, [
287
+ { keys: '/compact', label: 'compress context' },
288
+ { keys: '/clear', label: 'reset session' },
289
+ ], DEFAULT_PANEL_PALETTE),
290
+ ],
260
291
  palette: DEFAULT_PANEL_PALETTE,
261
292
  });
262
293
  });
@@ -369,10 +400,10 @@ export class TokenBudgetPanel extends BasePanel {
369
400
  let barColor: string;
370
401
  let warnSuffix = '';
371
402
  if (pct >= WARN_RED) {
372
- barColor = C.warnRed;
403
+ barColor = C.bad;
373
404
  warnSuffix = ' ! CRITICAL';
374
405
  } else if (pct >= WARN_YELLOW) {
375
- barColor = C.warnYellow;
406
+ barColor = C.warn;
376
407
  warnSuffix = ' ! HIGH';
377
408
  } else {
378
409
  barColor = C.good;
@@ -382,12 +413,12 @@ export class TokenBudgetPanel extends BasePanel {
382
413
  const suffix = ` ${fmtTok(this.lastInputTokens)}/${fmtTok(this.contextWindow)} (${pctInt}%)${warnSuffix}`;
383
414
  const BAR_W = Math.max(8, width - label.length - suffix.length - 2);
384
415
  const filled = Math.round(pct * BAR_W);
385
- lines.push(buildStyledPanelLine(width, [
386
- { text: label, fg: C.label },
387
- { text: '#'.repeat(filled), fg: barColor, dim: pct < WARN_YELLOW },
388
- { text: '.'.repeat(Math.max(0, BAR_W - filled)), fg: C.barBg, dim: pct < WARN_YELLOW },
389
- { text: suffix, fg: barColor, dim: pct < WARN_YELLOW },
390
- ]));
416
+ // Context fill is the headline metric — render it with the shared meter
417
+ // primitive so the bar glyphs and width handling match every other panel.
418
+ lines.push(buildMeterLine(width, filled, BAR_W,
419
+ { filled: barColor, empty: C.barBg, label: C.label },
420
+ { prefix: label, suffix },
421
+ ));
391
422
  return lines;
392
423
  }
393
424
 
@@ -420,41 +451,37 @@ export class TokenBudgetPanel extends BasePanel {
420
451
 
421
452
  /** Last N turns table: turn#, in, out, CR, CW, total. */
422
453
  private renderTurnHistory(width: number, maxRows: number): Line[] {
423
- const lines: Line[] = [];
424
-
425
- // Column headers
426
- const colLine = createEmptyLine(width);
427
- const headers = [' #', ' Input', ' Output', ' CR', ' CW', ' Total'];
428
- const cols = [3, 9, 9, 6, 6, 9];
429
- let hx = 0;
430
- for (let i = 0; i < headers.length; i++) {
431
- const h = headers[i]!;
432
- for (const ch of h.slice(0, cols[i]!)) {
433
- if (hx >= width) break;
434
- colLine[hx++] = createStyledCell(ch, { fg: C.turnHeader, dim: true });
435
- }
436
- }
437
- lines.push(colLine);
438
-
439
454
  const available = Math.max(0, maxRows - 1); // minus col header
440
455
  const toShow = this.turnHistory.slice(-Math.max(0, available));
441
456
 
442
- toShow.forEach((t, i) => {
443
- if (lines.length >= maxRows) return;
444
- const turnNum = this.turnHistory.length - toShow.length + i + 1;
445
- const total = t.input + t.output + t.cacheRead + t.cacheWrite;
446
- const cells: Array<[string, string]> = [
447
- [String(turnNum).padStart(3), C.dim],
448
- [fmtTok(t.input).padStart(9), C.input],
449
- [fmtTok(t.output).padStart(9), C.output],
450
- [fmtTok(t.cacheRead).padStart(6), C.cacheRead],
451
- [fmtTok(t.cacheWrite).padStart(6), C.cacheWrite],
452
- [fmtTok(total).padStart(9), C.value],
453
- ];
454
- lines.push(buildStyledPanelLine(width, cells.map(([val, color]) => ({ text: val, fg: color }))));
455
- });
456
-
457
- return lines;
457
+ // Width-aware aligned table via the shared primitive (replaces manual
458
+ // per-cell column math, which miscounts on wide glyphs).
459
+ return buildTable(
460
+ width,
461
+ [
462
+ { label: '#', width: 4, align: 'right' },
463
+ { label: 'Input', width: 9, align: 'right' },
464
+ { label: 'Output', width: 9, align: 'right' },
465
+ { label: 'CR', width: 7, align: 'right' },
466
+ { label: 'CW', width: 7, align: 'right' },
467
+ { label: 'Total', align: 'right' },
468
+ ],
469
+ toShow.map((t, i) => {
470
+ const turnNum = this.turnHistory.length - toShow.length + i + 1;
471
+ const total = t.input + t.output + t.cacheRead + t.cacheWrite;
472
+ return {
473
+ cells: [
474
+ { text: String(turnNum), fg: C.dim },
475
+ { text: fmtTok(t.input), fg: C.input },
476
+ { text: fmtTok(t.output), fg: C.output },
477
+ { text: fmtTok(t.cacheRead), fg: C.cacheRead },
478
+ { text: fmtTok(t.cacheWrite), fg: C.cacheWrite },
479
+ { text: fmtTok(total), fg: C.value },
480
+ ],
481
+ };
482
+ }),
483
+ { ...DEFAULT_PANEL_PALETTE, label: C.turnHeader },
484
+ );
458
485
  }
459
486
 
460
487
  /** Paint a plain text string into a new Line. */
@@ -13,26 +13,19 @@ import {
13
13
  buildPanelWorkspace,
14
14
  resolveScrollablePanelSection,
15
15
  DEFAULT_PANEL_PALETTE,
16
+ extendPalette,
16
17
  type PanelWorkspaceSection,
17
18
  } from './polish.ts';
18
19
  import { truncateDisplay } from '../utils/terminal-width.ts';
20
+ import { formatDuration } from '../utils/format-duration.ts';
19
21
 
20
- const C = {
21
- headerBg: '#1a1a2e',
22
- headerFg: '#ffffff',
23
- statusBar: '#222233',
24
- statusFg: '#aaaaaa',
25
- toolFg: '#00ccff',
26
- resultFg: '#66ddff',
27
- errorFg: '#ff6666',
28
- durationFg: '#aaaa66',
29
- argsFg: '#aaaaaa',
30
- dimFg: '#555566',
31
- selected: '#00ffff',
32
- selectedBg: '#1a2a3a',
33
- labelFg: '#8888bb',
34
- filterFg: '#ffcc44',
35
- } as const;
22
+ // Panel-specific accents only; shared tones come from DEFAULT_PANEL_PALETTE so
23
+ // theme changes propagate. selectedBg->selectBg, errorFg->bad are shared keys.
24
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
25
+ selected: '#00ffff',
26
+ toolFg: '#00ccff',
27
+ argsFg: '#aaaaaa',
28
+ });
36
29
 
37
30
  const MAX_ENTRIES = 500;
38
31
 
@@ -64,14 +57,12 @@ function shortTime(ms: number): string {
64
57
  }
65
58
 
66
59
  function formatMs(ms: number): string {
67
- if (ms < 1000) return `${ms}ms`;
68
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
69
- return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
60
+ return formatDuration(ms);
70
61
  }
71
62
 
72
63
  function truncateJson(val: unknown, maxLen = 120): string {
73
64
  const s = JSON.stringify(val) ?? 'null';
74
- return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s;
65
+ return truncateDisplay(s, maxLen);
75
66
  }
76
67
 
77
68
  function summarizeResult(result: unknown): string | undefined {
@@ -81,7 +72,7 @@ function summarizeResult(result: unknown): string | undefined {
81
72
  const record = result as Record<string, unknown>;
82
73
  if (typeof record.preview === 'string' && record.preview.trim()) {
83
74
  const compact = record.preview.replace(/\s+/g, ' ').trim();
84
- return compact.length > 72 ? `${compact.slice(0, 69)}\u2026` : compact;
75
+ return truncateDisplay(compact, 72);
85
76
  }
86
77
  if (typeof record.kind === 'string' && typeof record.byteSize === 'number') {
87
78
  return `${record.kind} (${record.byteSize}B)`;
@@ -150,22 +141,23 @@ export class ToolInspectorPanel extends BasePanel {
150
141
  if (height <= 0 || width <= 0) return [];
151
142
 
152
143
  const running = this.records.filter(r => r.endMs === undefined).length;
144
+ const distinctTools = new Set(this.records.map((r) => r.tool)).size;
153
145
  const filterLabel = this.filterMode === 'all' ? '' : ` [${this.filterMode}]`;
154
146
  const title = ` Tools [${this.records.length} calls${running > 0 ? `, ${running} running` : ''}]${filterLabel}`;
155
- const footerLines = [
156
- buildPanelLine(width, [
157
- [' Up/Down', DEFAULT_PANEL_PALETTE.info],
158
- [' scroll', DEFAULT_PANEL_PALETTE.dim],
159
- [' Enter', DEFAULT_PANEL_PALETTE.info],
160
- [' expand', DEFAULT_PANEL_PALETTE.dim],
161
- [' f', DEFAULT_PANEL_PALETTE.info],
162
- [' filter', DEFAULT_PANEL_PALETTE.dim],
163
- [' c', DEFAULT_PANEL_PALETTE.info],
164
- [' clear', DEFAULT_PANEL_PALETTE.dim],
165
- [' g', DEFAULT_PANEL_PALETTE.info],
166
- [' end', DEFAULT_PANEL_PALETTE.dim],
167
- ]),
147
+ // Context-aware footer: only advertise keys that do something in the current
148
+ // state — filter needs >1 tool, clear/end need at least one call.
149
+ const footerSegments: Array<[string, string, string?]> = [
150
+ [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim],
151
+ [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
168
152
  ];
153
+ if (distinctTools > 1) {
154
+ footerSegments.push([' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim]);
155
+ }
156
+ if (this.records.length > 0) {
157
+ footerSegments.push([' c', DEFAULT_PANEL_PALETTE.info], [' clear', DEFAULT_PANEL_PALETTE.dim]);
158
+ footerSegments.push([' g', DEFAULT_PANEL_PALETTE.info], [this.autoScroll ? ' end (live)' : ' jump to end', DEFAULT_PANEL_PALETTE.dim]);
159
+ }
160
+ const footerLines = [buildPanelLine(width, footerSegments)];
169
161
 
170
162
  const flat = this._getFlat();
171
163
 
@@ -182,9 +174,15 @@ export class ToolInspectorPanel extends BasePanel {
182
174
  title: 'Calls',
183
175
  lines: buildEmptyState(
184
176
  width,
185
- ' No tool calls yet',
186
- 'Tool executions appear here as the agent works. Expand a call to inspect its arguments and result payload.',
187
- [],
177
+ this.records.length > 0 && this.filterMode !== 'all'
178
+ ? ` No "${this.filterMode}" calls`
179
+ : ' No tool calls yet',
180
+ this.records.length > 0 && this.filterMode !== 'all'
181
+ ? 'The active tool filter hides every recorded call.'
182
+ : 'Tool executions appear here as the agent works. Expand a call to inspect its arguments and result payload.',
183
+ this.records.length > 0 && this.filterMode !== 'all'
184
+ ? [{ command: 'f', summary: 'cycle the tool filter back to all calls' }]
185
+ : [{ command: '/spawn <task>', summary: 'run an agent to populate the tool-call timeline' }],
188
186
  DEFAULT_PANEL_PALETTE,
189
187
  ),
190
188
  },
@@ -271,7 +269,7 @@ export class ToolInspectorPanel extends BasePanel {
271
269
  }
272
270
 
273
271
  private _renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
274
- const bg = isCursor ? C.selectedBg : '';
272
+ const bg = isCursor ? C.selectBg : '';
275
273
  if (row.kind === 'call') {
276
274
  return buildStyledPanelLine(width, [
277
275
  { text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
@@ -280,7 +278,7 @@ export class ToolInspectorPanel extends BasePanel {
280
278
  }
281
279
  return buildStyledPanelLine(width, [
282
280
  { text: ' ', fg: C.argsFg, bg },
283
- { text: truncateDisplay(row.text, Math.max(0, width - 2)), fg: row.isError ? C.errorFg : C.argsFg, bg },
281
+ { text: truncateDisplay(row.text, Math.max(0, width - 2)), fg: row.isError ? C.bad : C.argsFg, bg },
284
282
  ]);
285
283
  }
286
284
 
@@ -1,5 +1,20 @@
1
1
  import type { Line } from '../types/grid.ts';
2
+ import type { StatusState } from '../renderer/status-glyphs.ts';
2
3
  import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
4
+ // Routed through the `@/` alias (not `./panel-manager.ts`) so this foundational
5
+ // types module stays a leaf in the relative-import graph the architecture
6
+ // cycle-checker walks. Type-only, erased at runtime — no real dependency edge.
7
+ import type { PanelManager } from '@/panels/panel-manager.ts';
8
+
9
+ /**
10
+ * Context passed to a panel's `handlePanelIntegrationAction` hook so it can
11
+ * drive cross-panel behavior (e.g. file-explorer opening the preview panel)
12
+ * without the input layer needing `instanceof` knowledge of each panel type.
13
+ */
14
+ export interface PanelIntegrationContext {
15
+ readonly panelManager: PanelManager;
16
+ readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
17
+ }
3
18
 
4
19
  /**
5
20
  * Named logical key identifiers emitted by the input tokenizer.
@@ -67,6 +82,16 @@ export interface Panel {
67
82
  // Scroll input (optional)
68
83
  // Positive delta scrolls down; negative delta scrolls up.
69
84
  handleScroll?(deltaRows: number): boolean;
85
+
86
+ /**
87
+ * Cross-panel integration hook (optional). Called before the panel's own
88
+ * `handleInput` when a navigation/confirm key is pressed, so a panel can
89
+ * drive another panel (e.g. open a file in the preview panel). Return `true`
90
+ * to consume the key. The legacy `handlePanelIntegrationAction` router in
91
+ * `src/input/panel-integration-actions.ts` consults this first, then falls
92
+ * back to its built-in `instanceof` routing.
93
+ */
94
+ handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
70
95
  }
71
96
 
72
97
  export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
@@ -4,9 +4,11 @@ import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
4
  import type { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
5
5
  import { truncateDisplay } from '../utils/terminal-width.ts';
6
6
  import {
7
+ buildDetailBlock,
7
8
  buildEmptyState,
8
9
  buildGuidanceLine,
9
10
  buildKeyValueLine,
11
+ buildKeyboardHints,
10
12
  buildPanelLine,
11
13
  buildPanelWorkspace,
12
14
  DEFAULT_PANEL_PALETTE,
@@ -59,10 +61,18 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
59
61
  public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
60
62
  super('watchers', 'Watchers', 'W', 'monitoring');
61
63
  this.showSelectionGutter = true; // I5: non-color selection affordance
64
+ this.filterEnabled = true;
65
+ this.filterLabel = 'Filter watchers';
62
66
  this.readModel = readModel;
63
67
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
64
68
  }
65
69
 
70
+ protected override filterMatches(watcher: WatcherEntry, q: string): boolean {
71
+ return watcher.label.toLowerCase().includes(q)
72
+ || watcher.state.toLowerCase().includes(q)
73
+ || String(watcher.sourceStatus ?? '').toLowerCase().includes(q);
74
+ }
75
+
66
76
  public override onDestroy(): void {
67
77
  this.unsub?.();
68
78
  }
@@ -144,7 +154,7 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
144
154
  this.clampSelection();
145
155
  const selected = watchers[this.selectedIndex]!;
146
156
 
147
- const footerLines: Line[] = [
157
+ const detailRows: Line[] = [
148
158
  buildPanelLine(width, [
149
159
  [' Watcher: ', C.label],
150
160
  [selected.label, C.value],
@@ -171,23 +181,33 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
171
181
  ]),
172
182
  ];
173
183
  if (selected.degradedReason) {
174
- footerLines.push(buildPanelLine(width, [
184
+ detailRows.push(buildPanelLine(width, [
175
185
  [' Reason: ', C.label],
176
186
  [truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
177
187
  ]));
178
188
  }
179
189
  if (selected.lastError) {
180
- footerLines.push(buildPanelLine(width, [
190
+ detailRows.push(buildPanelLine(width, [
181
191
  [' Error: ', C.label],
182
192
  [truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
183
193
  ]));
184
194
  }
185
- footerLines.push(buildPanelLine(width, [[' Up/Down move through watchers', C.dim]]));
195
+
196
+ const hints = this.filterActive
197
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
198
+ : [
199
+ { keys: 'Up/Down', label: 'move' },
200
+ { keys: '/schedule list', label: 'consumers' },
201
+ { keys: '/', label: 'filter' },
202
+ ];
186
203
 
187
204
  return this.renderList(width, height, {
188
205
  title: 'Watchers',
189
206
  header: headerLines,
190
- footer: footerLines,
207
+ footer: [
208
+ ...buildDetailBlock(width, `Watcher · ${selected.label}`, detailRows, C),
209
+ buildKeyboardHints(width, hints, C),
210
+ ],
191
211
  });
192
212
  }
193
213
  }