@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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. 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
- }