@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. 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,6 +17,7 @@ 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';
20
22
  import { formatDuration } from '../utils/format-duration.ts';
21
23
 
@@ -33,15 +35,23 @@ interface ToolCallRecord {
33
35
  callId: string;
34
36
  tool: string;
35
37
  args: Record<string, unknown>;
38
+ /** Wall-clock time the call was received from the LLM (queued, not yet executing). */
36
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;
37
48
  endMs?: number;
38
49
  result?: unknown;
39
50
  error?: string;
51
+ cancelled?: boolean;
40
52
  expanded: boolean;
41
53
  approved?: boolean;
42
54
  outputClass?: string;
43
- policyAction?: string;
44
- spillBackend?: string;
45
55
  resultSummary?: string;
46
56
  }
47
57
 
@@ -80,6 +90,20 @@ function summarizeResult(result: unknown): string | undefined {
80
90
  return undefined;
81
91
  }
82
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
+
83
107
  function detectOutputClass(tool: string, args: Record<string, unknown>): string {
84
108
  const name = tool.toLowerCase();
85
109
  if (name.includes('read') || name.includes('find') || name.includes('inspect') || name.includes('grep')) return 'read';
@@ -97,6 +121,10 @@ export class ToolInspectorPanel extends BasePanel {
97
121
  private scrollOffset = 0;
98
122
  private autoScroll = true;
99
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;
100
128
 
101
129
  constructor(
102
130
  private readonly toolEvents: UiEventFeed<ToolEvent>,
@@ -124,19 +152,56 @@ export class ToolInspectorPanel extends BasePanel {
124
152
  }
125
153
 
126
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
+
127
169
  switch (key) {
128
170
  case 'up': this._move(-1); this.autoScroll = false; return true;
129
171
  case 'down': this._move(1); return true;
130
172
  case 'pageup': this._move(-10); this.autoScroll = false; return true;
131
173
  case 'pagedown': this._move(10); return true;
132
174
  case 'return': this._toggleExpand(); return true;
133
- 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
+ }
134
181
  case 'g': this.autoScroll = true; this.markDirty(); return true;
135
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
+ }
136
189
  default: return false;
137
190
  }
138
191
  }
139
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
+
140
205
  render(width: number, height: number): Line[] {
141
206
  if (height <= 0 || width <= 0) return [];
142
207
 
@@ -157,8 +222,20 @@ export class ToolInspectorPanel extends BasePanel {
157
222
  footerSegments.push([' c', DEFAULT_PANEL_PALETTE.info], [' clear', DEFAULT_PANEL_PALETTE.dim]);
158
223
  footerSegments.push([' g', DEFAULT_PANEL_PALETTE.info], [this.autoScroll ? ' end (live)' : ' jump to end', DEFAULT_PANEL_PALETTE.dim]);
159
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
+ }
160
229
  const footerLines = [buildPanelLine(width, footerSegments)];
161
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
+ }
238
+
162
239
  const flat = this._getFlat();
163
240
 
164
241
  if (this.autoScroll) {
@@ -168,7 +245,7 @@ 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',
@@ -179,7 +256,7 @@ export class ToolInspectorPanel extends BasePanel {
179
256
  : ' No tool calls yet',
180
257
  this.records.length > 0 && this.filterMode !== 'all'
181
258
  ? '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.',
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.',
183
260
  this.records.length > 0 && this.filterMode !== 'all'
184
261
  ? [{ command: 'f', summary: 'cycle the tool filter back to all calls' }]
185
262
  : [{ command: '/spawn <task>', summary: 'run an agent to populate the tool-call timeline' }],
@@ -215,22 +292,14 @@ export class ToolInspectorPanel extends BasePanel {
215
292
  : this.records.filter(r => r.tool === this.filterMode);
216
293
  const rec = filtered[selected.recordIndex];
217
294
  if (rec) {
218
- 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]]));
219
- 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)]]));
220
297
  detailLines.push(buildPanelLine(width, [
221
- [' Risk ', DEFAULT_PANEL_PALETTE.label],
298
+ [' Class ', DEFAULT_PANEL_PALETTE.label],
222
299
  [rec.outputClass ?? detectOutputClass(rec.tool, rec.args), DEFAULT_PANEL_PALETTE.warn],
223
300
  [' Approved ', DEFAULT_PANEL_PALETTE.label],
224
301
  [rec.approved === undefined ? 'unknown' : rec.approved ? 'yes' : 'no', rec.approved === false ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.value],
225
302
  ]));
