@pellux/goodvibes-tui 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,238 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // cockpit-read-model.ts
3
- //
4
- // TASK-046: Cockpit agent roster slice + cost/token aggregates + stalledAgentCount
5
- //
6
- // Provides a thin read-model over AgentManager.list() / getStatus() so the
7
- // CockpitPanel can display an agent roster without depending on the full
8
- // AgentInspectorPanel object.
9
- //
10
- // Design notes:
11
- // - Per-agent cost delegates to calcSessionCost() from cost-utils.ts (canonical
12
- // billing formula) and requires real usage data from AgentRecord.usage. When
13
- // usage is absent (agent spawned but not yet completed), cost/tokens show as n/a
14
- // rather than fabricated values (39327f86 honest-UX standard).
15
- // - stalledAgentCount delegates to countStalledAgents() from agent-inspector-shared.ts
16
- // (canonical stall-count function extracted from TASK-046 review) — no reimplementation.
17
- // - The read-model is a plain object (snapshot + subscribe) so it can be
18
- // wired in tests without a full runtime.
19
- // ---------------------------------------------------------------------------
20
-
21
- import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
22
- import { truncateDisplay } from '../utils/terminal-width.ts';
23
- import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
24
- import {
25
- AGENT_TERMINAL_STATUSES,
26
- AGENT_STALL_THRESHOLD_MS,
27
- countStalledAgents,
28
- hasReportedUsage,
29
- } from './agent-inspector-shared.ts';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Public types
33
- // ---------------------------------------------------------------------------
34
-
35
- /** Status of a single agent in the cockpit roster. */
36
- export type CockpitAgentStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
37
-
38
- /** A single row in the cockpit agent roster. */
39
- export interface CockpitAgentRosterEntry {
40
- /** Full agent id. */
41
- readonly id: string;
42
- /** Short task description (truncated at 50 chars). */
43
- readonly task: string;
44
- /** Model identifier, or 'unknown' when not yet resolved. */
45
- readonly model: string;
46
- /** Agent lifecycle status. */
47
- readonly status: CockpitAgentStatus;
48
- /** True when the agent is non-terminal and has exceeded AGENT_STALL_THRESHOLD_MS. */
49
- readonly stalled: boolean;
50
- /** Input tokens consumed (including cache read+write), or null when unavailable. */
51
- readonly inputTokens: number | null;
52
- /** Output tokens produced, or null when unavailable. */
53
- readonly outputTokens: number | null;
54
- /** Estimated cost in USD, or null when token data is unavailable. */
55
- readonly cost: number | null;
56
- }
57
-
58
- /** Aggregate snapshot produced by the cockpit roster read-model. */
59
- export interface CockpitRosterSnapshot {
60
- /** All agents in the manager, newest-first by startedAt. */
61
- readonly roster: readonly CockpitAgentRosterEntry[];
62
- /** Number of non-terminal agents running past AGENT_STALL_THRESHOLD_MS. */
63
- readonly stalledAgentCount: number;
64
- /**
65
- * Sum of all input tokens across agents with real usage data.
66
- * null when NO agent has usage data yet (avoids showing 0 when data is simply absent).
67
- */
68
- readonly totalInputTokens: number | null;
69
- /**
70
- * Sum of all output tokens across agents with real usage data.
71
- * null when NO agent has usage data yet.
72
- */
73
- readonly totalOutputTokens: number | null;
74
- /**
75
- * Total estimated cost in USD across agents with real pricing data.
76
- * null when NO agent has priceable data yet.
77
- */
78
- readonly totalCost: number | null;
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // AgentManager minimal interface (subset used here)
83
- // ---------------------------------------------------------------------------
84
-
85
- export interface CockpitRosterAgentManager {
86
- list(): AgentRecord[];
87
- }
88
-
89
- // ---------------------------------------------------------------------------
90
- // Snapshot builder — pure, testable
91
- // ---------------------------------------------------------------------------
92
-
93
- /**
94
- * Build a CockpitRosterSnapshot from a raw AgentRecord list.
95
- * Exported so unit tests can drive it directly without a manager stub.
96
- */
97
- export function buildCockpitRosterSnapshot(
98
- records: AgentRecord[],
99
- now: number = Date.now(),
100
- ): CockpitRosterSnapshot {
101
- // Sort newest-first by startedAt
102
- const sorted = [...records].sort((a, b) => b.startedAt - a.startedAt);
103
-
104
- let hasUsage = false;
105
- let totalInputTokens = 0;
106
- let totalOutputTokens = 0;
107
- let totalCost = 0;
108
-
109
- const roster: CockpitAgentRosterEntry[] = sorted.map((rec) => {
110
- const isTerminal = AGENT_TERMINAL_STATUSES.has(rec.status);
111
- const elapsed = now - rec.startedAt;
112
- const stalled = !isTerminal && elapsed >= AGENT_STALL_THRESHOLD_MS;
113
-
114
- let inputTokens: number | null = null;
115
- let outputTokens: number | null = null;
116
- let cost: number | null = null;
117
-
118
- if (hasReportedUsage(rec.usage)) {
119
- hasUsage = true;
120
- const usage = rec.usage;
121
- const inp =
122
- usage.inputTokens +
123
- (usage.cacheReadTokens ?? 0) +
124
- (usage.cacheWriteTokens ?? 0);
125
- const out = usage.outputTokens;
126
- const agentCost = calcSessionCost(
127
- usage.inputTokens,
128
- usage.outputTokens,
129
- usage.cacheReadTokens ?? 0,
130
- usage.cacheWriteTokens ?? 0,
131
- rec.model ?? 'unknown',
132
- );
133
-
134
- inputTokens = inp;
135
- outputTokens = out;
136
- // Usage exists but the model never resolved to a real price — reuse the
137
- // existing "usage absent" null/n-a convention rather than showing a
138
- // fabricated $0.00 (WO-315).
139
- cost = isModelPriced(rec.model ?? 'unknown') ? agentCost : null;
140
-
141
- totalInputTokens += inp;
142
- totalOutputTokens += out;
143
- totalCost += agentCost;
144
- }
145
-
146
- const task = truncateDisplay(rec.task, 50);
147
-
148
- return {
149
- id: rec.id,
150
- task,
151
- model: rec.model ?? 'unknown',
152
- status: rec.status,
153
- stalled,
154
- inputTokens,
155
- outputTokens,
156
- cost,
157
- };
158
- });
159
-
160
- return {
161
- roster,
162
- stalledAgentCount: countStalledAgents(sorted, now),
163
- totalInputTokens: hasUsage ? totalInputTokens : null,
164
- totalOutputTokens: hasUsage ? totalOutputTokens : null,
165
- totalCost: hasUsage ? totalCost : null,
166
- };
167
- }
168
-
169
- // ---------------------------------------------------------------------------
170
- // Read-model factory
171
- // ---------------------------------------------------------------------------
172
-
173
- /** Minimal read-model interface matching the existing UiReadModel shape. */
174
- export interface CockpitRosterReadModel {
175
- getSnapshot(): CockpitRosterSnapshot;
176
- /**
177
- * Notify all subscribers that the roster has changed and the cockpit panel
178
- * should re-render. Wire this to an AgentManager event feed so live agent
179
- * state changes propagate:
180
- *
181
- * const roster = createCockpitRosterReadModel(agentManager);
182
- * agentEvents.subscribe(() => roster.markDirty());
183
- *
184
- * For static/test fixtures the implementation is a no-op.
185
- */
186
- markDirty(): void;
187
- /**
188
- * Subscribe to changes. The listener is called whenever markDirty() is
189
- * invoked. Returns an unsubscribe function.
190
- *
191
- * For static/test fixtures, returns a no-op unsubscribe.
192
- */
193
- subscribe(listener: () => void): () => void;
194
- }
195
-
196
- /**
197
- * Create a live CockpitRosterReadModel backed by an AgentManager.
198
- *
199
- * The returned read-model re-derives its snapshot on every getSnapshot() call
200
- * so it always reflects the current agent state without needing an event bus.
201
- * Callers that want reactive updates should poll or wire in their own
202
- * event-driven markDirty() path.
203
- */
204
- export function createCockpitRosterReadModel(
205
- agentManager: CockpitRosterAgentManager,
206
- ): CockpitRosterReadModel {
207
- const listeners = new Set<() => void>();
208
-
209
- function markDirty(): void {
210
- for (const listener of listeners) {
211
- listener();
212
- }
213
- }
214
-
215
- return {
216
- getSnapshot(): CockpitRosterSnapshot {
217
- return buildCockpitRosterSnapshot(agentManager.list());
218
- },
219
- markDirty,
220
- subscribe(listener: () => void): () => void {
221
- listeners.add(listener);
222
- return () => listeners.delete(listener);
223
- },
224
- };
225
- }
226
-
227
- /**
228
- * Create a static CockpitRosterReadModel for testing.
229
- */
230
- export function createStaticCockpitRosterReadModel(
231
- snapshot: CockpitRosterSnapshot,
232
- ): CockpitRosterReadModel {
233
- return {
234
- getSnapshot: () => snapshot,
235
- markDirty: () => {},
236
- subscribe: () => () => {},
237
- };
238
- }
@@ -1,256 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
- import type { PanelIntegrationContext } from './types.ts';
4
- import type { UiCommunicationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
5
- import { truncateDisplay } from '../utils/terminal-width.ts';
6
- import {
7
- buildBodyText,
8
- buildDetailBlock,
9
- buildEmptyState,
10
- buildGuidanceLine,
11
- buildKeyValueLine,
12
- buildKeyboardHints,
13
- buildPanelLine,
14
- buildPanelWorkspace,
15
- buildStatusBadge,
16
- DEFAULT_PANEL_PALETTE,
17
- type PanelPalette,
18
- type StatusBadgeKind,
19
- } from './polish.ts';
20
- import { createEmptyLine } from '../types/grid.ts';
21
-
22
- // Base chrome only — title band, state colors, and text tokens all come
23
- // straight from DEFAULT_PANEL_PALETTE (WO-002).
24
- const C = DEFAULT_PANEL_PALETTE;
25
-
26
- type CommunicationRecord = UiCommunicationSnapshot['records'][number];
27
-
28
- function statusBadgeKind(status: CommunicationRecord['status']): StatusBadgeKind {
29
- return status === 'blocked' ? 'blocked' : status === 'delivered' ? 'completed' : 'running';
30
- }
31
-
32
- function fmtAgo(ts: number): string {
33
- const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
34
- if (sec < 60) return `${sec}s`;
35
- if (sec < 3600) return `${Math.floor(sec / 60)}m`;
36
- return `${Math.floor(sec / 3600)}h`;
37
- }
38
-
39
- // Set by handleInput (enter/o) and consumed on the very next
40
- // handlePanelIntegrationAction dispatch of that same key — handleInput has
41
- // no access to the panelManager/openAgentDetail callback.
42
- type PendingCommunicationAction =
43
- | { readonly kind: 'open-orchestration' }
44
- | { readonly kind: 'open-agent-detail'; readonly agentId: string };
45
-
46
- export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord> {
47
- private readonly readModel?: UiReadModel<UiCommunicationSnapshot>;
48
- private readonly unsub: (() => void) | null;
49
- private readonly openAgentDetail?: (agentId: string) => void;
50
- /** When true, the list is narrowed to blocked messages only (b toggles). */
51
- private blockedOnly = false;
52
- private pendingAction: PendingCommunicationAction | null = null;
53
-
54
- public constructor(
55
- readModel?: UiReadModel<UiCommunicationSnapshot>,
56
- openAgentDetail?: (agentId: string) => void,
57
- ) {
58
- super('communication', 'Communication', 'Y', 'runtime-ops');
59
- this.showSelectionGutter = true; // I5: non-color selection affordance
60
- this.filterEnabled = true;
61
- this.filterLabel = 'Filter messages';
62
- this.readModel = readModel;
63
- this.openAgentDetail = openAgentDetail;
64
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
65
- }
66
-
67
- public override onDestroy(): void {
68
- this.unsub?.();
69
- }
70
-
71
- protected override getPalette(): PanelPalette {
72
- return C;
73
- }
74
-
75
- protected getItems(): readonly CommunicationRecord[] {
76
- if (!this.readModel) return [];
77
- const records = this.readModel.getSnapshot().records;
78
- return this.blockedOnly ? records.filter((r) => r.status === 'blocked') : records;
79
- }
80
-
81
- protected override filterMatches(record: CommunicationRecord, q: string): boolean {
82
- return (record.content ?? '').toLowerCase().includes(q)
83
- || record.kind.toLowerCase().includes(q)
84
- || String(record.fromId ?? '').toLowerCase().includes(q)
85
- || String(record.toId ?? '').toLowerCase().includes(q);
86
- }
87
-
88
- public override handleInput(key: string): boolean {
89
- // `b` toggles the blocked-lanes-only view — guarded so it only acts when not
90
- // typing into the filter input.
91
- if (key === 'b' && !this.filterActive) {
92
- this.blockedOnly = !this.blockedOnly;
93
- this.selectedIndex = 0;
94
- this.markDirty();
95
- return true;
96
- }
97
- // Enter jumps to the orchestration workspace that actually produces this
98
- // traffic (a direct panel jump, not a printed "/orchestration" signpost).
99
- // Guarded so it doesn't hijack the filter input's commit-on-enter.
100
- if ((key === 'enter' || key === 'return') && !this.filterActive) {
101
- this.pendingAction = { kind: 'open-orchestration' };
102
- return true;
103
- }
104
- // `o` on a blocked record opens the sender's agent detail (falling back
105
- // to the receiver when the sender id is missing) via deps.openAgentDetail.
106
- if (key === 'o' && !this.filterActive) {
107
- const record = this.getSelectedItem();
108
- if (!record || record.status !== 'blocked') return false;
109
- const agentId = record.fromId || record.toId;
110
- if (!agentId) return false;
111
- this.pendingAction = { kind: 'open-agent-detail', agentId };
112
- return true;
113
- }
114
- return super.handleInput(key);
115
- }
116
-
117
- public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
118
- if (!this.pendingAction) return false;
119
- const action = this.pendingAction;
120
- this.pendingAction = null;
121
- if (action.kind === 'open-orchestration') {
122
- ctx.panelManager.open('orchestration');
123
- return true;
124
- }
125
- this.openAgentDetail?.(action.agentId);
126
- return true;
127
- }
128
-
129
- protected renderItem(record: CommunicationRecord, _index: number, selected: boolean, width: number): Line {
130
- const bg = selected ? C.selectBg : undefined;
131
- const badge = buildStatusBadge(statusBadgeKind(record.status), record.status)[0]!;
132
- const route = truncateDisplay(`${record.fromId} → ${record.toId}`, 26);
133
- const contentWidth = Math.max(0, width - 50);
134
- // Blocked messages have no delivered content; surface the policy reason
135
- // inline so the most important detail (why a lane is blocked) is visible
136
- // without selecting the row.
137
- const tail = record.status === 'blocked' && record.reason
138
- ? { text: record.reason, fg: C.warn }
139
- : { text: record.content, fg: C.dim };
140
- return buildPanelLine(width, [
141
- [' ', C.label, bg],
142
- [`${fmtAgo(record.timestamp)} `.padStart(5), C.dim, bg],
143
- [` ${badge.text} `.padEnd(12), badge.fg, bg],
144
- [`${record.kind} `.padEnd(9), C.info, bg],
145
- [`${route} `.padEnd(27), C.value, bg],
146
- [truncateDisplay(tail.text, contentWidth), tail.fg, bg],
147
- ]);
148
- }
149
-
150
- protected override getEmptyStateMessage(): string {
151
- return ' No structured communication recorded yet.';
152
- }
153
-
154
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
155
- return [
156
- { command: '/orchestration', summary: 'review graphs and recursive agent activity that emit messages' },
157
- ];
158
- }
159
-
160
- public render(width: number, height: number): Line[] {
161
- const intro = 'Structured agent communication, routing policy outcomes, and delivery status across orchestration trees.';
162
-
163
- if (!this.readModel) {
164
- const workspace = buildPanelWorkspace(width, height, {
165
- title: 'Communication Control Room',
166
- intro,
167
- sections: [{
168
- lines: buildEmptyState(
169
- width,
170
- ' Runtime store not wired into this panel yet.',
171
- 'This workspace needs the live runtime store before it can show communication history and policy outcomes.',
172
- [{ command: '/orchestration', summary: 'inspect recursive routing and agent activity while this workspace is unwired' }],
173
- C,
174
- ),
175
- }],
176
- palette: C,
177
- });
178
- while (workspace.length < height) workspace.push(createEmptyLine(width));
179
- return workspace;
180
- }
181
-
182
- const snapshot = this.readModel.getSnapshot();
183
- const records = this.getItems();
184
-
185
- // Posture: blocked lanes / message flow surfaced first per the panel's purpose.
186
- const postureLines = (): Line[] => [
187
- buildPanelLine(width, [
188
- [' ', C.label],
189
- ...buildStatusBadge('blocked', 'blocked', { count: snapshot.totalBlocked }),
190
- [' ', C.dim],
191
- ...buildStatusBadge('completed', 'delivered', { count: snapshot.totalDelivered }),
192
- [' ', C.dim],
193
- ...buildStatusBadge('running', 'sent', { count: snapshot.totalSent }),
194
- ...(this.blockedOnly ? ([[' (blocked-only view)', C.warn]] as Array<[string, string]>) : []),
195
- ]),
196
- snapshot.totalBlocked > 0
197
- ? buildPanelLine(width, [[` ${snapshot.totalBlocked} message${snapshot.totalBlocked !== 1 ? 's' : ''} blocked by routing policy — press b to isolate blocked lanes.`, C.warn]])
198
- : buildPanelLine(width, [[' No blocked lanes — press Enter on a record to inspect routing in the orchestration console.', C.dim]]),
199
- ];
200
-
201
- if (records.length === 0) {
202
- return this.renderList(width, height, {
203
- title: 'Communication Control Room',
204
- header: postureLines(),
205
- footer: [buildKeyboardHints(width, this.footerHints(false), C)],
206
- });
207
- }
208
-
209
- this.clampSelection();
210
- const selected = this.getSelectedItem();
211
-
212
- const detailLines: Line[] = [];
213
- if (selected) {
214
- detailLines.push(...buildDetailBlock(width, `Message · ${selected.kind}`, [
215
- buildKeyValueLine(width, [
216
- { label: 'status', value: selected.status, valueColor: selected.status === 'blocked' ? C.bad : C.good },
217
- { label: 'scope', value: selected.scope, valueColor: C.value },
218
- { label: 'when', value: `${fmtAgo(selected.timestamp)} ago`, valueColor: C.dim },
219
- ], C),
220
- buildKeyValueLine(width, [
221
- { label: 'from', value: `${selected.fromId} (${selected.fromRole ?? 'unknown'})`, valueColor: C.value },
222
- { label: 'to', value: `${selected.toId} (${selected.toRole ?? 'unknown'})`, valueColor: C.value },
223
- ], C),
224
- ...(selected.reason
225
- ? buildBodyText(width, `blocked: ${selected.reason}`, C, C.warn)
226
- : []),
227
- ...buildBodyText(width, selected.content, C, C.value),
228
- ], C));
229
- }
230
-
231
- return this.renderList(width, height, {
232
- title: 'Communication Control Room',
233
- header: postureLines(),
234
- footer: [...detailLines, buildKeyboardHints(width, this.footerHints(true, selected?.status === 'blocked'), C)],
235
- });
236
- }
237
-
238
- // Context-aware hints: filter keys reflect filter state, `b` reflects the
239
- // blocked-only toggle, `o` only appears on a blocked selection, and the
240
- // inspect hint only appears when rows exist.
241
- private footerHints(hasRows: boolean, onBlockedSelection = false): Array<{ keys: string; label: string }> {
242
- const hints: Array<{ keys: string; label: string }> = [];
243
- if (hasRows) hints.push({ keys: '↑/↓', label: 'select' });
244
- hints.push({ keys: 'Enter', label: 'orchestration' });
245
- if (onBlockedSelection) hints.push({ keys: 'o', label: 'sender detail' });
246
- hints.push({ keys: 'b', label: this.blockedOnly ? 'show all lanes' : 'blocked only' });
247
- if (this.filterActive) {
248
- hints.push({ keys: 'Esc', label: 'clear filter' });
249
- } else if (this.filterQuery) {
250
- hints.push({ keys: '/', label: 'edit filter' }, { keys: 'Esc', label: 'clear filter' });
251
- } else {
252
- hints.push({ keys: '/', label: 'filter' });
253
- }
254
- return hints;
255
- }
256
- }