@pellux/goodvibes-agent 0.1.102 → 0.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/docs/README.md +1 -1
  4. package/docs/getting-started.md +17 -3
  5. package/package.json +1 -1
  6. package/src/agent/memory-safety.ts +16 -0
  7. package/src/cli/help.ts +86 -0
  8. package/src/cli/local-library-command.ts +516 -0
  9. package/src/cli/management.ts +17 -0
  10. package/src/cli/memory-command.ts +630 -0
  11. package/src/cli/package-verification.ts +10 -0
  12. package/src/cli/parser.ts +8 -0
  13. package/src/cli/types.ts +3 -0
  14. package/src/input/agent-workspace-activation.ts +170 -0
  15. package/src/input/agent-workspace-categories.ts +8 -1
  16. package/src/input/agent-workspace-editors.ts +36 -0
  17. package/src/input/agent-workspace-memory-editor.ts +88 -0
  18. package/src/input/agent-workspace-setup.ts +7 -5
  19. package/src/input/agent-workspace-snapshot.ts +40 -4
  20. package/src/input/agent-workspace-token.ts +51 -0
  21. package/src/input/agent-workspace-types.ts +13 -3
  22. package/src/input/agent-workspace.ts +130 -185
  23. package/src/input/feed-context-factory.ts +1 -3
  24. package/src/input/handler-feed.ts +1 -4
  25. package/src/input/handler-interactions.ts +0 -1
  26. package/src/input/handler-modal-stack.ts +0 -1
  27. package/src/input/handler-modal-token-routes.ts +0 -11
  28. package/src/input/handler-picker-routes.ts +11 -20
  29. package/src/input/handler-ui-state.ts +0 -6
  30. package/src/input/handler.ts +1 -17
  31. package/src/main.ts +0 -6
  32. package/src/panels/builtin/agent.ts +0 -17
  33. package/src/panels/index.ts +0 -2
  34. package/src/renderer/agent-workspace.ts +8 -3
  35. package/src/renderer/conversation-overlays.ts +0 -6
  36. package/src/renderer/live-tail-modal.ts +10 -69
  37. package/src/renderer/process-modal.ts +28 -530
  38. package/src/runtime/bootstrap-core.ts +1 -1
  39. package/src/runtime/services.ts +3 -4
  40. package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
  41. package/src/version.ts +1 -1
  42. package/src/panels/agent-inspector-panel.ts +0 -521
  43. package/src/panels/agent-inspector-shared.ts +0 -94
  44. package/src/panels/agent-logs-panel.ts +0 -559
  45. package/src/panels/agent-logs-shared.ts +0 -129
  46. package/src/renderer/agent-detail-modal.ts +0 -331
  47. package/src/renderer/process-summary.ts +0 -67
@@ -782,9 +782,3 @@ function narrowStringEnumProperty(
782
782
  property.enum = [...values];
783
783
  property.description = description;
784
784
  }
