@pellux/goodvibes-tui 0.28.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. 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
- function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
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
@@ -24,6 +24,7 @@ import { GitStatusProvider } from './renderer/git-status.ts';
24
24
  import type { GitHeaderInfo } from './renderer/git-status.ts';
25
25
  import { createShellLayout } from './renderer/layout-engine.ts';
26
26
  import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
27
+ import { computePromptContentWidth } from './renderer/prompt-content-width.ts';
27
28
  import { buildConversationViewport } from './renderer/conversation-layout.ts';
28
29
  import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
29
30
  import { buildPanelCompositeData } from './renderer/panel-composite.ts';
@@ -36,6 +37,7 @@ import {
36
37
  checkRecoveryFile,
37
38
  deleteRecoveryFile,
38
39
  loadRecoveryConversation,
40
+ readLastSessionPointer,
39
41
  writeRecoveryFile,
40
42
  } from '@/runtime/index.ts';
41
43
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
@@ -52,13 +54,14 @@ import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
52
54
  import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
53
55
  import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
54
56
  import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
57
+ import { createRenderScheduler } from './runtime/render-scheduler.ts';
55
58
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
56
59
  import { summarizeRunningAgents } from './renderer/process-summary.ts';
57
60
  import { formatUserFacingErrorLine } from './core/format-user-error.ts';
58
61
  import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
59
62
  import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
60
63
  import { buildContextStatusHint } from './renderer/context-status-hint.ts';
61
- import { evaluateSessionMaintenance } from './panels/session-maintenance.ts';
64
+ import { evaluateSessionMaintenance } from '@/runtime/index.ts';
62
65
 
63
66
  const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
64
67
  const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
@@ -179,19 +182,19 @@ async function main() {
179
182
  activeToolName: undefined,
180
183
  };
181
184
 
182
- const getPromptContentWidth = () => {
183
- const w = stdout.columns || 80;
184
- const boxMargin = 2;
185
- const boxWidth = w - (boxMargin * 2);
186
- return boxWidth - 4 - 3; // minus padding (4) minus prefix width (3: ' > ')
187
- };
185
+ const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
188
186
 
189
187
  const getViewportHeight = (): number => {
190
188
  if (input.onboardingWizard.active) return stdout.rows || 24;
191
189
  const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
192
190
  const currentModel = providerRegistry.getCurrentModel();
193
191
  const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
194
- return (stdout.rows || 24) - 2 - estimateShellFooterHeight(promptLines, contextWindow);
192
+ const rows = stdout.rows || 24;
193
+ // Compact threshold must match buildShellFooter's `compact: height < 30`
194
+ // posture below — otherwise the cached-height fast path in
195
+ // estimateShellFooterHeight can answer a compact-vs-non-compact query
196
+ // with the wrong cached mode and throw the viewport math off by several rows.
197
+ return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30);
195
198
  };
196
199
 
197
200
  const scroll = (delta: number) => {
@@ -222,7 +225,7 @@ async function main() {
222
225
  noAltScreen: cli.flags.noAltScreen,
223
226
  ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
224
227
  getInput: () => input,
225
- render: () => render(),
228
+ render: () => renderScheduler.flushNow(), // resize: synchronous immediate path
226
229
  getPromptContentWidth,
227
230
  getTerminalOutputGuard: () => terminalOutputGuard,
228
231
  buildSessionContinuityHints,
@@ -428,9 +431,10 @@ async function main() {
428
431
  model: runtime.model,
429
432
  provider: runtime.provider,
430
433
  toolCount,
434
+ lastSessionId: readLastSessionPointer({ workingDirectory: workingDir, homeDirectory, surfaceRoot: 'tui' }) ?? undefined,
431
435
  };
432
436
 
433
- const render = () => {
437
+ const renderNow = () => {
434
438
  const width = stdout.columns || 80;
435
439
  const height = stdout.rows || 24;
436
440
 
@@ -490,6 +494,8 @@ async function main() {
490
494
  provider: runtime.provider,
491
495
  contextWindow,
492
496
  contextStatusHint,
497
+ // Compact footer posture on short terminals so the shell stays usable.
498
+ compact: height < 30,
493
499
  // behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
494
500
  // the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
495
501
  compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
@@ -644,9 +650,11 @@ async function main() {
644
650
  panelWidth: panelComposite.panelWidth,
645
651
  });
646
652
  };
653
+ const renderScheduler = createRenderScheduler(renderNow); // WO-208 same-tick coalescer
654
+ const render = (): void => renderScheduler.schedule();
647
655
  const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, notify: (message) => { systemMessageRouter.low(message); render(); } });
648
656
 
649
- setRenderRequest(render);
657
+ setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
650
658
  orchestratorRefs.requestRender = render;
651
659
  commandContext.renderRequest = render;
652
660
  wireShellUiOpeners({
@@ -1,6 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // AgentInspectorPanel — detailed view of a specific agent's messages and tool
3
- // calls, with expandable tool call details, scroll, and agent selector.
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
- const COLOR = {
51
- // Header / chrome
52
- headerBg: '#1a1a2e',
53
- headerFg: '#ffffff',
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
- stalled: '#f59e0b', // amber — stalled-agent warning
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.stalled], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
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, [[' 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]]),
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
- this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, allRows.length - 1));
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.dimmed, bg: COLOR.statusBar, dim: true },
421
+ { text: ' (no agents)', fg: COLOR.dim, bg: COLOR.statusBar, dim: true },
385
422
  ]);
386
423
  }
387
- const segments: StyledPanelSegment[] = [{ text: ' ', fg: COLOR.statusFg, bg: COLOR.statusBar }];
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.dimmed,
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.cursorBg : '';
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
- // Deduplicate: bus messages that already appear in JSONL will have
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
- /** Returns whether an agent is considered stalled (non-terminal, running past threshold). */
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
- return (now - rec.startedAt) >= AGENT_STALL_THRESHOLD_MS;
773
+ const lastActivity = this.lastActivityByAgent.get(rec.id) ?? rec.startedAt;
774
+ return (now - lastActivity) >= AGENT_STALL_THRESHOLD_MS;
686
775
  }
687
776
 
688
777
  /**