@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,233 +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 } from '../export/cost-utils.ts';
24
- import {
25
- AGENT_TERMINAL_STATUSES,
26
- AGENT_STALL_THRESHOLD_MS,
27
- countStalledAgents,
28
- } from './agent-inspector-shared.ts';
29
-
30
- // ---------------------------------------------------------------------------
31
- // Public types
32
- // ---------------------------------------------------------------------------
33
-
34
- /** Status of a single agent in the cockpit roster. */
35
- export type CockpitAgentStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
36
-
37
- /** A single row in the cockpit agent roster. */
38
- export interface CockpitAgentRosterEntry {
39
- /** Full agent id. */
40
- readonly id: string;
41
- /** Short task description (truncated at 50 chars). */
42
- readonly task: string;
43
- /** Model identifier, or 'unknown' when not yet resolved. */
44
- readonly model: string;
45
- /** Agent lifecycle status. */
46
- readonly status: CockpitAgentStatus;
47
- /** True when the agent is non-terminal and has exceeded AGENT_STALL_THRESHOLD_MS. */
48
- readonly stalled: boolean;
49
- /** Input tokens consumed (including cache read+write), or null when unavailable. */
50
- readonly inputTokens: number | null;
51
- /** Output tokens produced, or null when unavailable. */
52
- readonly outputTokens: number | null;
53
- /** Estimated cost in USD, or null when token data is unavailable. */
54
- readonly cost: number | null;
55
- }
56
-
57
- /** Aggregate snapshot produced by the cockpit roster read-model. */
58
- export interface CockpitRosterSnapshot {
59
- /** All agents in the manager, newest-first by startedAt. */
60
- readonly roster: readonly CockpitAgentRosterEntry[];
61
- /** Number of non-terminal agents running past AGENT_STALL_THRESHOLD_MS. */
62
- readonly stalledAgentCount: number;
63
- /**
64
- * Sum of all input tokens across agents with real usage data.
65
- * null when NO agent has usage data yet (avoids showing 0 when data is simply absent).
66
- */
67
- readonly totalInputTokens: number | null;
68
- /**
69
- * Sum of all output tokens across agents with real usage data.
70
- * null when NO agent has usage data yet.
71
- */
72
- readonly totalOutputTokens: number | null;
73
- /**
74
- * Total estimated cost in USD across agents with real pricing data.
75
- * null when NO agent has priceable data yet.
76
- */
77
- readonly totalCost: number | null;
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // AgentManager minimal interface (subset used here)
82
- // ---------------------------------------------------------------------------
83
-
84
- export interface CockpitRosterAgentManager {
85
- list(): AgentRecord[];
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Snapshot builder — pure, testable
90
- // ---------------------------------------------------------------------------
91
-
92
- /**
93
- * Build a CockpitRosterSnapshot from a raw AgentRecord list.
94
- * Exported so unit tests can drive it directly without a manager stub.
95
- */
96
- export function buildCockpitRosterSnapshot(
97
- records: AgentRecord[],
98
- now: number = Date.now(),
99
- ): CockpitRosterSnapshot {
100
- // Sort newest-first by startedAt
101
- const sorted = [...records].sort((a, b) => b.startedAt - a.startedAt);
102
-
103
- let hasUsage = false;
104
- let totalInputTokens = 0;
105
- let totalOutputTokens = 0;
106
- let totalCost = 0;
107
-
108
- const roster: CockpitAgentRosterEntry[] = sorted.map((rec) => {
109
- const isTerminal = AGENT_TERMINAL_STATUSES.has(rec.status);
110
- const elapsed = now - rec.startedAt;
111
- const stalled = !isTerminal && elapsed >= AGENT_STALL_THRESHOLD_MS;
112
-
113
- let inputTokens: number | null = null;
114
- let outputTokens: number | null = null;
115
- let cost: number | null = null;
116
-
117
- if (rec.usage) {
118
- hasUsage = true;
119
- const inp =
120
- rec.usage.inputTokens +
121
- (rec.usage.cacheReadTokens ?? 0) +
122
- (rec.usage.cacheWriteTokens ?? 0);
123
- const out = rec.usage.outputTokens;
124
- const agentCost = calcSessionCost(
125
- rec.usage.inputTokens,
126
- rec.usage.outputTokens,
127
- rec.usage.cacheReadTokens ?? 0,
128
- rec.usage.cacheWriteTokens ?? 0,
129
- rec.model ?? 'unknown',
130
- );
131
-
132
- inputTokens = inp;
133
- outputTokens = out;
134
- cost = agentCost;
135
-
136
- totalInputTokens += inp;
137
- totalOutputTokens += out;
138
- totalCost += agentCost;
139
- }
140
-
141
- const task = truncateDisplay(rec.task, 50);
142
-
143
- return {
144
- id: rec.id,
145
- task,
146
- model: rec.model ?? 'unknown',
147
- status: rec.status,
148
- stalled,
149
- inputTokens,
150
- outputTokens,
151
- cost,
152
- };
153
- });
154
-
155
- return {
156
- roster,
157
- stalledAgentCount: countStalledAgents(sorted, now),
158
- totalInputTokens: hasUsage ? totalInputTokens : null,
159
- totalOutputTokens: hasUsage ? totalOutputTokens : null,
160
- totalCost: hasUsage ? totalCost : null,
161
- };
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // Read-model factory
166
- // ---------------------------------------------------------------------------
167
-
168
- /** Minimal read-model interface matching the existing UiReadModel shape. */
169
- export interface CockpitRosterReadModel {
170
- getSnapshot(): CockpitRosterSnapshot;
171
- /**
172
- * Notify all subscribers that the roster has changed and the cockpit panel
173
- * should re-render. Wire this to an AgentManager event feed so live agent
174
- * state changes propagate:
175
- *
176
- * const roster = createCockpitRosterReadModel(agentManager);
177
- * agentEvents.subscribe(() => roster.markDirty());
178
- *
179
- * For static/test fixtures the implementation is a no-op.
180
- */
181
- markDirty(): void;
182
- /**
183
- * Subscribe to changes. The listener is called whenever markDirty() is
184
- * invoked. Returns an unsubscribe function.
185
- *
186
- * For static/test fixtures, returns a no-op unsubscribe.
187
- */
188
- subscribe(listener: () => void): () => void;
189
- }
190
-
191
- /**
192
- * Create a live CockpitRosterReadModel backed by an AgentManager.
193
- *
194
- * The returned read-model re-derives its snapshot on every getSnapshot() call
195
- * so it always reflects the current agent state without needing an event bus.
196
- * Callers that want reactive updates should poll or wire in their own
197
- * event-driven markDirty() path.
198
- */
199
- export function createCockpitRosterReadModel(
200
- agentManager: CockpitRosterAgentManager,
201
- ): CockpitRosterReadModel {
202
- const listeners = new Set<() => void>();
203
-
204
- function markDirty(): void {
205
- for (const listener of listeners) {
206
- listener();
207
- }
208
- }
209
-
210
- return {
211
- getSnapshot(): CockpitRosterSnapshot {
212
- return buildCockpitRosterSnapshot(agentManager.list());
213
- },
214
- markDirty,
215
- subscribe(listener: () => void): () => void {
216
- listeners.add(listener);
217
- return () => listeners.delete(listener);
218
- },
219
- };
220
- }
221
-
222
- /**
223
- * Create a static CockpitRosterReadModel for testing.
224
- */
225
- export function createStaticCockpitRosterReadModel(
226
- snapshot: CockpitRosterSnapshot,
227
- ): CockpitRosterReadModel {
228
- return {
229
- getSnapshot: () => snapshot,
230
- markDirty: () => {},
231
- subscribe: () => () => {},
232
- };
233
- }
@@ -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
- }