@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
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createEmptyLine,
|
|
1
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
2
|
+
import { createEmptyLine, type Line } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import type { TurnEvent } from '@/runtime/index.ts';
|
|
4
5
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
5
6
|
import type { Orchestrator } from '../core/orchestrator';
|
|
6
7
|
import {
|
|
8
|
+
buildAlignedRow,
|
|
9
|
+
buildBodyText,
|
|
7
10
|
buildEmptyState,
|
|
11
|
+
buildKeyboardHints,
|
|
12
|
+
buildKeyValueLine,
|
|
8
13
|
buildPanelLine,
|
|
9
|
-
buildStyledPanelLine,
|
|
10
14
|
buildPanelWorkspace,
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
buildStyledPanelLine,
|
|
16
|
+
resolvePrimaryScrollableSection,
|
|
13
17
|
DEFAULT_PANEL_PALETTE,
|
|
14
18
|
extendPalette,
|
|
19
|
+
type ColumnSpec,
|
|
15
20
|
type PanelWorkspaceSection,
|
|
16
21
|
} from './polish.ts';
|
|
22
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
23
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
24
|
+
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
25
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
17
26
|
|
|
18
27
|
// ---------------------------------------------------------------------------
|
|
19
28
|
// Types
|
|
@@ -34,7 +43,12 @@ export interface ApiCallEntry {
|
|
|
34
43
|
outputTokens: number;
|
|
35
44
|
/** End-to-end latency in ms (stream-start → llm-response, or turn-start → llm-response). */
|
|
36
45
|
latencyMs: number;
|
|
37
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* HTTP-level status code hint. Only ever set from a real signal (scraped
|
|
48
|
+
* from an error message); 0 means no real code is known. Never fabricated
|
|
49
|
+
* (WO-137 dropped the old always-200-on-success convention — this layer
|
|
50
|
+
* doesn't actually observe HTTP status codes on success).
|
|
51
|
+
*/
|
|
38
52
|
statusCode: number;
|
|
39
53
|
/** 'ok' | 'error' */
|
|
40
54
|
status: ApiCallStatus;
|
|
@@ -48,6 +62,7 @@ export interface ApiCallEntry {
|
|
|
48
62
|
|
|
49
63
|
const MAX_CALL_LOG = 50;
|
|
50
64
|
const MAX_ERROR_LOG = 20;
|
|
65
|
+
const AGES_REFRESH_MS = 1_000;
|
|
51
66
|
|
|
52
67
|
// ---------------------------------------------------------------------------
|
|
53
68
|
// Colors
|
|
@@ -68,25 +83,29 @@ const LATENCY_BAD_MS = 5_000;
|
|
|
68
83
|
// ---------------------------------------------------------------------------
|
|
69
84
|
|
|
70
85
|
function fmtTok(n: number): string {
|
|
71
|
-
|
|
72
|
-
if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
|
|
73
|
-
return (n / 1_000_000).toFixed(2) + 'M';
|
|
86
|
+
return abbreviateCount(n, { guard: 10_000, decimals: 1, mDecimals: 2 });
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
function fmtMs(ms: number): string {
|
|
77
|
-
|
|
78
|
-
if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
79
|
-
if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
80
|
-
return `${Math.round(ms)}ms`;
|
|
90
|
+
return formatLatencyMs(ms);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
function fmtAgo(ts: number): string {
|
|
84
|
-
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
94
|
+
const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
85
95
|
if (sec < 60) return `${sec}s ago`;
|
|
86
96
|
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
87
97
|
return `${Math.floor(sec / 3600)}h ago`;
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
function fmtUsd(value: number): string {
|
|
101
|
+
if (value <= 0) return '$0';
|
|
102
|
+
return value >= 1 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function callCost(entry: ApiCallEntry): number {
|
|
106
|
+
return calcSessionCost(entry.inputTokens, entry.outputTokens, 0, 0, entry.model);
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
function latColor(ms: number): string {
|
|
91
110
|
if (ms >= LATENCY_BAD_MS) return C.bad;
|
|
92
111
|
if (ms >= LATENCY_WARN_MS) return C.warn;
|
|
@@ -98,6 +117,17 @@ function statusCodeFromError(msg: string): number {
|
|
|
98
117
|
return m ? parseInt(m[1]!, 10) : 0;
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
const CALL_COLUMNS: ColumnSpec[] = [
|
|
121
|
+
{ width: 8 }, // time
|
|
122
|
+
{ width: 1 }, // status glyph
|
|
123
|
+
{ width: 12 }, // provider
|
|
124
|
+
{ width: 26 }, // model (+ scraped status code suffix)
|
|
125
|
+
{ width: 8, align: 'right' }, // in
|
|
126
|
+
{ width: 8, align: 'right' }, // out
|
|
127
|
+
{ width: 8, align: 'right' }, // latency
|
|
128
|
+
{ width: 9, align: 'right' }, // cost
|
|
129
|
+
];
|
|
130
|
+
|
|
101
131
|
// ---------------------------------------------------------------------------
|
|
102
132
|
// DebugPanel
|
|
103
133
|
// ---------------------------------------------------------------------------
|
|
@@ -105,12 +135,16 @@ function statusCodeFromError(msg: string): number {
|
|
|
105
135
|
/**
|
|
106
136
|
* Real-time API debug panel.
|
|
107
137
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
138
|
+
* Migrated onto ScrollableListPanel<ApiCallEntry> (WO-137) so nav
|
|
139
|
+
* (up/down/j/k/pageup/pagedown/g/G), the '/' inline filter, and a
|
|
140
|
+
* selected-row detail section come from the shared base class instead of a
|
|
141
|
+
* bespoke render (SystemMessagesPanel precedent).
|
|
110
142
|
*
|
|
111
|
-
*
|
|
143
|
+
* Shows per-call log (model, provider, input/output tokens, latency, status,
|
|
144
|
+
* cost), running session call total, and error history. Subscribes to typed
|
|
145
|
+
* turn runtime events.
|
|
112
146
|
*/
|
|
113
|
-
export class DebugPanel extends
|
|
147
|
+
export class DebugPanel extends ScrollableListPanel<ApiCallEntry> {
|
|
114
148
|
private _unsubs: Array<() => void> = [];
|
|
115
149
|
|
|
116
150
|
// Timing state
|
|
@@ -122,17 +156,32 @@ export class DebugPanel extends BasePanel {
|
|
|
122
156
|
private _prevInput = 0;
|
|
123
157
|
private _prevOutput = 0;
|
|
124
158
|
|
|
159
|
+
// Real provider/model attribution for TURN_ERROR rows: the provider/model
|
|
160
|
+
// named on the in-flight LLM request is the authoritative attribution
|
|
161
|
+
// target when the turn subsequently errors (provider-health-panel.ts
|
|
162
|
+
// precedent) — never a fabricated 'unknown'.
|
|
163
|
+
private _inflightProvider: string | null = null;
|
|
164
|
+
private _inflightModel: string | null = null;
|
|
165
|
+
|
|
125
166
|
// Session data
|
|
126
167
|
private _calls: ApiCallEntry[] = [];
|
|
127
168
|
private _errors: ApiCallEntry[] = [];
|
|
128
169
|
private _totalCalls = 0;
|
|
129
170
|
private _totalErrors = 0;
|
|
130
171
|
|
|
172
|
+
// c=clear confirmation
|
|
173
|
+
private confirm: ConfirmState<'clear'> | null = null;
|
|
174
|
+
|
|
175
|
+
// Re-render timer so "Xs ago" ages stay live even without new events.
|
|
176
|
+
private agesTimerId: ReturnType<typeof setInterval> | null = null;
|
|
177
|
+
|
|
131
178
|
constructor(
|
|
132
179
|
private readonly turnEvents: UiEventFeed<TurnEvent>,
|
|
133
180
|
private readonly requestRender: () => void = () => {},
|
|
134
181
|
) {
|
|
135
|
-
super('debug', 'Debug', '
|
|
182
|
+
super('debug', 'Debug', '▧', 'incidents-diagnostics');
|
|
183
|
+
this.filterEnabled = true;
|
|
184
|
+
this.filterLabel = 'Filter calls';
|
|
136
185
|
this._subscribe();
|
|
137
186
|
}
|
|
138
187
|
|
|
@@ -166,6 +215,13 @@ export class DebugPanel extends BasePanel {
|
|
|
166
215
|
}),
|
|
167
216
|
);
|
|
168
217
|
|
|
218
|
+
this._unsubs.push(
|
|
219
|
+
this.turnEvents.on('LLM_REQUEST_STARTED', (env) => {
|
|
220
|
+
this._inflightProvider = env.provider;
|
|
221
|
+
this._inflightModel = env.model;
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
169
225
|
this._unsubs.push(
|
|
170
226
|
this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
|
|
171
227
|
const now = Date.now();
|
|
@@ -176,6 +232,8 @@ export class DebugPanel extends BasePanel {
|
|
|
176
232
|
? now - this._turnStartMs
|
|
177
233
|
: 0;
|
|
178
234
|
this._streamStartMs = null;
|
|
235
|
+
this._inflightProvider = null;
|
|
236
|
+
this._inflightModel = null;
|
|
179
237
|
|
|
180
238
|
// Compute per-call token delta if orchestrator is wired
|
|
181
239
|
let inputTokens = env.inputTokens + (env.cacheReadTokens ?? 0) + (env.cacheWriteTokens ?? 0);
|
|
@@ -195,7 +253,9 @@ export class DebugPanel extends BasePanel {
|
|
|
195
253
|
inputTokens,
|
|
196
254
|
outputTokens,
|
|
197
255
|
latencyMs,
|
|
198
|
-
|
|
256
|
+
// WO-137: no fabricated 200 — this layer never observes a real
|
|
257
|
+
// HTTP status code on success, so 0 means "no code, just OK".
|
|
258
|
+
statusCode: 0,
|
|
199
259
|
status: 'ok',
|
|
200
260
|
};
|
|
201
261
|
this._pushCall(entry);
|
|
@@ -214,8 +274,8 @@ export class DebugPanel extends BasePanel {
|
|
|
214
274
|
|
|
215
275
|
const entry: ApiCallEntry = {
|
|
216
276
|
ts: Date.now(),
|
|
217
|
-
provider: '
|
|
218
|
-
model: '
|
|
277
|
+
provider: this._inflightProvider ?? 'n/a',
|
|
278
|
+
model: this._inflightModel ?? 'n/a',
|
|
219
279
|
inputTokens: 0,
|
|
220
280
|
outputTokens: 0,
|
|
221
281
|
latencyMs: 0,
|
|
@@ -223,18 +283,42 @@ export class DebugPanel extends BasePanel {
|
|
|
223
283
|
status: 'error',
|
|
224
284
|
errorMessage: msg.slice(0, 120),
|
|
225
285
|
};
|
|
286
|
+
this._inflightProvider = null;
|
|
287
|
+
this._inflightModel = null;
|
|
226
288
|
this._pushCall(entry);
|
|
227
289
|
this._pushError(entry);
|
|
228
290
|
this.markDirty();
|
|
229
291
|
this.requestRender();
|
|
230
292
|
}),
|
|
231
293
|
);
|
|
294
|
+
|
|
295
|
+
this._unsubs.push(
|
|
296
|
+
this.turnEvents.on('TURN_COMPLETED', () => {
|
|
297
|
+
this._inflightProvider = null;
|
|
298
|
+
this._inflightModel = null;
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
this._unsubs.push(
|
|
303
|
+
this.turnEvents.on('TURN_CANCEL', () => {
|
|
304
|
+
this._inflightProvider = null;
|
|
305
|
+
this._inflightModel = null;
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
232
308
|
}
|
|
233
309
|
|
|
234
310
|
private _pushCall(entry: ApiCallEntry): void {
|
|
235
311
|
this._totalCalls++;
|
|
312
|
+
// Follow-mode: only auto-jump to the new row when the selection was
|
|
313
|
+
// already at (or past) the previous tail — mirrors the SystemMessagesPanel
|
|
314
|
+
// "don't yank the cursor while the user is reviewing history" contract.
|
|
315
|
+
const wasAtTail = this._calls.length === 0 || this.selectedIndex >= this._calls.length - 1;
|
|
236
316
|
this._calls.push(entry);
|
|
237
|
-
if (this._calls.length > MAX_CALL_LOG)
|
|
317
|
+
if (this._calls.length > MAX_CALL_LOG) {
|
|
318
|
+
this._calls.shift();
|
|
319
|
+
if (this.selectedIndex > 0) this.selectedIndex--;
|
|
320
|
+
}
|
|
321
|
+
if (wasAtTail) this.selectedIndex = this._calls.length - 1;
|
|
238
322
|
}
|
|
239
323
|
|
|
240
324
|
private _pushError(entry: ApiCallEntry): void {
|
|
@@ -247,80 +331,232 @@ export class DebugPanel extends BasePanel {
|
|
|
247
331
|
// Lifecycle
|
|
248
332
|
// -------------------------------------------------------------------------
|
|
249
333
|
|
|
334
|
+
override onActivate(): void {
|
|
335
|
+
super.onActivate();
|
|
336
|
+
this._startAgesTimer();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
override onDeactivate(): void {
|
|
340
|
+
this._stopAgesTimer();
|
|
341
|
+
}
|
|
342
|
+
|
|
250
343
|
override onDestroy(): void {
|
|
344
|
+
this._stopAgesTimer();
|
|
251
345
|
for (const unsub of this._unsubs) unsub();
|
|
252
346
|
this._unsubs = [];
|
|
253
347
|
}
|
|
254
348
|
|
|
349
|
+
private _startAgesTimer(): void {
|
|
350
|
+
if (this.agesTimerId) return;
|
|
351
|
+
this.agesTimerId = this.registerTimer(setInterval(() => {
|
|
352
|
+
if (this._calls.length === 0) return;
|
|
353
|
+
this.markDirty();
|
|
354
|
+
this.requestRender();
|
|
355
|
+
}, AGES_REFRESH_MS));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private _stopAgesTimer(): void {
|
|
359
|
+
if (this.agesTimerId) {
|
|
360
|
+
this.clearTimer(this.agesTimerId);
|
|
361
|
+
this.agesTimerId = null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// -------------------------------------------------------------------------
|
|
366
|
+
// ScrollableListPanel contract
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
protected getItems(): readonly ApiCallEntry[] {
|
|
370
|
+
return this._calls;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
protected override getPalette() { return C; }
|
|
374
|
+
protected override getEmptyStateMessage() { return ' No calls yet'; }
|
|
375
|
+
|
|
376
|
+
protected override filterMatches(entry: ApiCallEntry, q: string): boolean {
|
|
377
|
+
return entry.provider.toLowerCase().includes(q)
|
|
378
|
+
|| entry.model.toLowerCase().includes(q)
|
|
379
|
+
|| entry.status.includes(q)
|
|
380
|
+
|| (entry.errorMessage?.toLowerCase().includes(q) ?? false);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected renderItem(entry: ApiCallEntry, _index: number, selected: boolean, width: number): Line {
|
|
384
|
+
const statusChar = entry.status === 'ok' ? '✓' : '✕';
|
|
385
|
+
const statusFg = entry.status === 'ok' ? C.good : C.bad;
|
|
386
|
+
const codeSuffix = entry.status === 'error' && entry.statusCode > 0 ? ` [${entry.statusCode}]` : '';
|
|
387
|
+
return buildAlignedRow(
|
|
388
|
+
width,
|
|
389
|
+
[
|
|
390
|
+
{ text: fmtAgo(entry.ts), fg: C.dim },
|
|
391
|
+
{ text: statusChar, fg: statusFg },
|
|
392
|
+
{ text: entry.provider, fg: C.provName },
|
|
393
|
+
{ text: entry.model + codeSuffix, fg: C.value },
|
|
394
|
+
{ text: fmtTok(entry.inputTokens), fg: C.input },
|
|
395
|
+
{ text: fmtTok(entry.outputTokens), fg: C.output },
|
|
396
|
+
{ text: fmtMs(entry.latencyMs), fg: latColor(entry.latencyMs) },
|
|
397
|
+
{ text: fmtUsd(callCost(entry)), fg: C.value },
|
|
398
|
+
],
|
|
399
|
+
CALL_COLUMNS,
|
|
400
|
+
{ selected, selectedBg: C.selectBg },
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// -------------------------------------------------------------------------
|
|
405
|
+
// Input
|
|
406
|
+
// -------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
handleInput(key: string): boolean {
|
|
409
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
410
|
+
if (confirmResult === 'confirmed') {
|
|
411
|
+
this.confirm = null;
|
|
412
|
+
this._calls = [];
|
|
413
|
+
this._errors = [];
|
|
414
|
+
this._totalCalls = 0;
|
|
415
|
+
this._totalErrors = 0;
|
|
416
|
+
this.selectedIndex = 0;
|
|
417
|
+
this.markDirty();
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (confirmResult === 'cancelled') {
|
|
421
|
+
this.confirm = null;
|
|
422
|
+
this.markDirty();
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
if (confirmResult === 'absorbed') return true;
|
|
426
|
+
|
|
427
|
+
if (!this.filterActive && key === 'c') {
|
|
428
|
+
if (this._calls.length === 0) return false;
|
|
429
|
+
this.confirm = { subject: 'clear', label: `${this._calls.length} API call(s)`, verb: 'Clear' };
|
|
430
|
+
this.markDirty();
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return super.handleInput(key);
|
|
435
|
+
}
|
|
436
|
+
|
|
255
437
|
// -------------------------------------------------------------------------
|
|
256
438
|
// Rendering
|
|
257
439
|
// -------------------------------------------------------------------------
|
|
258
440
|
|
|
259
441
|
override render(width: number, height: number): Line[] {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
442
|
+
this.clampSelection();
|
|
443
|
+
|
|
444
|
+
if (this.confirm) {
|
|
445
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
446
|
+
title: ' API Debug',
|
|
447
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
448
|
+
palette: C,
|
|
449
|
+
});
|
|
450
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
451
|
+
return lines.slice(0, height);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const intro = 'Recent provider calls, token deltas, latency, status codes, and cost, with error history.';
|
|
266
455
|
|
|
267
456
|
if (this._calls.length === 0) {
|
|
268
|
-
|
|
269
|
-
title: 'API
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
457
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
458
|
+
title: ' API Debug',
|
|
459
|
+
intro,
|
|
460
|
+
sections: [{
|
|
461
|
+
title: 'Session',
|
|
462
|
+
lines: buildEmptyState(
|
|
463
|
+
width,
|
|
464
|
+
this.getEmptyStateMessage(),
|
|
465
|
+
'Completed provider calls and API failures will appear here with timing, token counts, status, and cost.',
|
|
466
|
+
[],
|
|
467
|
+
C,
|
|
468
|
+
),
|
|
469
|
+
}],
|
|
470
|
+
palette: C,
|
|
277
471
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (this._errors.length > 0) {
|
|
281
|
-
const errors = this._renderErrorHistory(width);
|
|
282
|
-
const [callSection, errorSection] = resolveStackedScrollableSections(width, height, {
|
|
283
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
284
|
-
beforeSections: sections,
|
|
285
|
-
sections: [
|
|
286
|
-
{
|
|
287
|
-
title: 'API Call Log',
|
|
288
|
-
scrollableLines: rows,
|
|
289
|
-
scrollOffset: Math.max(0, rows.length - 1),
|
|
290
|
-
minRows: 8,
|
|
291
|
-
weight: 2,
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
title: 'Error History',
|
|
295
|
-
scrollableLines: errors,
|
|
296
|
-
scrollOffset: Math.max(0, errors.length - 1),
|
|
297
|
-
minRows: 4,
|
|
298
|
-
weight: 1,
|
|
299
|
-
},
|
|
300
|
-
],
|
|
301
|
-
});
|
|
302
|
-
sections.push(callSection!.section, errorSection!.section);
|
|
303
|
-
} else {
|
|
304
|
-
const callSection = resolveScrollablePanelSection(width, height, {
|
|
305
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
306
|
-
beforeSections: sections,
|
|
307
|
-
section: {
|
|
308
|
-
title: 'API Call Log',
|
|
309
|
-
scrollableLines: rows,
|
|
310
|
-
scrollOffset: Math.max(0, rows.length - 1),
|
|
311
|
-
minRows: 8,
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
sections.push(callSection.section);
|
|
315
|
-
}
|
|
472
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
473
|
+
return lines.slice(0, height);
|
|
316
474
|
}
|
|
317
475
|
|
|
318
|
-
|
|
476
|
+
const summarySection: PanelWorkspaceSection = { title: 'Session', lines: this._renderSummary(width) };
|
|
477
|
+
const filterSection: PanelWorkspaceSection = { lines: [this.buildFilterLine(width)] };
|
|
478
|
+
|
|
479
|
+
const visible = this.getVisibleItems();
|
|
480
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, visible.length - 1)));
|
|
481
|
+
const selected = this.getSelectedItem();
|
|
482
|
+
|
|
483
|
+
const rows: Line[] = visible.length > 0
|
|
484
|
+
? visible.map((entry, index) => this.renderItem(entry, index, index === this.selectedIndex, width))
|
|
485
|
+
: [buildPanelLine(width, [[` No calls match "${this.filterQuery.trim()}" (Esc to clear)`, C.dim]])];
|
|
486
|
+
|
|
487
|
+
const detailSection: PanelWorkspaceSection = selected
|
|
488
|
+
? {
|
|
489
|
+
title: 'Selected Call',
|
|
490
|
+
lines: [
|
|
491
|
+
buildKeyValueLine(width, [
|
|
492
|
+
{ label: 'provider', value: selected.provider, valueColor: C.provName },
|
|
493
|
+
{ label: 'model', value: selected.model, valueColor: C.value },
|
|
494
|
+
{ label: 'status', value: selected.status === 'ok' ? 'OK' : 'ERROR', valueColor: selected.status === 'ok' ? C.good : C.bad },
|
|
495
|
+
{ label: 'when', value: fmtAgo(selected.ts), valueColor: C.dim },
|
|
496
|
+
], C),
|
|
497
|
+
buildKeyValueLine(width, [
|
|
498
|
+
{ label: 'in', value: String(selected.inputTokens), valueColor: C.input },
|
|
499
|
+
{ label: 'out', value: String(selected.outputTokens), valueColor: C.output },
|
|
500
|
+
{ label: 'latency', value: fmtMs(selected.latencyMs), valueColor: latColor(selected.latencyMs) },
|
|
501
|
+
{ label: 'cost', value: fmtUsd(callCost(selected)), valueColor: C.value },
|
|
502
|
+
], C),
|
|
503
|
+
...(selected.errorMessage
|
|
504
|
+
? buildBodyText(width, selected.errorMessage, C, C.bad)
|
|
505
|
+
: []),
|
|
506
|
+
],
|
|
507
|
+
}
|
|
508
|
+
: { title: 'Selected Call', lines: [] };
|
|
509
|
+
|
|
510
|
+
const latestError = this._errors[this._errors.length - 1];
|
|
511
|
+
const hints = this.filterActive
|
|
512
|
+
? [
|
|
513
|
+
{ keys: 'type', label: 'filter calls' },
|
|
514
|
+
{ keys: 'Enter', label: 'apply' },
|
|
515
|
+
{ keys: 'Esc', label: 'clear' },
|
|
516
|
+
]
|
|
517
|
+
: [
|
|
518
|
+
{ keys: '↑/↓', label: 'select call' },
|
|
519
|
+
{ keys: '/', label: 'filter' },
|
|
520
|
+
{ keys: 'c', label: 'clear log' },
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
const footerLines: Line[] = [
|
|
524
|
+
...(latestError
|
|
525
|
+
? [buildStyledPanelLine(width, [
|
|
526
|
+
{ text: ' ✕ latest error ', fg: C.bad },
|
|
527
|
+
{ text: truncateDisplay(latestError.errorMessage ?? 'unknown error', Math.max(0, width - 18)), fg: C.dim },
|
|
528
|
+
])]
|
|
529
|
+
: []),
|
|
530
|
+
buildKeyboardHints(width, hints, C),
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
const callSection = resolvePrimaryScrollableSection(width, height, {
|
|
534
|
+
intro,
|
|
535
|
+
footerLines,
|
|
536
|
+
palette: C,
|
|
537
|
+
beforeSections: [summarySection, filterSection],
|
|
538
|
+
section: {
|
|
539
|
+
title: `API Call Log (${visible.length} of ${this._calls.length})`,
|
|
540
|
+
scrollableLines: rows,
|
|
541
|
+
selectedIndex: this.selectedIndex,
|
|
542
|
+
scrollOffset: this.scrollStart,
|
|
543
|
+
minRows: 6,
|
|
544
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
545
|
+
},
|
|
546
|
+
afterSections: [detailSection],
|
|
547
|
+
});
|
|
548
|
+
this.scrollStart = callSection.scrollOffset;
|
|
549
|
+
|
|
550
|
+
this.needsRender = false;
|
|
551
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
319
552
|
title: ' API Debug',
|
|
320
|
-
intro
|
|
321
|
-
sections,
|
|
322
|
-
|
|
553
|
+
intro,
|
|
554
|
+
sections: [summarySection, filterSection, callSection.section, detailSection],
|
|
555
|
+
footerLines,
|
|
556
|
+
palette: C,
|
|
323
557
|
});
|
|
558
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
559
|
+
return lines.slice(0, height);
|
|
324
560
|
}
|
|
325
561
|
|
|
326
562
|
// -------------------------------------------------------------------------
|
|
@@ -330,92 +566,44 @@ export class DebugPanel extends BasePanel {
|
|
|
330
566
|
private _renderSummary(width: number): Line[] {
|
|
331
567
|
const errCount = this._totalErrors;
|
|
332
568
|
const okCount = this._totalCalls - this._totalErrors;
|
|
333
|
-
|
|
569
|
+
const last = this._calls[this._calls.length - 1];
|
|
570
|
+
const recent = this._calls.slice(-10);
|
|
571
|
+
const avgLat = recent.length > 0
|
|
572
|
+
? Math.round(recent.reduce((s, c) => s + c.latencyMs, 0) / recent.length)
|
|
573
|
+
: 0;
|
|
574
|
+
const sessionTokens = this._calls.reduce((s, c) => s + c.inputTokens + c.outputTokens, 0);
|
|
575
|
+
const sessionCost = this._calls.reduce((s, c) => s + callCost(c), 0);
|
|
576
|
+
|
|
577
|
+
const lines: Line[] = [
|
|
334
578
|
buildStyledPanelLine(width, [
|
|
335
|
-
{ text: ' Calls
|
|
579
|
+
{ text: ' Calls ', fg: C.label },
|
|
336
580
|
{ text: String(this._totalCalls), fg: C.value },
|
|
337
|
-
{ text: '
|
|
581
|
+
{ text: ' OK ', fg: C.label },
|
|
338
582
|
{ text: String(okCount), fg: C.good },
|
|
339
|
-
{ text: '
|
|
583
|
+
{ text: ' Errors ', fg: C.label },
|
|
340
584
|
{ text: String(errCount), fg: errCount > 0 ? C.bad : C.dim },
|
|
585
|
+
{ text: ' Avg latency ', fg: C.label },
|
|
586
|
+
{ text: fmtMs(avgLat), fg: avgLat > 0 ? latColor(avgLat) : C.dim },
|
|
587
|
+
{ text: ' Tokens ', fg: C.label },
|
|
588
|
+
{ text: fmtTok(sessionTokens), fg: C.value },
|
|
589
|
+
{ text: ' Cost ', fg: C.label },
|
|
590
|
+
{ text: fmtUsd(sessionCost), fg: C.value },
|
|
341
591
|
]),
|
|
342
592
|
];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
593
|
+
// Live status: most recent call (latency / age) or wiring hint.
|
|
594
|
+
if (last) {
|
|
595
|
+
lines.push(buildStyledPanelLine(width, [
|
|
596
|
+
{ text: ' Last ', fg: C.label },
|
|
597
|
+
{ text: last.status === 'ok' ? '✓ ' : '✕ ', fg: last.status === 'ok' ? C.good : C.bad },
|
|
598
|
+
{ text: fitDisplay(last.model, 22), fg: C.value },
|
|
599
|
+
{ text: ' ' + fmtMs(last.latencyMs), fg: latColor(last.latencyMs) },
|
|
600
|
+
{ text: ' ' + fmtAgo(last.ts), fg: C.dim },
|
|
601
|
+
]));
|
|
602
|
+
} else if (!this._orchestrator) {
|
|
603
|
+
lines.push(buildStyledPanelLine(width, [
|
|
604
|
+
{ text: ' Per-call token deltas need the orchestrator wired (wireOrchestrator).', fg: C.dim },
|
|
605
|
+
]));
|
|
350
606
|
}
|
|
351
|
-
|
|
352
607
|
return lines;
|
|
353
608
|
}
|
|
354
|
-
|
|
355
|
-
private _callLogHeader(width: number): Line {
|
|
356
|
-
// Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
|
|
357
|
-
const header = ' Time S Provider Model In Out Lat';
|
|
358
|
-
return this._textLine(header.slice(0, width), C.colHdr, width, { dim: true });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private _callLogRow(e: ApiCallEntry, width: number): Line {
|
|
362
|
-
const timeStr = fmtAgo(e.ts).padEnd(8);
|
|
363
|
-
const statusChar = e.status === 'ok' ? '✓' : '✕';
|
|
364
|
-
const statusFg = e.status === 'ok' ? C.good : C.bad;
|
|
365
|
-
const provStr = e.provider.slice(0, 11).padEnd(12);
|
|
366
|
-
const modelStr = e.model.slice(0, 19).padEnd(20);
|
|
367
|
-
const inStr = fmtTok(e.inputTokens).padStart(8);
|
|
368
|
-
const outStr = fmtTok(e.outputTokens).padStart(8);
|
|
369
|
-
const latStr = fmtMs(e.latencyMs).padStart(8);
|
|
370
|
-
|
|
371
|
-
const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
|
|
372
|
-
{ text: ' ' + timeStr, fg: C.dim },
|
|
373
|
-
{ text: statusChar + ' ', fg: statusFg },
|
|
374
|
-
{ text: provStr, fg: C.provName },
|
|
375
|
-
{ text: modelStr, fg: C.value },
|
|
376
|
-
{ text: inStr, fg: C.input },
|
|
377
|
-
{ text: outStr, fg: C.output },
|
|
378
|
-
{ text: latStr, fg: latColor(e.latencyMs) },
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
// Append status code for errors
|
|
382
|
-
if (e.status === 'error' && e.statusCode > 0) {
|
|
383
|
-
segments.push({ text: ` [${e.statusCode}]`, fg: C.bad });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return buildStyledPanelLine(
|
|
387
|
-
width,
|
|
388
|
-
segments.map((seg) => ({ text: seg.text, fg: seg.fg, bold: seg.bold })),
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private _renderErrorHistory(width: number): Line[] {
|
|
393
|
-
const lines: Line[] = [];
|
|
394
|
-
for (const e of this._errors) {
|
|
395
|
-
lines.push(this._errorRow(e, width));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return lines;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private _errorRow(e: ApiCallEntry, width: number): Line {
|
|
402
|
-
const timeStr = fmtAgo(e.ts).padEnd(8);
|
|
403
|
-
const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
|
|
404
|
-
const msgStr = (e.errorMessage ?? 'unknown error').slice(0, width - 12 - codeStr.length);
|
|
405
|
-
const full = ` ${timeStr} ${codeStr}${msgStr}`;
|
|
406
|
-
return this._textLine(full.slice(0, width), C.bad, width);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// -------------------------------------------------------------------------
|
|
410
|
-
// Line-builder helpers
|
|
411
|
-
// -------------------------------------------------------------------------
|
|
412
|
-
|
|
413
|
-
private _textLine(
|
|
414
|
-
text: string,
|
|
415
|
-
fg: string,
|
|
416
|
-
width: number,
|
|
417
|
-
opts: { dim?: boolean } = {},
|
|
418
|
-
): Line {
|
|
419
|
-
return buildStyledPanelLine(width, [{ text, fg, dim: opts.dim }]);
|
|
420
|
-
}
|
|
421
609
|
}
|