@pellux/goodvibes-agent 0.1.101 → 0.1.103

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 (43) 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 -18
  6. package/src/cli/help.ts +86 -0
  7. package/src/cli/local-library-command.ts +516 -0
  8. package/src/cli/management.ts +17 -0
  9. package/src/cli/memory-command.ts +646 -0
  10. package/src/cli/package-verification.ts +10 -0
  11. package/src/cli/parser.ts +8 -0
  12. package/src/cli/types.ts +3 -0
  13. package/src/input/agent-workspace-setup.ts +2 -2
  14. package/src/input/agent-workspace-snapshot.ts +4 -4
  15. package/src/input/agent-workspace-types.ts +2 -2
  16. package/src/input/command-registry.ts +0 -8
  17. package/src/input/feed-context-factory.ts +1 -3
  18. package/src/input/handler-feed.ts +1 -4
  19. package/src/input/handler-interactions.ts +0 -1
  20. package/src/input/handler-modal-stack.ts +0 -1
  21. package/src/input/handler-modal-token-routes.ts +0 -11
  22. package/src/input/handler-picker-routes.ts +11 -20
  23. package/src/input/handler-ui-state.ts +0 -6
  24. package/src/input/handler.ts +1 -17
  25. package/src/main.ts +0 -6
  26. package/src/panels/builtin/agent.ts +0 -17
  27. package/src/panels/index.ts +0 -2
  28. package/src/renderer/agent-workspace.ts +3 -3
  29. package/src/renderer/conversation-overlays.ts +0 -6
  30. package/src/renderer/live-tail-modal.ts +10 -69
  31. package/src/renderer/process-modal.ts +28 -530
  32. package/src/runtime/bootstrap-command-parts.ts +0 -28
  33. package/src/runtime/bootstrap-core.ts +1 -1
  34. package/src/runtime/bootstrap.ts +3 -12
  35. package/src/runtime/services.ts +3 -4
  36. package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
  37. package/src/version.ts +1 -1
  38. package/src/panels/agent-inspector-panel.ts +0 -521
  39. package/src/panels/agent-inspector-shared.ts +0 -94
  40. package/src/panels/agent-logs-panel.ts +0 -559
  41. package/src/panels/agent-logs-shared.ts +0 -129
  42. package/src/renderer/agent-detail-modal.ts +0 -331
  43. package/src/renderer/process-summary.ts +0 -67
