@pellux/goodvibes-tui 0.28.0 → 1.0.0
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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,635 +0,0 @@
|
|
|
1
|
-
import { promises as fsPromises, watch, type FSWatcher } from 'fs';
|
|
2
|
-
import type { Line } from '../types/grid.ts';
|
|
3
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
5
|
-
import type { AgentEvent } from '@/runtime/index.ts';
|
|
6
|
-
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
7
|
-
import {
|
|
8
|
-
buildEmptyState,
|
|
9
|
-
buildPanelLine,
|
|
10
|
-
buildStyledPanelLine,
|
|
11
|
-
buildPanelWorkspace,
|
|
12
|
-
resolveScrollablePanelSection,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
} from './polish.ts';
|
|
15
|
-
import {
|
|
16
|
-
type AgentLogEntry as LogEntry,
|
|
17
|
-
type AgentLogFilterType as FilterType,
|
|
18
|
-
AGENT_LOG_COLORS as COLOR,
|
|
19
|
-
AGENT_LOG_FILTER_CYCLE as FILTER_CYCLE,
|
|
20
|
-
AGENT_LOG_FILTER_LABELS as FILTER_LABELS,
|
|
21
|
-
parseAgentJsonl,
|
|
22
|
-
} from './agent-logs-shared.ts';
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Constants
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const POLL_INTERVAL_MS = 500;
|
|
29
|
-
|
|
30
|
-
export interface AgentLogsPanelDeps {
|
|
31
|
-
readonly agentManager: Pick<AgentManager, 'list'> & Partial<Pick<AgentManager, 'getStatus'>>;
|
|
32
|
-
readonly workingDirectory: string;
|
|
33
|
-
/**
|
|
34
|
-
* Request a compositor repaint. The 500ms poll timer, fs.watch callback and
|
|
35
|
-
* other async update paths call this (via markDirty) so new log lines and
|
|
36
|
-
* live streaming output are painted while the main thread is idle — render()
|
|
37
|
-
* is otherwise only invoked on input/turn/resize, which makes a running
|
|
38
|
-
* agent's log tail look frozen. Optional: when omitted the panel still marks
|
|
39
|
-
* dirty and repaints on the next event.
|
|
40
|
-
*/
|
|
41
|
-
readonly requestRender?: () => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// AgentLogsPanel
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
50
|
-
// ── Agent state ─────────────────────────────────────────────────────────
|
|
51
|
-
private agents: AgentRecord[] = [];
|
|
52
|
-
private selectedAgentIndex = 0;
|
|
53
|
-
|
|
54
|
-
// ── Log state ────────────────────────────────────────────────────────────
|
|
55
|
-
private allEntries: LogEntry[] = []; // raw parsed JSONL for selected agent
|
|
56
|
-
private filteredEntries: LogEntry[] = []; // after filter applied
|
|
57
|
-
private lastFileSize = 0;
|
|
58
|
-
/**
|
|
59
|
-
* Length of the selected agent's live streamingContent buffer at the last
|
|
60
|
-
* poll. During an LLM turn the record's streamingContent grows token-by-token
|
|
61
|
-
* with NO JSONL append, so the file-size gate alone never repaints the live
|
|
62
|
-
* tail; we also watch this length and markDirty when it advances.
|
|
63
|
-
*/
|
|
64
|
-
private lastStreamLen = 0;
|
|
65
|
-
|
|
66
|
-
// ── Modes ────────────────────────────────────────────────────────────────
|
|
67
|
-
private autoFollow = true;
|
|
68
|
-
private paused = false;
|
|
69
|
-
private filter: FilterType = 'all';
|
|
70
|
-
|
|
71
|
-
/** True while this panel is the active view — gates async repaint requests. */
|
|
72
|
-
private _active = false;
|
|
73
|
-
|
|
74
|
-
// ── Infrastructure ───────────────────────────────────────────────────────
|
|
75
|
-
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
76
|
-
private fsWatcher: FSWatcher | null = null;
|
|
77
|
-
private unsubs: Array<() => void> = [];
|
|
78
|
-
private readonly agentEvents: UiEventFeed<AgentEvent>;
|
|
79
|
-
|
|
80
|
-
constructor(agentEvents: UiEventFeed<AgentEvent>, private readonly deps: AgentLogsPanelDeps) {
|
|
81
|
-
super('agent-logs', 'Agents', 'A', 'agent');
|
|
82
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
83
|
-
this.agentEvents = agentEvents;
|
|
84
|
-
this._refreshAgents();
|
|
85
|
-
this._startPolling();
|
|
86
|
-
this._subscribeEvents();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── ScrollableListPanel<LogEntry> contract ────────────────────────────────
|
|
90
|
-
|
|
91
|
-
protected getItems(): readonly LogEntry[] {
|
|
92
|
-
return this.filteredEntries;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
protected renderItem(entry: LogEntry, _index: number, _selected: boolean, width: number): Line {
|
|
96
|
-
return this._renderEntry(entry, width);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
override onActivate(): void {
|
|
102
|
-
this._active = true;
|
|
103
|
-
super.onActivate();
|
|
104
|
-
this._refreshAgents();
|
|
105
|
-
this._pollCurrentAgent();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
override onDeactivate(): void {
|
|
109
|
-
this._active = false;
|
|
110
|
-
super.onDeactivate();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
override onDestroy(): void {
|
|
114
|
-
this._stopPolling();
|
|
115
|
-
this._unsubscribeEvents();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* T17: a bare markDirty() does not repaint — render() only runs on
|
|
120
|
-
* input/turn/resize. While active, ask the compositor for a frame so the
|
|
121
|
-
* background poll / fs.watch updates (new log lines, live streaming output)
|
|
122
|
-
* are not stuck off-screen. Gated on active state so the always-on poller
|
|
123
|
-
* does not force full-frame rebuilds while this panel is hidden.
|
|
124
|
-
*/
|
|
125
|
-
protected override markDirty(): void {
|
|
126
|
-
super.markDirty();
|
|
127
|
-
if (this._active) this.deps.requestRender?.();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Input ─────────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
handleInput(key: string): boolean {
|
|
133
|
-
switch (key) {
|
|
134
|
-
case 'tab':
|
|
135
|
-
case '\t': // Tab — cycle to next agent
|
|
136
|
-
this._selectNextAgent();
|
|
137
|
-
return true;
|
|
138
|
-
case ' ': // Space — pause/resume
|
|
139
|
-
this._togglePause();
|
|
140
|
-
return true;
|
|
141
|
-
case 'f': // f — cycle filter
|
|
142
|
-
this._cycleFilter();
|
|
143
|
-
return true;
|
|
144
|
-
case 'g': // g — jump to top
|
|
145
|
-
this.selectedIndex = 0;
|
|
146
|
-
this.autoFollow = false;
|
|
147
|
-
this.markDirty();
|
|
148
|
-
return true;
|
|
149
|
-
case 'G': // G — jump to bottom / re-enable auto-follow
|
|
150
|
-
this.autoFollow = true;
|
|
151
|
-
this._clampScroll();
|
|
152
|
-
this.markDirty();
|
|
153
|
-
return true;
|
|
154
|
-
default:
|
|
155
|
-
return super.handleInput(key);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── Render ────────────────────────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
render(width: number, height: number): Line[] {
|
|
162
|
-
this.needsRender = false;
|
|
163
|
-
// Context-aware footer: when there are no agents only the Tab cycle is
|
|
164
|
-
// meaningless, so hide the per-stream keys; otherwise reflect live state
|
|
165
|
-
// (Space toggles pause→resume, g/G only matter once there is a tail).
|
|
166
|
-
const runningCount = this.agents.filter((a) => a.status === 'running' || a.status === 'pending').length;
|
|
167
|
-
const footerLines = this.agents.length === 0
|
|
168
|
-
? [buildPanelLine(width, [[' Waiting for an agent session to start...', DEFAULT_PANEL_PALETTE.dim]])]
|
|
169
|
-
: [
|
|
170
|
-
buildPanelLine(width, [
|
|
171
|
-
[` ${this.selectedAgentIndex + 1}/${this.agents.length}`, DEFAULT_PANEL_PALETTE.dim],
|
|
172
|
-
[' Tab', DEFAULT_PANEL_PALETTE.info], [' next agent', DEFAULT_PANEL_PALETTE.dim],
|
|
173
|
-
[' Space', DEFAULT_PANEL_PALETTE.info], [this.paused ? ' resume' : ' pause', DEFAULT_PANEL_PALETTE.dim],
|
|
174
|
-
[' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
|
|
175
|
-
[' g/G', DEFAULT_PANEL_PALETTE.info], [this.autoFollow ? ' top/follow' : ' top/bottom', DEFAULT_PANEL_PALETTE.dim],
|
|
176
|
-
]),
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
const summaryLines = [
|
|
180
|
-
buildPanelLine(width, [
|
|
181
|
-
[' Agents ', DEFAULT_PANEL_PALETTE.label],
|
|
182
|
-
[String(this.agents.length), DEFAULT_PANEL_PALETTE.value],
|
|
183
|
-
[' Running ', DEFAULT_PANEL_PALETTE.label],
|
|
184
|
-
[String(runningCount), runningCount > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
185
|
-
[' Filter ', DEFAULT_PANEL_PALETTE.label],
|
|
186
|
-
[FILTER_LABELS[this.filter], DEFAULT_PANEL_PALETTE.info],
|
|
187
|
-
[' Mode ', DEFAULT_PANEL_PALETTE.label],
|
|
188
|
-
[this.paused ? 'paused' : this.autoFollow ? 'auto-follow' : 'manual', this.paused ? DEFAULT_PANEL_PALETTE.warn : this.autoFollow ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
189
|
-
]),
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
if (this.agents.length === 0) {
|
|
193
|
-
return buildPanelWorkspace(width, height, {
|
|
194
|
-
title: ' Agents',
|
|
195
|
-
intro: 'View-only live session stream for running agents, with per-agent switching and filtered event tails.',
|
|
196
|
-
sections: [
|
|
197
|
-
{ title: 'Summary', lines: summaryLines },
|
|
198
|
-
{
|
|
199
|
-
lines: buildEmptyState(
|
|
200
|
-
width,
|
|
201
|
-
' No agents running',
|
|
202
|
-
'Spawn or attach to an agent session and its structured logs stream here in real time.',
|
|
203
|
-
[
|
|
204
|
-
{ command: '/spawn <task>', summary: 'launch an agent and watch its live session log here' },
|
|
205
|
-
{ command: '/inspector', summary: 'open the inspector for a per-agent timeline and tool activity' },
|
|
206
|
-
],
|
|
207
|
-
DEFAULT_PANEL_PALETTE,
|
|
208
|
-
),
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
footerLines,
|
|
212
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const selectedAgent = this._selectedAgent();
|
|
217
|
-
const selectorLine = this._renderAgentSelector(width);
|
|
218
|
-
if (selectedAgent) {
|
|
219
|
-
summaryLines.push(buildPanelLine(width, [
|
|
220
|
-
[' Selected ', DEFAULT_PANEL_PALETTE.label],
|
|
221
|
-
[selectedAgent.id, DEFAULT_PANEL_PALETTE.info],
|
|
222
|
-
[' Status ', DEFAULT_PANEL_PALETTE.label],
|
|
223
|
-
[selectedAgent.status, selectedAgent.status === 'running' ? DEFAULT_PANEL_PALETTE.good : selectedAgent.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
224
|
-
]));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// T13: surface the selected agent's live streaming output (running) or
|
|
228
|
-
// final output (terminal) — the per-turn JSONL tail never carries it. Pull
|
|
229
|
-
// the freshest record via getStatus so the streaming buffer reflects the
|
|
230
|
-
// current turn; the list() snapshot in this.agents only refreshes on
|
|
231
|
-
// spawn/complete/fail events, never mid-stream.
|
|
232
|
-
const liveAgent = selectedAgent
|
|
233
|
-
? (this.deps.agentManager.getStatus?.(selectedAgent.id) ?? selectedAgent)
|
|
234
|
-
: null;
|
|
235
|
-
const liveLines = liveAgent ? this._buildLiveOutputLines(width, liveAgent) : [];
|
|
236
|
-
|
|
237
|
-
if (this.filteredEntries.length === 0 && liveLines.length === 0) {
|
|
238
|
-
return buildPanelWorkspace(width, height, {
|
|
239
|
-
title: ' Agents',
|
|
240
|
-
intro: 'View-only live session stream for running agents, with per-agent switching and filtered event tails.',
|
|
241
|
-
sections: [
|
|
242
|
-
{ title: 'Summary', lines: summaryLines },
|
|
243
|
-
{ title: 'Agents', lines: [selectorLine] },
|
|
244
|
-
{
|
|
245
|
-
lines: buildEmptyState(
|
|
246
|
-
width,
|
|
247
|
-
` No ${this.filter === 'all' ? '' : `${this.filter} `}log entries yet`,
|
|
248
|
-
'Once the selected agent writes session events, they will appear here and can be filtered by type.',
|
|
249
|
-
[],
|
|
250
|
-
DEFAULT_PANEL_PALETTE,
|
|
251
|
-
),
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
footerLines,
|
|
255
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const scrollableLines = [
|
|
260
|
-
...this.filteredEntries.map((entry) => this._renderEntry(entry, width)),
|
|
261
|
-
...liveLines,
|
|
262
|
-
];
|
|
263
|
-
const focusIndex = this.autoFollow
|
|
264
|
-
? Math.max(0, scrollableLines.length - 1)
|
|
265
|
-
: Math.min(this.selectedIndex, Math.max(0, scrollableLines.length - 1));
|
|
266
|
-
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
267
|
-
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
268
|
-
const logStreamSection = resolveScrollablePanelSection(width, height, {
|
|
269
|
-
intro: 'Tail per-agent JSONL session logs, filter entries, and switch between running or completed agents.',
|
|
270
|
-
footerLines,
|
|
271
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
272
|
-
beforeSections: [summarySection, agentsSection],
|
|
273
|
-
section: {
|
|
274
|
-
title: 'Log Stream',
|
|
275
|
-
scrollableLines,
|
|
276
|
-
selectedIndex: focusIndex,
|
|
277
|
-
scrollOffset: this.scrollStart,
|
|
278
|
-
minRows: 8,
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
this.scrollStart = logStreamSection.scrollOffset;
|
|
282
|
-
|
|
283
|
-
return buildPanelWorkspace(width, height, {
|
|
284
|
-
title: ' Agents',
|
|
285
|
-
intro: 'View-only live session stream for running agents, with per-agent switching and filtered event tails.',
|
|
286
|
-
sections: [
|
|
287
|
-
summarySection,
|
|
288
|
-
agentsSection,
|
|
289
|
-
logStreamSection.section,
|
|
290
|
-
],
|
|
291
|
-
footerLines,
|
|
292
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ── Private: polling ─────────────────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
private _startPolling(): void {
|
|
299
|
-
if (this.pollTimer !== null) return;
|
|
300
|
-
this.pollTimer = setInterval(() => {
|
|
301
|
-
if (!this.paused) {
|
|
302
|
-
this._pollCurrentAgent();
|
|
303
|
-
}
|
|
304
|
-
}, POLL_INTERVAL_MS);
|
|
305
|
-
// Also do an immediate read
|
|
306
|
-
this._pollCurrentAgent();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private _stopPolling(): void {
|
|
310
|
-
if (this.pollTimer !== null) {
|
|
311
|
-
clearInterval(this.pollTimer);
|
|
312
|
-
this.pollTimer = null;
|
|
313
|
-
}
|
|
314
|
-
this._stopWatcher();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private _pollCurrentAgent(): void {
|
|
318
|
-
void this._pollCurrentAgentAsync();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private async _pollCurrentAgentAsync(): Promise<void> {
|
|
322
|
-
const agent = this._selectedAgent();
|
|
323
|
-
if (!agent) return;
|
|
324
|
-
|
|
325
|
-
// Detect live streaming growth even when no JSONL is written this tick.
|
|
326
|
-
// During an LLM turn the record's streamingContent grows token-by-token
|
|
327
|
-
// with no session-log append, so the file-size gate below would never fire
|
|
328
|
-
// and the live tail (_buildLiveOutputLines) would never repaint.
|
|
329
|
-
let streamChanged = false;
|
|
330
|
-
const live = this.deps.agentManager.getStatus?.(agent.id);
|
|
331
|
-
if (live) {
|
|
332
|
-
const streamLen = (live.streamingContent ?? '').length;
|
|
333
|
-
if (streamLen !== this.lastStreamLen) {
|
|
334
|
-
this.lastStreamLen = streamLen;
|
|
335
|
-
streamChanged = true;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const sessionFile = this._sessionFilePath(agent.id);
|
|
340
|
-
let fileExists = true;
|
|
341
|
-
try {
|
|
342
|
-
await fsPromises.access(sessionFile);
|
|
343
|
-
} catch {
|
|
344
|
-
fileExists = false;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (fileExists) {
|
|
348
|
-
try {
|
|
349
|
-
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
350
|
-
if (content.length !== this.lastFileSize) {
|
|
351
|
-
this.lastFileSize = content.length;
|
|
352
|
-
|
|
353
|
-
// Re-parse all lines (simple: no partial-line tracking needed at 500ms)
|
|
354
|
-
this.allEntries = parseAgentJsonl(content);
|
|
355
|
-
this._applyFilter();
|
|
356
|
-
if (this.autoFollow) {
|
|
357
|
-
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
358
|
-
}
|
|
359
|
-
this.markDirty();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
} catch {
|
|
363
|
-
// Non-fatal: file may be mid-write
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// File unchanged or absent, but the live stream advanced — repaint the tail.
|
|
368
|
-
if (streamChanged) this.markDirty();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ── Private: fs.watch (supplemental) ─────────────────────────────────────
|
|
372
|
-
|
|
373
|
-
private _watchAgent(agentId: string): void {
|
|
374
|
-
this._stopWatcher();
|
|
375
|
-
const sessionFile = this._sessionFilePath(agentId);
|
|
376
|
-
// Start watching immediately; the watcher setup itself is synchronous,
|
|
377
|
-
// the file-existence check is skipped to avoid blocking — if the file
|
|
378
|
-
// does not yet exist watch() will throw and we catch it below.
|
|
379
|
-
try {
|
|
380
|
-
this.fsWatcher = watch(sessionFile, () => {
|
|
381
|
-
if (!this.paused) {
|
|
382
|
-
this._pollCurrentAgent();
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
} catch {
|
|
386
|
-
// Non-fatal: polling covers us
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private _stopWatcher(): void {
|
|
391
|
-
if (this.fsWatcher) {
|
|
392
|
-
try { this.fsWatcher.close(); } catch { /* ignore */ }
|
|
393
|
-
this.fsWatcher = null;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── Private: event subscriptions ─────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
private _subscribeEvents(): void {
|
|
400
|
-
const onSpawned = (data: { id: string; task: string }) => {
|
|
401
|
-
void data;
|
|
402
|
-
this._refreshAgents();
|
|
403
|
-
// Auto-select the newest agent if none selected or all done
|
|
404
|
-
const running = this.agents.filter(a => a.status === 'running' || a.status === 'pending');
|
|
405
|
-
if (running.length === 1) {
|
|
406
|
-
const idx = this.agents.findIndex(a => a.id === running[0]!.id);
|
|
407
|
-
if (idx >= 0) this._selectAgent(idx);
|
|
408
|
-
}
|
|
409
|
-
this.markDirty();
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const onComplete = (data: { id: string }) => {
|
|
413
|
-
void data;
|
|
414
|
-
this._refreshAgents();
|
|
415
|
-
this.markDirty();
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const onError = (data: { id: string; error: Error }) => {
|
|
419
|
-
void data;
|
|
420
|
-
this._refreshAgents();
|
|
421
|
-
this.markDirty();
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
this.unsubs.push(
|
|
425
|
-
this.agentEvents.on('AGENT_SPAWNING', (payload) => onSpawned({ id: payload.agentId, task: payload.task })),
|
|
426
|
-
this.agentEvents.on('AGENT_COMPLETED', (payload) => onComplete({ id: payload.agentId })),
|
|
427
|
-
this.agentEvents.on('AGENT_FAILED', (payload) => onError({ id: payload.agentId, error: new Error(payload.error) })),
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private _unsubscribeEvents(): void {
|
|
432
|
-
for (const unsub of this.unsubs) unsub();
|
|
433
|
-
this.unsubs = [];
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ── Private: agent management ─────────────────────────────────────────────
|
|
437
|
-
|
|
438
|
-
private _refreshAgents(): void {
|
|
439
|
-
const prev = this._selectedAgent();
|
|
440
|
-
this.agents = this.deps.agentManager.list()
|
|
441
|
-
.sort((a, b) => b.startedAt - a.startedAt); // newest first
|
|
442
|
-
|
|
443
|
-
// Try to keep same agent selected
|
|
444
|
-
if (prev) {
|
|
445
|
-
const idx = this.agents.findIndex(a => a.id === prev.id);
|
|
446
|
-
this.selectedAgentIndex = idx >= 0 ? idx : 0;
|
|
447
|
-
} else {
|
|
448
|
-
this.selectedAgentIndex = 0;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Re-watch if agent changed
|
|
452
|
-
const current = this._selectedAgent();
|
|
453
|
-
if (current) {
|
|
454
|
-
this._watchAgent(current.id);
|
|
455
|
-
this._reloadAgent(current);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
private _selectAgent(index: number): void {
|
|
460
|
-
if (index < 0 || index >= this.agents.length) return;
|
|
461
|
-
this.selectedAgentIndex = index;
|
|
462
|
-
this.allEntries = [];
|
|
463
|
-
this.filteredEntries = [];
|
|
464
|
-
this.lastFileSize = 0;
|
|
465
|
-
this.lastStreamLen = 0;
|
|
466
|
-
this.selectedIndex = 0;
|
|
467
|
-
this.scrollStart = 0;
|
|
468
|
-
this.autoFollow = true;
|
|
469
|
-
const agent = this._selectedAgent();
|
|
470
|
-
if (agent) {
|
|
471
|
-
this._watchAgent(agent.id);
|
|
472
|
-
this._reloadAgent(agent);
|
|
473
|
-
}
|
|
474
|
-
this.markDirty();
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private _selectNextAgent(): void {
|
|
478
|
-
if (this.agents.length === 0) return;
|
|
479
|
-
this._selectAgent((this.selectedAgentIndex + 1) % this.agents.length);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private _selectedAgent(): AgentRecord | null {
|
|
483
|
-
return this.agents[this.selectedAgentIndex] ?? null;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
private _reloadAgent(agent: AgentRecord): void {
|
|
487
|
-
void this._reloadAgentAsync(agent);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private async _reloadAgentAsync(agent: AgentRecord): Promise<void> {
|
|
491
|
-
const sessionFile = this._sessionFilePath(agent.id);
|
|
492
|
-
try {
|
|
493
|
-
await fsPromises.access(sessionFile);
|
|
494
|
-
} catch {
|
|
495
|
-
this.allEntries = [];
|
|
496
|
-
this.filteredEntries = [];
|
|
497
|
-
this.lastFileSize = 0;
|
|
498
|
-
this.lastStreamLen = 0;
|
|
499
|
-
this.markDirty();
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
504
|
-
this.lastFileSize = content.length;
|
|
505
|
-
this.allEntries = parseAgentJsonl(content);
|
|
506
|
-
this._applyFilter();
|
|
507
|
-
if (this.autoFollow) {
|
|
508
|
-
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
509
|
-
}
|
|
510
|
-
this.markDirty();
|
|
511
|
-
} catch {
|
|
512
|
-
this.allEntries = [];
|
|
513
|
-
this.filteredEntries = [];
|
|
514
|
-
this.markDirty();
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
private _sessionFilePath(agentId: string): string {
|
|
519
|
-
return `${this.deps.workingDirectory}/.goodvibes/tui/sessions/${agentId}.jsonl`;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// ── Private: filter ───────────────────────────────────────────────────────
|
|
523
|
-
|
|
524
|
-
private _applyFilter(): void {
|
|
525
|
-
if (this.filter === 'all') {
|
|
526
|
-
this.filteredEntries = [...this.allEntries];
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
this.filteredEntries = this.allEntries.filter(e => {
|
|
530
|
-
if (this.filter === 'assistant') return e.type === 'assistant';
|
|
531
|
-
if (this.filter === 'tool') return e.type === 'tool';
|
|
532
|
-
if (this.filter === 'error') return e.type === 'error';
|
|
533
|
-
return true;
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private _cycleFilter(): void {
|
|
538
|
-
const idx = FILTER_CYCLE.indexOf(this.filter);
|
|
539
|
-
this.filter = FILTER_CYCLE[(idx + 1) % FILTER_CYCLE.length]!;
|
|
540
|
-
this._applyFilter();
|
|
541
|
-
if (this.autoFollow) {
|
|
542
|
-
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
543
|
-
}
|
|
544
|
-
this.markDirty();
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private _togglePause(): void {
|
|
548
|
-
this.paused = !this.paused;
|
|
549
|
-
this.markDirty();
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private _clampScroll(): void {
|
|
553
|
-
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// ── Private: rendering helpers ─────────────────────────────────────────────
|
|
557
|
-
|
|
558
|
-
/** Agent selector bar: shows running agents with cycle indicator */
|
|
559
|
-
private _renderAgentSelector(width: number): Line {
|
|
560
|
-
const prefix = ' Agents: ';
|
|
561
|
-
const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
|
|
562
|
-
{ text: prefix, fg: COLOR.header_label },
|
|
563
|
-
];
|
|
564
|
-
for (let i = 0; i < this.agents.length; i++) {
|
|
565
|
-
const agent = this.agents[i]!;
|
|
566
|
-
const isSelected = i === this.selectedAgentIndex;
|
|
567
|
-
const statusColor = this._agentStatusColor(agent.status);
|
|
568
|
-
const shortId = agent.id.replace('agent-', '');
|
|
569
|
-
const label = isSelected
|
|
570
|
-
? `[${shortId}:${agent.status}] `
|
|
571
|
-
: `${shortId}:${agent.status} `;
|
|
572
|
-
segments.push({
|
|
573
|
-
text: label,
|
|
574
|
-
fg: isSelected ? COLOR.agent_selected : statusColor,
|
|
575
|
-
bold: isSelected,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
return buildStyledPanelLine(width, segments);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private _renderEntry(entry: LogEntry, width: number): Line {
|
|
582
|
-
// Indent non-session entries
|
|
583
|
-
const prefix = entry.type === 'session_start' ? '' : ' ';
|
|
584
|
-
const fullText = prefix + entry.text;
|
|
585
|
-
return buildStyledPanelLine(width, [{ text: fullText, fg: entry.color, bold: entry.bold }]);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Build trailing lines exposing the selected agent's live model output. The
|
|
590
|
-
* JSONL tail is written per-turn, so while an agent is mid-turn its actual
|
|
591
|
-
* work is invisible here; this surfaces the streaming token buffer
|
|
592
|
-
* (tail-truncated like AgentDetailModal) for a running agent, and the final
|
|
593
|
-
* assistant text once it terminates. Empty when there is nothing live to show.
|
|
594
|
-
*/
|
|
595
|
-
private _buildLiveOutputLines(width: number, agent: AgentRecord): Line[] {
|
|
596
|
-
const STREAM_MAX_CHARS = 500;
|
|
597
|
-
if (agent.status === 'running' && agent.streamingContent) {
|
|
598
|
-
const sc = agent.streamingContent;
|
|
599
|
-
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
600
|
-
const display = truncated ? sc.slice(-STREAM_MAX_CHARS) : sc;
|
|
601
|
-
const header = truncated
|
|
602
|
-
? ` streaming (last ${STREAM_MAX_CHARS} of ${sc.length} chars):`
|
|
603
|
-
: ' streaming:';
|
|
604
|
-
const lines: Line[] = [
|
|
605
|
-
buildStyledPanelLine(width, [{ text: header, fg: COLOR.auto_follow, bold: true }]),
|
|
606
|
-
];
|
|
607
|
-
for (const raw of display.split('\n')) {
|
|
608
|
-
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
609
|
-
}
|
|
610
|
-
return lines;
|
|
611
|
-
}
|
|
612
|
-
if (agent.status !== 'running' && agent.status !== 'pending' && agent.fullOutput) {
|
|
613
|
-
const fo = agent.fullOutput;
|
|
614
|
-
const lines: Line[] = [
|
|
615
|
-
buildStyledPanelLine(width, [{ text: ' output:', fg: COLOR.header_accent, bold: true }]),
|
|
616
|
-
];
|
|
617
|
-
for (const raw of fo.split('\n')) {
|
|
618
|
-
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
619
|
-
}
|
|
620
|
-
return lines;
|
|
621
|
-
}
|
|
622
|
-
return [];
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
private _agentStatusColor(status: AgentRecord['status']): string {
|
|
626
|
-
switch (status) {
|
|
627
|
-
case 'running': return COLOR.agent_running;
|
|
628
|
-
case 'pending': return COLOR.agent_pending;
|
|
629
|
-
case 'completed': return COLOR.agent_done;
|
|
630
|
-
case 'failed': return COLOR.agent_error;
|
|
631
|
-
case 'cancelled': return COLOR.agent_done;
|
|
632
|
-
default: return COLOR.dim;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|