@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,465 +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 } 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 (rec.usage) {
287
- const totalIn = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
288
- sections.push({ type: 'text', content: `Tokens in : ${totalIn.toLocaleString()}` });
289
- sections.push({ type: 'text', content: `Tokens out : ${rec.usage.outputTokens.toLocaleString()}` });
290
- } else {
291
- sections.push({
292
- type: 'text',
293
- content: 'Tokens : n/a (agent running)',
294
- style: { dim: true },
295
- });
296
- }
297
-
298
- // SDK 0.23.0: systemPromptAddendum indicator — confirms WRFC constraint addendum was injected
299
- if (rec.systemPromptAddendum) {
300
- sections.push({
301
- type: 'text',
302
- content: 'Addendum : yes (WRFC constraint layer injected)',
303
- style: { fg: AGENT_DETAIL_NEON.mint },
304
- });
305
- }
306
-
307
- // SDK 0.23.0: constraint data from WRFC chain (engineer constraints + reviewer findings)
308
- if (rec.wrfcId && modal.deps.wrfcController) {
309
- try {
310
- const chain = modal.deps.wrfcController.getChain(rec.wrfcId);
311
- if (chain && chain.constraints.length > 0) {
312
- sections.push({ type: 'separator' });
313
- sections.push({
314
- type: 'text',
315
- content: `Constraints (${chain.constraints.length}):`,
316
- style: { dim: true },
317
- });
318
- for (const c of chain.constraints) {
319
- const text = c.text.length > 80 ? c.text.slice(0, 77) + '…' : c.text;
320
- sections.push({
321
- type: 'text',
322
- content: ` [${c.id}] ${text}`,
323
- style: { fg: '246' },
324
- });
325
- }
326
- // Reviewer constraint findings (if review has completed)
327
- const findings = chain.reviewerReport?.constraintFindings;
328
- if (findings && findings.length > 0) {
329
- const unsatisfied = findings.filter((f) => !f.satisfied);
330
- sections.push({
331
- type: 'text',
332
- content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
333
- style: { fg: unsatisfied.length > 0 ? AGENT_DETAIL_NEON.bad : AGENT_DETAIL_NEON.good },
334
- });
335
- }
336
- }
337
- } catch {
338
- // wrfcController.getChain throws when chain not found — normal during teardown
339
- }
340
- }
341
-
342
- // Progress
343
- if (rec.progress) {
344
- sections.push({ type: 'separator' });
345
- sections.push({
346
- type: 'text',
347
- content: `Progress: ${rec.progress}`,
348
- style: { fg: AGENT_DETAIL_NEON.accent },
349
- });
350
- }
351
-
352
- // Error
353
- if (rec.error) {
354
- sections.push({ type: 'separator' });
355
- sections.push({
356
- type: 'text',
357
- content: `Error: ${rec.error}`,
358
- style: { fg: AGENT_DETAIL_NEON.bad },
359
- });
360
- }
361
-
362
- // Recent messages from AgentMessageBus
363
- const recentMessages = modal.deps.agentMessageBus
364
- .getMessages(modal.agentId)
365
- .slice(-4); // last 4 messages
366
-
367
- if (recentMessages.length > 0) {
368
- sections.push({ type: 'separator' });
369
- sections.push({
370
- type: 'text',
371
- content: `Recent messages (${recentMessages.length}):`,
372
- style: { dim: true },
373
- });
374
- for (const msg of recentMessages) {
375
- const fromLabel = msg.from === '*' ? 'broadcast' : msg.from.slice(0, 12);
376
- const preview = msg.content.length > 50
377
- ? msg.content.slice(0, 47) + '\u2026'
378
- : msg.content;
379
- sections.push({
380
- type: 'text',
381
- content: ` [${fromLabel}] ${preview}`,
382
- style: { fg: '246' },
383
- });
384
- }
385
- }
386
-
387
- // Execution history from cached JSONL session log (loaded on open())
388
- if (modal.logEntries.length > 0) {
389
- sections.push({ type: 'separator' });
390
- sections.push({
391
- type: 'text',
392
- content: `Execution Log (${modal.logTotal} events, showing last ${modal.logEntries.length}):`,
393
- style: { dim: true },
394
- });
395
- for (const entry of modal.logEntries) {
396
- const entryType = String(entry.type ?? 'unknown');
397
- const rawTs = entry.timestamp;
398
- const ts = typeof rawTs === 'string' && rawTs.length >= 19
399
- ? rawTs.slice(11, 19)
400
- : '';
401
- let detail = '';
402
- if (entryType === 'tool_execution') detail = ` ${entry.toolName}`;
403
- if (entryType === 'llm_response') detail = ` (${entry.toolCallCount} tools, ${entry.contentLength} chars)`;
404
- if (entryType === 'session_end') detail = ` [${entry.status}]`;
405
- sections.push({
406
- type: 'text',
407
- content: ` ${ts} ${entryType}${detail}`,
408
- style: { fg: '246' },
409
- });
410
- }
411
- }
412
-
413
- // Streaming content — show live output when agent is actively streaming
414
- const STREAMING_MAX_CHARS = 500;
415
- if (rec.status === 'running' && rec.streamingContent) {
416
- const content = rec.streamingContent;
417
- const truncated = content.length > STREAMING_MAX_CHARS;
418
- const display = truncated ? content.slice(-STREAMING_MAX_CHARS) : content;
419
- sections.push({ type: 'separator' });
420
- sections.push({
421
- type: 'text',
422
- content: truncated
423
- ? `Streaming (last ${STREAMING_MAX_CHARS} of ${content.length} chars \u2191 scroll for more):`
424
- : 'Streaming:',
425
- style: { fg: AGENT_DETAIL_NEON.accent, dim: true },
426
- });
427
- // Split into display lines, capped at width for readability
428
- const maxLineWidth = Math.max(width - 10, 40);
429
- const streamLines = display.split('\n');
430
- for (const line of streamLines) {
431
- const trimmed = line.length > maxLineWidth ? line.slice(0, maxLineWidth - 1) + '\u2026' : line;
432
- sections.push({
433
- type: 'text',
434
- content: ` ${trimmed}`,
435
- style: { fg: AGENT_DETAIL_NEON.mint },
436
- });
437
- }
438
- }
439
-
440
- // Cancel confirm overlay (when pending)
441
- const cancellable = !MODAL_TERMINAL_STATUSES.has(rec.status);
442
- if (modal.confirmCancel) {
443
- sections.push({ type: 'separator' });
444
- sections.push({
445
- type: 'text',
446
- content: `Cancel agent "${modal.confirmCancel.label}"?`,
447
- style: { fg: UI_TONES.state.warn },
448
- });
449
- sections.push({
450
- type: 'text',
451
- content: 'y / Enter confirm n / Esc cancel',
452
- style: { dim: true },
453
- });
454
- }
455
-
456
- const cancelHint = cancellable ? '[c] Cancel ' : '';
457
- return ModalFactory.createModal({
458
- title: `Agent: ${rec.id.slice(0, AGENT_ID_DISPLAY_LENGTH)}`,
459
- width: metrics.boxWidth,
460
- margin: metrics.margin,
461
- targetContentRows,
462
- sections,
463
- hints: [cancelHint + '[Esc] Close'],
464
- }, width);
465
- }
@@ -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
- }