@@ -150,18 +150,11 @@ export function createBootstrapCommandActions(
150
150
  | 'exit'
151
151
  | 'reloadSystemPrompt'
152
152
  | 'showPanel'
153
- | 'openForensicsPanel'
154
- | 'openIncidentPanel'
155
153
  | 'openPolicyPanel'
156
- | 'openHooksPanel'
157
- | 'openCommunicationPanel'
158
- | 'openOrchestrationPanel'
159
- | 'openCockpitPanel'
160
154
  | 'openMcpWorkspace'
161
155
  | 'openAgentWorkspace'
162
156
  | 'openSecurityPanel'
163
157
  | 'openKnowledgePanel'
164
- | 'openRemotePanel'
165
158
  | 'openSubscriptionPanel'
166
159
  > {
167
160
  const {
@@ -244,27 +237,9 @@ export function createBootstrapCommandActions(
244
237
  exit: () => unwiredShellAction('exit'),
245
238
  reloadSystemPrompt: loadSystemPrompt,
246
239
  showPanel,
247
- openForensicsPanel: () => {
248
- showPanel('forensics');
249
- },
250
- openIncidentPanel: () => {
251
- showPanel('incident');
252
- },
253
240
  openPolicyPanel: () => {
254
241
  showPanel('policy');
255
242
  },
256
- openHooksPanel: () => {
257
- showPanel('hooks');
258
- },
259
- openCommunicationPanel: () => {
260
- showPanel('communication');
261
- },
262
- openOrchestrationPanel: () => {
263
- showPanel('orchestration');
264
- },
265
- openCockpitPanel: () => {
266
- showPanel('cockpit');
267
- },
268
243
  openMcpWorkspace: () => unwiredShellAction('openMcpWorkspace'),
269
244
  openAgentWorkspace: () => unwiredShellAction('openAgentWorkspace'),
270
245
  openSecurityPanel: () => {
@@ -273,9 +248,6 @@ export function createBootstrapCommandActions(
273
248
  openKnowledgePanel: () => {
274
249
  showPanel('knowledge');
275
250
  },
276
- openRemotePanel: () => {
277
- showPanel('remote');
278
- },
279
251
  openSubscriptionPanel: () => {
280
252
  showPanel('subscription');
281
253
  },
@@ -29,7 +29,7 @@ import { registerBootstrapHookBridge } from '@/runtime/index.ts';
29
29
  import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
30
30
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
31
31
  import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
32
- import { installAgentToolPolicyGuard } from '../tools/wrfc-agent-guard.ts';
32
+ import { installAgentToolPolicyGuard } from '../tools/agent-tool-policy-guard.ts';
33
33
  import { registerAgentLocalRegistryTool } from '../tools/agent-local-registry-tool.ts';
34
34
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
35
35
 
@@ -553,19 +553,10 @@ export async function bootstrapRuntime(
553
553
  };
554
554
 
555
555
  // ── Phase 12b: Operator intervention wiring (feature-gated) ──────────────
556
- // Wire the OpsControlPlane into CommandContext when the feature flag is enabled.
557
- // The store and task manager are created unconditionally so they reflect the
558
- // real runtime state (tasks registered before the flag check are visible).
556
+ // Keep the copied control-plane state internal. GoodVibes Agent does not
557
+ // expose the copied local ops-control panel; operator control is surfaced
558
+ // through Agent-owned status, approvals, automation, and delegation flows.
559
559
  ctx.commandContext.ops.acpManager = acpManager;
560
- if (opsControlPlane) {
561
- ctx.commandContext.openOpsPanel = () => {
562
- if (ctx.commandContext.showPanel) ctx.commandContext.showPanel('ops-control');
563
- else {
564
- panelManager.open('ops-control');
565
- requestRender();
566
- }
567
- };
568
- }
569
560
 
570
561
  // Wire exit from options if provided; otherwise main.ts binds the shell bridge.
571
562
  if (options?.exit) {
@@ -341,7 +341,8 @@ export interface RuntimeServices {
341
341
  readonly fileUndoManager: FileUndoManager;
342
342
  readonly integrationHelpers: IntegrationHelperService;
343
343
  /**
344
- * Re-root path-bound stores (MemoryStore, ProjectIndex) to a new working directory.
344
+ * Re-root workspace-bound stores to a new working directory.
345
+ * Agent memory is home/profile-owned and intentionally does not move on workspace swap.
345
346
  * Called by WorkspaceSwapManager after the new directory has been verified.
346
347
  * Stores that require a process restart emit a warn-level log; they continue serving
347
348
  * the old path until the daemon restarts with the new --working-dir.
@@ -472,7 +473,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
472
473
  });
473
474
  const artifactStore = new ArtifactStore({ configManager });
474
475
  const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
475
- const memoryDbPath = join(workingDirectory, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
476
+ const memoryDbPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
476
477
  const memoryStore = new MemoryStore(memoryDbPath, {
477
478
  embeddingRegistry: memoryEmbeddingRegistry,
478
479
  });
@@ -733,8 +734,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
733
734
  fileUndoManager,
734
735
  integrationHelpers,
735
736
  async rerootStores(newWorkingDir: string): Promise<void> {
736
- const newMemoryDbPath = join(newWorkingDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
737
- await memoryStore.reroot(newMemoryDbPath);
738
737
  await projectIndex.reroot(newWorkingDir);
739
738
  },
740
739
  };
@@ -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.101';
9
+ let _version = '0.1.103';
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
- }