@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.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- 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
|
-
|
|
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
|
|
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
|
|
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':
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
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
|
-
['
|
|
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
|
-
|
|
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
|
|
292
|
-
const
|
|
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: `
|
|
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.
|
|
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) {
|
package/src/panels/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
}
|