@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.
- package/CHANGELOG.md +14 -0
- package/README.md +10 -0
- package/docs/README.md +1 -1
- package/docs/getting-started.md +17 -3
- package/package.json +1 -18
- package/src/cli/help.ts +86 -0
- package/src/cli/local-library-command.ts +516 -0
- package/src/cli/management.ts +17 -0
- package/src/cli/memory-command.ts +646 -0
- package/src/cli/package-verification.ts +10 -0
- package/src/cli/parser.ts +8 -0
- package/src/cli/types.ts +3 -0
- package/src/input/agent-workspace-setup.ts +2 -2
- package/src/input/agent-workspace-snapshot.ts +4 -4
- package/src/input/agent-workspace-types.ts +2 -2
- package/src/input/command-registry.ts +0 -8
- package/src/input/feed-context-factory.ts +1 -3
- package/src/input/handler-feed.ts +1 -4
- package/src/input/handler-interactions.ts +0 -1
- package/src/input/handler-modal-stack.ts +0 -1
- package/src/input/handler-modal-token-routes.ts +0 -11
- package/src/input/handler-picker-routes.ts +11 -20
- package/src/input/handler-ui-state.ts +0 -6
- package/src/input/handler.ts +1 -17
- package/src/main.ts +0 -6
- package/src/panels/builtin/agent.ts +0 -17
- package/src/panels/index.ts +0 -2
- package/src/renderer/agent-workspace.ts +3 -3
- package/src/renderer/conversation-overlays.ts +0 -6
- package/src/renderer/live-tail-modal.ts +10 -69
- package/src/renderer/process-modal.ts +28 -530
- package/src/runtime/bootstrap-command-parts.ts +0 -28
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap.ts +3 -12
- package/src/runtime/services.ts +3 -4
- package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -521
- package/src/panels/agent-inspector-shared.ts +0 -94
- package/src/panels/agent-logs-panel.ts +0 -559
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/renderer/agent-detail-modal.ts +0 -331
- 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
|
-
}
|