@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
@@ -6,6 +6,7 @@ import type { Line } from '../types/grid.ts';
6
6
  import { BasePanel } from './base-panel.ts';
7
7
  import type { ToolEvent, TurnEvent } from '@/runtime/index.ts';
8
8
  import type { UiEventFeed } from '../runtime/ui-events.ts';
9
+ import type { PanelIntegrationContext } from './types.ts';
9
10
  import {
10
11
  buildEmptyState,
11
12
  buildPanelLine,
@@ -16,7 +17,9 @@ import {
16
17
  extendPalette,
17
18
  type PanelWorkspaceSection,
18
19
  } from './polish.ts';
20
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
19
21
  import { truncateDisplay } from '../utils/terminal-width.ts';
22
+ import { formatDuration } from '../utils/format-duration.ts';
20
23
 
21
24
  // Panel-specific accents only; shared tones come from DEFAULT_PANEL_PALETTE so
22
25
  // theme changes propagate. selectedBg->selectBg, errorFg->bad are shared keys.
@@ -32,15 +35,23 @@ interface ToolCallRecord {
32
35
  callId: string;
33
36
  tool: string;
34
37
  args: Record<string, unknown>;
38
+ /** Wall-clock time the call was received from the LLM (queued, not yet executing). */
35
39
  startMs: number;
40
+ /**
41
+ * Real execution-start time from TOOL_EXECUTING.startedAt — used for duration
42
+ * math when present so displayed durations reflect actual run time rather
43
+ * than time spent in permission/hook queueing. Falls back to `startMs` when
44
+ * a call completes without ever reaching TOOL_EXECUTING (e.g. denied before
45
+ * execution).
46
+ */
47
+ execStartMs?: number;
36
48
  endMs?: number;
37
49
  result?: unknown;
38
50
  error?: string;
51
+ cancelled?: boolean;
39
52
  expanded: boolean;
40
53
  approved?: boolean;
41
54
  outputClass?: string;
42
- policyAction?: string;
43
- spillBackend?: string;
44
55
  resultSummary?: string;
45
56
  }
46
57
 
@@ -56,14 +67,12 @@ function shortTime(ms: number): string {
56
67
  }
57
68
 
58
69
  function formatMs(ms: number): string {
59
- if (ms < 1000) return `${ms}ms`;
60
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
61
- return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
70
+ return formatDuration(ms);
62
71
  }
63
72
 
64
73
  function truncateJson(val: unknown, maxLen = 120): string {
65
74
  const s = JSON.stringify(val) ?? 'null';
66
- return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s;
75
+ return truncateDisplay(s, maxLen);
67
76
  }
68
77
 
69
78
  function summarizeResult(result: unknown): string | undefined {
@@ -73,7 +82,7 @@ function summarizeResult(result: unknown): string | undefined {
73
82
  const record = result as Record<string, unknown>;
74
83
  if (typeof record.preview === 'string' && record.preview.trim()) {
75
84
  const compact = record.preview.replace(/\s+/g, ' ').trim();
76
- return compact.length > 72 ? `${compact.slice(0, 69)}\u2026` : compact;
85
+ return truncateDisplay(compact, 72);
77
86
  }
78
87
  if (typeof record.kind === 'string' && typeof record.byteSize === 'number') {
79
88
  return `${record.kind} (${record.byteSize}B)`;
@@ -81,6 +90,20 @@ function summarizeResult(result: unknown): string | undefined {
81
90
  return undefined;
82
91
  }
83
92
 
93
+ function statusLabel(rec: ToolCallRecord): string {
94
+ if (rec.endMs === undefined) return 'running';
95
+ if (rec.cancelled) return 'cancelled';
96
+ if (rec.error) return 'error';
97
+ return 'completed';
98
+ }
99
+
100
+ function statusColor(rec: ToolCallRecord): string {
101
+ if (rec.endMs === undefined) return DEFAULT_PANEL_PALETTE.warn;
102
+ if (rec.cancelled) return DEFAULT_PANEL_PALETTE.dim;
103
+ if (rec.error) return DEFAULT_PANEL_PALETTE.bad;
104
+ return DEFAULT_PANEL_PALETTE.good;
105
+ }
106
+
84
107
  function detectOutputClass(tool: string, args: Record<string, unknown>): string {
85
108
  const name = tool.toLowerCase();
86
109
  if (name.includes('read') || name.includes('find') || name.includes('inspect') || name.includes('grep')) return 'read';
@@ -98,6 +121,10 @@ export class ToolInspectorPanel extends BasePanel {
98
121
  private scrollOffset = 0;
99
122
  private autoScroll = true;
100
123
  private _flatCache: FlatRow[] | null = null;
124
+ /** Pending confirm for 'c' clear — destructive (drops call history), so it goes through the project-standard confirm contract. */
125
+ private _confirm: ConfirmState<'clear'> | null = null;
126
+ /** Set by handleInput('a') when the selected call is awaiting a permission decision; consumed by handlePanelIntegrationAction. */
127
+ private _pendingApprovalJump = false;
101
128
 
102
129
  constructor(
103
130
  private readonly toolEvents: UiEventFeed<ToolEvent>,
@@ -125,39 +152,89 @@ export class ToolInspectorPanel extends BasePanel {
125
152
  }
126
153
 
127
154
  handleInput(key: string): boolean {
155
+ const confirmResult = handleConfirmInput(this._confirm, key);
156
+ if (confirmResult === 'confirmed') {
157
+ this.records = [];
158
+ this._confirm = null;
159
+ this.markDirty();
160
+ return true;
161
+ }
162
+ if (confirmResult === 'cancelled') {
163
+ this._confirm = null;
164
+ this.markDirty();
165
+ return true;
166
+ }
167
+ if (confirmResult === 'absorbed') return true;
168
+
128
169
  switch (key) {
129
170
  case 'up': this._move(-1); this.autoScroll = false; return true;
130
171
  case 'down': this._move(1); return true;
131
172
  case 'pageup': this._move(-10); this.autoScroll = false; return true;
132
173
  case 'pagedown': this._move(10); return true;
133
174
  case 'return': this._toggleExpand(); return true;
134
- case 'c': this.records = []; this.markDirty(); return true;
175
+ case 'c': {
176
+ if (this.records.length === 0) return false;
177
+ this._confirm = { subject: 'clear', label: `${this.records.length} tool call${this.records.length === 1 ? '' : 's'}`, verb: 'Clear' };
178
+ this.markDirty();
179
+ return true;
180
+ }
135
181
  case 'g': this.autoScroll = true; this.markDirty(); return true;
136
182
  case 'f': this._cycleFilter(); return true;
183
+ case 'a': {
184
+ const rec = this._selectedRecord();
185
+ if (!rec || rec.approved !== undefined || rec.endMs !== undefined) return false;
186
+ this._pendingApprovalJump = true;
187
+ return true;
188
+ }
137
189
  default: return false;
138
190
  }
139
191
  }
140
192
 
193
+ /**
194
+ * Cross-panel hook — invoked immediately after handleInput('a') returns true
195
+ * for a call still awaiting a permission decision. Jumps to the Approval
196
+ * panel; the actual PanelManager reference is only available here.
197
+ */
198
+ handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
199
+ if (!this._pendingApprovalJump) return false;
200
+ this._pendingApprovalJump = false;
201
+ ctx.panelManager.open('approval');
202
+ return true;
203
+ }
204
+
141
205
  render(width: number, height: number): Line[] {
142
206
  if (height <= 0 || width <= 0) return [];
143
207
 
144
208
  const running = this.records.filter(r => r.endMs === undefined).length;
209
+ const distinctTools = new Set(this.records.map((r) => r.tool)).size;
145
210
  const filterLabel = this.filterMode === 'all' ? '' : ` [${this.filterMode}]`;
146
211
  const title = ` Tools [${this.records.length} calls${running > 0 ? `, ${running} running` : ''}]${filterLabel}`;
147
- const footerLines = [
148
- buildPanelLine(width, [
149
- [' Up/Down', DEFAULT_PANEL_PALETTE.info],
150
- [' scroll', DEFAULT_PANEL_PALETTE.dim],
151
- [' Enter', DEFAULT_PANEL_PALETTE.info],
152
- [' expand', DEFAULT_PANEL_PALETTE.dim],
153
- [' f', DEFAULT_PANEL_PALETTE.info],
154
- [' filter', DEFAULT_PANEL_PALETTE.dim],
155
- [' c', DEFAULT_PANEL_PALETTE.info],
156
- [' clear', DEFAULT_PANEL_PALETTE.dim],
157
- [' g', DEFAULT_PANEL_PALETTE.info],
158
- [' end', DEFAULT_PANEL_PALETTE.dim],
159
- ]),
212
+ // Context-aware footer: only advertise keys that do something in the current
213
+ // state — filter needs >1 tool, clear/end need at least one call.
214
+ const footerSegments: Array<[string, string, string?]> = [
215
+ [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim],
216
+ [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
160
217
  ];
218
+ if (distinctTools > 1) {
219
+ footerSegments.push([' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim]);
220
+ }
221
+ if (this.records.length > 0) {
222
+ footerSegments.push([' c', DEFAULT_PANEL_PALETTE.info], [' clear', DEFAULT_PANEL_PALETTE.dim]);
223
+ footerSegments.push([' g', DEFAULT_PANEL_PALETTE.info], [this.autoScroll ? ' end (live)' : ' jump to end', DEFAULT_PANEL_PALETTE.dim]);
224
+ }
225
+ const selectedRecordForFooter = this._selectedRecord();
226
+ if (selectedRecordForFooter && selectedRecordForFooter.approved === undefined && selectedRecordForFooter.endMs === undefined) {
227
+ footerSegments.push([' a', DEFAULT_PANEL_PALETTE.info], [' review approval', DEFAULT_PANEL_PALETTE.dim]);
228
+ }
229
+ const footerLines = [buildPanelLine(width, footerSegments)];
230
+
231
+ if (this._confirm) {
232
+ return buildPanelWorkspace(width, height, {
233
+ title,
234
+ sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this._confirm) }],
235
+ palette: DEFAULT_PANEL_PALETTE,
236
+ });
237
+ }
161
238
 
162
239
  const flat = this._getFlat();
163
240
 
@@ -168,15 +245,21 @@ export class ToolInspectorPanel extends BasePanel {
168
245
  if (flat.length === 0) {
169
246
  return buildPanelWorkspace(width, height, {
170
247
  title,
171
- intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls.',
248
+ intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls — including calls made by delegated subagents.',
172
249
  sections: [
173
250
  {
174
251
  title: 'Calls',
175
252
  lines: buildEmptyState(
176
253
  width,
177
- ' No tool calls yet',
178
- 'Tool executions appear here as the agent works. Expand a call to inspect its arguments and result payload.',
179
- [],
254
+ this.records.length > 0 && this.filterMode !== 'all'
255
+ ? ` No "${this.filterMode}" calls`
256
+ : ' No tool calls yet',
257
+ this.records.length > 0 && this.filterMode !== 'all'
258
+ ? 'The active tool filter hides every recorded call.'
259
+ : 'Tool executions appear here as the agent works, including calls made by delegated subagents. Expand a call to inspect its arguments and result payload.',
260
+ this.records.length > 0 && this.filterMode !== 'all'
261
+ ? [{ command: 'f', summary: 'cycle the tool filter back to all calls' }]
262
+ : [{ command: '/spawn <task>', summary: 'run an agent to populate the tool-call timeline' }],
180
263
  DEFAULT_PANEL_PALETTE,
181
264
  ),
182
265
  },
@@ -209,22 +292,14 @@ export class ToolInspectorPanel extends BasePanel {
209
292
  : this.records.filter(r => r.tool === this.filterMode);
210
293
  const rec = filtered[selected.recordIndex];
211
294
  if (rec) {
212
- detailLines.push(buildPanelLine(width, [[' Tool ', DEFAULT_PANEL_PALETTE.label], [rec.tool, DEFAULT_PANEL_PALETTE.info], [' Started ', DEFAULT_PANEL_PALETTE.label], [shortTime(rec.startMs), DEFAULT_PANEL_PALETTE.value]]));
213
- detailLines.push(buildPanelLine(width, [[' Status ', DEFAULT_PANEL_PALETTE.label], [rec.endMs === undefined ? 'running' : rec.error ? 'error' : 'completed', rec.endMs === undefined ? DEFAULT_PANEL_PALETTE.warn : rec.error ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good]]));
295
+ detailLines.push(buildPanelLine(width, [[' Tool ', DEFAULT_PANEL_PALETTE.label], [rec.tool, DEFAULT_PANEL_PALETTE.info], [' Started ', DEFAULT_PANEL_PALETTE.label], [shortTime(rec.execStartMs ?? rec.startMs), DEFAULT_PANEL_PALETTE.value]]));
296
+ detailLines.push(buildPanelLine(width, [[' Status ', DEFAULT_PANEL_PALETTE.label], [statusLabel(rec), statusColor(rec)]]));
214
297
  detailLines.push(buildPanelLine(width, [
215
- [' Risk ', DEFAULT_PANEL_PALETTE.label],
298
+ [' Class ', DEFAULT_PANEL_PALETTE.label],
216
299
  [rec.outputClass ?? detectOutputClass(rec.tool, rec.args), DEFAULT_PANEL_PALETTE.warn],
217
300
  [' Approved ', DEFAULT_PANEL_PALETTE.label],
218
301
  [rec.approved === undefined ? 'unknown' : rec.approved ? 'yes' : 'no', rec.approved === false ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.value],
219
302
  ]));
220
- if (rec.policyAction || rec.spillBackend) {
221
- detailLines.push(buildPanelLine(width, [
222
- [' Output ', DEFAULT_PANEL_PALETTE.label],
223
- [rec.policyAction ?? 'none', rec.policyAction && rec.policyAction !== 'none' ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.value],
224
- [' Spill ', DEFAULT_PANEL_PALETTE.label],
225
- [rec.spillBackend ?? 'none', rec.spillBackend ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
226
- ]));
227
- }
228
303
  if (rec.resultSummary) {
229
304
  detailLines.push(buildPanelLine(width, [[' Summary ', DEFAULT_PANEL_PALETTE.label], [rec.resultSummary, DEFAULT_PANEL_PALETTE.value]]));
230
305
  }
@@ -234,7 +309,7 @@ export class ToolInspectorPanel extends BasePanel {
234
309
 
235
310
  const selectedSection: PanelWorkspaceSection = { title: 'Selected', lines: detailLines };
236
311
  const callsSection = resolveScrollablePanelSection(width, height, {
237
- intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls.',
312
+ intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls — including calls made by delegated subagents.',
238
313
  footerLines,
239
314
  palette: DEFAULT_PANEL_PALETTE,
240
315
  beforeSections: [summary],
@@ -251,7 +326,7 @@ export class ToolInspectorPanel extends BasePanel {
251
326
 
252
327
  return buildPanelWorkspace(width, height, {
253
328
  title,
254
- intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls.',
329
+ intro: 'Inspect chronological tool activity, arguments, results, errors, and running calls — including calls made by delegated subagents.',
255
330
  sections: [
256
331
  summary,
257
332
  callsSection.section,
@@ -285,25 +360,24 @@ export class ToolInspectorPanel extends BasePanel {
285
360
 
286
361
  for (let i = 0; i < filtered.length; i++) {
287
362
  const rec = filtered[i]!;
288
- const dur = rec.endMs !== undefined ? ` (${formatMs(rec.endMs - rec.startMs)})` : ' (running)';
363
+ // Duration is measured from real execution start (TOOL_EXECUTING.startedAt) when
364
+ // known, not from queue/receive time, so it reflects actual run time rather than
365
+ // time spent waiting on permission/hook checks. Falls back to startMs for calls
366
+ // that never reached TOOL_EXECUTING (e.g. denied before execution).
367
+ const durBaseMs = rec.execStartMs ?? rec.startMs;
368
+ const dur = rec.cancelled
369
+ ? ` (cancelled${rec.endMs !== undefined ? ` ${formatMs(rec.endMs - durBaseMs)}` : ''})`
370
+ : rec.endMs !== undefined ? ` (${formatMs(rec.endMs - durBaseMs)})` : ' (running)';
289
371
  const expand = rec.expanded ? ' ▾' : ' ▸';
290
372
  const ts = shortTime(rec.startMs);
291
- const risk = rec.outputClass ?? detectOutputClass(rec.tool, rec.args);
292
- const action = rec.policyAction && rec.policyAction !== 'none' ? ` ${rec.policyAction}` : '';
293
- const callText = `${ts} ${rec.tool} [${risk}]${action}${dur}${expand}`;
373
+ const cls = rec.outputClass ?? detectOutputClass(rec.tool, rec.args);
374
+ const callText = `${ts} ${rec.tool} [${cls}]${dur}${expand}`;
294
375
  flat.push({ kind: 'call', recordIndex: i, text: callText });
295
376
 
296
377
  if (rec.expanded) {
297
378
  const argsStr = truncateJson(rec.args, 200);
298
379
  flat.push({ kind: 'detail', text: `Args: ${argsStr}`, isError: false });
299
- flat.push({ kind: 'detail', text: `Risk: ${risk}${rec.approved === undefined ? '' : ` Approved: ${rec.approved ? 'yes' : 'no'}`}`, isError: false });
300
- if (rec.policyAction || rec.spillBackend) {
301
- flat.push({
302
- kind: 'detail',
303
- text: `Output policy: ${rec.policyAction ?? 'none'}${rec.spillBackend ? ` Spill: ${rec.spillBackend}` : ''}`,
304
- isError: false,
305
- });
306
- }
380
+ flat.push({ kind: 'detail', text: `Class: ${cls}${rec.approved === undefined ? '' : ` Approved: ${rec.approved ? 'yes' : 'no'}`}`, isError: false });
307
381
  if (rec.resultSummary) {
308
382
  flat.push({ kind: 'detail', text: `Summary: ${rec.resultSummary}`, isError: false });
309
383
  }
@@ -332,6 +406,17 @@ export class ToolInspectorPanel extends BasePanel {
332
406
  }
333
407
  }
334
408
 
409
+ /** The tool-call record backing the currently-selected flat row, if any. */
410
+ private _selectedRecord(): ToolCallRecord | undefined {
411
+ const flat = this._getFlat();
412
+ const row = flat[this.cursorIndex];
413
+ if (!row || row.kind !== 'call') return undefined;
414
+ const filtered = this.filterMode === 'all'
415
+ ? this.records
416
+ : this.records.filter(r => r.tool === this.filterMode);
417
+ return filtered[row.recordIndex];
418
+ }
419
+
335
420
  private _cycleFilter(): void {
336
421
  const tools = [...new Set(this.records.map(r => r.tool))];
337
422
  if (tools.length === 0) return;
@@ -344,6 +429,18 @@ export class ToolInspectorPanel extends BasePanel {
344
429
  this.markDirty();
345
430
  }
346
431
 
432
+ /**
433
+ * Public counterpart to the 'f' cycle — sets the filter directly to a named
434
+ * tool. Used by cross-panel jumps (WO-136: DocsPanel Enter on a tool row)
435
+ * so the inspector opens already scoped to the tool the operator picked.
436
+ */
437
+ public filterByTool(tool: string): void {
438
+ this.filterMode = tool;
439
+ this.cursorIndex = 0;
440
+ this.autoScroll = false;
441
+ this.markDirty();
442
+ }
443
+
347
444
  private _move(delta: number): void {
348
445
  const flat = this._getFlat();
349
446
  if (flat.length === 0) return;
@@ -376,11 +473,17 @@ export class ToolInspectorPanel extends BasePanel {
376
473
  this.markDirty();
377
474
  }));
378
475
 
476
+ // Real execution start — recorded separately from TOOL_RECEIVED's queue time so
477
+ // displayed durations reflect actual run time, not time spent waiting on
478
+ // permission/hook checks ahead of execution.
479
+ this.unsubs.push(this.toolEvents.on('TOOL_EXECUTING', (data) => {
480
+ const rec = this.records.findLast(r => r.callId === data.callId);
481
+ if (rec) rec.execStartMs = data.startedAt;
482
+ this.markDirty();
483
+ }));
484
+
379
485
  // NOTE: After SDK OBS-05 (0.21.31), TOOL_SUCCEEDED/TOOL_FAILED.result is a ToolResultSummary
380
- // ({ kind, byteSize, preview? }) rather than the raw ToolResult object. The previous
381
- // `_policyAudit` extraction is no longer reachable via this event — policy audit metadata
382
- // must be sourced from a different channel (approval broker / tool result store) if the
383
- // Tool Inspector is to display it in future.
486
+ // ({ kind, byteSize, preview? }) rather than the raw ToolResult object.
384
487
  this.unsubs.push(this.toolEvents.on('TOOL_SUCCEEDED', (data) => {
385
488
  const rec = this.records.findLast(r => r.callId === data.callId);
386
489
  if (rec) {
@@ -402,6 +505,19 @@ export class ToolInspectorPanel extends BasePanel {
402
505
  this.markDirty();
403
506
  }));
404
507
 
508
+ // Terminal state for a cancelled call — without this subscription, a call
509
+ // cancelled mid-flight (e.g. via Ctrl+C) never gets an endMs and shows as
510
+ // "running" forever.
511
+ this.unsubs.push(this.toolEvents.on('TOOL_CANCELLED', (data) => {
512
+ const rec = this.records.findLast(r => r.callId === data.callId);
513
+ if (rec) {
514
+ rec.endMs = Date.now();
515
+ rec.cancelled = true;
516
+ rec.resultSummary = data.reason ?? 'cancelled';
517
+ }
518
+ this.markDirty();
519
+ }));
520
+
405
521
  this.unsubs.push(this.turnEvents.on('TURN_ERROR', (data) => {
406
522
  // Mark any running calls as errored
407
523
  for (const rec of this.records) {
@@ -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.
@@ -28,7 +43,28 @@ export type NamedKey =
28
43
  */
29
44
  export type KeyName = NamedKey | (string & {});
30
45
 
31
- export type PanelCategory = 'development' | 'agent' | 'monitoring' | 'session' | 'ai';
46
+ /**
47
+ * WO-152: the former single 'monitoring' bucket held 33 panels pre-merge —
48
+ * too coarse for the picker to be useful and too large for any one operator
49
+ * mental model. Split into named operator domains so each category holds a
50
+ * bounded, coherent set (no category may exceed 10 registrations):
51
+ * - providers: provider/model connectivity, cost, and token usage
52
+ * - security-policy: auth, policy governance, and isolation posture
53
+ * - automation-control: hooks, plugins, marketplace, automation jobs, worktrees
54
+ * - incidents-diagnostics: failure review, eval gates, API call debugging
55
+ * - runtime-ops: live operator consoles (cockpit, tasks, orchestration, comms)
56
+ * 'development' | 'agent' | 'session' | 'ai' are unchanged from before the split.
57
+ */
58
+ export type PanelCategory =
59
+ | 'development'
60
+ | 'agent'
61
+ | 'session'
62
+ | 'ai'
63
+ | 'providers'
64
+ | 'security-policy'
65
+ | 'automation-control'
66
+ | 'incidents-diagnostics'
67
+ | 'runtime-ops';
32
68
 
33
69
  export interface Panel {
34
70
  id: string;
@@ -67,15 +103,40 @@ export interface Panel {
67
103
  // Scroll input (optional)
68
104
  // Positive delta scrolls down; negative delta scrolls up.
69
105
  handleScroll?(deltaRows: number): boolean;
106
+
107
+ /**
108
+ * Cross-panel integration hook (optional). Called before the panel's own
109
+ * `handleInput` when a navigation/confirm key is pressed, so a panel can
110
+ * drive another panel (e.g. open a file in the preview panel). Return `true`
111
+ * to consume the key. The legacy `handlePanelIntegrationAction` router in
112
+ * `src/input/panel-integration-actions.ts` consults this first, then falls
113
+ * back to its built-in `instanceof` routing.
114
+ */
115
+ handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
70
116
  }
71
117
 
72
118
  export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
73
119
  factory: () => Panel;
74
120
  description: string;
75
121
  /**
76
- * Instantiate this panel during bootstrap and retain the instance when it is
77
- * closed so its background data continues to accumulate before the user
78
- * actively opens the workspace.
122
+ * WO-152 lifecycle flags. `preload` and `retainOnClose` are independent
123
+ * a panel can eagerly instantiate without being kept alive on close, or be
124
+ * kept alive on close without eager bootstrap instantiation. They happened
125
+ * to be identical for all builtin panels before this split (both driven by
126
+ * a single `preload: true`); that existing 10-panel retained set is
127
+ * preserved unchanged by giving each of those 10 registrations both flags.
128
+ *
129
+ * - `preload` — instantiate this panel during `PanelManager.prewarmRegistered()`
130
+ * (called once at bootstrap) so its factory runs and it starts
131
+ * accumulating background data (subscriptions, timers) before the user
132
+ * ever opens the workspace. Panels without `preload` are instantiated
133
+ * lazily on first `open()`.
134
+ * - `retainOnClose` — when this panel is closed, keep the live instance in
135
+ * `PanelManager`'s retained-panel map (background subscriptions/timers
136
+ * keep running) instead of calling `onDestroy()`. Reopening returns the
137
+ * same instance with its accumulated state intact. Panels without
138
+ * `retainOnClose` are destroyed on close and rebuilt fresh next open.
79
139
  */
80
140
  preload?: boolean;
141
+ retainOnClose?: boolean;
81
142
  }