@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,466 +0,0 @@
1
- import { readFile } from 'fs/promises';
2
- import { type Line } from '../types/grid.ts';
3
- import { ModalFactory } from './modal-factory.ts';
4
- import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
5
- import type { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
6
- import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
7
- import { formatElapsed } from '../utils/format-elapsed.ts';
8
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
9
- import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
10
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
11
- import { handleConfirmInput, type ConfirmState } from '../panels/confirm-state.ts';
12
- import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS, hasReportedUsage } from '../panels/agent-inspector-shared.ts';
13
- import { UI_TONES } from './ui-primitives.ts';
14
-
15
- /**
16
- * Agent-detail-modal neon accent palette — WRFC/streaming panel-native hues
17
- * with no UI_TONES.state/accent role. Preserved byte-exact as named
18
- * accents per WO-207b (2026-07-02-renderer-plan.md decisions: "Agent-detail
19
- * neon palette: preserved byte-exact as named accents, zero visual change").
20
- */
21
- const AGENT_DETAIL_NEON = {
22
- /** Addendum note + streaming content text. */
23
- mint: '#aaffee',
24
- /** Unsatisfied WRFC findings + agent error text. */
25
- bad: '#ff6666',
26
- /** Satisfied WRFC findings. */
27
- good: '#44ff88',
28
- /** Progress line + streaming label. */
29
- accent: '#00ffcc',
30
- } as const;
31
-
32
- // ─── Constants ────────────────────────────────────────────────────────────────
33
-
34
- const MAX_LOG_ENTRIES = 10;
35
- const AGENT_ID_DISPLAY_LENGTH = 16;
36
-
37
- // MODAL_TERMINAL_STATUSES and MODAL_STALL_THRESHOLD_MS are re-exported aliases
38
- // from agent-inspector-shared.ts (imported above alongside ConfirmState).
39
-
40
- /**
41
- * formatStalledLabel — the "[STALLED — N+ min no activity]" suffix appended
42
- * to the Status line for a stalled agent. The minute count is derived from
43
- * the stall threshold (in ms) rather than hardcoded, so the label can never
44
- * drift out of sync with the actual threshold that decided isStalled.
45
- */
46
- export function formatStalledLabel(thresholdMs: number): string {
47
- const minutes = Math.floor(thresholdMs / 60000);
48
- return ` [STALLED — ${minutes}+ min no activity]`;
49
- }
50
-
51
- export interface AgentDetailModalDeps {
52
- readonly agentManager: Pick<AgentManager, 'getStatus' | 'list'>;
53
- readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
54
- readonly sessionLogPathResolver: (agentId: string) => string;
55
- /** Optional — when supplied, constraint data from the agent's WRFC chain is shown (SDK 0.23.0). */
56
- readonly wrfcController?: Pick<WrfcController, 'getChain'>;
57
- /** Cancel the agent by id using the same orphan-free path as WRFC. Returns true if cancelled. */
58
- readonly cancelAgent: (agentId: string) => boolean;
59
- }
60
-
61
- // ─── AgentDetailModal ─────────────────────────────────────────────────────────
62
-
63
- /**
64
- * AgentDetailModal — deep-view modal for a single running/completed agent.
65
- *
66
- * Displays task description, template, model, status, duration, tool-call
67
- * count, estimated token usage, recent messages from AgentMessageBus, and
68
- * the agent's current progress note.
69
- */
70
- export class AgentDetailModal {
71
- public active = false;
72
- public agentId: string | null = null;
73
-
74
- /** Cached JSONL log entries, loaded on open(). */
75
- public logEntries: Record<string, unknown>[] = [];
76
- public logTotal = 0;
77
-
78
- /** Pending cancel confirmation. Subject is the agent id to cancel. */
79
- public confirmCancel: ConfirmState<string> | null = null;
80
-
81
- private refreshTimer: ReturnType<typeof setInterval> | null = null;
82
- private onRefresh: (() => void) | null = null;
83
-
84
- constructor(readonly deps: AgentDetailModalDeps) {}
85
-
86
- /** Set a callback to trigger re-render when log data updates. */
87
- setOnRefresh(fn: () => void): void {
88
- this.onRefresh = fn;
89
- }
90
-
91
- open(agentId: string): void {
92
- this.agentId = agentId;
93
- this.active = true;
94
- this.logEntries = [];
95
- this.logTotal = 0;
96
- this.loadLog().catch((err) => { logger.debug('agent detail log load failed', { err }); });
97
- // Auto-refresh log every 500ms while modal is open
98
- if (this.refreshTimer) clearInterval(this.refreshTimer);
99
- this.refreshTimer = setInterval(() => {
100
- this.loadLog().then(() => this.onRefresh?.()).catch((err) => { logger.debug('agent detail log refresh tick failed', { err }); });
101
- }, 500);
102
- }
103
-
104
- close(): void {
105
- this.active = false;
106
- this.agentId = null;
107
- this.logEntries = [];
108
- this.logTotal = 0;
109
- this.confirmCancel = null;
110
- if (this.refreshTimer) {
111
- clearInterval(this.refreshTimer);
112
- this.refreshTimer = null;
113
- }
114
- }
115
-
116
- /**
117
- * Handle a key press while the modal is active.
118
- * Must be called BEFORE the Esc handler closes the modal.
119
- *
120
- * Routes:
121
- * - 'c' initiates cancel confirm (if agent is non-terminal)
122
- * - confirm keys (Enter/y/n/Esc) are forwarded to handleConfirmInput
123
- *
124
- * Returns true when the key was consumed (caller should NOT propagate).
125
- */
126
- handleKey(key: string): boolean {
127
- if (!this.active) return false;
128
-
129
- if (this.confirmCancel) {
130
- const result = handleConfirmInput(this.confirmCancel, key);
131
- if (result === 'confirmed') {
132
- if (this.agentId) {
133
- const rec = this.deps.agentManager.getStatus(this.agentId);
134
- if (rec && !MODAL_TERMINAL_STATUSES.has(rec.status)) {
135
- this.deps.cancelAgent(rec.id);
136
- }
137
- }
138
- this.confirmCancel = null;
139
- this.onRefresh?.();
140
- return true;
141
- }
142
- if (result === 'cancelled') {
143
- this.confirmCancel = null;
144
- this.onRefresh?.();
145
- return true;
146
- }
147
- // absorbed — key swallowed while confirm is pending
148
- return true;
149
- }
150
-
151
- if (key === 'c') {
152
- if (this.agentId) {
153
- const rec = this.deps.agentManager.getStatus(this.agentId);
154
- if (rec && !MODAL_TERMINAL_STATUSES.has(rec.status)) {
155
- const label = rec.task.split('\n')[0]?.slice(0, 40) ?? rec.id.slice(-8);
156
- this.confirmCancel = { subject: rec.id, label };
157
- this.onRefresh?.();
158
- return true;
159
- }
160
- }
161
- // Non-cancellable — absorb key silently
162
- return true;
163
- }
164
-
165
- return false;
166
- }
167
-
168
- /**
169
- * Returns whether the current agent is considered stalled.
170
- * Non-terminal agent with elapsed time exceeding MODAL_STALL_THRESHOLD_MS.
171
- */
172
- isCurrentAgentStalled(): boolean {
173
- if (!this.agentId) return false;
174
- const rec = this.deps.agentManager.getStatus(this.agentId);
175
- if (!rec || MODAL_TERMINAL_STATUSES.has(rec.status)) return false;
176
- return (Date.now() - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
177
- }
178
-
179
- /**
180
- * Count of all stalled agents across the agentManager list.
181
- * Non-terminal agents with elapsed time >= MODAL_STALL_THRESHOLD_MS.
182
- */
183
- getStalledAgentCount(): number {
184
- const now = Date.now();
185
- return this.deps.agentManager.list().filter(rec => {
186
- if (MODAL_TERMINAL_STATUSES.has(rec.status)) return false;
187
- return (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
188
- }).length;
189
- }
190
-
191
- async loadLog(): Promise<void> {
192
- if (!this.agentId) { this.logEntries = []; this.logTotal = 0; return; }
193
- try {
194
- const sessionFile = this.deps.sessionLogPathResolver(this.agentId);
195
- const logContent = await readFile(sessionFile, 'utf-8');
196
- const logLines = logContent.trim().split('\n');
197
- this.logTotal = logLines.length;
198
- const parsed = logLines.slice(-MAX_LOG_ENTRIES).map(line => {
199
- try { return JSON.parse(line) as Record<string, unknown>; } catch { return null; }
200
- });
201
- const failedCount = parsed.filter(e => e === null).length;
202
- if (failedCount > 0) {
203
- logger.debug('AgentDetailModal: skipped malformed JSONL lines', { count: failedCount });
204
- }
205
- this.logEntries = parsed.filter((e): e is Record<string, unknown> => e !== null);
206
- } catch (err) {
207
- const code = (err as NodeJS.ErrnoException).code;
208
- if (code !== 'ENOENT') {
209
- logger.debug('AgentDetailModal: failed to load session log', { error: summarizeError(err) });
210
- }
211
- this.logEntries = [];
212
- this.logTotal = 0;
213
- }
214
- }
215
- }
216
-
217
- // ─── renderAgentDetailModal ───────────────────────────────────────────────────
218
-
219
- /**
220
- * Render the agent detail modal as Line[] for overlay in the viewport.
221
- *
222
- * Shows a deep view of the selected agent: task, template, model, status,
223
- * duration, tool call count, token estimate, recent bus messages, and
224
- * progress text.
225
- *
226
- * @param modal AgentDetailModal state
227
- * @param width Terminal width
228
- */
229
- export function renderAgentDetailModal(
230
- modal: AgentDetailModal,
231
- width: number,
232
- viewportHeight = 24,
233
- ): Line[] {
234
- if (!modal.agentId) return [];
235
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
236
- margin: 2,
237
- maxWidth: width - 4,
238
- chromeRows: 6,
239
- minContentRows: 10,
240
- maxContentRows: 22,
241
- });
242
- const targetContentRows = Math.max(18, Math.min(22, getStableOverlayContentRows(metrics.contentRows, 12) + 8));
243
-
244
- const rec = modal.deps.agentManager.getStatus(modal.agentId);
245
- if (!rec) {
246
- return ModalFactory.createModal({
247
- title: 'Agent Detail',
248
- width: metrics.boxWidth,
249
- margin: metrics.margin,
250
- targetContentRows,
251
- sections: [
252
- { type: 'text', content: '(agent not found)' },
253
- ],
254
- hints: ['[Esc] Close'],
255
- }, width);
256
- }
257
-
258
- const now = Date.now();
259
- const elapsedMs = (rec.completedAt ?? now) - rec.startedAt;
260
-
261
- // ── Build sections ────────────────────────────────────────────────────────
262
-
263
- const sections: import('./modal-factory.ts').ModalSection[] = [];
264
-
265
- // Task — show first line only, capped at 120 chars
266
- const taskFirstLine = rec.task.split('\n')[0].replace(/^(WRFC\s+(Fix|Review)\s+Request\s*)/i, '').trim();
267
- const taskDisplay = taskFirstLine.length > 120 ? taskFirstLine.slice(0, 117) + '\u2026' : taskFirstLine;
268
- sections.push({
269
- type: 'text',
270
- content: `Task: ${taskDisplay}`,
271
- style: { bold: true },
272
- });
273
- sections.push({ type: 'separator' });
274
-
275
- // Metadata grid
276
- const modelStr = rec.model ? `${rec.provider ?? ''}/${rec.model}` : (rec.provider ?? '(default)');
277
- sections.push({ type: 'text', content: `Template : ${rec.template}` });
278
- sections.push({ type: 'text', content: `Model : ${modelStr}` });
279
- const isStalled = !MODAL_TERMINAL_STATUSES.has(rec.status) && (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
280
- sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ? formatStalledLabel(MODAL_STALL_THRESHOLD_MS) : ''}` });
281
- sections.push({ type: 'text', content: `Duration : ${formatElapsed(elapsedMs)}` });
282
- sections.push({ type: 'separator' });
283
-
284
- // Metrics
285
- sections.push({ type: 'text', content: `Tool calls : ${rec.toolCallCount}` });
286
- if (hasReportedUsage(rec.usage)) {
287
- const usage = rec.usage;
288
- const totalIn = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
289
- sections.push({ type: 'text', content: `Tokens in : ${totalIn.toLocaleString()}` });
290
- sections.push({ type: 'text', content: `Tokens out : ${usage.outputTokens.toLocaleString()}` });
291
- } else {
292
- sections.push({
293
- type: 'text',
294
- content: 'Tokens : n/a (agent running)',
295
- style: { dim: true },
296
- });
297
- }
298
-
299
- // SDK 0.23.0: systemPromptAddendum indicator — confirms WRFC constraint addendum was injected
300
- if (rec.systemPromptAddendum) {
301
- sections.push({
302
- type: 'text',
303
- content: 'Addendum : yes (WRFC constraint layer injected)',
304
- style: { fg: AGENT_DETAIL_NEON.mint },
305
- });
306
- }
307
-
308
- // SDK 0.23.0: constraint data from WRFC chain (engineer constraints + reviewer findings)
309
- if (rec.wrfcId && modal.deps.wrfcController) {
310
- try {
311
- const chain = modal.deps.wrfcController.getChain(rec.wrfcId);
312
- if (chain && chain.constraints.length > 0) {
313
- sections.push({ type: 'separator' });
314
- sections.push({
315
- type: 'text',
316
- content: `Constraints (${chain.constraints.length}):`,
317
- style: { dim: true },
318
- });
319
- for (const c of chain.constraints) {
320
- const text = c.text.length > 80 ? c.text.slice(0, 77) + '…' : c.text;
321
- sections.push({
322
- type: 'text',
323
- content: ` [${c.id}] ${text}`,
324
- style: { fg: '246' },
325
- });
326
- }
327
- // Reviewer constraint findings (if review has completed)
328
- const findings = chain.reviewerReport?.constraintFindings;
329
- if (findings && findings.length > 0) {
330
- const unsatisfied = findings.filter((f) => !f.satisfied);
331
- sections.push({
332
- type: 'text',
333
- content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
334
- style: { fg: unsatisfied.length > 0 ? AGENT_DETAIL_NEON.bad : AGENT_DETAIL_NEON.good },
335
- });
336
- }
337
- }
338
- } catch {
339
- // wrfcController.getChain throws when chain not found — normal during teardown
340
- }
341
- }
342
-
343
- // Progress
344
- if (rec.progress) {
345
- sections.push({ type: 'separator' });
346
- sections.push({
347
- type: 'text',
348
- content: `Progress: ${rec.progress}`,
349
- style: { fg: AGENT_DETAIL_NEON.accent },
350
- });
351
- }
352
-
353
- // Error
354
- if (rec.error) {
355
- sections.push({ type: 'separator' });
356
- sections.push({
357
- type: 'text',
358
- content: `Error: ${rec.error}`,
359
- style: { fg: AGENT_DETAIL_NEON.bad },
360
- });
361
- }
362
-
363
- // Recent messages from AgentMessageBus
364
- const recentMessages = modal.deps.agentMessageBus
365
- .getMessages(modal.agentId)
366
- .slice(-4); // last 4 messages
367
-
368
- if (recentMessages.length > 0) {
369
- sections.push({ type: 'separator' });
370
- sections.push({
371
- type: 'text',
372
- content: `Recent messages (${recentMessages.length}):`,
373
- style: { dim: true },
374
- });
375
- for (const msg of recentMessages) {
376
- const fromLabel = msg.from === '*' ? 'broadcast' : msg.from.slice(0, 12);
377
- const preview = msg.content.length > 50
378
- ? msg.content.slice(0, 47) + '\u2026'
379
- : msg.content;
380
- sections.push({
381
- type: 'text',
382
- content: ` [${fromLabel}] ${preview}`,
383
- style: { fg: '246' },
384
- });
385
- }
386
- }
387
-
388
- // Execution history from cached JSONL session log (loaded on open())
389
- if (modal.logEntries.length > 0) {
390
- sections.push({ type: 'separator' });
391
- sections.push({
392
- type: 'text',
393
- content: `Execution Log (${modal.logTotal} events, showing last ${modal.logEntries.length}):`,
394
- style: { dim: true },
395
- });
396
- for (const entry of modal.logEntries) {
397
- const entryType = String(entry.type ?? 'unknown');
398
- const rawTs = entry.timestamp;
399
- const ts = typeof rawTs === 'string' && rawTs.length >= 19
400
- ? rawTs.slice(11, 19)
401
- : '';
402
- let detail = '';
403
- if (entryType === 'tool_execution') detail = ` ${entry.toolName}`;
404
- if (entryType === 'llm_response') detail = ` (${entry.toolCallCount} tools, ${entry.contentLength} chars)`;
405
- if (entryType === 'session_end') detail = ` [${entry.status}]`;
406
- sections.push({
407
- type: 'text',
408
- content: ` ${ts} ${entryType}${detail}`,
409
- style: { fg: '246' },
410
- });
411
- }
412
- }
413
-
414
- // Streaming content — show live output when agent is actively streaming
415
- const STREAMING_MAX_CHARS = 500;
416
- if (rec.status === 'running' && rec.streamingContent) {
417
- const content = rec.streamingContent;
418
- const truncated = content.length > STREAMING_MAX_CHARS;
419
- const display = truncated ? content.slice(-STREAMING_MAX_CHARS) : content;
420
- sections.push({ type: 'separator' });
421
- sections.push({
422
- type: 'text',
423
- content: truncated
424
- ? `Streaming (last ${STREAMING_MAX_CHARS} of ${content.length} chars \u2191 scroll for more):`
425
- : 'Streaming:',
426
- style: { fg: AGENT_DETAIL_NEON.accent, dim: true },
427
- });
428
- // Split into display lines, capped at width for readability
429
- const maxLineWidth = Math.max(width - 10, 40);
430
- const streamLines = display.split('\n');
431
- for (const line of streamLines) {
432
- const trimmed = line.length > maxLineWidth ? line.slice(0, maxLineWidth - 1) + '\u2026' : line;
433
- sections.push({
434
- type: 'text',
435
- content: ` ${trimmed}`,
436
- style: { fg: AGENT_DETAIL_NEON.mint },
437
- });
438
- }
439
- }
440
-
441
- // Cancel confirm overlay (when pending)
442
- const cancellable = !MODAL_TERMINAL_STATUSES.has(rec.status);
443
- if (modal.confirmCancel) {
444
- sections.push({ type: 'separator' });
445
- sections.push({
446
- type: 'text',
447
- content: `Cancel agent "${modal.confirmCancel.label}"?`,
448
- style: { fg: UI_TONES.state.warn },
449
- });
450
- sections.push({
451
- type: 'text',
452
- content: 'y / Enter confirm n / Esc cancel',
453
- style: { dim: true },
454
- });
455
- }
456
-
457
- const cancelHint = cancellable ? '[c] Cancel ' : '';
458
- return ModalFactory.createModal({
459
- title: `Agent: ${rec.id.slice(0, AGENT_ID_DISPLAY_LENGTH)}`,
460
- width: metrics.boxWidth,
461
- margin: metrics.margin,
462
- targetContentRows,
463
- sections,
464
- hints: [cancelHint + '[Esc] Close'],
465
- }, width);
466
- }
@@ -1,156 +0,0 @@
1
- import { type Line } from '../types/grid.ts';
2
- import { ModalFactory } from './modal-factory.ts';
3
- import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
4
- import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
5
- import type { ProcessEntry } from './process-modal.ts';
6
- import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
7
-
8
- export interface LiveTailModalDeps {
9
- readonly agentManager: Pick<AgentManager, 'cancel' | 'getStatus'>;
10
- readonly processManager: Pick<ProcessManager, 'stop' | 'getOutput'>;
11
- }
12
-
13
- // ─── LiveTailModal ────────────────────────────────────────────────────────────
14
-
15
- /**
16
- * LiveTailModal — manages state for the live output peek modal.
17
- *
18
- * Shows streaming stdout/stderr from a selected background process or agent
19
- * progress notes. Auto-scrolls to the bottom unless the user scrolled up.
20
- */
21
- export class LiveTailModal {
22
- public active = false;
23
- public entry: ProcessEntry | null = null;
24
-
25
- /** Number of lines scrolled up from the bottom (0 = at bottom). */
26
- public scrollOffset = 0;
27
-
28
- constructor(private readonly deps: LiveTailModalDeps) {}
29
-
30
- open(entry: ProcessEntry): void {
31
- this.entry = entry;
32
- this.scrollOffset = 0;
33
- this.active = true;
34
- }
35
-
36
- close(): void {
37
- this.active = false;
38
- this.entry = null;
39
- this.scrollOffset = 0;
40
- }
41
-
42
- scrollUp(): void {
43
- this.scrollOffset++;
44
- }
45
-
46
- scrollDown(): void {
47
- this.scrollOffset = Math.max(0, this.scrollOffset - 1);
48
- }
49
-
50
- /**
51
- * Kill the current process.
52
- * Returns true if the process was found and stopped.
53
- */
54
- killProcess(): boolean {
55
- if (!this.entry) return false;
56
-
57
- if (this.entry.type === 'exec') {
58
- return this.deps.processManager.stop(this.entry.id);
59
- } else {
60
- return this.deps.agentManager.cancel(this.entry.id);
61
- }
62
- }
63
-
64
- /** Retrieve the current output text for the watched process. */
65
- getOutput(): string {
66
- if (!this.entry) return '';
67
-
68
- if (this.entry.type === 'exec') {
69
- const out = this.deps.processManager.getOutput(this.entry.id);
70
- if (!out) return '';
71
- const combined = [out.stdout, out.stderr].filter(Boolean).join('\n').trim();
72
- return combined || '(no output yet)';
73
- } else {
74
- // For agents, show progress note and status
75
- const rec = this.deps.agentManager.getStatus(this.entry.id);
76
- if (!rec) return '(process not found)';
77
- const parts: string[] = [
78
- `Task: ${rec.task}`,
79
- `Status: ${rec.status}`,
80
- `Tool calls: ${rec.toolCallCount}`,
81
- ];
82
- if (rec.progress) parts.push(`Progress: ${rec.progress}`);
83
- if (rec.error) parts.push(`Error: ${rec.error}`);
84
- return parts.join('\n');
85
- }
86
- }
87
- }
88
-
89
- // ─── renderLiveTailModal ──────────────────────────────────────────────────────
90
-
91
- /**
92
- * Render the live-tail peek modal as Line[] for overlay in the viewport.
93
- *
94
- * Shows a scrollable view of the process output. scrollOffset=0 means the
95
- * bottom of the output is visible (auto-scroll behaviour).
96
- *
97
- * @param modal LiveTailModal state
98
- * @param width Terminal width
99
- * @param maxOutputLines Maximum lines to show inside the box (default: 16)
100
- */
101
- export function renderLiveTailModal(
102
- modal: LiveTailModal,
103
- width: number,
104
- viewportHeight = 24,
105
- ): Line[] {
106
- const entry = modal.entry;
107
- if (!entry) return [];
108
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
109
- chromeRows: 4,
110
- minContentRows: 6,
111
- maxContentRows: 10,
112
- });
113
- const maxOutputLines = metrics.contentRows;
114
- const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8);
115
-
116
- const output = modal.getOutput();
117
- const allLines = output.split('\n');
118
-
119
- // Apply scroll: scrollOffset=0 → show tail; larger → scroll toward head
120
- const totalLines = allLines.length;
121
- // Clamp scrollOffset so we never scroll past the top of the content
122
- const maxScroll = Math.max(0, totalLines - maxOutputLines);
123
- const clampedOffset = Math.min(modal.scrollOffset, maxScroll);
124
- const endIdx = Math.max(maxOutputLines, totalLines - clampedOffset);
125
- const startIdx = Math.max(0, endIdx - maxOutputLines);
126
- const visibleLines = allLines.slice(startIdx, endIdx);
127
-
128
- const typeTag = entry.type === 'agent' ? '[agent]' : '[exec]';
129
- const maxLabelW = Math.max(20, width - 30);
130
- const title = `${typeTag} ${entry.label.slice(0, maxLabelW)}`;
131
-
132
- // Build scroll indicator for text section header
133
- const scrollInfo = totalLines > maxOutputLines
134
- ? ` Lines ${startIdx + 1}-${Math.min(endIdx, totalLines)} of ${totalLines} [Up/Down] Scroll`
135
- : '';
136
-
137
- const contentText = visibleLines.join('\n') || '(no output yet)';
138
-
139
- const sections: import('./modal-factory.ts').ModalSection[] = [];
140
-
141
- if (scrollInfo) {
142
- sections.push({ type: 'text', content: scrollInfo });
143
- sections.push({ type: 'separator' });
144
- }
145
-
146
- sections.push({ type: 'text', content: contentText });
147
-
148
- return ModalFactory.createModal({
149
- title,
150
- width: metrics.boxWidth,
151
- margin: 2,
152
- targetContentRows,
153
- sections,
154
- hints: ['[Up/Down] Scroll', '[k] Kill', '[Esc] Back'],
155
- }, width);
156
- }