@pellux/goodvibes-tui 0.28.0 → 0.29.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- 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-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/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- 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/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 +203 -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 +309 -128
- 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/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- 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/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -6,6 +6,10 @@ import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
|
|
|
6
6
|
import { FilePreviewPanel } from '../panels/file-preview-panel.ts';
|
|
7
7
|
import { SymbolOutlinePanel } from '../panels/symbol-outline-panel.ts';
|
|
8
8
|
import { ApprovalPanel } from '../panels/approval-panel.ts';
|
|
9
|
+
import { TasksPanel } from '../panels/tasks-panel.ts';
|
|
10
|
+
import { OrchestrationPanel } from '../panels/orchestration-panel.ts';
|
|
11
|
+
import { AgentInspectorPanel } from '../panels/agent-inspector-panel.ts';
|
|
12
|
+
import { DiffPanel } from '../panels/diff-panel.ts';
|
|
9
13
|
|
|
10
14
|
function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null {
|
|
11
15
|
const existing = panelManager.getPanel('preview');
|
|
@@ -24,7 +28,29 @@ function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null
|
|
|
24
28
|
return opened instanceof FilePreviewPanel ? opened : null;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
// WO-133: shared by explorer's and preview's 'd' (diff) key — same
|
|
32
|
+
// open/focus bridge as ensurePreviewPanel above.
|
|
33
|
+
function ensureDiffPanel(panelManager: PanelManager): DiffPanel | null {
|
|
34
|
+
const existing = panelManager.getPanel('diff');
|
|
35
|
+
if (existing instanceof DiffPanel) {
|
|
36
|
+
const pane = panelManager.getPaneOf('diff');
|
|
37
|
+
panelManager.activateById('diff');
|
|
38
|
+
if (pane) panelManager.focusPane(pane);
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
const targetPane: 'top' | 'bottom' = panelManager.isBottomPaneVisible()
|
|
42
|
+
? (panelManager.getFocusedPane() === 'top' ? 'bottom' : 'top')
|
|
43
|
+
: 'bottom';
|
|
44
|
+
const opened = panelManager.open('diff', targetPane);
|
|
45
|
+
panelManager.show();
|
|
46
|
+
panelManager.focusPane(targetPane);
|
|
47
|
+
return opened instanceof DiffPanel ? opened : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Exported so a future preview-reload action (e.g. an explicit "r" reload
|
|
51
|
+
// key) can re-sync the outline against the same file without duplicating
|
|
52
|
+
// this lookup — the panel-integration wiring for that key lands separately.
|
|
53
|
+
export function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
|
|
28
54
|
const symbols = panelManager.getPanel('symbols');
|
|
29
55
|
const filePath = previewPanel.getCurrentFilePath();
|
|
30
56
|
const source = previewPanel.getSource();
|
|
@@ -61,6 +87,44 @@ export function handlePanelIntegrationAction(
|
|
|
61
87
|
return true;
|
|
62
88
|
}
|
|
63
89
|
|
|
90
|
+
// WO-133: 'd' on the explorer diffs the currently focused file, and 'd' on
|
|
91
|
+
// the preview diffs whatever file is currently open — both against HEAD,
|
|
92
|
+
// both via the same DiffPanel.showFileDiffs entry point.
|
|
93
|
+
if (key === 'd' && activePanel instanceof FileExplorerPanel) {
|
|
94
|
+
const filePath = activePanel.getFocusedFilePath();
|
|
95
|
+
if (!filePath) return false;
|
|
96
|
+
const diffPanel = ensureDiffPanel(panelManager);
|
|
97
|
+
if (!diffPanel) return false;
|
|
98
|
+
void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
|
|
99
|
+
logger.debug('explorer diff dispatch failed', { err });
|
|
100
|
+
});
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (key === 'd' && activePanel instanceof FilePreviewPanel) {
|
|
105
|
+
const filePath = activePanel.getCurrentFilePath();
|
|
106
|
+
if (!filePath) return false;
|
|
107
|
+
const diffPanel = ensureDiffPanel(panelManager);
|
|
108
|
+
if (!diffPanel) return false;
|
|
109
|
+
void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
|
|
110
|
+
logger.debug('preview diff dispatch failed', { err });
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// WO-133: 'r' on the preview reloads the open file from disk, then
|
|
116
|
+
// re-syncs the symbol outline against the refreshed content (WO-126's
|
|
117
|
+
// async tree-sitter loadFile via syncSymbolOutlineFromPreview above) once
|
|
118
|
+
// the reload actually settles.
|
|
119
|
+
if (key === 'r' && activePanel instanceof FilePreviewPanel) {
|
|
120
|
+
const reloaded = activePanel.consumePendingReload();
|
|
121
|
+
if (!reloaded) return false;
|
|
122
|
+
void reloaded.then(() => syncSymbolOutlineFromPreview(panelManager, activePanel)).catch((err) => {
|
|
123
|
+
logger.debug('preview reload dispatch failed', { err });
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
64
128
|
if ((key === 'enter' || key === 'return') && activePanel instanceof SymbolOutlinePanel) {
|
|
65
129
|
const location = activePanel.getSelectedLocation();
|
|
66
130
|
if (!location) return false;
|
|
@@ -84,5 +148,47 @@ export function handlePanelIntegrationAction(
|
|
|
84
148
|
return true;
|
|
85
149
|
}
|
|
86
150
|
|
|
151
|
+
// WO-131: Enter on a Tasks row — agent-kind tasks jump straight to the Agent
|
|
152
|
+
// Inspector (which owns the deep per-agent timeline); everything else's
|
|
153
|
+
// advertised worktree follow-up is dispatched for real via ctx.executeCommand
|
|
154
|
+
// instead of being printed as a static "/worktree task <task-id>" signpost.
|
|
155
|
+
// w dispatches the task-family posture review the header line advertises.
|
|
156
|
+
if ((key === 'enter' || key === 'return' || key === 'w') && activePanel instanceof TasksPanel) {
|
|
157
|
+
const followUp = activePanel.consumePendingFollowUp();
|
|
158
|
+
if (!followUp) return false;
|
|
159
|
+
if (followUp.kind === 'agent-jump') {
|
|
160
|
+
const inspector = panelManager.open('inspector');
|
|
161
|
+
if (!(inspector instanceof AgentInspectorPanel)) return false;
|
|
162
|
+
inspector.inspectAgent(followUp.agentId);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (!commandContext?.executeCommand) return false;
|
|
166
|
+
if (followUp.kind === 'teamwork-review') {
|
|
167
|
+
void commandContext.executeCommand('teamwork', ['review']).catch((err) => {
|
|
168
|
+
logger.debug('tasks panel teamwork review dispatch failed', { err });
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
void commandContext.executeCommand('worktree', ['task', followUp.taskId]).catch((err) => {
|
|
173
|
+
logger.debug('tasks panel worktree review dispatch failed', { err });
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// WO-131: Enter on a node-focused Orchestration row jumps to the Agent
|
|
179
|
+
// Inspector (agent-backed nodes) or the Tasks panel (task-backed nodes).
|
|
180
|
+
if ((key === 'enter' || key === 'return') && activePanel instanceof OrchestrationPanel) {
|
|
181
|
+
const jump = activePanel.consumePendingNodeJump();
|
|
182
|
+
if (!jump) return false;
|
|
183
|
+
if (jump.kind === 'agent-jump') {
|
|
184
|
+
const inspector = panelManager.open('inspector');
|
|
185
|
+
if (!(inspector instanceof AgentInspectorPanel)) return false;
|
|
186
|
+
inspector.inspectAgent(jump.id);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
panelManager.open('tasks');
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
87
193
|
return false;
|
|
88
194
|
}
|
package/src/main.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
checkRecoveryFile,
|
|
37
37
|
deleteRecoveryFile,
|
|
38
38
|
loadRecoveryConversation,
|
|
39
|
+
readLastSessionPointer,
|
|
39
40
|
writeRecoveryFile,
|
|
40
41
|
} from '@/runtime/index.ts';
|
|
41
42
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
@@ -58,7 +59,7 @@ import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
|
58
59
|
import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
59
60
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
60
61
|
import { buildContextStatusHint } from './renderer/context-status-hint.ts';
|
|
61
|
-
import { evaluateSessionMaintenance } from '
|
|
62
|
+
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
62
63
|
|
|
63
64
|
const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
64
65
|
const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
@@ -428,6 +429,7 @@ async function main() {
|
|
|
428
429
|
model: runtime.model,
|
|
429
430
|
provider: runtime.provider,
|
|
430
431
|
toolCount,
|
|
432
|
+
lastSessionId: readLastSessionPointer({ workingDirectory: workingDir, homeDirectory, surfaceRoot: 'tui' }) ?? undefined,
|
|
431
433
|
};
|
|
432
434
|
|
|
433
435
|
const render = () => {
|
|
@@ -490,6 +492,8 @@ async function main() {
|
|
|
490
492
|
provider: runtime.provider,
|
|
491
493
|
contextWindow,
|
|
492
494
|
contextStatusHint,
|
|
495
|
+
// Compact footer posture on short terminals so the shell stays usable.
|
|
496
|
+
compact: height < 30,
|
|
493
497
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
494
498
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
495
499
|
compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// AgentInspectorPanel — detailed view of a specific agent's messages and tool
|
|
3
|
-
// calls
|
|
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.
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
|
|
6
7
|
import { join } from 'node:path';
|
|
@@ -10,6 +11,8 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
10
11
|
import { BasePanel } from './base-panel.ts';
|
|
11
12
|
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
12
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';
|
|
13
16
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
14
17
|
import {
|
|
15
18
|
buildEmptyState,
|
|
@@ -20,22 +23,29 @@ import {
|
|
|
20
23
|
buildPanelWorkspace,
|
|
21
24
|
resolveScrollablePanelSection,
|
|
22
25
|
DEFAULT_PANEL_PALETTE,
|
|
26
|
+
extendPalette,
|
|
23
27
|
} from './polish.ts';
|
|
24
28
|
import {
|
|
25
29
|
type ConfirmState,
|
|
26
30
|
handleConfirmInput,
|
|
27
31
|
} from './confirm-state.ts';
|
|
28
32
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
33
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
29
34
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
30
35
|
import {
|
|
31
36
|
type AgentDisplayRow as DisplayRow,
|
|
32
37
|
type AgentInspectorEntryKind as EntryKind,
|
|
33
38
|
type AgentTimelineEntry as TimelineEntry,
|
|
39
|
+
type AgentInspectorFilterType as FilterType,
|
|
34
40
|
agentKindStyle,
|
|
35
|
-
agentStatusColor,
|
|
36
41
|
formatAgentDuration as formatMs,
|
|
37
42
|
formatAgentTime as shortTime,
|
|
38
43
|
jsonlToTimeline,
|
|
44
|
+
matchesInspectorFilter,
|
|
45
|
+
buildWrfcCostSegments,
|
|
46
|
+
subscribeAgentActivity,
|
|
47
|
+
AGENT_INSPECTOR_FILTER_CYCLE,
|
|
48
|
+
AGENT_INSPECTOR_FILTER_LABELS,
|
|
39
49
|
AGENT_TERMINAL_STATUSES,
|
|
40
50
|
AGENT_STALL_THRESHOLD_MS,
|
|
41
51
|
} from './agent-inspector-shared.ts';
|
|
@@ -47,12 +57,12 @@ import {
|
|
|
47
57
|
const REFRESH_MS = 500;
|
|
48
58
|
const MAX_JSONL_ENTRIES = 200;
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
54
65
|
statusBar: '#222233',
|
|
55
|
-
statusFg: '#aaaaaa',
|
|
56
66
|
separator: '#333355',
|
|
57
67
|
|
|
58
68
|
// Timeline roles
|
|
@@ -64,22 +74,10 @@ const COLOR = {
|
|
|
64
74
|
system: '#888888', // dim grey
|
|
65
75
|
timestamp: '#555577',
|
|
66
76
|
|
|
67
|
-
// Status badges
|
|
68
|
-
pending: '#ffcc44',
|
|
69
|
-
running: '#44ffcc',
|
|
70
|
-
completed: '#44ff88',
|
|
71
|
-
failed: '#ff4444',
|
|
72
|
-
cancelled: '#888888',
|
|
73
|
-
|
|
74
77
|
// Selector / labels
|
|
75
|
-
label: '#8888bb',
|
|
76
|
-
value: '#ccccdd',
|
|
77
78
|
selected: '#ffee88',
|
|
78
|
-
dimmed: '#555566',
|
|
79
79
|
expandHint: '#445566',
|
|
80
|
-
|
|
81
|
-
cursorBg: '#1a2233', // timeline cursor row background
|
|
82
|
-
} as const;
|
|
80
|
+
} as const);
|
|
83
81
|
|
|
84
82
|
// ---------------------------------------------------------------------------
|
|
85
83
|
// AgentInspectorPanel
|
|
@@ -93,6 +91,8 @@ export interface AgentInspectorPanelDeps {
|
|
|
93
91
|
readonly workingDirectory: string;
|
|
94
92
|
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
95
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
96
|
/**
|
|
97
97
|
* Request a compositor repaint. The 500ms refresh timer and other async
|
|
98
98
|
* update paths call this (via markDirty) so live agent output is painted
|
|
@@ -129,8 +129,19 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
129
129
|
/** True while this panel is the active view — gates async repaint requests. */
|
|
130
130
|
private _active = false;
|
|
131
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
|
+
|
|
132
142
|
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
133
143
|
super('inspector', 'Inspector', 'I', 'agent');
|
|
144
|
+
this._subscribeAgentEvents();
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
// -------------------------------------------------------------------------
|
|
@@ -161,6 +172,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
161
172
|
this.scrollOffset = 0;
|
|
162
173
|
this.cursorIndex = 0;
|
|
163
174
|
this.timeline = [];
|
|
175
|
+
this.autoFollow = true;
|
|
164
176
|
this.markDirty();
|
|
165
177
|
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh failed', { err }); });
|
|
166
178
|
}
|
|
@@ -182,6 +194,8 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
182
194
|
|
|
183
195
|
override onDestroy(): void {
|
|
184
196
|
this._stopRefresh();
|
|
197
|
+
for (const unsub of this.unsubs) unsub();
|
|
198
|
+
this.unsubs = [];
|
|
185
199
|
super.onDestroy();
|
|
186
200
|
}
|
|
187
201
|
|
|
@@ -220,6 +234,10 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
220
234
|
case 'return': this._toggleExpand(); return true;
|
|
221
235
|
case 'tab': this._nextAgent(); return true;
|
|
222
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;
|
|
223
241
|
default: return false;
|
|
224
242
|
}
|
|
225
243
|
}
|
|
@@ -243,6 +261,10 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
243
261
|
[String(agents.length), DEFAULT_PANEL_PALETTE.value],
|
|
244
262
|
[' Selected ', DEFAULT_PANEL_PALETTE.label],
|
|
245
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],
|
|
246
268
|
]),
|
|
247
269
|
];
|
|
248
270
|
|
|
@@ -275,10 +297,12 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
summaryLines.push(this._renderAgentInfoSummary(width, rec));
|
|
300
|
+
const wrfcCostLine = this._renderWrfcCostLine(width, rec);
|
|
301
|
+
if (wrfcCostLine) summaryLines.push(wrfcCostLine);
|
|
278
302
|
const now = Date.now();
|
|
279
303
|
const isStalled = this._isAgentStalled(rec, now);
|
|
280
304
|
if (isStalled) {
|
|
281
|
-
summaryLines.push(buildPanelLine(width, [[' STALLED', COLOR.
|
|
305
|
+
summaryLines.push(buildPanelLine(width, [[' STALLED', COLOR.warn], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
282
306
|
}
|
|
283
307
|
const allRows = this._getCachedRows();
|
|
284
308
|
if (allRows.length === 0) {
|
|
@@ -303,13 +327,23 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
303
327
|
},
|
|
304
328
|
],
|
|
305
329
|
footerLines: [
|
|
306
|
-
buildPanelLine(width, [
|
|
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
|
+
]),
|
|
307
337
|
],
|
|
308
338
|
palette: DEFAULT_PANEL_PALETTE,
|
|
309
339
|
});
|
|
310
340
|
}
|
|
311
341
|
|
|
312
|
-
|
|
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
|
+
}
|
|
313
347
|
const selectedRec = this.selectedAgentId
|
|
314
348
|
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
315
349
|
: null;
|
|
@@ -340,6 +374,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
340
374
|
[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim],
|
|
341
375
|
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
342
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],
|
|
343
380
|
[' c', cancelHintFg], [cancellable ? ' cancel' : ' cancel (n/a)', DEFAULT_PANEL_PALETTE.dim],
|
|
344
381
|
]);
|
|
345
382
|
|
|
@@ -381,17 +418,17 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
381
418
|
): Line {
|
|
382
419
|
if (agents.length === 0) {
|
|
383
420
|
return buildStyledPanelLine(width, [
|
|
384
|
-
{ text: ' (no agents)', fg: COLOR.
|
|
421
|
+
{ text: ' (no agents)', fg: COLOR.dim, bg: COLOR.statusBar, dim: true },
|
|
385
422
|
]);
|
|
386
423
|
}
|
|
387
|
-
const segments: StyledPanelSegment[] = [{ text: ' ', fg: COLOR.
|
|
424
|
+
const segments: StyledPanelSegment[] = [{ text: ' ', fg: COLOR.dim, bg: COLOR.statusBar }];
|
|
388
425
|
for (const agent of agents) {
|
|
389
426
|
const isSelected = agent.id === this.selectedAgentId;
|
|
390
427
|
const shortId = agent.id.slice(-8);
|
|
391
428
|
const badge = ` ${shortId} `;
|
|
392
429
|
segments.push({
|
|
393
430
|
text: badge,
|
|
394
|
-
fg: isSelected ? COLOR.selected : COLOR.
|
|
431
|
+
fg: isSelected ? COLOR.selected : COLOR.dim,
|
|
395
432
|
bg: COLOR.statusBar,
|
|
396
433
|
bold: isSelected,
|
|
397
434
|
});
|
|
@@ -422,6 +459,12 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
422
459
|
]);
|
|
423
460
|
}
|
|
424
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
|
+
|
|
425
468
|
// -------------------------------------------------------------------------
|
|
426
469
|
// Timeline row rendering
|
|
427
470
|
// -------------------------------------------------------------------------
|
|
@@ -431,7 +474,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
431
474
|
row: DisplayRow,
|
|
432
475
|
isCursor: boolean,
|
|
433
476
|
): Line {
|
|
434
|
-
const bg = isCursor ? COLOR.
|
|
477
|
+
const bg = isCursor ? COLOR.selectBg : '';
|
|
435
478
|
const ts = shortTime(row.timestamp);
|
|
436
479
|
const { fg, prefix } = agentKindStyle(row.kind, COLOR);
|
|
437
480
|
const hint = row.hasDetail ? (row.expanded ? ' ▾' : ' ▸') : '';
|
|
@@ -456,10 +499,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
456
499
|
// Private — data
|
|
457
500
|
// -------------------------------------------------------------------------
|
|
458
501
|
|
|
459
|
-
/**
|
|
460
|
-
* Build the flat list of DisplayRow items from timeline + expanded detail
|
|
461
|
-
* sub-rows. This is what the renderer walks.
|
|
462
|
-
*/
|
|
502
|
+
/** Flat DisplayRow list from timeline + expanded detail sub-rows, filtered by `this.filter`. */
|
|
463
503
|
private _buildVisibleRows(): DisplayRow[] {
|
|
464
504
|
const rows: DisplayRow[] = [];
|
|
465
505
|
|
|
@@ -481,10 +521,10 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
481
521
|
|
|
482
522
|
merged.sort((a, b) => a.timestamp - b.timestamp);
|
|
483
523
|
|
|
484
|
-
//
|
|
485
|
-
// approximate timestamps. We just show all — bus msgs tend to have
|
|
486
|
-
// unique content.
|
|
524
|
+
// Entries are filtered against `this.filter` here (WO-110).
|
|
487
525
|
for (const entry of merged) {
|
|
526
|
+
if (!matchesInspectorFilter(entry.kind, this.filter)) continue;
|
|
527
|
+
|
|
488
528
|
rows.push({
|
|
489
529
|
kind: entry.kind,
|
|
490
530
|
timestamp: entry.timestamp,
|
|
@@ -614,6 +654,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
614
654
|
private _startRefresh(): void {
|
|
615
655
|
if (this.refreshTimerId) return;
|
|
616
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;
|
|
617
660
|
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh tick failed', { err }); });
|
|
618
661
|
}, REFRESH_MS));
|
|
619
662
|
}
|
|
@@ -625,6 +668,51 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
625
668
|
}
|
|
626
669
|
}
|
|
627
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
|
+
|
|
628
716
|
// -------------------------------------------------------------------------
|
|
629
717
|
// Private — navigation
|
|
630
718
|
// -------------------------------------------------------------------------
|
|
@@ -679,10 +767,11 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
679
767
|
this.markDirty();
|
|
680
768
|
}
|
|
681
769
|
|
|
682
|
-
/**
|
|
770
|
+
/** Stalled = non-terminal + no AGENT_* activity for AGENT_STALL_THRESHOLD_MS (not just elapsed-since-start). */
|
|
683
771
|
private _isAgentStalled(rec: AgentRecord, now: number): boolean {
|
|
684
772
|
if (AGENT_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
685
|
-
|
|
773
|
+
const lastActivity = this.lastActivityByAgent.get(rec.id) ?? rec.startedAt;
|
|
774
|
+
return (now - lastActivity) >= AGENT_STALL_THRESHOLD_MS;
|
|
686
775
|
}
|
|
687
776
|
|
|
688
777
|
/**
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { formatDuration } from '../utils/format-duration.ts';
|
|
2
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
3
|
+
import type { AgentEvent } from '@/runtime/index.ts';
|
|
4
|
+
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
2
5
|
|
|
3
6
|
export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
|
|
4
7
|
|
|
@@ -121,3 +124,144 @@ export function agentKindStyle(kind: AgentInspectorEntryKind, colors: Record<str
|
|
|
121
124
|
default: return { fg: colors.dimmed ?? colors.system, prefix: '[?] ' };
|
|
122
125
|
}
|
|
123
126
|
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Inspector filter modes (ported from the merged agent-logs console — WO-110)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export type AgentInspectorFilterType = 'all' | 'assistant' | 'tool' | 'error';
|
|
133
|
+
|
|
134
|
+
export const AGENT_INSPECTOR_FILTER_CYCLE: readonly AgentInspectorFilterType[] = ['all', 'assistant', 'tool', 'error'];
|
|
135
|
+
|
|
136
|
+
export const AGENT_INSPECTOR_FILTER_LABELS: Record<AgentInspectorFilterType, string> = {
|
|
137
|
+
all: 'All',
|
|
138
|
+
assistant: 'Assistant',
|
|
139
|
+
tool: 'Tool',
|
|
140
|
+
error: 'Error',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* True when a timeline entry's kind should be visible under the given filter.
|
|
145
|
+
* Mapped against the REAL row kinds jsonlToTimeline() produces (tool_call /
|
|
146
|
+
* tool_result), not the legacy agent-logs-shared schema (assistant/tool_call/
|
|
147
|
+
* tool_result vs. the writer-emitted tool_execution/llm_response) that never
|
|
148
|
+
* matched anything.
|
|
149
|
+
*/
|
|
150
|
+
export function matchesInspectorFilter(kind: AgentInspectorEntryKind, filter: AgentInspectorFilterType): boolean {
|
|
151
|
+
switch (filter) {
|
|
152
|
+
case 'all': return true;
|
|
153
|
+
case 'assistant': return kind === 'assistant';
|
|
154
|
+
case 'tool': return kind === 'tool_call' || kind === 'tool_result';
|
|
155
|
+
case 'error': return kind === 'error';
|
|
156
|
+
default: return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Per-agent cost/token summary (calcSessionCost, cost-utils.ts)
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export function formatAgentCost(usd: number): string {
|
|
165
|
+
if (usd === 0) return '$0.00';
|
|
166
|
+
if (usd < 0.0001) return '<$0.0001';
|
|
167
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
168
|
+
return `$${usd.toFixed(3)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface AgentUsageSummary {
|
|
172
|
+
readonly tokens: number;
|
|
173
|
+
readonly cost: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface AgentUsageLike {
|
|
177
|
+
readonly usage?: {
|
|
178
|
+
readonly inputTokens: number;
|
|
179
|
+
readonly outputTokens: number;
|
|
180
|
+
readonly cacheReadTokens?: number;
|
|
181
|
+
readonly cacheWriteTokens?: number;
|
|
182
|
+
} | null;
|
|
183
|
+
readonly model?: string | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Total token count + cost for an agent record's usage, or null when no usage
|
|
188
|
+
* data has landed yet (honest-UX: never fabricate a $0.00/0-token reading for
|
|
189
|
+
* an agent that simply hasn't reported usage).
|
|
190
|
+
*/
|
|
191
|
+
export function summarizeAgentUsage(rec: AgentUsageLike): AgentUsageSummary | null {
|
|
192
|
+
if (!rec.usage) return null;
|
|
193
|
+
const inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
194
|
+
const cost = calcSessionCost(
|
|
195
|
+
rec.usage.inputTokens,
|
|
196
|
+
rec.usage.outputTokens,
|
|
197
|
+
rec.usage.cacheReadTokens ?? 0,
|
|
198
|
+
rec.usage.cacheWriteTokens ?? 0,
|
|
199
|
+
rec.model ?? 'unknown',
|
|
200
|
+
);
|
|
201
|
+
return { tokens: inputTokens + rec.usage.outputTokens, cost };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// WRFC badge segments (AgentRecord.wrfcId/wrfcRole)
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
interface WrfcLike {
|
|
209
|
+
readonly wrfcId?: string | null;
|
|
210
|
+
readonly wrfcRole?: string | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Segments for the WRFC chain badge + token/cost line, or null when neither applies. */
|
|
214
|
+
export function buildWrfcCostSegments(
|
|
215
|
+
rec: WrfcLike & AgentUsageLike,
|
|
216
|
+
palette: { readonly label: string; readonly info: string },
|
|
217
|
+
formatTokens: (n: number) => string,
|
|
218
|
+
): Array<[string, string]> | null {
|
|
219
|
+
const segments: Array<[string, string]> = [];
|
|
220
|
+
if (rec.wrfcId) {
|
|
221
|
+
segments.push([' WRFC ', palette.label]);
|
|
222
|
+
segments.push([`${rec.wrfcRole ?? 'agent'} · ${rec.wrfcId.slice(-8)}`, palette.info]);
|
|
223
|
+
}
|
|
224
|
+
const usage = summarizeAgentUsage(rec);
|
|
225
|
+
if (usage) {
|
|
226
|
+
segments.push([segments.length > 0 ? ' Tokens ' : ' Tokens ', palette.label]);
|
|
227
|
+
segments.push([formatTokens(usage.tokens), palette.info]);
|
|
228
|
+
segments.push([' Cost ', palette.label]);
|
|
229
|
+
segments.push([formatAgentCost(usage.cost), palette.info]);
|
|
230
|
+
}
|
|
231
|
+
return segments.length > 0 ? segments : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Agent lifecycle event subscriptions (ported from agent-logs — WO-110)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
export interface AgentActivityHooks {
|
|
239
|
+
/** Any AGENT_* activity event for this agent (running/progress/stream/awaiting/finalizing/spawning). */
|
|
240
|
+
readonly onActivity: (agentId: string) => void;
|
|
241
|
+
/** AGENT_SPAWNING only — used for auto-select-newest-agent. */
|
|
242
|
+
readonly onSpawn: (agentId: string) => void;
|
|
243
|
+
/** AGENT_COMPLETED/FAILED/CANCELLED. */
|
|
244
|
+
readonly onTerminal: (agentId: string) => void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Wires the standard set of agent lifecycle listeners; returns their unsubscribe functions. */
|
|
248
|
+
export function subscribeAgentActivity(agentEvents: UiEventFeed<AgentEvent>, hooks: AgentActivityHooks): Array<() => void> {
|
|
249
|
+
const unsubs: Array<() => void> = [];
|
|
250
|
+
const activityTypes = [
|
|
251
|
+
'AGENT_RUNNING', 'AGENT_PROGRESS', 'AGENT_STREAM_DELTA',
|
|
252
|
+
'AGENT_AWAITING_MESSAGE', 'AGENT_AWAITING_TOOL', 'AGENT_FINALIZING',
|
|
253
|
+
] as const;
|
|
254
|
+
for (const type of activityTypes) {
|
|
255
|
+
unsubs.push(agentEvents.on(type, (payload: { agentId: string }) => hooks.onActivity(payload.agentId)));
|
|
256
|
+
}
|
|
257
|
+
unsubs.push(agentEvents.on('AGENT_SPAWNING', (payload) => {
|
|
258
|
+
hooks.onActivity(payload.agentId);
|
|
259
|
+
hooks.onSpawn(payload.agentId);
|
|
260
|
+
}));
|
|
261
|
+
unsubs.push(
|
|
262
|
+
agentEvents.on('AGENT_COMPLETED', (payload) => hooks.onTerminal(payload.agentId)),
|
|
263
|
+
agentEvents.on('AGENT_FAILED', (payload) => hooks.onTerminal(payload.agentId)),
|
|
264
|
+
agentEvents.on('AGENT_CANCELLED', (payload) => hooks.onTerminal(payload.agentId)),
|
|
265
|
+
);
|
|
266
|
+
return unsubs;
|
|
267
|
+
}
|