@pellux/goodvibes-tui 1.1.0 → 1.7.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// AgentInspectorPanel — detailed view of a specific agent's messages and tool
|
|
3
|
-
// calls. WO-110: merged agent-logs-panel.ts in (pause/filter/follow/auto-
|
|
4
|
-
// select/stall-fix) on top of this panel's correct JSONL parser + cancel.
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { readFile } from 'node:fs/promises';
|
|
9
|
-
import type { Line } from '../types/grid.ts';
|
|
10
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
11
|
-
import { BasePanel } from './base-panel.ts';
|
|
12
|
-
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
13
|
-
import type { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
|
|
14
|
-
import type { AgentEvent } from '@/runtime/index.ts';
|
|
15
|
-
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
16
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
|
-
import {
|
|
18
|
-
buildEmptyState,
|
|
19
|
-
buildPanelLine,
|
|
20
|
-
buildSelectablePanelLine,
|
|
21
|
-
type StyledPanelSegment,
|
|
22
|
-
buildStyledPanelLine,
|
|
23
|
-
buildPanelWorkspace,
|
|
24
|
-
resolveScrollablePanelSection,
|
|
25
|
-
DEFAULT_PANEL_PALETTE,
|
|
26
|
-
extendPalette,
|
|
27
|
-
} from './polish.ts';
|
|
28
|
-
import {
|
|
29
|
-
type ConfirmState,
|
|
30
|
-
handleConfirmInput,
|
|
31
|
-
} from './confirm-state.ts';
|
|
32
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
33
|
-
import { abbreviateCount } from '../utils/format-number.ts';
|
|
34
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
|
-
import {
|
|
36
|
-
type AgentDisplayRow as DisplayRow,
|
|
37
|
-
type AgentInspectorEntryKind as EntryKind,
|
|
38
|
-
type AgentTimelineEntry as TimelineEntry,
|
|
39
|
-
type AgentInspectorFilterType as FilterType,
|
|
40
|
-
agentKindStyle,
|
|
41
|
-
formatAgentDuration as formatMs,
|
|
42
|
-
formatAgentTime as shortTime,
|
|
43
|
-
jsonlToTimeline,
|
|
44
|
-
matchesInspectorFilter,
|
|
45
|
-
buildWrfcCostSegments,
|
|
46
|
-
subscribeAgentActivity,
|
|
47
|
-
AGENT_INSPECTOR_FILTER_CYCLE,
|
|
48
|
-
AGENT_INSPECTOR_FILTER_LABELS,
|
|
49
|
-
AGENT_TERMINAL_STATUSES,
|
|
50
|
-
AGENT_STALL_THRESHOLD_MS,
|
|
51
|
-
} from './agent-inspector-shared.ts';
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Constants & colour palette
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
const REFRESH_MS = 500;
|
|
58
|
-
const MAX_JSONL_ENTRIES = 200;
|
|
59
|
-
|
|
60
|
-
// Domain accents only — base chrome (header/headerBg/label/value/dim/selectBg/
|
|
61
|
-
// warn) comes from DEFAULT_PANEL_PALETTE via extendPalette; the title band is
|
|
62
|
-
// the one canonical UI_TONES.bg.title, not a per-panel override.
|
|
63
|
-
const COLOR = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
64
|
-
// Agent selector chrome
|
|
65
|
-
statusBar: '#222233',
|
|
66
|
-
separator: '#333355',
|
|
67
|
-
|
|
68
|
-
// Timeline roles
|
|
69
|
-
user: '#cc88ff', // purple
|
|
70
|
-
assistant: '#ffffff', // white
|
|
71
|
-
tool: '#00ccff', // cyan
|
|
72
|
-
toolResult: '#66ddff', // light cyan
|
|
73
|
-
error: '#ff6666', // red
|
|
74
|
-
system: '#888888', // dim grey
|
|
75
|
-
timestamp: '#555577',
|
|
76
|
-
|
|
77
|
-
// Selector / labels
|
|
78
|
-
selected: '#ffee88',
|
|
79
|
-
expandHint: '#445566',
|
|
80
|
-
} as const);
|
|
81
|
-
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// AgentInspectorPanel
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
// AGENT_TERMINAL_STATUSES and AGENT_STALL_THRESHOLD_MS imported from agent-inspector-shared.ts
|
|
87
|
-
|
|
88
|
-
export interface AgentInspectorPanelDeps {
|
|
89
|
-
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
|
|
90
|
-
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
91
|
-
readonly workingDirectory: string;
|
|
92
|
-
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
93
|
-
readonly cancelAgent: (agentId: string) => boolean;
|
|
94
|
-
/** Agent lifecycle feed; drives auto-select-on-spawn + event-driven stall detection (WO-110). */
|
|
95
|
-
readonly agentEvents: UiEventFeed<AgentEvent>;
|
|
96
|
-
/**
|
|
97
|
-
* Request a compositor repaint. The 500ms refresh timer and other async
|
|
98
|
-
* update paths call this (via markDirty) so live agent output is painted
|
|
99
|
-
* while the main thread is idle — render() is otherwise only invoked on
|
|
100
|
-
* input/turn/resize, which makes a running agent's timeline look frozen.
|
|
101
|
-
* Optional: when omitted the panel still marks dirty and repaints on the
|
|
102
|
-
* next event.
|
|
103
|
-
*/
|
|
104
|
-
readonly requestRender?: () => void;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export class AgentInspectorPanel extends BasePanel {
|
|
108
|
-
// The agent currently being inspected
|
|
109
|
-
private selectedAgentId: string | null = null;
|
|
110
|
-
|
|
111
|
-
// Flattened timeline for the selected agent
|
|
112
|
-
private timeline: TimelineEntry[] = [];
|
|
113
|
-
|
|
114
|
-
// Scroll state
|
|
115
|
-
private scrollOffset = 0;
|
|
116
|
-
|
|
117
|
-
// Cursor index for expand/collapse
|
|
118
|
-
private cursorIndex = 0;
|
|
119
|
-
|
|
120
|
-
// Refresh timer (active only while panel is active)
|
|
121
|
-
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
122
|
-
|
|
123
|
-
// Row cache — cleared on markDirty(), computed once per render cycle
|
|
124
|
-
private _cachedRows: DisplayRow[] | null = null;
|
|
125
|
-
|
|
126
|
-
/** Pending cancel confirmation — subject is the agent id to cancel. */
|
|
127
|
-
private confirmCancel: ConfirmState<string> | null = null;
|
|
128
|
-
|
|
129
|
-
/** True while this panel is the active view — gates async repaint requests. */
|
|
130
|
-
private _active = false;
|
|
131
|
-
|
|
132
|
-
// ---- ported from agent-logs (WO-110 merge) -------------------------------
|
|
133
|
-
/** Freezes the 500ms JSONL refresh tick while true; manual navigation still works. */
|
|
134
|
-
private paused = false;
|
|
135
|
-
/** When true, cursor snaps to the newest row on every render (tail -f style). */
|
|
136
|
-
private autoFollow = true;
|
|
137
|
-
private filter: FilterType = 'all';
|
|
138
|
-
/** Per-agent last-activity timestamp derived from AGENT_* events — stall detection uses this, not startedAt. */
|
|
139
|
-
private readonly lastActivityByAgent = new Map<string, number>();
|
|
140
|
-
private unsubs: Array<() => void> = [];
|
|
141
|
-
|
|
142
|
-
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
143
|
-
super('inspector', 'Inspector', 'I', 'agent');
|
|
144
|
-
this._subscribeAgentEvents();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// -------------------------------------------------------------------------
|
|
148
|
-
// Row cache
|
|
149
|
-
// -------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
private _getCachedRows(): DisplayRow[] {
|
|
152
|
-
if (!this._cachedRows) this._cachedRows = this._buildVisibleRows();
|
|
153
|
-
return this._cachedRows;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
override markDirty(): void {
|
|
157
|
-
this._cachedRows = null;
|
|
158
|
-
super.markDirty();
|
|
159
|
-
// T17: a bare markDirty() does not repaint — render() only runs on
|
|
160
|
-
// input/turn/resize. While active, ask the compositor for a frame so
|
|
161
|
-
// timer/async refreshes (live streaming output) are not stuck off-screen.
|
|
162
|
-
if (this._active) this.deps.requestRender?.();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// -------------------------------------------------------------------------
|
|
166
|
-
// Public API
|
|
167
|
-
// -------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/** Focus this panel on a specific agent by ID. */
|
|
170
|
-
inspectAgent(agentId: string): void {
|
|
171
|
-
this.selectedAgentId = agentId;
|
|
172
|
-
this.scrollOffset = 0;
|
|
173
|
-
this.cursorIndex = 0;
|
|
174
|
-
this.timeline = [];
|
|
175
|
-
this.autoFollow = true;
|
|
176
|
-
this.markDirty();
|
|
177
|
-
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh failed', { err }); });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// -------------------------------------------------------------------------
|
|
181
|
-
// Lifecycle
|
|
182
|
-
// -------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
override onActivate(): void {
|
|
185
|
-
this._active = true;
|
|
186
|
-
this.needsRender = true;
|
|
187
|
-
this._startRefresh();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
override onDeactivate(): void {
|
|
191
|
-
this._active = false;
|
|
192
|
-
this._stopRefresh();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
override onDestroy(): void {
|
|
196
|
-
this._stopRefresh();
|
|
197
|
-
for (const unsub of this.unsubs) unsub();
|
|
198
|
-
this.unsubs = [];
|
|
199
|
-
super.onDestroy();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// -------------------------------------------------------------------------
|
|
203
|
-
// Input
|
|
204
|
-
// -------------------------------------------------------------------------
|
|
205
|
-
|
|
206
|
-
handleInput(key: string): boolean {
|
|
207
|
-
// Confirm-cancel flow takes priority — same contract as WRFC panel.
|
|
208
|
-
if (this.confirmCancel) {
|
|
209
|
-
const result = handleConfirmInput(this.confirmCancel, key);
|
|
210
|
-
if (result === 'confirmed') {
|
|
211
|
-
const rec = this.selectedAgentId
|
|
212
|
-
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
213
|
-
: null;
|
|
214
|
-
if (rec && !AGENT_TERMINAL_STATUSES.has(rec.status)) {
|
|
215
|
-
this.deps.cancelAgent(rec.id);
|
|
216
|
-
}
|
|
217
|
-
this.confirmCancel = null;
|
|
218
|
-
this.markDirty();
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
if (result === 'cancelled') {
|
|
222
|
-
this.confirmCancel = null;
|
|
223
|
-
this.markDirty();
|
|
224
|
-
}
|
|
225
|
-
// absorbed: confirm stays pending
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
switch (key) {
|
|
230
|
-
case 'up': this._moveCursor(-1); return true;
|
|
231
|
-
case 'down': this._moveCursor(1); return true;
|
|
232
|
-
case 'pageup': this._scroll(-10); return true;
|
|
233
|
-
case 'pagedown': this._scroll(10); return true;
|
|
234
|
-
case 'return': this._toggleExpand(); return true;
|
|
235
|
-
case 'tab': this._nextAgent(); return true;
|
|
236
|
-
case 'c': this._beginCancelConfirm(); return true;
|
|
237
|
-
case ' ': this._togglePause(); return true;
|
|
238
|
-
case 'f': this._cycleFilter(); return true;
|
|
239
|
-
case 'g': this._jumpTop(); return true;
|
|
240
|
-
case 'G': this._jumpBottom(); return true;
|
|
241
|
-
default: return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// -------------------------------------------------------------------------
|
|
246
|
-
// Render
|
|
247
|
-
// -------------------------------------------------------------------------
|
|
248
|
-
|
|
249
|
-
render(width: number, height: number): Line[] {
|
|
250
|
-
if (height <= 0 || width <= 0) return [];
|
|
251
|
-
|
|
252
|
-
const manager = this.deps.agentManager;
|
|
253
|
-
const agents = manager.list();
|
|
254
|
-
const rec = this.selectedAgentId
|
|
255
|
-
? manager.getStatus(this.selectedAgentId)
|
|
256
|
-
: null;
|
|
257
|
-
const selectorLine = this._renderSelector(width, agents);
|
|
258
|
-
const summaryLines = [
|
|
259
|
-
buildPanelLine(width, [
|
|
260
|
-
[' Agents ', DEFAULT_PANEL_PALETTE.label],
|
|
261
|
-
[String(agents.length), DEFAULT_PANEL_PALETTE.value],
|
|
262
|
-
[' Selected ', DEFAULT_PANEL_PALETTE.label],
|
|
263
|
-
[this.selectedAgentId ? this.selectedAgentId.slice(-8) : 'none', this.selectedAgentId ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
264
|
-
[' Filter ', DEFAULT_PANEL_PALETTE.label],
|
|
265
|
-
[AGENT_INSPECTOR_FILTER_LABELS[this.filter], DEFAULT_PANEL_PALETTE.info],
|
|
266
|
-
[' Mode ', DEFAULT_PANEL_PALETTE.label],
|
|
267
|
-
[this.paused ? 'paused' : this.autoFollow ? 'auto-follow' : 'manual', this.paused ? DEFAULT_PANEL_PALETTE.warn : this.autoFollow ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
268
|
-
]),
|
|
269
|
-
];
|
|
270
|
-
|
|
271
|
-
if (!rec) {
|
|
272
|
-
return buildPanelWorkspace(width, height, {
|
|
273
|
-
title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
|
|
274
|
-
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
275
|
-
sections: [
|
|
276
|
-
{ title: 'Summary', lines: summaryLines },
|
|
277
|
-
{ title: 'Agents', lines: [selectorLine] },
|
|
278
|
-
{
|
|
279
|
-
lines: buildEmptyState(
|
|
280
|
-
width,
|
|
281
|
-
agents.length === 0 ? ' No agents running' : ' No agent selected',
|
|
282
|
-
agents.length === 0
|
|
283
|
-
? 'Spawn an agent to inspect its live and historical timeline, tool calls, and output.'
|
|
284
|
-
: 'Press Tab to cycle through available agents and open one in the inspector.',
|
|
285
|
-
agents.length === 0
|
|
286
|
-
? [{ command: '/spawn <task>', summary: 'launch an agent, then Tab here to inspect its timeline' }]
|
|
287
|
-
: [{ command: 'Tab', summary: 'select the first available agent' }],
|
|
288
|
-
DEFAULT_PANEL_PALETTE,
|
|
289
|
-
),
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
footerLines: [
|
|
293
|
-
buildPanelLine(width, [[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
294
|
-
],
|
|
295
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
summaryLines.push(this._renderAgentInfoSummary(width, rec));
|
|
300
|
-
const wrfcCostLine = this._renderWrfcCostLine(width, rec);
|
|
301
|
-
if (wrfcCostLine) summaryLines.push(wrfcCostLine);
|
|
302
|
-
const now = Date.now();
|
|
303
|
-
const isStalled = this._isAgentStalled(rec, now);
|
|
304
|
-
if (isStalled) {
|
|
305
|
-
summaryLines.push(buildPanelLine(width, [[' STALLED', COLOR.warn], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
306
|
-
}
|
|
307
|
-
const allRows = this._getCachedRows();
|
|
308
|
-
if (allRows.length === 0) {
|
|
309
|
-
return buildPanelWorkspace(width, height, {
|
|
310
|
-
title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
|
|
311
|
-
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
312
|
-
sections: [
|
|
313
|
-
{ title: 'Summary', lines: summaryLines },
|
|
314
|
-
{ title: 'Agents', lines: [selectorLine] },
|
|
315
|
-
{
|
|
316
|
-
lines: buildEmptyState(
|
|
317
|
-
width,
|
|
318
|
-
' No messages yet',
|
|
319
|
-
rec.status === 'running'
|
|
320
|
-
? 'The selected agent is running but has not emitted visible timeline entries yet — live output streams in as it works.'
|
|
321
|
-
: 'The selected agent has not emitted any visible timeline entries.',
|
|
322
|
-
rec.status === 'running'
|
|
323
|
-
? [{ command: 'c', summary: 'cancel this running agent if it appears stalled' }]
|
|
324
|
-
: [{ command: 'Tab', summary: 'cycle to another agent with timeline activity' }],
|
|
325
|
-
DEFAULT_PANEL_PALETTE,
|
|
326
|
-
),
|
|
327
|
-
},
|
|
328
|
-
],
|
|
329
|
-
footerLines: [
|
|
330
|
-
buildPanelLine(width, [
|
|
331
|
-
[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim],
|
|
332
|
-
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
333
|
-
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
334
|
-
[' Space', DEFAULT_PANEL_PALETTE.info], [this.paused ? ' resume' : ' pause', DEFAULT_PANEL_PALETTE.dim],
|
|
335
|
-
[' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
|
|
336
|
-
]),
|
|
337
|
-
],
|
|
338
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (this.autoFollow) {
|
|
343
|
-
this.cursorIndex = allRows.length - 1;
|
|
344
|
-
} else {
|
|
345
|
-
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, allRows.length - 1));
|
|
346
|
-
}
|
|
347
|
-
const selectedRec = this.selectedAgentId
|
|
348
|
-
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
349
|
-
: null;
|
|
350
|
-
const cancellable = selectedRec && !AGENT_TERMINAL_STATUSES.has(selectedRec.status);
|
|
351
|
-
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
352
|
-
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
353
|
-
|
|
354
|
-
// Confirm-cancel overlay section.
|
|
355
|
-
const confirmSection = this.confirmCancel ? {
|
|
356
|
-
title: 'Confirm Cancel',
|
|
357
|
-
lines: [
|
|
358
|
-
buildPanelLine(width, [
|
|
359
|
-
[' Cancel agent "', DEFAULT_PANEL_PALETTE.warn],
|
|
360
|
-
[this.confirmCancel.label, DEFAULT_PANEL_PALETTE.value],
|
|
361
|
-
['"?', DEFAULT_PANEL_PALETTE.warn],
|
|
362
|
-
]),
|
|
363
|
-
buildPanelLine(width, [
|
|
364
|
-
[' y', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
365
|
-
[' Enter', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
366
|
-
[' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
367
|
-
]),
|
|
368
|
-
],
|
|
369
|
-
} : null;
|
|
370
|
-
|
|
371
|
-
const cancelHintFg = cancellable ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
372
|
-
const footerLine = buildPanelLine(width, [
|
|
373
|
-
[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim],
|
|
374
|
-
[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim],
|
|
375
|
-
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
376
|
-
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
377
|
-
[' Space', DEFAULT_PANEL_PALETTE.info], [this.paused ? ' resume' : ' pause', DEFAULT_PANEL_PALETTE.dim],
|
|
378
|
-
[' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
|
|
379
|
-
[' g/G', DEFAULT_PANEL_PALETTE.info], [this.autoFollow ? ' top/follow' : ' top/bottom', DEFAULT_PANEL_PALETTE.dim],
|
|
380
|
-
[' c', cancelHintFg], [cancellable ? ' cancel' : ' cancel (n/a)', DEFAULT_PANEL_PALETTE.dim],
|
|
381
|
-
]);
|
|
382
|
-
|
|
383
|
-
const timelineSection = resolveScrollablePanelSection(width, height, {
|
|
384
|
-
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
385
|
-
footerLines: [footerLine],
|
|
386
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
387
|
-
beforeSections: [summarySection, agentsSection],
|
|
388
|
-
section: {
|
|
389
|
-
title: 'Timeline',
|
|
390
|
-
scrollableLines: allRows.map((row, index) => this._renderTimelineRow(width, row, index === this.cursorIndex)),
|
|
391
|
-
selectedIndex: this.cursorIndex,
|
|
392
|
-
scrollOffset: this.scrollOffset,
|
|
393
|
-
minRows: 8,
|
|
394
|
-
},
|
|
395
|
-
afterSections: confirmSection ? [confirmSection] : undefined,
|
|
396
|
-
});
|
|
397
|
-
this.scrollOffset = timelineSection.scrollOffset;
|
|
398
|
-
|
|
399
|
-
const sections = [
|
|
400
|
-
summarySection,
|
|
401
|
-
agentsSection,
|
|
402
|
-
timelineSection.section,
|
|
403
|
-
...(confirmSection ? [confirmSection] : []),
|
|
404
|
-
];
|
|
405
|
-
|
|
406
|
-
return buildPanelWorkspace(width, height, {
|
|
407
|
-
title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
|
|
408
|
-
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
409
|
-
sections,
|
|
410
|
-
footerLines: [footerLine],
|
|
411
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
private _renderSelector(
|
|
416
|
-
width: number,
|
|
417
|
-
agents: AgentRecord[],
|
|
418
|
-
): Line {
|
|
419
|
-
if (agents.length === 0) {
|
|
420
|
-
return buildStyledPanelLine(width, [
|
|
421
|
-
{ text: ' (no agents)', fg: COLOR.dim, bg: COLOR.statusBar, dim: true },
|
|
422
|
-
]);
|
|
423
|
-
}
|
|
424
|
-
const segments: StyledPanelSegment[] = [{ text: ' ', fg: COLOR.dim, bg: COLOR.statusBar }];
|
|
425
|
-
for (const agent of agents) {
|
|
426
|
-
const isSelected = agent.id === this.selectedAgentId;
|
|
427
|
-
const shortId = agent.id.slice(-8);
|
|
428
|
-
const badge = ` ${shortId} `;
|
|
429
|
-
segments.push({
|
|
430
|
-
text: badge,
|
|
431
|
-
fg: isSelected ? COLOR.selected : COLOR.dim,
|
|
432
|
-
bg: COLOR.statusBar,
|
|
433
|
-
bold: isSelected,
|
|
434
|
-
});
|
|
435
|
-
segments.push({ text: '│', fg: COLOR.separator, bg: COLOR.statusBar });
|
|
436
|
-
}
|
|
437
|
-
return buildStyledPanelLine(width, segments);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private _renderAgentInfoSummary(width: number, rec: AgentRecord): Line {
|
|
441
|
-
const now = Date.now();
|
|
442
|
-
const elapsed = (rec.completedAt ?? now) - rec.startedAt;
|
|
443
|
-
const taskPreview = rec.task.split('\n')[0] ?? '';
|
|
444
|
-
const maxTask = Math.max(0, width - 40);
|
|
445
|
-
const taskDisplay = truncateDisplay(taskPreview, maxTask);
|
|
446
|
-
return buildPanelLine(width, [
|
|
447
|
-
[' Status ', DEFAULT_PANEL_PALETTE.label],
|
|
448
|
-
[rec.status.toUpperCase(), rec.status === 'running' ? DEFAULT_PANEL_PALETTE.good : rec.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
449
|
-
[' Duration ', DEFAULT_PANEL_PALETTE.label],
|
|
450
|
-
[formatMs(elapsed), DEFAULT_PANEL_PALETTE.value],
|
|
451
|
-
[' Tools ', DEFAULT_PANEL_PALETTE.label],
|
|
452
|
-
[String(rec.toolCallCount), DEFAULT_PANEL_PALETTE.info],
|
|
453
|
-
// SDK 0.23.0: show addendum indicator when WRFC injected a constraint addendum
|
|
454
|
-
...(rec.systemPromptAddendum
|
|
455
|
-
? [[' Addendum ', DEFAULT_PANEL_PALETTE.label] as [string, string], ['yes', DEFAULT_PANEL_PALETTE.info] as [string, string]]
|
|
456
|
-
: []),
|
|
457
|
-
[' Task ', DEFAULT_PANEL_PALETTE.label],
|
|
458
|
-
[taskDisplay, DEFAULT_PANEL_PALETTE.value],
|
|
459
|
-
]);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/** WRFC badge + per-agent tokens/cost (WO-110); null when neither applies. */
|
|
463
|
-
private _renderWrfcCostLine(width: number, rec: AgentRecord): Line | null {
|
|
464
|
-
const segments = buildWrfcCostSegments(rec, DEFAULT_PANEL_PALETTE, abbreviateCount);
|
|
465
|
-
return segments ? buildPanelLine(width, segments) : null;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// -------------------------------------------------------------------------
|
|
469
|
-
// Timeline row rendering
|
|
470
|
-
// -------------------------------------------------------------------------
|
|
471
|
-
|
|
472
|
-
private _renderTimelineRow(
|
|
473
|
-
width: number,
|
|
474
|
-
row: DisplayRow,
|
|
475
|
-
isCursor: boolean,
|
|
476
|
-
): Line {
|
|
477
|
-
const bg = isCursor ? COLOR.selectBg : '';
|
|
478
|
-
const ts = shortTime(row.timestamp);
|
|
479
|
-
const { fg, prefix } = agentKindStyle(row.kind, COLOR);
|
|
480
|
-
const hint = row.hasDetail ? (row.expanded ? ' ▾' : ' ▸') : '';
|
|
481
|
-
const prefixText = `${isCursor ? '▸' : ' '} ${ts} ${prefix} `;
|
|
482
|
-
const reserved = prefixText.length + hint.length;
|
|
483
|
-
const contentBudget = Math.max(0, width - reserved);
|
|
484
|
-
const text = truncateDisplay(row.content, contentBudget);
|
|
485
|
-
|
|
486
|
-
return buildSelectablePanelLine(width, [
|
|
487
|
-
{ text: isCursor ? '▸' : ' ', fg: COLOR.selected, bg, bold: isCursor },
|
|
488
|
-
{ text: ' ', fg: COLOR.value, bg },
|
|
489
|
-
{ text: ts, fg: COLOR.timestamp, bg, dim: true },
|
|
490
|
-
{ text: ' ', fg: COLOR.value, bg },
|
|
491
|
-
{ text: prefix, fg, bg, bold: true },
|
|
492
|
-
{ text: ' ', fg: COLOR.value, bg },
|
|
493
|
-
{ text: text, fg: COLOR.value, bg },
|
|
494
|
-
{ text: hint.length > 0 ? hint.padStart(Math.max(hint.length, width - (prefixText.length + text.length))) : '', fg: COLOR.expandHint, bg, dim: true },
|
|
495
|
-
], { selected: isCursor, selectedBg: bg, fillFg: isCursor ? COLOR.selected : '' });
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// -------------------------------------------------------------------------
|
|
499
|
-
// Private — data
|
|
500
|
-
// -------------------------------------------------------------------------
|
|
501
|
-
|
|
502
|
-
/** Flat DisplayRow list from timeline + expanded detail sub-rows, filtered by `this.filter`. */
|
|
503
|
-
private _buildVisibleRows(): DisplayRow[] {
|
|
504
|
-
const rows: DisplayRow[] = [];
|
|
505
|
-
|
|
506
|
-
// Merge bus messages (live) + JSONL (historical), sorted by timestamp
|
|
507
|
-
const busEntries = this._busToTimeline();
|
|
508
|
-
const merged = [...this.timeline, ...busEntries];
|
|
509
|
-
|
|
510
|
-
// T13: the JSONL timeline only records coarse per-turn summaries
|
|
511
|
-
// ([assistant] N chars, M tool calls) and never the model's actual text,
|
|
512
|
-
// so a mid-turn agent looks idle here. Surface the selected agent's live
|
|
513
|
-
// streaming output while it runs, and its final output once terminal.
|
|
514
|
-
const liveRec = this.selectedAgentId
|
|
515
|
-
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
516
|
-
: null;
|
|
517
|
-
if (liveRec) {
|
|
518
|
-
const liveEntry = this._buildLiveOutputEntry(liveRec);
|
|
519
|
-
if (liveEntry) merged.push(liveEntry);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
merged.sort((a, b) => a.timestamp - b.timestamp);
|
|
523
|
-
|
|
524
|
-
// Entries are filtered against `this.filter` here (WO-110).
|
|
525
|
-
for (const entry of merged) {
|
|
526
|
-
if (!matchesInspectorFilter(entry.kind, this.filter)) continue;
|
|
527
|
-
|
|
528
|
-
rows.push({
|
|
529
|
-
kind: entry.kind,
|
|
530
|
-
timestamp: entry.timestamp,
|
|
531
|
-
content: entry.content,
|
|
532
|
-
hasDetail: !!entry.detail,
|
|
533
|
-
expanded: entry.expanded,
|
|
534
|
-
entryRef: entry,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// If expanded and has detail — insert sub-rows
|
|
538
|
-
if (entry.expanded && entry.detail) {
|
|
539
|
-
const detailLines = entry.detail.split('\n');
|
|
540
|
-
for (const dl of detailLines) {
|
|
541
|
-
rows.push({
|
|
542
|
-
kind: 'tool_result',
|
|
543
|
-
timestamp: entry.timestamp,
|
|
544
|
-
content: dl,
|
|
545
|
-
hasDetail: false,
|
|
546
|
-
expanded: false,
|
|
547
|
-
entryRef: null,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return rows;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private _busToTimeline(): TimelineEntry[] {
|
|
557
|
-
if (!this.selectedAgentId) return [];
|
|
558
|
-
const messages = this.deps.agentMessageBus.getMessages(this.selectedAgentId);
|
|
559
|
-
const DEDUP_WINDOW_MS = 2000;
|
|
560
|
-
const seen = new Map<string, number>(); // hash -> last timestamp
|
|
561
|
-
const result: TimelineEntry[] = [];
|
|
562
|
-
for (const msg of messages) {
|
|
563
|
-
const isFromUser = msg.from === 'orchestrator' || msg.from === 'system';
|
|
564
|
-
const kind: EntryKind = isFromUser ? 'user' : 'assistant';
|
|
565
|
-
const contentSnippet = msg.content.slice(0, 50);
|
|
566
|
-
const dedupKey = `${kind}:${contentSnippet}`;
|
|
567
|
-
const lastSeen = seen.get(dedupKey);
|
|
568
|
-
if (lastSeen !== undefined && msg.timestamp - lastSeen < DEDUP_WINDOW_MS) {
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
seen.set(dedupKey, msg.timestamp);
|
|
572
|
-
result.push({
|
|
573
|
-
kind,
|
|
574
|
-
timestamp: msg.timestamp,
|
|
575
|
-
label: msg.from,
|
|
576
|
-
content: truncateDisplay(msg.content, 200),
|
|
577
|
-
expanded: false,
|
|
578
|
-
} satisfies TimelineEntry);
|
|
579
|
-
}
|
|
580
|
-
return result;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Build a synthetic timeline entry exposing the selected agent's live model
|
|
585
|
-
* output. While the agent runs this is the streaming token buffer
|
|
586
|
-
* (tail-truncated like AgentDetailModal); once it terminates it is the final
|
|
587
|
-
* assistant text (expandable). Exactly one is produced — the SDK clears
|
|
588
|
-
* streamingContent on completion — so live and final rows never coexist.
|
|
589
|
-
* Returns null when there is nothing to show.
|
|
590
|
-
*/
|
|
591
|
-
private _buildLiveOutputEntry(rec: AgentRecord): TimelineEntry | null {
|
|
592
|
-
const STREAM_MAX_CHARS = 500;
|
|
593
|
-
if (rec.status === 'running' && rec.streamingContent) {
|
|
594
|
-
const sc = rec.streamingContent;
|
|
595
|
-
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
596
|
-
const tail = (truncated ? sc.slice(-STREAM_MAX_CHARS) : sc)
|
|
597
|
-
.replace(/\s+/g, ' ')
|
|
598
|
-
.trim();
|
|
599
|
-
return {
|
|
600
|
-
kind: 'assistant',
|
|
601
|
-
timestamp: Date.now(),
|
|
602
|
-
label: 'streaming',
|
|
603
|
-
content: `(live) ${truncated ? '…' : ''}${tail}`,
|
|
604
|
-
expanded: false,
|
|
605
|
-
} satisfies TimelineEntry;
|
|
606
|
-
}
|
|
607
|
-
if (AGENT_TERMINAL_STATUSES.has(rec.status) && rec.fullOutput) {
|
|
608
|
-
const fo = rec.fullOutput;
|
|
609
|
-
const firstLine = (fo.split('\n').find((l) => l.trim().length > 0) ?? '').trim();
|
|
610
|
-
return {
|
|
611
|
-
kind: 'assistant',
|
|
612
|
-
timestamp: rec.completedAt ?? Date.now(),
|
|
613
|
-
label: 'output',
|
|
614
|
-
content: `(final) ${firstLine}`,
|
|
615
|
-
detail: fo,
|
|
616
|
-
expanded: false,
|
|
617
|
-
} satisfies TimelineEntry;
|
|
618
|
-
}
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// -------------------------------------------------------------------------
|
|
623
|
-
// Private — refresh
|
|
624
|
-
// -------------------------------------------------------------------------
|
|
625
|
-
|
|
626
|
-
private async _refreshTimeline(): Promise<void> {
|
|
627
|
-
if (!this.selectedAgentId) return;
|
|
628
|
-
try {
|
|
629
|
-
const sessionFile = join(
|
|
630
|
-
this.deps.workingDirectory,
|
|
631
|
-
'.goodvibes', 'tui', 'sessions',
|
|
632
|
-
`${this.selectedAgentId}.jsonl`,
|
|
633
|
-
);
|
|
634
|
-
const raw = await readFile(sessionFile, 'utf-8');
|
|
635
|
-
const logLines = raw.trim().split('\n').filter(Boolean);
|
|
636
|
-
const rows = logLines
|
|
637
|
-
.slice(-MAX_JSONL_ENTRIES)
|
|
638
|
-
.map((line) => {
|
|
639
|
-
try { return JSON.parse(line) as Record<string, unknown>; }
|
|
640
|
-
catch { return null; }
|
|
641
|
-
})
|
|
642
|
-
.filter((r): r is Record<string, unknown> => r !== null);
|
|
643
|
-
this.timeline = jsonlToTimeline(rows);
|
|
644
|
-
} catch (err) {
|
|
645
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
646
|
-
if (code !== 'ENOENT') {
|
|
647
|
-
logger.debug('AgentInspectorPanel: failed to load session log', { error: summarizeError(err) });
|
|
648
|
-
}
|
|
649
|
-
this.timeline = [];
|
|
650
|
-
}
|
|
651
|
-
this.markDirty();
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private _startRefresh(): void {
|
|
655
|
-
if (this.refreshTimerId) return;
|
|
656
|
-
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
657
|
-
// WO-110: Space pause/resume freezes the JSONL tail (ported from
|
|
658
|
-
// agent-logs); manual navigation and agent switching still work.
|
|
659
|
-
if (this.paused) return;
|
|
660
|
-
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh tick failed', { err }); });
|
|
661
|
-
}, REFRESH_MS));
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
private _stopRefresh(): void {
|
|
665
|
-
if (this.refreshTimerId) {
|
|
666
|
-
this.clearTimer(this.refreshTimerId);
|
|
667
|
-
this.refreshTimerId = null;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// -------------------------------------------------------------------------
|
|
672
|
-
// Private — agent lifecycle event subscriptions (WO-110 merge)
|
|
673
|
-
// -------------------------------------------------------------------------
|
|
674
|
-
|
|
675
|
-
/** Auto-select-on-spawn + real event-driven last-activity (stall fix), ported from agent-logs. */
|
|
676
|
-
private _subscribeAgentEvents(): void {
|
|
677
|
-
this.unsubs.push(...subscribeAgentActivity(this.deps.agentEvents, {
|
|
678
|
-
onActivity: (agentId) => {
|
|
679
|
-
this.lastActivityByAgent.set(agentId, Date.now());
|
|
680
|
-
if (!this.paused && agentId === this.selectedAgentId) this.markDirty();
|
|
681
|
-
},
|
|
682
|
-
onSpawn: (agentId) => {
|
|
683
|
-
const current = this.selectedAgentId ? this.deps.agentManager.getStatus(this.selectedAgentId) : null;
|
|
684
|
-
if (!current || AGENT_TERMINAL_STATUSES.has(current.status)) this.inspectAgent(agentId);
|
|
685
|
-
},
|
|
686
|
-
onTerminal: (agentId) => {
|
|
687
|
-
this.lastActivityByAgent.delete(agentId);
|
|
688
|
-
this.markDirty();
|
|
689
|
-
},
|
|
690
|
-
}));
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
private _togglePause(): void {
|
|
694
|
-
this.paused = !this.paused;
|
|
695
|
-
this.markDirty();
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
private _cycleFilter(): void {
|
|
699
|
-
const idx = AGENT_INSPECTOR_FILTER_CYCLE.indexOf(this.filter);
|
|
700
|
-
this.filter = AGENT_INSPECTOR_FILTER_CYCLE[(idx + 1) % AGENT_INSPECTOR_FILTER_CYCLE.length]!;
|
|
701
|
-
this.cursorIndex = 0;
|
|
702
|
-
this.markDirty();
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private _jumpTop(): void {
|
|
706
|
-
this.autoFollow = false;
|
|
707
|
-
this.cursorIndex = 0;
|
|
708
|
-
this.markDirty();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private _jumpBottom(): void {
|
|
712
|
-
this.autoFollow = true;
|
|
713
|
-
this.markDirty();
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// -------------------------------------------------------------------------
|
|
717
|
-
// Private — navigation
|
|
718
|
-
// -------------------------------------------------------------------------
|
|
719
|
-
|
|
720
|
-
private _moveCursor(delta: number): void {
|
|
721
|
-
const rows = this._getCachedRows();
|
|
722
|
-
if (rows.length === 0) return;
|
|
723
|
-
this.cursorIndex = Math.max(0, Math.min(rows.length - 1, this.cursorIndex + delta));
|
|
724
|
-
this.markDirty();
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
private _scroll(delta: number): void {
|
|
728
|
-
this._moveCursor(delta);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
private _toggleExpand(): void {
|
|
732
|
-
const rows = this._getCachedRows();
|
|
733
|
-
const row = rows[this.cursorIndex];
|
|
734
|
-
if (!row?.entryRef || !row.hasDetail) return;
|
|
735
|
-
row.entryRef.expanded = !row.entryRef.expanded;
|
|
736
|
-
this.markDirty();
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private _nextAgent(): void {
|
|
740
|
-
const agents = this.deps.agentManager.list();
|
|
741
|
-
if (agents.length === 0) return;
|
|
742
|
-
|
|
743
|
-
if (!this.selectedAgentId) {
|
|
744
|
-
this.inspectAgent(agents[0]!.id);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const idx = agents.findIndex(a => a.id === this.selectedAgentId);
|
|
749
|
-
const next = agents[(idx + 1) % agents.length];
|
|
750
|
-
if (next) {
|
|
751
|
-
this.inspectAgent(next.id);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// -------------------------------------------------------------------------
|
|
756
|
-
// Private — cancel + stall
|
|
757
|
-
// -------------------------------------------------------------------------
|
|
758
|
-
|
|
759
|
-
/** Initiate cancel-confirm flow for the selected agent (noop if terminal or none selected). */
|
|
760
|
-
private _beginCancelConfirm(): void {
|
|
761
|
-
if (!this.selectedAgentId) return;
|
|
762
|
-
const rec = this.deps.agentManager.getStatus(this.selectedAgentId);
|
|
763
|
-
if (!rec || AGENT_TERMINAL_STATUSES.has(rec.status)) return;
|
|
764
|
-
const firstLine = rec.task.split('\n')[0];
|
|
765
|
-
const label = firstLine ? truncateDisplay(firstLine, 40) : rec.id.slice(-8);
|
|
766
|
-
this.confirmCancel = { subject: rec.id, label };
|
|
767
|
-
this.markDirty();
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/** Stalled = non-terminal + no AGENT_* activity for AGENT_STALL_THRESHOLD_MS (not just elapsed-since-start). */
|
|
771
|
-
private _isAgentStalled(rec: AgentRecord, now: number): boolean {
|
|
772
|
-
if (AGENT_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
773
|
-
const lastActivity = this.lastActivityByAgent.get(rec.id) ?? rec.startedAt;
|
|
774
|
-
return (now - lastActivity) >= AGENT_STALL_THRESHOLD_MS;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Count of all tracked agents that are stalled (non-terminal, no activity
|
|
779
|
-
* for AGENT_STALL_THRESHOLD_MS). Exposed so callers can aggregate a
|
|
780
|
-
* stalledAgentCount for cockpit / roster read-models.
|
|
781
|
-
*/
|
|
782
|
-
getStalledAgentCount(): number {
|
|
783
|
-
const now = Date.now();
|
|
784
|
-
return this.deps.agentManager.list().filter(rec => this._isAgentStalled(rec, now)).length;
|
|
785
|
-
}
|
|
786
|
-
}
|