@pellux/goodvibes-agent 0.1.59 → 0.1.60

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.
@@ -1,223 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
- import { CodeIntelligence } from '@pellux/goodvibes-sdk/platform/intelligence';
5
- import type { DocumentSymbol } from '@pellux/goodvibes-sdk/platform/intelligence';
6
- import type { SymbolInfo } from '@pellux/goodvibes-sdk/platform/intelligence';
7
- import { openCommandPanel, requireReadModels, requireShellPaths } from './runtime-services.ts';
8
-
9
- function resolveTargetPath(pathArg: string, ctx: CommandContext): string {
10
- return requireShellPaths(ctx).resolveWorkspacePath(pathArg);
11
- }
12
-
13
- function parsePosition(lineArg: string | undefined, columnArg: string | undefined): { line: number; column: number } | null {
14
- const line = Number.parseInt(lineArg ?? '', 10);
15
- const column = Number.parseInt(columnArg ?? '', 10);
16
- if (!Number.isFinite(line) || line < 1 || !Number.isFinite(column) || column < 1) return null;
17
- return { line: line - 1, column: column - 1 };
18
- }
19
-
20
- function formatSymbolKind(kind: number | string | undefined): string {
21
- if (typeof kind === 'number') return `kind=${kind}`;
22
- if (typeof kind === 'string' && kind.trim().length > 0) return kind;
23
- return 'symbol';
24
- }
25
-
26
- function formatDocumentSymbol(symbol: DocumentSymbol): string {
27
- const line = (symbol.selectionRange?.start.line ?? symbol.range.start.line) + 1;
28
- const column = (symbol.selectionRange?.start.character ?? symbol.range.start.character) + 1;
29
- return ` ${symbol.name} ${formatSymbolKind(symbol.kind)} ${line}:${column}`;
30
- }
31
-
32
- function formatTreeSitterSymbol(symbol: SymbolInfo): string {
33
- return ` ${symbol.name} ${formatSymbolKind(symbol.kind)} ${symbol.line + 1}:${symbol.column + 1}`;
34
- }
35
-
36
- function ensureExistingFile(pathArg: string | undefined, ctx: CommandContext): string | null {
37
- if (!pathArg) {
38
- ctx.print('Intelligence Review\n Missing file path.');
39
- return null;
40
- }
41
- const targetPath = resolveTargetPath(pathArg, ctx);
42
- if (!existsSync(targetPath)) {
43
- ctx.print(`Intelligence Review\n File not found: ${targetPath}`);
44
- return null;
45
- }
46
- return targetPath;
47
- }
48
-
49
- export function registerIntelligenceRuntimeCommands(registry: CommandRegistry): void {
50
- registry.register({
51
- name: 'intelligence',
52
- aliases: ['intel'],
53
- description: 'Review workspace intelligence readiness, diagnostics posture, and symbol search availability',
54
- usage: '[review|panel|diagnostics [file]|symbols <file>|outline <file>|definition <file> <line> <column>|references <file> <line> <column>|hover <file> <line> <column>|repair]',
55
- async handler(args, ctx) {
56
- const sub = (args[0] ?? 'review').toLowerCase();
57
- if (sub === 'panel' || sub === 'open') {
58
- openCommandPanel(ctx, 'intelligence');
59
- return;
60
- }
61
-
62
- const intelligence = new CodeIntelligence();
63
- const state = requireReadModels(ctx).intelligence.getSnapshot();
64
-
65
- if (sub === 'symbols' || sub === 'outline') {
66
- const targetPath = ensureExistingFile(args[1], ctx);
67
- if (!targetPath) return;
68
- const content = readFileSync(targetPath, 'utf-8');
69
- if (sub === 'symbols') {
70
- const symbols = await intelligence.getDocumentSymbols(targetPath, content);
71
- const entries = symbols.slice(0, 12).map((symbol) => ('selectionRange' in symbol ? formatDocumentSymbol(symbol) : formatTreeSitterSymbol(symbol)));
72
- ctx.print([
73
- `Intelligence Symbols: ${targetPath}`,
74
- ` source: ${state.symbolSearchStatus === 'ready' ? 'LSP/tree-sitter' : 'best-effort tree-sitter/LSP fallback'}`,
75
- ` status: ${state.symbolSearchStatus}`,
76
- ` results: ${symbols.length}`,
77
- ...(entries.length > 0 ? entries : [' No symbols available for this file.']),
78
- ' next: /health intelligence',
79
- ].join('\n'));
80
- return;
81
- }
82
-
83
- const outline = await intelligence.getOutline(targetPath, content);
84
- ctx.print([
85
- `Intelligence Outline: ${targetPath}`,
86
- ` source: tree-sitter outline extraction`,
87
- ` language ready: ${intelligence.hasTreeSitter(targetPath) ? 'yes' : 'no'}`,
88
- ` results: ${outline.length}`,
89
- ...(outline.slice(0, 12).map((entry) => ` ${entry.signature || entry.name} line ${entry.line}`)),
90
- ...(outline.length === 0 ? [' No outline entries available for this file.'] : []),
91
- ' next: /intelligence symbols ' + targetPath,
92
- ].join('\n'));
93
- return;
94
- }
95
-
96
- if (sub === 'definition' || sub === 'references' || sub === 'hover') {
97
- const targetPath = ensureExistingFile(args[1], ctx);
98
- if (!targetPath) return;
99
- const position = parsePosition(args[2], args[3]);
100
- if (!position) {
101
- ctx.print(`Intelligence ${sub[0]!.toUpperCase()}${sub.slice(1)}\n Usage: /intelligence ${sub} <file> <line> <column>`);
102
- return;
103
- }
104
-
105
- if (sub === 'definition') {
106
- const definition = await intelligence.getDefinition(targetPath, position.line, position.column);
107
- ctx.print([
108
- `Intelligence Definition: ${targetPath}:${position.line + 1}:${position.column + 1}`,
109
- ` status: ${state.hoverStatus === 'ready' || state.symbolSearchStatus === 'ready' ? 'available' : 'best-effort'}`,
110
- ...(definition
111
- ? [
112
- ` target: ${definition.uri}`,
113
- ` line: ${definition.range.start.line + 1}`,
114
- ` column: ${definition.range.start.character + 1}`,
115
- ' next: open the target file or use /intelligence references on the same symbol',
116
- ]
117
- : [' No definition was returned for that position.', ' next: /health intelligence']),
118
- ].join('\n'));
119
- return;
120
- }
121
-
122
- if (sub === 'references') {
123
- const references = await intelligence.getReferences(targetPath, position.line, position.column);
124
- ctx.print([
125
- `Intelligence References: ${targetPath}:${position.line + 1}:${position.column + 1}`,
126
- ` status: ${state.symbolSearchStatus}`,
127
- ` results: ${references.length}`,
128
- ...(references.slice(0, 12).map((reference) => ` ${reference.uri} ${reference.range.start.line + 1}:${reference.range.start.character + 1}`)),
129
- ...(references.length === 0 ? [' No references were returned for that position.'] : []),
130
- ' next: /intelligence definition ' + `${targetPath} ${position.line + 1} ${position.column + 1}`,
131
- ].join('\n'));
132
- return;
133
- }
134
-
135
- const hover = await intelligence.getHover(targetPath, position.line, position.column);
136
- const hoverLines = typeof hover?.contents === 'string'
137
- ? hover.contents.split('\n')
138
- : Array.isArray(hover?.contents)
139
- ? hover.contents.flatMap((entry) => typeof entry === 'string' ? entry : entry.value.split('\n'))
140
- : hover?.contents && 'value' in hover.contents
141
- ? hover.contents.value.split('\n')
142
- : [];
143
- ctx.print([
144
- `Intelligence Hover: ${targetPath}:${position.line + 1}:${position.column + 1}`,
145
- ` status: ${state.hoverStatus}`,
146
- ...(hoverLines.length > 0 ? hoverLines.slice(0, 8).map((line) => ` ${line}`) : [' No hover information was returned for that position.']),
147
- ' next: /health intelligence',
148
- ].join('\n'));
149
- return;
150
- }
151
-
152
- if (sub === 'diagnostics') {
153
- const file = args[1];
154
- const entries = [...state.diagnostics.entries()]
155
- .map(([filePath, diagnostics]) => ({
156
- filePath,
157
- diagnostics,
158
- errors: diagnostics.filter((entry) => entry.severity === 'error').length,
159
- warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
160
- }))
161
- .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath));
162
- const selected = file
163
- ? entries.find((entry) => entry.filePath === file)
164
- : entries[0];
165
- if (!selected) {
166
- ctx.print('Intelligence Diagnostics\n No diagnostics are currently tracked.');
167
- return;
168
- }
169
- ctx.print([
170
- `Intelligence Diagnostics: ${selected.filePath}`,
171
- ` errors: ${selected.errors}`,
172
- ` warnings: ${selected.warnings}`,
173
- ...selected.diagnostics.slice(0, 8).map((diagnostic) => (
174
- ` [${diagnostic.severity}] ${diagnostic.line + 1}:${diagnostic.column + 1} ${diagnostic.message}`
175
- )),
176
- ...(entries.length > 1 ? [` next: /intelligence diagnostics ${entries[1]!.filePath}`] : []),
177
- ].join('\n'));
178
- return;
179
- }
180
-
181
- if (sub === 'repair') {
182
- const lines = [
183
- 'Intelligence Repair',
184
- ' verify: /health intelligence',
185
- ...(state.diagnosticsStatus !== 'ready' ? [' /setup review', ' /health intelligence'] : []),
186
- ...(state.symbolSearchStatus !== 'ready' ? [' /symbols', ' /intelligence symbols <file>', ' /health intelligence'] : []),
187
- ...(state.completionsStatus !== 'ready' || state.hoverStatus !== 'ready'
188
- ? [' /intelligence review', ' /intelligence hover <file> <line> <column>', ' /setup onboarding']
189
- : []),
190
- ];
191
- ctx.print(lines.length > 1 ? lines.join('\n') : 'Intelligence Repair\n No active repair actions suggested.');
192
- return;
193
- }
194
-
195
- const issues: string[] = [];
196
- if (state.diagnosticsStatus !== 'ready') issues.push(`diagnostics=${state.diagnosticsStatus}`);
197
- if (state.symbolSearchStatus !== 'ready') issues.push(`symbols=${state.symbolSearchStatus}`);
198
- if (state.completionsStatus !== 'ready') issues.push(`completions=${state.completionsStatus}`);
199
- if (state.hoverStatus !== 'ready') issues.push(`hover=${state.hoverStatus}`);
200
-
201
- ctx.print([
202
- 'Intelligence Review',
203
- ` diagnostics: ${state.diagnosticsStatus}`,
204
- ` symbols: ${state.symbolSearchStatus}`,
205
- ` completions: ${state.completionsStatus}`,
206
- ` hover: ${state.hoverStatus}`,
207
- ` errors: ${state.errorCount}`,
208
- ` warnings: ${state.warningCount}`,
209
- ` requests: ${state.totalRequests}`,
210
- ` avg latency: ${Math.round(state.avgLatencyMs)}ms`,
211
- ...(issues.length > 0 ? [` issues: ${issues.join(', ')}`] : [' issues: none']),
212
- ` diagnostic files: ${state.diagnostics.size}`,
213
- ...(state.diagnostics.size > 0 ? [' next: /intelligence diagnostics'] : []),
214
- ' next: /intelligence symbols <file>',
215
- ' next: /intelligence outline <file>',
216
- ' next: /intelligence references <file> <line> <column>',
217
- ' next: /intelligence definition <file> <line> <column>',
218
- ' next: /intelligence hover <file> <line> <column>',
219
- ' next: /intelligence repair',
220
- ].join('\n'));
221
- },
222
- });
223
- }
@@ -1,432 +0,0 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
3
- import type { TurnEvent } from '@/runtime/index.ts';
4
- import type { UiEventFeed } from '../runtime/ui-events.ts';
5
- import type { Orchestrator } from '../core/orchestrator';
6
- import {
7
- buildEmptyState,
8
- buildPanelLine,
9
- buildStyledPanelLine,
10
- buildPanelWorkspace,
11
- resolveScrollablePanelSection,
12
- resolveStackedScrollableSections,
13
- DEFAULT_PANEL_PALETTE,
14
- type PanelWorkspaceSection,
15
- } from './polish.ts';
16
-
17
- // ---------------------------------------------------------------------------
18
- // Types
19
- // ---------------------------------------------------------------------------
20
-
21
- export type ApiCallStatus = 'ok' | 'error';
22
-
23
- export interface ApiCallEntry {
24
- /** Wall-clock timestamp when the call completed. */
25
- ts: number;
26
- /** Provider name (e.g. "anthropic"). */
27
- provider: string;
28
- /** Model id (e.g. "claude-sonnet-4-5"). */
29
- model: string;
30
- /** Input tokens for this call. */
31
- inputTokens: number;
32
- /** Output tokens for this call. */
33
- outputTokens: number;
34
- /** End-to-end latency in ms (stream-start → llm-response, or turn-start → llm-response). */
35
- latencyMs: number;
36
- /** HTTP-level status code hint; 0 when unknown. */
37
- statusCode: number;
38
- /** 'ok' | 'error' */
39
- status: ApiCallStatus;
40
- /** Trimmed error message when status === 'error'. */
41
- errorMessage?: string;
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Constants / limits
46
- // ---------------------------------------------------------------------------
47
-
48
- const MAX_CALL_LOG = 50;
49
- const MAX_ERROR_LOG = 20;
50
-
51
- // ---------------------------------------------------------------------------
52
- // Colors
53
- // ---------------------------------------------------------------------------
54
-
55
- const C = {
56
- title: '#00ffff',
57
- ok: '#5fd700',
58
- error: '#ff5f5f',
59
- warn: '#ffaf00',
60
- label: '244',
61
- value: '252',
62
- dim: '240',
63
- provName: '#e2e8f0',
64
- separator: '#374151',
65
- input: '#00ffff',
66
- output: '#d000ff',
67
- latGood: '#5fd700',
68
- latWarn: '#ffaf00',
69
- latBad: '#ff5f5f',
70
- sectionHdr: '238',
71
- colHdr: '242',
72
- } as const;
73
-
74
- const LATENCY_WARN_MS = 2_000;
75
- const LATENCY_BAD_MS = 5_000;
76
-
77
- // ---------------------------------------------------------------------------
78
- // Helpers
79
- // ---------------------------------------------------------------------------
80
-
81
- function fmtTok(n: number): string {
82
- if (n < 10_000) return String(n);
83
- if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
84
- return (n / 1_000_000).toFixed(2) + 'M';
85
- }
86
-
87
- function fmtMs(ms: number): string {
88
- if (ms <= 0) return 'n/a';
89
- if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
90
- if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
91
- return `${Math.round(ms)}ms`;
92
- }
93
-
94
- function fmtAgo(ts: number): string {
95
- const sec = Math.floor((Date.now() - ts) / 1000);
96
- if (sec < 60) return `${sec}s ago`;
97
- if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
98
- return `${Math.floor(sec / 3600)}h ago`;
99
- }
100
-
101
- function latColor(ms: number): string {
102
- if (ms >= LATENCY_BAD_MS) return C.latBad;
103
- if (ms >= LATENCY_WARN_MS) return C.latWarn;
104
- return C.latGood;
105
- }
106
-
107
- function statusCodeFromError(msg: string): number {
108
- const m = msg.match(/\b(4\d{2}|5\d{2})\b/);
109
- return m ? parseInt(m[1]!, 10) : 0;
110
- }
111
-
112
- // ---------------------------------------------------------------------------
113
- // DebugPanel
114
- // ---------------------------------------------------------------------------
115
-
116
- /**
117
- * Real-time API debug panel.
118
- *
119
- * Shows per-call log (model, provider, input/output tokens, latency, status code),
120
- * running session call total, and error history.
121
- *
122
- * Subscribes to typed turn runtime events.
123
- */
124
- export class DebugPanel extends BasePanel {
125
- private _unsubs: Array<() => void> = [];
126
-
127
- // Timing state
128
- private _turnStartMs: number | null = null;
129
- private _streamStartMs: number | null = null;
130
-
131
- // Token delta tracking (requires wired orchestrator)
132
- private _orchestrator: Orchestrator | null = null;
133
- private _prevInput = 0;
134
- private _prevOutput = 0;
135
-
136
- // Session data
137
- private _calls: ApiCallEntry[] = [];
138
- private _errors: ApiCallEntry[] = [];
139
- private _totalCalls = 0;
140
- private _totalErrors = 0;
141
-
142
- constructor(
143
- private readonly turnEvents: UiEventFeed<TurnEvent>,
144
- private readonly requestRender: () => void = () => {},
145
- ) {
146
- super('debug', 'Debug', 'B', 'monitoring');
147
- this._subscribe();
148
- }
149
-
150
- // -------------------------------------------------------------------------
151
- // External wiring
152
- // -------------------------------------------------------------------------
153
-
154
- /**
155
- * Optionally wire to the main Orchestrator so per-call token deltas can be
156
- * computed. Call once after construction.
157
- */
158
- wireOrchestrator(orchestrator: Orchestrator): void {
159
- this._orchestrator = orchestrator;
160
- }
161
-
162
- // -------------------------------------------------------------------------
163
- // Event subscription
164
- // -------------------------------------------------------------------------
165
-
166
- private _subscribe(): void {
167
- this._unsubs.push(
168
- this.turnEvents.on('TURN_SUBMITTED', () => {
169
- this._turnStartMs = Date.now();
170
- this._streamStartMs = null;
171
- }),
172
- );
173
-
174
- this._unsubs.push(
175
- this.turnEvents.on('STREAM_START', () => {
176
- this._streamStartMs = Date.now();
177
- }),
178
- );
179
-
180
- this._unsubs.push(
181
- this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
182
- const now = Date.now();
183
- const latencyMs =
184
- this._streamStartMs !== null
185
- ? now - this._streamStartMs
186
- : this._turnStartMs !== null
187
- ? now - this._turnStartMs
188
- : 0;
189
- this._streamStartMs = null;
190
-
191
- // Compute per-call token delta if orchestrator is wired
192
- let inputTokens = env.inputTokens + (env.cacheReadTokens ?? 0) + (env.cacheWriteTokens ?? 0);
193
- let outputTokens = env.outputTokens;
194
- if (this._orchestrator) {
195
- const cu = this._orchestrator.usage;
196
- inputTokens = Math.max(0, cu.input - this._prevInput);
197
- outputTokens = Math.max(0, cu.output - this._prevOutput);
198
- this._prevInput = cu.input;
199
- this._prevOutput = cu.output;
200
- }
201
-
202
- const entry: ApiCallEntry = {
203
- ts: now,
204
- provider: env.provider,
205
- model: env.model,
206
- inputTokens,
207
- outputTokens,
208
- latencyMs,
209
- statusCode: 200,
210
- status: 'ok',
211
- };
212
- this._pushCall(entry);
213
- this.markDirty();
214
- this.requestRender();
215
- }),
216
- );
217
-
218
- this._unsubs.push(
219
- this.turnEvents.on('TURN_ERROR', (env) => {
220
- this._streamStartMs = null;
221
- this._turnStartMs = null;
222
-
223
- const msg = env.error;
224
- const code = statusCodeFromError(msg);
225
-
226
- const entry: ApiCallEntry = {
227
- ts: Date.now(),
228
- provider: 'unknown',
229
- model: 'unknown',
230
- inputTokens: 0,
231
- outputTokens: 0,
232
- latencyMs: 0,
233
- statusCode: code,
234
- status: 'error',
235
- errorMessage: msg.slice(0, 120),
236
- };
237
- this._pushCall(entry);
238
- this._pushError(entry);
239
- this.markDirty();
240
- this.requestRender();
241
- }),
242
- );
243
- }
244
-
245
- private _pushCall(entry: ApiCallEntry): void {
246
- this._totalCalls++;
247
- this._calls.push(entry);
248
- if (this._calls.length > MAX_CALL_LOG) this._calls.shift();
249
- }
250
-
251
- private _pushError(entry: ApiCallEntry): void {
252
- this._totalErrors++;
253
- this._errors.push(entry);
254
- if (this._errors.length > MAX_ERROR_LOG) this._errors.shift();
255
- }
256
-
257
- // -------------------------------------------------------------------------
258
- // Lifecycle
259
- // -------------------------------------------------------------------------
260
-
261
- override onDestroy(): void {
262
- for (const unsub of this._unsubs) unsub();
263
- this._unsubs = [];
264
- }
265
-
266
- // -------------------------------------------------------------------------
267
- // Rendering
268
- // -------------------------------------------------------------------------
269
-
270
- override render(width: number, height: number): Line[] {
271
- const sections: PanelWorkspaceSection[] = [
272
- {
273
- title: 'Session',
274
- lines: this._renderSummary(width),
275
- },
276
- ];
277
-
278
- if (this._calls.length === 0) {
279
- sections.push({
280
- title: 'API Call Log',
281
- lines: buildEmptyState(
282
- width,
283
- ' No calls yet',
284
- 'Completed provider calls and API failures will appear here with timing, token counts, and status codes.',
285
- [],
286
- DEFAULT_PANEL_PALETTE,
287
- ),
288
- });
289
- } else {
290
- const rows = this._renderCallLog(width);
291
- if (this._errors.length > 0) {
292
- const errors = this._renderErrorHistory(width);
293
- const [callSection, errorSection] = resolveStackedScrollableSections(width, height, {
294
- palette: DEFAULT_PANEL_PALETTE,
295
- beforeSections: sections,
296
- sections: [
297
- {
298
- title: 'API Call Log',
299
- scrollableLines: rows,
300
- scrollOffset: Math.max(0, rows.length - 1),
301
- minRows: 8,
302
- weight: 2,
303
- },
304
- {
305
- title: 'Error History',
306
- scrollableLines: errors,
307
- scrollOffset: Math.max(0, errors.length - 1),
308
- minRows: 4,
309
- weight: 1,
310
- },
311
- ],
312
- });
313
- sections.push(callSection!.section, errorSection!.section);
314
- } else {
315
- const callSection = resolveScrollablePanelSection(width, height, {
316
- palette: DEFAULT_PANEL_PALETTE,
317
- beforeSections: sections,
318
- section: {
319
- title: 'API Call Log',
320
- scrollableLines: rows,
321
- scrollOffset: Math.max(0, rows.length - 1),
322
- minRows: 8,
323
- },
324
- });
325
- sections.push(callSection.section);
326
- }
327
- }
328
-
329
- return buildPanelWorkspace(width, height, {
330
- title: ' API Debug',
331
- intro: 'Recent provider calls, token deltas, latency, status codes, and error history.',
332
- sections,
333
- palette: DEFAULT_PANEL_PALETTE,
334
- });
335
- }
336
-
337
- // -------------------------------------------------------------------------
338
- // Section renderers
339
- // -------------------------------------------------------------------------
340
-
341
- private _renderSummary(width: number): Line[] {
342
- const errCount = this._totalErrors;
343
- const okCount = this._totalCalls - this._totalErrors;
344
- return [
345
- buildStyledPanelLine(width, [
346
- { text: ' Calls: ', fg: C.label },
347
- { text: String(this._totalCalls), fg: C.value },
348
- { text: ' OK: ', fg: C.label },
349
- { text: String(okCount), fg: C.ok },
350
- { text: ' Errors: ', fg: C.label },
351
- { text: String(errCount), fg: errCount > 0 ? C.error : C.dim },
352
- ]),
353
- ];
354
- }
355
-
356
- private _renderCallLog(width: number): Line[] {
357
- const lines: Line[] = [];
358
- lines.push(this._callLogHeader(width));
359
- for (const entry of this._calls) {
360
- lines.push(this._callLogRow(entry, width));
361
- }
362
-
363
- return lines;
364
- }
365
-
366
- private _callLogHeader(width: number): Line {
367
- // Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
368
- const header = ' Time S Provider Model In Out Lat';
369
- return this._textLine(header.slice(0, width), C.colHdr, width, { dim: true });
370
- }
371
-
372
- private _callLogRow(e: ApiCallEntry, width: number): Line {
373
- const timeStr = fmtAgo(e.ts).padEnd(8);
374
- const statusChar = e.status === 'ok' ? '✓' : '✕';
375
- const statusFg = e.status === 'ok' ? C.ok : C.error;
376
- const provStr = e.provider.slice(0, 11).padEnd(12);
377
- const modelStr = e.model.slice(0, 19).padEnd(20);
378
- const inStr = fmtTok(e.inputTokens).padStart(8);
379
- const outStr = fmtTok(e.outputTokens).padStart(8);
380
- const latStr = fmtMs(e.latencyMs).padStart(8);
381
-
382
- const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
383
- { text: ' ' + timeStr, fg: C.dim },
384
- { text: statusChar + ' ', fg: statusFg },
385
- { text: provStr, fg: C.provName },
386
- { text: modelStr, fg: C.value },
387
- { text: inStr, fg: C.input },
388
- { text: outStr, fg: C.output },
389
- { text: latStr, fg: latColor(e.latencyMs) },
390
- ];
391
-
392
- // Append status code for errors
393
- if (e.status === 'error' && e.statusCode > 0) {
394
- segments.push({ text: ` [${e.statusCode}]`, fg: C.error });
395
- }
396
-
397
- return buildStyledPanelLine(
398
- width,
399
- segments.map((seg) => ({ text: seg.text, fg: seg.fg, bold: seg.bold })),
400
- );
401
- }
402
-
403
- private _renderErrorHistory(width: number): Line[] {
404
- const lines: Line[] = [];
405
- for (const e of this._errors) {
406
- lines.push(this._errorRow(e, width));
407
- }
408
-
409
- return lines;
410
- }
411
-
412
- private _errorRow(e: ApiCallEntry, width: number): Line {
413
- const timeStr = fmtAgo(e.ts).padEnd(8);
414
- const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
415
- const msgStr = (e.errorMessage ?? 'unknown error').slice(0, width - 12 - codeStr.length);
416
- const full = ` ${timeStr} ${codeStr}${msgStr}`;
417
- return this._textLine(full.slice(0, width), C.error, width);
418
- }
419
-
420
- // -------------------------------------------------------------------------
421
- // Line-builder helpers
422
- // -------------------------------------------------------------------------
423
-
424
- private _textLine(
425
- text: string,
426
- fg: string,
427
- width: number,
428
- opts: { dim?: boolean } = {},
429
- ): Line {
430
- return buildStyledPanelLine(width, [{ text, fg, dim: opts.dim }]);
431
- }
432
- }