785
-
786
- // Compatibility exports for inherited terminal-foundation tests/imports.
787
- export const installWrfcAgentToolGuard = installAgentToolPolicyGuard;
788
- export const wrapWrfcAgentTool = wrapAgentToolForAgentPolicy;
789
- export const validateWrfcAgentToolInvocation = validateAgentToolInvocationForAgentPolicy;
790
- export const normalizeWrfcAgentToolInvocation = normalizeAgentToolInvocationForAgentPolicy;
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.102';
9
+ let _version = '0.1.104';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
@@ -1,521 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // AgentInspectorPanel — detailed view of a specific agent's messages and tool
3
- // calls, with expandable tool call details, scroll, and agent selector.
4
- // ---------------------------------------------------------------------------
5
-
6
- import { join } from 'node:path';
7
- import { readFile } from 'node:fs/promises';
8
- import type { Line } from '../types/grid.ts';
9
- import { createEmptyLine } from '../types/grid.ts';
10
- import { BasePanel } from './base-panel.ts';
11
- import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
12
- import type { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
13
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
14
- import {
15
- buildEmptyState,
16
- buildPanelLine,
17
- buildSelectablePanelLine,
18
- type StyledPanelSegment,
19
- buildStyledPanelLine,
20
- buildPanelWorkspace,
21
- resolveScrollablePanelSection,
22
- DEFAULT_PANEL_PALETTE,
23
- } from './polish.ts';
24
- import { truncateDisplay } from '../utils/terminal-width.ts';
25
- import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
26
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
27
- import {
28
- type AgentDisplayRow as DisplayRow,
29
- type AgentInspectorEntryKind as EntryKind,
30
- type AgentTimelineEntry as TimelineEntry,
31
- agentKindStyle,
32
- agentStatusColor,
33
- formatAgentDuration as formatMs,
34
- formatAgentTime as shortTime,
35
- jsonlToTimeline,
36
- } from './agent-inspector-shared.ts';
37
-
38
- // ---------------------------------------------------------------------------
39
- // Constants & colour palette
40
- // ---------------------------------------------------------------------------
41
-
42
- const REFRESH_MS = 500;
43
- const MAX_JSONL_ENTRIES = 200;
44
-
45
- const COLOR = {
46
- // Header / chrome
47
- headerBg: '#1a1a2e',
48
- headerFg: '#ffffff',
49
- statusBar: '#222233',
50
- statusFg: '#aaaaaa',
51
- separator: '#333355',
52
-
53
- // Timeline roles
54
- user: '#cc88ff', // purple
55
- assistant: '#ffffff', // white
56
- tool: '#00ccff', // cyan
57
- toolResult: '#66ddff', // light cyan
58
- error: '#ff6666', // red
59
- system: '#888888', // dim grey
60
- timestamp: '#555577',
61
-
62
- // Status badges
63
- pending: '#ffcc44',
64
- running: '#44ffcc',
65
- completed: '#44ff88',
66
- failed: '#ff4444',
67
- cancelled: '#888888',
68
-
69
- // Selector / labels
70
- label: '#8888bb',
71
- value: '#ccccdd',
72
- selected: '#ffee88',
73
- dimmed: '#555566',
74
- expandHint: '#445566',
75
- } as const;
76
-
77
- // ---------------------------------------------------------------------------
78
- // AgentInspectorPanel
79
- // ---------------------------------------------------------------------------
80
-
81
- export interface AgentInspectorPanelDeps {
82
- readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
83
- readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
84
- readonly workingDirectory: string;
85
- }
86
-
87
- export class AgentInspectorPanel extends BasePanel {
88
- // The agent currently being inspected
89
- private selectedAgentId: string | null = null;
90
-
91
- // Flattened timeline for the selected agent
92
- private timeline: TimelineEntry[] = [];
93
-
94
- // Scroll state
95
- private scrollOffset = 0;
96
-
97
- // Cursor index for expand/collapse
98
- private cursorIndex = 0;
99
-
100
- // Refresh timer (active only while panel is active)
101
- private refreshTimerId: ReturnType<typeof setInterval> | null = null;
102
-
103
- // Row cache — cleared on markDirty(), computed once per render cycle
104
- private _cachedRows: DisplayRow[] | null = null;
105
-
106
- constructor(private readonly deps: AgentInspectorPanelDeps) {
107
- super('inspector', 'Inspector', 'I', 'agent');
108
- }
109
-
110
- // -------------------------------------------------------------------------
111
- // Row cache
112
- // -------------------------------------------------------------------------
113
-
114
- private _getCachedRows(): DisplayRow[] {
115
- if (!this._cachedRows) this._cachedRows = this._buildVisibleRows();
116
- return this._cachedRows;
117
- }
118
-
119
- override markDirty(): void {
120
- this._cachedRows = null;
121
- super.markDirty();
122
- }
123
-
124
- // -------------------------------------------------------------------------
125
- // Public API
126
- // -------------------------------------------------------------------------
127
-
128
- /** Focus this panel on a specific agent by ID. */
129
- inspectAgent(agentId: string): void {
130
- this.selectedAgentId = agentId;
131
- this.scrollOffset = 0;
132
- this.cursorIndex = 0;
133
- this.timeline = [];
134
- this.markDirty();
135
- this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh failed', { err }); });
136
- }
137
-
138
- // -------------------------------------------------------------------------
139
- // Lifecycle
140
- // -------------------------------------------------------------------------
141
-
142
- override onActivate(): void {
143
- this.needsRender = true;
144
- this._startRefresh();
145
- }
146
-
147
- override onDeactivate(): void {
148
- this._stopRefresh();
149
- }
150
-
151
- override onDestroy(): void {
152
- this._stopRefresh();
153
- super.onDestroy();
154
- }
155
-
156
- // -------------------------------------------------------------------------
157
- // Input
158
- // -------------------------------------------------------------------------
159
-
160
- handleInput(key: string): boolean {
161
- switch (key) {
162
- case 'up': this._moveCursor(-1); return true;
163
- case 'down': this._moveCursor(1); return true;
164
- case 'pageup': this._scroll(-10); return true;
165
- case 'pagedown': this._scroll(10); return true;
166
- case 'return': this._toggleExpand(); return true;
167
- case 'tab': this._nextAgent(); return true;
168
- default: return false;
169
- }
170
- }
171
-
172
- // -------------------------------------------------------------------------
173
- // Render
174
- // -------------------------------------------------------------------------
175
-
176
- render(width: number, height: number): Line[] {
177
- if (height <= 0 || width <= 0) return [];
178
-
179
- const manager = this.deps.agentManager;
180
- const agents = manager.list();
181
- const rec = this.selectedAgentId
182
- ? manager.getStatus(this.selectedAgentId)
183
- : null;
184
- const selectorLine = this._renderSelector(width, agents);
185
- const summaryLines = [
186
- buildPanelLine(width, [
187
- [' Agents ', DEFAULT_PANEL_PALETTE.label],
188
- [String(agents.length), DEFAULT_PANEL_PALETTE.value],
189
- [' Selected ', DEFAULT_PANEL_PALETTE.label],
190
- [this.selectedAgentId ? this.selectedAgentId.slice(-8) : 'none', this.selectedAgentId ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
191
- ]),
192
- ];
193
-
194
- if (!rec) {
195
- return buildPanelWorkspace(width, height, {
196
- title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
197
- intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
198
- sections: [
199
- { title: 'Summary', lines: summaryLines },
200
- { title: 'Agents', lines: [selectorLine] },
201
- {
202
- lines: buildEmptyState(
203
- width,
204
- agents.length === 0 ? ' No agents running' : ' No agent selected',
205
- agents.length === 0
206
- ? 'Spawn an agent to inspect its live and historical timeline.'
207
- : 'Press Tab to cycle through available agents and open one in the inspector.',
208
- [],
209
- DEFAULT_PANEL_PALETTE,
210
- ),
211
- },
212
- ],
213
- footerLines: [
214
- buildPanelLine(width, [[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
215
- ],
216
- palette: DEFAULT_PANEL_PALETTE,
217
- });
218
- }
219
-
220
- summaryLines.push(this._renderAgentInfoSummary(width, rec));
221
- const allRows = this._getCachedRows();
222
- if (allRows.length === 0) {
223
- return buildPanelWorkspace(width, height, {
224
- title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
225
- intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
226
- sections: [
227
- { title: 'Summary', lines: summaryLines },
228
- { title: 'Agents', lines: [selectorLine] },
229
- {
230
- lines: buildEmptyState(
231
- width,
232
- ' No messages yet',
233
- 'The selected agent has not emitted any visible timeline entries yet.',
234
- [],
235
- DEFAULT_PANEL_PALETTE,
236
- ),
237
- },
238
- ],
239
- footerLines: [
240
- buildPanelLine(width, [[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
241
- ],
242
- palette: DEFAULT_PANEL_PALETTE,
243
- });
244
- }
245
-
246
- this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, allRows.length - 1));
247
- const summarySection = { title: 'Summary', lines: summaryLines } as const;
248
- const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
249
- const timelineSection = resolveScrollablePanelSection(width, height, {
250
- intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
251
- footerLines: [
252
- buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
253
- ],
254
- palette: DEFAULT_PANEL_PALETTE,
255
- beforeSections: [summarySection, agentsSection],
256
- section: {
257
- title: 'Timeline',
258
- scrollableLines: allRows.map((row, index) => this._renderTimelineRow(width, row, index === this.cursorIndex)),
259
- selectedIndex: this.cursorIndex,
260
- scrollOffset: this.scrollOffset,
261
- minRows: 8,
262
- },
263
- });
264
- this.scrollOffset = timelineSection.scrollOffset;
265
-
266
- return buildPanelWorkspace(width, height, {
267
- title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
268
- intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
269
- sections: [
270
- summarySection,
271
- agentsSection,
272
- timelineSection.section,
273
- ],
274
- footerLines: [
275
- buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
276
- ],
277
- palette: DEFAULT_PANEL_PALETTE,
278
- });
279
- }
280
-
281
- private _renderSelector(
282
- width: number,
283
- agents: AgentRecord[],
284
- ): Line {
285
- if (agents.length === 0) {
286
- return buildStyledPanelLine(width, [
287
- { text: ' (no agents)', fg: COLOR.dimmed, bg: COLOR.statusBar, dim: true },
288
- ]);
289
- }
290
- const segments: StyledPanelSegment[] = [{ text: ' ', fg: COLOR.statusFg, bg: COLOR.statusBar }];
291
- for (const agent of agents) {
292
- const isSelected = agent.id === this.selectedAgentId;
293
- const shortId = agent.id.slice(-8);
294
- const badge = ` ${shortId} `;
295
- segments.push({
296
- text: badge,
297
- fg: isSelected ? COLOR.selected : COLOR.dimmed,
298
- bg: COLOR.statusBar,
299
- bold: isSelected,
300
- });
301
- segments.push({ text: '│', fg: COLOR.separator, bg: COLOR.statusBar });
302
- }
303
- return buildStyledPanelLine(width, segments);
304
- }
305
-
306
- private _renderAgentInfoSummary(width: number, rec: AgentRecord): Line {
307
- const now = Date.now();
308
- const elapsed = (rec.completedAt ?? now) - rec.startedAt;
309
- const taskPreview = rec.task.split('\n')[0] ?? '';
310
- const maxTask = Math.max(0, width - 40);
311
- const taskDisplay = taskPreview.length > maxTask
312
- ? taskPreview.slice(0, maxTask - 1) + '\u2026'
313
- : taskPreview;
314
- return buildPanelLine(width, [
315
- [' Status ', DEFAULT_PANEL_PALETTE.label],
316
- [rec.status.toUpperCase(), rec.status === 'running' ? DEFAULT_PANEL_PALETTE.good : rec.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
317
- [' Duration ', DEFAULT_PANEL_PALETTE.label],
318
- [formatMs(elapsed), DEFAULT_PANEL_PALETTE.value],
319
- [' Tools ', DEFAULT_PANEL_PALETTE.label],
320
- [String(rec.toolCallCount), DEFAULT_PANEL_PALETTE.info],
321
- // SDK 0.23.0: show addendum indicator when WRFC injected a constraint addendum
322
- ...(rec.systemPromptAddendum
323
- ? [[' Addendum ', DEFAULT_PANEL_PALETTE.label] as [string, string], ['yes', DEFAULT_PANEL_PALETTE.info] as [string, string]]
324
- : []),
325
- [' Task ', DEFAULT_PANEL_PALETTE.label],
326
- [taskDisplay, DEFAULT_PANEL_PALETTE.value],
327
- ]);
328
- }
329
-
330
- // -------------------------------------------------------------------------
331
- // Timeline row rendering
332
- // -------------------------------------------------------------------------
333
-
334
- private _renderTimelineRow(
335
- width: number,
336
- row: DisplayRow,
337
- isCursor: boolean,
338
- ): Line {
339
- const bg = isCursor ? '#1a2233' : '';
340
- const ts = shortTime(row.timestamp);
341
- const { fg, prefix } = agentKindStyle(row.kind, COLOR);
342
- const hint = row.hasDetail ? (row.expanded ? ' ▾' : ' ▸') : '';
343
- const prefixText = `${isCursor ? '▸' : ' '} ${ts} ${prefix} `;
344
- const reserved = prefixText.length + hint.length;
345
- const contentBudget = Math.max(0, width - reserved);
346
- const text = truncateDisplay(row.content, contentBudget);
347
-
348
- return buildSelectablePanelLine(width, [
349
- { text: isCursor ? '▸' : ' ', fg: COLOR.selected, bg, bold: isCursor },
350
- { text: ' ', fg: COLOR.value, bg },
351
- { text: ts, fg: COLOR.timestamp, bg, dim: true },
352
- { text: ' ', fg: COLOR.value, bg },
353
- { text: prefix, fg, bg, bold: true },
354
- { text: ' ', fg: COLOR.value, bg },
355
- { text: text, fg: COLOR.value, bg },
356
- { text: hint.length > 0 ? hint.padStart(Math.max(hint.length, width - (prefixText.length + text.length))) : '', fg: COLOR.expandHint, bg, dim: true },
357
- ], { selected: isCursor, selectedBg: bg, fillFg: isCursor ? COLOR.selected : '' });
358
- }
359
-
360
- // -------------------------------------------------------------------------
361
- // Private — data
362
- // -------------------------------------------------------------------------
363
-
364
- /**
365
- * Build the flat list of DisplayRow items from timeline + expanded detail
366
- * sub-rows. This is what the renderer walks.
367
- */
368
- private _buildVisibleRows(): DisplayRow[] {
369
- const rows: DisplayRow[] = [];
370
-
371
- // Merge bus messages (live) + JSONL (historical), sorted by timestamp
372
- const busEntries = this._busToTimeline();
373
- const merged = [...this.timeline, ...busEntries]
374
- .sort((a, b) => a.timestamp - b.timestamp);
375
-
376
- // Deduplicate: bus messages that already appear in JSONL will have
377
- // approximate timestamps. We just show all — bus msgs tend to have
378
- // unique content.
379
- for (const entry of merged) {
380
- rows.push({
381
- kind: entry.kind,
382
- timestamp: entry.timestamp,
383
- content: entry.content,
384
- hasDetail: !!entry.detail,
385
- expanded: entry.expanded,
386
- entryRef: entry,
387
- });
388
-
389
- // If expanded and has detail — insert sub-rows
390
- if (entry.expanded && entry.detail) {
391
- const detailLines = entry.detail.split('\n');
392
- for (const dl of detailLines) {
393
- rows.push({
394
- kind: 'tool_result',
395
- timestamp: entry.timestamp,
396
- content: dl,
397
- hasDetail: false,
398
- expanded: false,
399
- entryRef: null,
400
- });
401
- }
402
- }
403
- }
404
-
405
- return rows;
406
- }
407
-
408
- private _busToTimeline(): TimelineEntry[] {
409
- if (!this.selectedAgentId) return [];
410
- const messages = this.deps.agentMessageBus.getMessages(this.selectedAgentId);
411
- const DEDUP_WINDOW_MS = 2000;
412
- const seen = new Map<string, number>(); // hash -> last timestamp
413
- const result: TimelineEntry[] = [];
414
- for (const msg of messages) {
415
- const isFromUser = msg.from === 'orchestrator' || msg.from === 'system';
416
- const kind: EntryKind = isFromUser ? 'user' : 'assistant';
417
- const contentSnippet = msg.content.slice(0, 50);
418
- const dedupKey = `${kind}:${contentSnippet}`;
419
- const lastSeen = seen.get(dedupKey);
420
- if (lastSeen !== undefined && msg.timestamp - lastSeen < DEDUP_WINDOW_MS) {
421
- continue;
422
- }
423
- seen.set(dedupKey, msg.timestamp);
424
- result.push({
425
- kind,
426
- timestamp: msg.timestamp,
427
- label: msg.from,
428
- content: msg.content.length > 200
429
- ? msg.content.slice(0, 197) + '\u2026'
430
- : msg.content,
431
- expanded: false,
432
- } satisfies TimelineEntry);
433
- }
434
- return result;
435
- }
436
-
437
- // -------------------------------------------------------------------------
438
- // Private — refresh
439
- // -------------------------------------------------------------------------
440
-
441
- private async _refreshTimeline(): Promise<void> {
442
- if (!this.selectedAgentId) return;
443
- try {
444
- const sessionFile = join(
445
- this.deps.workingDirectory,
446
- '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'sessions',
447
- `${this.selectedAgentId}.jsonl`,
448
- );
449
- const raw = await readFile(sessionFile, 'utf-8');
450
- const logLines = raw.trim().split('\n').filter(Boolean);
451
- const rows = logLines
452
- .slice(-MAX_JSONL_ENTRIES)
453
- .map((line) => {
454
- try { return JSON.parse(line) as Record<string, unknown>; }
455
- catch { return null; }
456
- })
457
- .filter((r): r is Record<string, unknown> => r !== null);
458
- this.timeline = jsonlToTimeline(rows);
459
- } catch (err) {
460
- const code = (err as NodeJS.ErrnoException).code;
461
- if (code !== 'ENOENT') {
462
- logger.debug('AgentInspectorPanel: failed to load session log', { error: summarizeError(err) });
463
- }
464
- this.timeline = [];
465
- }
466
- this.markDirty();
467
- }
468
-
469
- private _startRefresh(): void {
470
- if (this.refreshTimerId) return;
471
- this.refreshTimerId = this.registerTimer(setInterval(() => {
472
- this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh tick failed', { err }); });
473
- }, REFRESH_MS));
474
- }
475
-
476
- private _stopRefresh(): void {
477
- if (this.refreshTimerId) {
478
- this.clearTimer(this.refreshTimerId);
479
- this.refreshTimerId = null;
480
- }
481
- }
482
-
483
- // -------------------------------------------------------------------------
484
- // Private — navigation
485
- // -------------------------------------------------------------------------
486
-
487
- private _moveCursor(delta: number): void {
488
- const rows = this._getCachedRows();
489
- if (rows.length === 0) return;
490
- this.cursorIndex = Math.max(0, Math.min(rows.length - 1, this.cursorIndex + delta));
491
- this.markDirty();
492
- }
493
-
494
- private _scroll(delta: number): void {
495
- this._moveCursor(delta);
496
- }
497
-
498
- private _toggleExpand(): void {
499
- const rows = this._getCachedRows();
500
- const row = rows[this.cursorIndex];
501
- if (!row?.entryRef || !row.hasDetail) return;
502
- row.entryRef.expanded = !row.entryRef.expanded;
503
- this.markDirty();
504
- }
505
-
506
- private _nextAgent(): void {
507
- const agents = this.deps.agentManager.list();
508
- if (agents.length === 0) return;
509
-
510
- if (!this.selectedAgentId) {
511
- this.inspectAgent(agents[0]!.id);
512
- return;
513
- }
514
-
515
- const idx = agents.findIndex(a => a.id === this.selectedAgentId);
516
- const next = agents[(idx + 1) % agents.length];
517
- if (next) {
518
- this.inspectAgent(next.id);
519
- }
520
- }
521
- }
@@ -1,94 +0,0 @@
1
- export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
2
-
3
- export interface AgentTimelineEntry {
4
- kind: AgentInspectorEntryKind;
5
- timestamp: number;
6
- label: string;
7
- content: string;
8
- detail?: string;
9
- expanded: boolean;
10
- }
11
-
12
- export interface AgentDisplayRow {
13
- kind: AgentInspectorEntryKind;
14
- timestamp: number;
15
- content: string;
16
- hasDetail: boolean;
17
- expanded: boolean;
18
- entryRef: AgentTimelineEntry | null;
19
- }
20
-
21
- type JsonlRow = Record<string, unknown>;
22
-
23
- export function agentStatusColor(status: string, colors: Record<string, string>): string {
24
- switch (status) {
25
- case 'pending': return colors.pending ?? colors.system;
26
- case 'running': return colors.running ?? colors.system;
27
- case 'completed': return colors.completed ?? colors.system;
28
- case 'failed': return colors.failed ?? colors.system;
29
- case 'cancelled': return colors.cancelled ?? colors.system;
30
- default: return colors.system;
31
- }
32
- }
33
-
34
- export function formatAgentDuration(ms: number): string {
35
- if (ms < 1000) return `${ms}ms`;
36
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
37
- return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
38
- }
39
-
40
- export function formatAgentTime(ts: number): string {
41
- const d = new Date(ts);
42
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
43
- }
44
-
45
- export function jsonlToTimeline(rows: JsonlRow[]): AgentTimelineEntry[] {
46
- const entries: AgentTimelineEntry[] = [];
47
- for (const row of rows) {
48
- const type = String(row.type ?? 'unknown');
49
- const rawTs = row.timestamp;
50
- const ts = typeof rawTs === 'string' ? Date.parse(rawTs) : typeof rawTs === 'number' ? rawTs : Date.now();
51
- switch (type) {
52
- case 'tool_execution': {
53
- const toolName = String(row.toolName ?? 'tool');
54
- const argsStr = row.args !== undefined ? JSON.stringify(row.args, null, 2) : undefined;
55
- const resultStr = row.result !== undefined ? JSON.stringify(row.result, null, 2) : undefined;
56
- const detail = [argsStr ? `Args:\n${argsStr}` : '', resultStr ? `Result:\n${resultStr}` : ''].filter(Boolean).join('\n\n');
57
- entries.push({ kind: 'tool_call', timestamp: ts, label: toolName, content: `[tool] ${toolName}` + (row.durationMs !== undefined ? ` (${row.durationMs}ms)` : ''), detail: detail || undefined, expanded: false });
58
- break;
59
- }
60
- case 'llm_response': {
61
- const toolCount = Number(row.toolCallCount ?? 0);
62
- const charLen = Number(row.contentLength ?? 0);
63
- entries.push({ kind: 'assistant', timestamp: ts, label: 'assistant', content: `[assistant] ${charLen} chars, ${toolCount} tool calls`, expanded: false });
64
- break;
65
- }
66
- case 'meta':
67
- case 'session_start':
68
- entries.push({ kind: 'session', timestamp: ts, label: 'session', content: `[session start] ${String(row.agentId ?? '')}`, expanded: false });
69
- break;
70
- case 'session_end':
71
- entries.push({ kind: 'session', timestamp: ts, label: 'session', content: `[session end] ${String(row.status ?? 'unknown')}`, expanded: false });
72
- break;
73
- case 'error':
74
- entries.push({ kind: 'error', timestamp: ts, label: 'error', content: `[error] ${String(row.message ?? row.error ?? 'unknown error')}`, expanded: false });
75
- break;
76
- default:
77
- entries.push({ kind: 'session', timestamp: ts, label: type, content: `[${type}]`, expanded: false });
78
- break;
79
- }
80
- }
81
- return entries;
82
- }
83
-
84
- export function agentKindStyle(kind: AgentInspectorEntryKind, colors: Record<string, string>): { fg: string; prefix: string } {
85
- switch (kind) {
86
- case 'user': return { fg: colors.user, prefix: '[user] ' };
87
- case 'assistant': return { fg: colors.assistant, prefix: '[assistant]' };
88
- case 'tool_call': return { fg: colors.tool, prefix: '[tool] ' };
89
- case 'tool_result': return { fg: colors.toolResult, prefix: ' \u2514 ' };
90
- case 'session': return { fg: colors.system, prefix: '[session] ' };
91
- case 'error': return { fg: colors.error, prefix: '[error] ' };
92
- default: return { fg: colors.dimmed ?? colors.system, prefix: '[?] ' };
93
- }
94
- }