226
- if (rec.policyAction || rec.spillBackend) {
227
- detailLines.push(buildPanelLine(width, [
228
- [' Output ', DEFAULT_PANEL_PALETTE.label],
229
- [rec.policyAction ?? 'none', rec.policyAction && rec.policyAction !== 'none' ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.value],
230
- [' Spill ', DEFAULT_PANEL_PALETTE.label],
231
- [rec.spillBackend ?? 'none', rec.spillBackend ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
232
- ]));
233
- }
234
303
  if (rec.resultSummary) {
235
304
  detailLines.push(buildPanelLine(width, [[' Summary ', DEFAULT_PANEL_PALETTE.label], [rec.resultSummary, DEFAULT_PANEL_PALETTE.value]]));
236
305
  }
@@ -240,7 +309,7 @@ export class ToolInspectorPanel extends BasePanel {
240
309
 
241
310
  const selectedSection: PanelWorkspaceSection = { title: 'Selected', lines: detailLines };
242
311
  const callsSection = resolveScrollablePanelSection(width, height, {
243
- 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.',
244
313
  footerLines,
245
314
  palette: DEFAULT_PANEL_PALETTE,
246
315
  beforeSections: [summary],
@@ -257,7 +326,7 @@ export class ToolInspectorPanel extends BasePanel {
257
326
 
258
327
  return buildPanelWorkspace(width, height, {
259
328
  title,
260
- 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.',
261
330
  sections: [
262
331
  summary,
263
332
  callsSection.section,
@@ -291,25 +360,24 @@ export class ToolInspectorPanel extends BasePanel {
291
360
 
292
361
  for (let i = 0; i < filtered.length; i++) {
293
362
  const rec = filtered[i]!;
294
- 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)';
295
371
  const expand = rec.expanded ? ' ▾' : ' ▸';
296
372
  const ts = shortTime(rec.startMs);
297
- const risk = rec.outputClass ?? detectOutputClass(rec.tool, rec.args);
298
- const action = rec.policyAction && rec.policyAction !== 'none' ? ` ${rec.policyAction}` : '';
299
- 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}`;
300
375
  flat.push({ kind: 'call', recordIndex: i, text: callText });
301
376
 
302
377
  if (rec.expanded) {
303
378
  const argsStr = truncateJson(rec.args, 200);
304
379
  flat.push({ kind: 'detail', text: `Args: ${argsStr}`, isError: false });
305
- flat.push({ kind: 'detail', text: `Risk: ${risk}${rec.approved === undefined ? '' : ` Approved: ${rec.approved ? 'yes' : 'no'}`}`, isError: false });
306
- if (rec.policyAction || rec.spillBackend) {
307
- flat.push({
308
- kind: 'detail',
309
- text: `Output policy: ${rec.policyAction ?? 'none'}${rec.spillBackend ? ` Spill: ${rec.spillBackend}` : ''}`,
310
- isError: false,
311
- });
312
- }
380
+ flat.push({ kind: 'detail', text: `Class: ${cls}${rec.approved === undefined ? '' : ` Approved: ${rec.approved ? 'yes' : 'no'}`}`, isError: false });
313
381
  if (rec.resultSummary) {
314
382
  flat.push({ kind: 'detail', text: `Summary: ${rec.resultSummary}`, isError: false });
315
383
  }
@@ -338,6 +406,17 @@ export class ToolInspectorPanel extends BasePanel {
338
406
  }
339
407
  }
340
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
+
341
420
  private _cycleFilter(): void {
342
421
  const tools = [...new Set(this.records.map(r => r.tool))];
343
422
  if (tools.length === 0) return;
@@ -350,6 +429,18 @@ export class ToolInspectorPanel extends BasePanel {
350
429
  this.markDirty();
351
430
  }
352
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
+
353
444
  private _move(delta: number): void {
354
445
  const flat = this._getFlat();
355
446
  if (flat.length === 0) return;
@@ -382,11 +473,17 @@ export class ToolInspectorPanel extends BasePanel {
382
473
  this.markDirty();
383
474
  }));
384
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
+
385
485
  // NOTE: After SDK OBS-05 (0.21.31), TOOL_SUCCEEDED/TOOL_FAILED.result is a ToolResultSummary
386
- // ({ kind, byteSize, preview? }) rather than the raw ToolResult object. The previous
387
- // `_policyAudit` extraction is no longer reachable via this event — policy audit metadata
388
- // must be sourced from a different channel (approval broker / tool result store) if the
389
- // Tool Inspector is to display it in future.
486
+ // ({ kind, byteSize, preview? }) rather than the raw ToolResult object.
390
487
  this.unsubs.push(this.toolEvents.on('TOOL_SUCCEEDED', (data) => {
391
488
  const rec = this.records.findLast(r => r.callId === data.callId);
392
489
  if (rec) {
@@ -408,6 +505,19 @@ export class ToolInspectorPanel extends BasePanel {
408
505
  this.markDirty();
409
506
  }));
410
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
+
411
521
  this.unsubs.push(this.turnEvents.on('TURN_ERROR', (data) => {
412
522
  // Mark any running calls as errored
413
523
  for (const rec of this.records) {
@@ -43,7 +43,28 @@ export type NamedKey =
43
43
  */
44
44
  export type KeyName = NamedKey | (string & {});
45
45
 
46
- 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';
47
68
 
48
69
  export interface Panel {
49
70
  id: string;
@@ -98,9 +119,24 @@ export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' |
98
119
  factory: () => Panel;
99
120
  description: string;
100
121
  /**
101
- * Instantiate this panel during bootstrap and retain the instance when it is
102
- * closed so its background data continues to accumulate before the user
103
- * 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.
104
139
  */
105
140
  preload?: boolean;
141
+ retainOnClose?: boolean;
106
142
  }