@pellux/goodvibes-agent 0.1.59 → 0.1.61

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,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
- }
@@ -1,176 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { BasePanel } from './base-panel.ts';
4
- import type { UiIntelligenceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
5
- import {
6
- buildEmptyState,
7
- buildGuidanceLine,
8
- buildKeyValueLine,
9
- buildPanelLine,
10
- buildPanelWorkspace,
11
- DEFAULT_PANEL_PALETTE,
12
- type PanelWorkspaceSection,
13
- } from './polish.ts';
14
-
15
- const C = {
16
- ...DEFAULT_PANEL_PALETTE,
17
- good: '#22c55e',
18
- warn: '#f59e0b',
19
- bad: '#ef4444',
20
- info: '#38bdf8',
21
- headerBg: '#1e293b',
22
- } as const;
23
-
24
- function statusColor(status: string): string {
25
- switch (status) {
26
- case 'ready':
27
- return C.good;
28
- case 'loading':
29
- return C.info;
30
- case 'degraded':
31
- return C.warn;
32
- case 'unavailable':
33
- default:
34
- return C.bad;
35
- }
36
- }
37
-
38
- export class IntelligencePanel extends BasePanel {
39
- private readonly unsub: (() => void) | null;
40
-
41
- public constructor(private readonly readModel?: UiReadModel<UiIntelligenceSnapshot>) {
42
- super('intelligence', 'Intelligence', 'J', 'development');
43
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
44
- }
45
-
46
- public override onDestroy(): void {
47
- this.unsub?.();
48
- }
49
-
50
- public render(width: number, height: number): Line[] {
51
- this.needsRender = false;
52
- if (!this.readModel) {
53
- const lines = buildPanelWorkspace(width, height, {
54
- title: 'Intelligence Control Room',
55
- intro: 'Workspace intelligence posture across diagnostics, symbols, completions, and hover readiness.',
56
- sections: [{
57
- lines: buildEmptyState(
58
- width,
59
- ' Intelligence runtime store unavailable.',
60
- 'This surface needs the live runtime store so it can show diagnostics, symbol readiness, and recovery guidance.',
61
- [{ command: '/intelligence review', summary: 'review intelligence readiness from the command surface' }],
62
- C,
63
- ),
64
- }],
65
- palette: C,
66
- });
67
- while (lines.length < height) lines.push(createEmptyLine(width));
68
- return lines.slice(0, height);
69
- }
70
-
71
- const state = this.readModel.getSnapshot();
72
- const degraded = [
73
- state.diagnosticsStatus,
74
- state.completionsStatus,
75
- state.symbolSearchStatus,
76
- state.hoverStatus,
77
- ].filter((status) => status !== 'ready').length;
78
-
79
- const sections: PanelWorkspaceSection[] = [
80
- {
81
- title: 'Intelligence posture',
82
- lines: [
83
- buildKeyValueLine(width, [
84
- { label: 'diagnostics', value: state.diagnosticsStatus, valueColor: statusColor(state.diagnosticsStatus) },
85
- { label: 'symbols', value: state.symbolSearchStatus, valueColor: statusColor(state.symbolSearchStatus) },
86
- { label: 'completions', value: state.completionsStatus, valueColor: statusColor(state.completionsStatus) },
87
- { label: 'hover', value: state.hoverStatus, valueColor: statusColor(state.hoverStatus) },
88
- ], C),
89
- buildKeyValueLine(width, [
90
- { label: 'errors', value: String(state.errorCount), valueColor: state.errorCount > 0 ? C.bad : C.dim },
91
- { label: 'warnings', value: String(state.warningCount), valueColor: state.warningCount > 0 ? C.warn : C.dim },
92
- { label: 'requests', value: String(state.totalRequests), valueColor: C.value },
93
- { label: 'avg latency', value: `${Math.round(state.avgLatencyMs)}ms`, valueColor: C.info },
94
- ], C),
95
- ],
96
- },
97
- {
98
- title: 'Next Actions',
99
- lines: [
100
- buildGuidanceLine(width, '/intelligence diagnostics', 'review readiness posture and current diagnostics activity', C),
101
- buildGuidanceLine(width, '/intelligence repair', 'surface repair-oriented guidance when symbols, hover, or completions degrade', C),
102
- ],
103
- },
104
- {
105
- title: 'Readiness',
106
- lines: [
107
- buildPanelLine(width, [[` Diagnostics are ${state.diagnosticsStatus}. Symbol search is ${state.symbolSearchStatus}.`, state.diagnosticsStatus === 'ready' && state.symbolSearchStatus === 'ready' ? C.dim : C.warn]]),
108
- buildPanelLine(width, [[` Hover is ${state.hoverStatus}. Completions are ${state.completionsStatus}.`, state.hoverStatus === 'ready' && state.completionsStatus === 'ready' ? C.dim : C.warn]]),
109
- ...(state.hover.active && state.hover.filePath
110
- ? [buildPanelLine(width, [[` Active hover: ${state.hover.filePath}`, C.info]])]
111
- : []),
112
- ],
113
- },
114
- ];
115
-
116
- const diagnosticFiles = [...state.diagnostics.entries()]
117
- .map(([filePath, diagnostics]) => ({
118
- filePath,
119
- errors: diagnostics.filter((entry) => entry.severity === 'error').length,
120
- warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
121
- }))
122
- .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath))
123
- .slice(0, 4);
124
- sections.push({
125
- title: 'Diagnostics',
126
- lines: diagnosticFiles.length > 0
127
- ? diagnosticFiles.map((entry) => buildPanelLine(width, [[
128
- ` ${entry.filePath} errors=${entry.errors} warnings=${entry.warnings}`,
129
- entry.errors > 0 ? C.bad : entry.warnings > 0 ? C.warn : C.dim,
130
- ]]))
131
- : [buildPanelLine(width, [[' No tracked diagnostics yet.', C.dim]])],
132
- });
133
-
134
- sections.push({
135
- title: 'Workflows',
136
- lines: [
137
- buildGuidanceLine(width, '/intelligence symbols <file>', 'inspect document symbols for a file and verify symbol-surface readiness', C),
138
- buildGuidanceLine(width, '/intelligence outline <file>', 'review structural outline extraction without leaving the control room', C),
139
- buildGuidanceLine(width, '/intelligence definition <file> <line> <column>', 'check definition lookup for an exact source position', C),
140
- buildGuidanceLine(width, '/intelligence references <file> <line> <column>', 'review reference lookup for a symbol under the cursor', C),
141
- buildGuidanceLine(width, '/intelligence hover <file> <line> <column>', 'inspect hover/details posture for a specific source position', C),
142
- ],
143
- });
144
-
145
- if (degraded > 0) {
146
- sections.push({
147
- title: 'Recovery',
148
- lines: [
149
- buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and workspace language configuration.', C.warn]]),
150
- buildGuidanceLine(width, '/health review', 'check setup and readiness failures that could block diagnostics and symbol search', C),
151
- buildGuidanceLine(width, '/setup review', 'review startup and environment posture for intelligence dependencies', C),
152
- buildGuidanceLine(width, '/intelligence repair', 'show repair-oriented commands for diagnostics, symbols, hover, and completions', C),
153
- buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification for the intelligence domain', C),
154
- ],
155
- });
156
- } else {
157
- sections.push({
158
- title: 'Recovery',
159
- lines: [
160
- buildPanelLine(width, [[' Intelligence surfaces are healthy and ready for code-aware workflows.', C.dim]]),
161
- buildGuidanceLine(width, '/health intelligence', 'verify readiness posture after setup changes or dependency recovery', C),
162
- ],
163
- });
164
- }
165
-
166
- const lines = buildPanelWorkspace(width, height, {
167
- title: 'Intelligence Control Room',
168
- intro: 'Workspace intelligence posture across diagnostics, symbol search, hover, and completion readiness.',
169
- sections,
170
- footerLines: [buildPanelLine(width, [[' /symbols /intelligence diagnostics /intelligence symbols <file> /intelligence definition <file> <line> <column> ', C.dim]])],
171
- palette: C,
172
- });
173
- while (lines.length < height) lines.push(createEmptyLine(width));
174
- return lines.slice(0, height);
175
- }
176
- }