@pellux/goodvibes-tui 1.1.0 → 1.9.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }