@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
@@ -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
- }
@@ -1,559 +0,0 @@
1
- import { promises as fsPromises, watch, type FSWatcher } from 'fs';
2
- import type { Line } from '../types/grid.ts';
3
- import { createEmptyLine, createStyledCell } from '../types/grid.ts';
4
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
5
- import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
6
- import type { AgentEvent } from '@/runtime/index.ts';
7
- import type { UiEventFeed } from '../runtime/ui-events.ts';
8
- import {
9
- buildEmptyState,
10
- buildPanelLine,
11
- buildStyledPanelLine,
12
- buildPanelWorkspace,
13
- resolveScrollablePanelSection,
14
- DEFAULT_PANEL_PALETTE,
15
- } from './polish.ts';
16
- import {
17
- type AgentLogEntry as LogEntry,
18
- type AgentLogFilterType as FilterType,
19
- AGENT_LOG_COLORS as COLOR,
20
- AGENT_LOG_FILTER_CYCLE as FILTER_CYCLE,
21
- AGENT_LOG_FILTER_LABELS as FILTER_LABELS,
22
- parseAgentJsonl,
23
- } from './agent-logs-shared.ts';
24
- import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
25
-
26
- // ---------------------------------------------------------------------------
27
- // Constants
28
- // ---------------------------------------------------------------------------
29
-
30
- const POLL_INTERVAL_MS = 500;
31
-
32
- export interface AgentLogsPanelDeps {
33
- readonly agentManager: Pick<AgentManager, 'list'>;
34
- readonly workingDirectory: string;
35
- }
36
-
37
-
38
- // ---------------------------------------------------------------------------
39
- // AgentLogsPanel
40
- // ---------------------------------------------------------------------------
41
-
42
- export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
43
- // ── Agent state ─────────────────────────────────────────────────────────
44
- private agents: AgentRecord[] = [];
45
- private selectedAgentIndex = 0;
46
-
47
- // ── Log state ────────────────────────────────────────────────────────────
48
- private allEntries: LogEntry[] = []; // raw parsed JSONL for selected agent
49
- private filteredEntries: LogEntry[] = []; // after filter applied
50
- private lastFileSize = 0;
51
-
52
- // ── Modes ────────────────────────────────────────────────────────────────
53
- private autoFollow = true;
54
- private paused = false;
55
- private filter: FilterType = 'all';
56
-
57
- // ── Infrastructure ───────────────────────────────────────────────────────
58
- private pollTimer: ReturnType<typeof setInterval> | null = null;
59
- private fsWatcher: FSWatcher | null = null;
60
- private unsubs: Array<() => void> = [];
61
- private readonly agentEvents: UiEventFeed<AgentEvent>;
62
-
63
- constructor(agentEvents: UiEventFeed<AgentEvent>, private readonly deps: AgentLogsPanelDeps) {
64
- super('agent-logs', 'Agents', 'A', 'agent');
65
- this.showSelectionGutter = true; // I5: non-color selection affordance
66
- this.agentEvents = agentEvents;
67
- this._refreshAgents();
68
- this._startPolling();
69
- this._subscribeEvents();
70
- }
71
-
72
- // ── ScrollableListPanel<LogEntry> contract ────────────────────────────────
73
-
74
- protected getItems(): readonly LogEntry[] {
75
- return this.filteredEntries;
76
- }
77
-
78
- protected renderItem(entry: LogEntry, _index: number, _selected: boolean, width: number): Line {
79
- return this._renderEntry(entry, width);
80
- }
81
-
82
- // ── Lifecycle ─────────────────────────────────────────────────────────────
83
-
84
- override onActivate(): void {
85
- super.onActivate();
86
- this._refreshAgents();
87
- this._pollCurrentAgent();
88
- }
89
-
90
- override onDeactivate(): void {
91
- super.onDeactivate();
92
- }
93
-
94
- override onDestroy(): void {
95
- this._stopPolling();
96
- this._unsubscribeEvents();
97
- }
98
-
99
- // ── Input ─────────────────────────────────────────────────────────────────
100
-
101
- handleInput(key: string): boolean {
102
- switch (key) {
103
- case 'tab':
104
- case '\t': // Tab — cycle to next agent
105
- this._selectNextAgent();
106
- return true;
107
- case ' ': // Space — pause/resume
108
- this._togglePause();
109
- return true;
110
- case 'f': // f — cycle filter
111
- this._cycleFilter();
112
- return true;
113
- case 'g': // g — jump to top
114
- this.selectedIndex = 0;
115
- this.autoFollow = false;
116
- this.markDirty();
117
- return true;
118
- case 'G': // G — jump to bottom / re-enable auto-follow
119
- this.autoFollow = true;
120
- this._clampScroll();
121
- this.markDirty();
122
- return true;
123
- default:
124
- return super.handleInput(key);
125
- }
126
- }
127
-
128
- // ── Render ────────────────────────────────────────────────────────────────
129
-
130
- render(width: number, height: number): Line[] {
131
- this.needsRender = false;
132
- const footerLines = [
133
- buildPanelLine(width, [
134
- [' Tab', DEFAULT_PANEL_PALETTE.info], [' next agent', DEFAULT_PANEL_PALETTE.dim],
135
- [' Space', DEFAULT_PANEL_PALETTE.info], [' pause', DEFAULT_PANEL_PALETTE.dim],
136
- [' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
137
- [' g/G', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim],
138
- ]),
139
- ];
140
-
141
- const summaryLines = [
142
- buildPanelLine(width, [
143
- [' Agents ', DEFAULT_PANEL_PALETTE.label],
144
- [String(this.agents.length), DEFAULT_PANEL_PALETTE.value],
145
- [' Filter ', DEFAULT_PANEL_PALETTE.label],
146
- [FILTER_LABELS[this.filter], DEFAULT_PANEL_PALETTE.info],
147
- [' Mode ', DEFAULT_PANEL_PALETTE.label],
148
- [this.paused ? 'paused' : this.autoFollow ? 'auto-follow' : 'manual', this.paused ? DEFAULT_PANEL_PALETTE.warn : this.autoFollow ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
149
- ]),
150
- ];
151
-
152
- if (this.agents.length === 0) {
153
- return buildPanelWorkspace(width, height, {
154
- title: ' Agents',
155
- intro: 'View-only stream for explicit delegated build/review session records; normal assistant work stays in the main conversation.',
156
- sections: [
157
- { title: 'Summary', lines: summaryLines },
158
- {
159
- lines: buildEmptyState(
160
- width,
161
- ' No delegated agent sessions tracked',
162
- 'Explicit GoodVibes TUI build/review delegation records will appear here when available.',
163
- [],
164
- DEFAULT_PANEL_PALETTE,
165
- ),
166
- },
167
- ],
168
- footerLines,
169
- palette: DEFAULT_PANEL_PALETTE,
170
- });
171
- }
172
-
173
- const selectedAgent = this._selectedAgent();
174
- const selectorLine = this._renderAgentSelector(width);
175
- if (selectedAgent) {
176
- summaryLines.push(buildPanelLine(width, [
177
- [' Selected ', DEFAULT_PANEL_PALETTE.label],
178
- [selectedAgent.id, DEFAULT_PANEL_PALETTE.info],
179
- [' Status ', DEFAULT_PANEL_PALETTE.label],
180
- [selectedAgent.status, selectedAgent.status === 'running' ? DEFAULT_PANEL_PALETTE.good : selectedAgent.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
181
- ]));
182
- }
183
-
184
- if (this.filteredEntries.length === 0) {
185
- return buildPanelWorkspace(width, height, {
186
- title: ' Agents',
187
- intro: 'View-only stream for explicit delegated build/review session records; normal assistant work stays in the main conversation.',
188
- sections: [
189
- { title: 'Summary', lines: summaryLines },
190
- { title: 'Agents', lines: [selectorLine] },
191
- {
192
- lines: buildEmptyState(
193
- width,
194
- ` No ${this.filter === 'all' ? '' : `${this.filter} `}log entries yet`,
195
- 'Once the selected agent writes session events, they will appear here and can be filtered by type.',
196
- [],
197
- DEFAULT_PANEL_PALETTE,
198
- ),
199
- },
200
- ],
201
- footerLines,
202
- palette: DEFAULT_PANEL_PALETTE,
203
- });
204
- }
205
-
206
- const focusIndex = this.autoFollow
207
- ? Math.max(0, this.filteredEntries.length - 1)
208
- : Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));
209
- const summarySection = { title: 'Summary', lines: summaryLines } as const;
210
- const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
211
- const logStreamSection = resolveScrollablePanelSection(width, height, {
212
- intro: 'Tail explicit delegated-session JSONL logs, filter entries, and switch between tracked sessions.',
213
- footerLines,
214
- palette: DEFAULT_PANEL_PALETTE,
215
- beforeSections: [summarySection, agentsSection],
216
- section: {
217
- title: 'Log Stream',
218
- scrollableLines: this.filteredEntries.map((entry) => this._renderEntry(entry, width)),
219
- selectedIndex: focusIndex,
220
- scrollOffset: this.scrollStart,
221
- minRows: 8,
222
- },
223
- });
224
- this.scrollStart = logStreamSection.scrollOffset;
225
-
226
- return buildPanelWorkspace(width, height, {
227
- title: ' Agents',
228
- intro: 'View-only stream for explicit delegated build/review session records; normal assistant work stays in the main conversation.',
229
- sections: [
230
- summarySection,
231
- agentsSection,
232
- logStreamSection.section,
233
- ],
234
- footerLines,
235
- palette: DEFAULT_PANEL_PALETTE,
236
- });
237
- }
238
-
239
- // ── Private: polling ─────────────────────────────────────────────────────
240
-
241
- private _startPolling(): void {
242
- if (this.pollTimer !== null) return;
243
- this.pollTimer = setInterval(() => {
244
- if (!this.paused) {
245
- this._pollCurrentAgent();
246
- }
247
- }, POLL_INTERVAL_MS);
248
- // Also do an immediate read
249
- this._pollCurrentAgent();
250
- }
251
-
252
- private _stopPolling(): void {
253
- if (this.pollTimer !== null) {
254
- clearInterval(this.pollTimer);
255
- this.pollTimer = null;
256
- }
257
- this._stopWatcher();
258
- }
259
-
260
- private _pollCurrentAgent(): void {
261
- void this._pollCurrentAgentAsync();
262
- }
263
-
264
- private async _pollCurrentAgentAsync(): Promise<void> {
265
- const agent = this._selectedAgent();
266
- if (!agent) return;
267
-
268
- const sessionFile = this._sessionFilePath(agent.id);
269
- try {
270
- await fsPromises.access(sessionFile);
271
- } catch {
272
- return;
273
- }
274
-
275
- try {
276
- const content = await fsPromises.readFile(sessionFile, 'utf-8');
277
- if (content.length === this.lastFileSize) return;
278
- this.lastFileSize = content.length;
279
-
280
- // Re-parse all lines (simple: no partial-line tracking needed at 500ms)
281
- this.allEntries = parseAgentJsonl(content);
282
- this._applyFilter();
283
- if (this.autoFollow) {
284
- this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
285
- }
286
- this.markDirty();
287
- } catch {
288
- // Non-fatal: file may be mid-write
289
- }
290
- }
291
-
292
- // ── Private: fs.watch (supplemental) ─────────────────────────────────────
293
-
294
- private _watchAgent(agentId: string): void {
295
- this._stopWatcher();
296
- const sessionFile = this._sessionFilePath(agentId);
297
- // Start watching immediately; the watcher setup itself is synchronous,
298
- // the file-existence check is skipped to avoid blocking — if the file
299
- // does not yet exist watch() will throw and we catch it below.
300
- try {
301
- this.fsWatcher = watch(sessionFile, () => {
302
- if (!this.paused) {
303
- this._pollCurrentAgent();
304
- }
305
- });
306
- } catch {
307
- // Non-fatal: polling covers us
308
- }
309
- }
310
-
311
- private _stopWatcher(): void {
312
- if (this.fsWatcher) {
313
- try { this.fsWatcher.close(); } catch { /* ignore */ }
314
- this.fsWatcher = null;
315
- }
316
- }
317
-
318
- // ── Private: event subscriptions ─────────────────────────────────────────
319
-
320
- private _subscribeEvents(): void {
321
- const onSpawned = (data: { id: string; task: string }) => {
322
- void data;
323
- this._refreshAgents();
324
- // Auto-select the newest agent if none selected or all done
325
- const running = this.agents.filter(a => a.status === 'running' || a.status === 'pending');
326
- if (running.length === 1) {
327
- const idx = this.agents.findIndex(a => a.id === running[0]!.id);
328
- if (idx >= 0) this._selectAgent(idx);
329
- }
330
- this.markDirty();
331
- };
332
-
333
- const onComplete = (data: { id: string }) => {
334
- void data;
335
- this._refreshAgents();
336
- this.markDirty();
337
- };
338
-
339
- const onError = (data: { id: string; error: Error }) => {
340
- void data;
341
- this._refreshAgents();
342
- this.markDirty();
343
- };
344
-
345
- this.unsubs.push(
346
- this.agentEvents.on('AGENT_SPAWNING', (payload) => onSpawned({ id: payload.agentId, task: payload.task })),
347
- this.agentEvents.on('AGENT_COMPLETED', (payload) => onComplete({ id: payload.agentId })),
348
- this.agentEvents.on('AGENT_FAILED', (payload) => onError({ id: payload.agentId, error: new Error(payload.error) })),
349
- );
350
- }
351
-
352
- private _unsubscribeEvents(): void {
353
- for (const unsub of this.unsubs) unsub();
354
- this.unsubs = [];
355
- }
356
-
357
- // ── Private: agent management ─────────────────────────────────────────────
358
-
359
- private _refreshAgents(): void {
360
- const prev = this._selectedAgent();
361
- this.agents = this.deps.agentManager.list()
362
- .sort((a, b) => b.startedAt - a.startedAt); // newest first
363
-
364
- // Try to keep same agent selected
365
- if (prev) {
366
- const idx = this.agents.findIndex(a => a.id === prev.id);
367
- this.selectedAgentIndex = idx >= 0 ? idx : 0;
368
- } else {
369
- this.selectedAgentIndex = 0;
370
- }
371
-
372
- // Re-watch if agent changed
373
- const current = this._selectedAgent();
374
- if (current) {
375
- this._watchAgent(current.id);
376
- this._reloadAgent(current);
377
- }
378
- }
379
-
380
- private _selectAgent(index: number): void {
381
- if (index < 0 || index >= this.agents.length) return;
382
- this.selectedAgentIndex = index;
383
- this.allEntries = [];
384
- this.filteredEntries = [];
385
- this.lastFileSize = 0;
386
- this.selectedIndex = 0;
387
- this.scrollStart = 0;
388
- this.autoFollow = true;
389
- const agent = this._selectedAgent();
390
- if (agent) {
391
- this._watchAgent(agent.id);
392
- this._reloadAgent(agent);
393
- }
394
- this.markDirty();
395
- }
396
-
397
- private _selectNextAgent(): void {
398
- if (this.agents.length === 0) return;
399
- this._selectAgent((this.selectedAgentIndex + 1) % this.agents.length);
400
- }
401
-
402
- private _selectedAgent(): AgentRecord | null {
403
- return this.agents[this.selectedAgentIndex] ?? null;
404
- }
405
-
406
- private _reloadAgent(agent: AgentRecord): void {
407
- void this._reloadAgentAsync(agent);
408
- }
409
-
410
- private async _reloadAgentAsync(agent: AgentRecord): Promise<void> {
411
- const sessionFile = this._sessionFilePath(agent.id);
412
- try {
413
- await fsPromises.access(sessionFile);
414
- } catch {
415
- this.allEntries = [];
416
- this.filteredEntries = [];
417
- this.lastFileSize = 0;
418
- this.markDirty();
419
- return;
420
- }
421
- try {
422
- const content = await fsPromises.readFile(sessionFile, 'utf-8');
423
- this.lastFileSize = content.length;
424
- this.allEntries = parseAgentJsonl(content);
425
- this._applyFilter();
426
- if (this.autoFollow) {
427
- this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
428
- }
429
- this.markDirty();
430
- } catch {
431
- this.allEntries = [];
432
- this.filteredEntries = [];
433
- this.markDirty();
434
- }
435
- }
436
-
437
- private _sessionFilePath(agentId: string): string {
438
- return `${this.deps.workingDirectory}/.goodvibes/${GOODVIBES_AGENT_SURFACE_ROOT}/sessions/${agentId}.jsonl`;
439
- }
440
-
441
- // ── Private: filter ───────────────────────────────────────────────────────
442
-
443
- private _applyFilter(): void {
444
- if (this.filter === 'all') {
445
- this.filteredEntries = [...this.allEntries];
446
- return;
447
- }
448
- this.filteredEntries = this.allEntries.filter(e => {
449
- if (this.filter === 'assistant') return e.type === 'assistant';
450
- if (this.filter === 'tool') return e.type === 'tool';
451
- if (this.filter === 'error') return e.type === 'error';
452
- return true;
453
- });
454
- }
455
-
456
- private _cycleFilter(): void {
457
- const idx = FILTER_CYCLE.indexOf(this.filter);
458
- this.filter = FILTER_CYCLE[(idx + 1) % FILTER_CYCLE.length]!;
459
- this._applyFilter();
460
- if (this.autoFollow) {
461
- this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
462
- }
463
- this.markDirty();
464
- }
465
-
466
- private _togglePause(): void {
467
- this.paused = !this.paused;
468
- this.markDirty();
469
- }
470
-
471
- private _clampScroll(): void {
472
- this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));
473
- }
474
-
475
- // ── Private: rendering helpers ─────────────────────────────────────────────
476
-
477
- /** Top header bar: title + filter label + mode indicators */
478
- private _renderHeader(width: number): Line {
479
- const title = ' Agent Logs ';
480
- const filterLabel = `[${FILTER_LABELS[this.filter]}] `;
481
- const pause = this.paused ? ' PAUSED ' : '';
482
- const follow = this.autoFollow ? ' AUTO-FOLLOW ' : '';
483
- const keyhints = ' Tab:next Space:pause f:filter g/G:scroll ';
484
- return buildStyledPanelLine(width, [
485
- { text: title, fg: COLOR.header_accent, bold: true },
486
- { text: filterLabel, fg: COLOR.filter_active },
487
- { text: pause, fg: COLOR.paused },
488
- { text: follow, fg: COLOR.auto_follow },
489
- { text: keyhints, fg: COLOR.header_label },
490
- ]);
491
- }
492
-
493
- /** Agent selector bar: shows tracked delegated sessions with cycle indicator */
494
- private _renderAgentSelector(width: number): Line {
495
- const prefix = ' Agents: ';
496
- const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
497
- { text: prefix, fg: COLOR.header_label },
498
- ];
499
- for (let i = 0; i < this.agents.length; i++) {
500
- const agent = this.agents[i]!;
501
- const isSelected = i === this.selectedAgentIndex;
502
- const statusColor = this._agentStatusColor(agent.status);
503
- const shortId = agent.id.replace('agent-', '');
504
- const label = isSelected
505
- ? `[${shortId}:${agent.status}] `
506
- : `${shortId}:${agent.status} `;
507
- segments.push({
508
- text: label,
509
- fg: isSelected ? COLOR.agent_selected : statusColor,
510
- bold: isSelected,
511
- });
512
- }
513
- return buildStyledPanelLine(width, segments);
514
- }
515
-
516
- private _renderNoAgents(width: number): Line {
517
- const msg = ' No agents running. ';
518
- return buildStyledPanelLine(width, [{ text: msg, fg: COLOR.dim }]);
519
- }
520
-
521
- private _renderSeparator(width: number): Line {
522
- return buildStyledPanelLine(width, [{ text: '─'.repeat(width), fg: COLOR.separator }]);
523
- }
524
-
525
- private _renderEmpty(width: number, bodyHeight: number): Line[] {
526
- const lines: Line[] = [];
527
- const msg = this.agents.length === 0
528
- ? ' No agents running '
529
- : ` No ${this.filter === 'all' ? '' : this.filter + ' '}log entries yet `;
530
- const offset = Math.max(0, Math.floor((width - msg.length) / 2));
531
- const textLine = buildStyledPanelLine(width, [
532
- { text: ' '.repeat(offset), fg: COLOR.dim },
533
- { text: msg, fg: COLOR.dim },
534
- ]);
535
- lines.push(textLine);
536
- while (lines.length < bodyHeight) {
537
- lines.push(createEmptyLine(width));
538
- }
539
- return lines;
540
- }
541
-
542
- private _renderEntry(entry: LogEntry, width: number): Line {
543
- // Indent non-session entries
544
- const prefix = entry.type === 'session_start' ? '' : ' ';
545
- const fullText = prefix + entry.text;
546
- return buildStyledPanelLine(width, [{ text: fullText, fg: entry.color, bold: entry.bold }]);
547
- }
548
-
549
- private _agentStatusColor(status: AgentRecord['status']): string {
550
- switch (status) {
551
- case 'running': return COLOR.agent_running;
552
- case 'pending': return COLOR.agent_pending;
553
- case 'completed': return COLOR.agent_done;
554
- case 'failed': return COLOR.agent_error;
555
- case 'cancelled': return COLOR.agent_done;
556
- default: return COLOR.dim;
557
- }
558
- }
559
- }