@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,534 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { BasePanel } from './base-panel.ts';
4
- import type { PanelIntegrationContext } from './types.ts';
5
- import type { UiReadModel, UiRemoteSnapshot } from '../runtime/ui-read-models.ts';
6
- import {
7
- buildDetailBlock,
8
- buildEmptyState,
9
- buildPanelListRow,
10
- buildPanelLine,
11
- buildSummaryBlock,
12
- buildPanelWorkspace,
13
- DEFAULT_PANEL_PALETTE,
14
- resolvePrimaryScrollableSection,
15
- type PanelWorkspaceSection,
16
- } from './polish.ts';
17
- import { truncateDisplay } from '../utils/terminal-width.ts';
18
- import { getTrackedVisibleWindow } from '../renderer/surface-layout.ts';
19
-
20
- // Base chrome only — title band, state colors, and text tokens all come
21
- // straight from DEFAULT_PANEL_PALETTE (WO-002).
22
- const C = DEFAULT_PANEL_PALETTE;
23
-
24
- function stateColor(state: string): string {
25
- switch (state) {
26
- case 'connected':
27
- case 'syncing':
28
- return C.good;
29
- case 'degraded':
30
- case 'reconnecting':
31
- case 'authenticating':
32
- case 'initializing':
33
- return C.warn;
34
- case 'terminal_failure':
35
- return C.bad;
36
- default:
37
- return C.dim;
38
- }
39
- }
40
-
41
- function formatTimestamp(value?: number): string {
42
- return value ? new Date(value).toLocaleTimeString() : 'n/a';
43
- }
44
-
45
- function truncate(text: string, width: number): string {
46
- return truncateDisplay(text, width);
47
- }
48
-
49
- // Splits a machine-generated slash-command string (e.g. `/remote show foo`)
50
- // into the { name, args } shape ctx.executeCommand expects — the same
51
- // leading-slash-stripping approach panel-integration-actions.ts uses for
52
- // ApprovalPanel's dispatched command.
53
- function parseCommand(command: string): { name: string; args: string[] } | null {
54
- const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
55
- const [name, ...args] = parts;
56
- return name ? { name, args } : null;
57
- }
58
-
59
- // Mirrors RemoteSupervisor's own stateIsDegraded()/degradedConnections
60
- // definition (sdk/platform/runtime/remote/supervisor.js) so "degraded/stale"
61
- // means the same thing here as it does in the posture summary above.
62
- function isSupervisorEntryDegraded(entry: { transportState: string; heartbeat: { status: string } }): boolean {
63
- const degradedTransport = entry.transportState === 'degraded'
64
- || entry.transportState === 'reconnecting'
65
- || entry.transportState === 'terminal_failure';
66
- return degradedTransport || entry.heartbeat.status !== 'fresh';
67
- }
68
-
69
- export class RemotePanel extends BasePanel {
70
- private readonly readModel?: UiReadModel<UiRemoteSnapshot>;
71
- private readonly unsub: (() => void) | null;
72
- private selectedIndex = 0;
73
- private scrollOffset = 0;
74
- private browseMode: 'connections' | 'contracts' = 'connections';
75
- // Set by handleInput (enter/r) and consumed on the very next
76
- // handlePanelIntegrationAction dispatch of that same key, mirroring the
77
- // token-budget-panel pattern — handleInput has no ctx.executeCommand.
78
- private pendingCommand: { name: string; args: string[] } | null = null;
79
-
80
- public constructor(readModel?: UiReadModel<UiRemoteSnapshot>) {
81
- super('remote', 'Remote', '▰', 'providers');
82
- this.readModel = readModel;
83
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
84
- }
85
-
86
- public override onDestroy(): void {
87
- this.unsub?.();
88
- }
89
-
90
- public handleInput(key: string): boolean {
91
- // r = dispatch /remote recover for whatever is currently selected (or
92
- // the runtime's first non-fresh session when nothing is selected).
93
- if (this.readModel && key === 'r') {
94
- const runnerId = this.getSelectedRunnerId();
95
- const parsed = parseCommand(runnerId ? `/remote recover ${runnerId}` : '/remote recover');
96
- if (parsed) {
97
- this.pendingCommand = parsed;
98
- return true;
99
- }
100
- }
101
- // Enter on a degraded/stale supervisor selection dispatches its
102
- // top recovery action (already machine-readable via .recovery[0].command)
103
- // instead of requiring the operator to retype it.
104
- if (this.readModel && (key === 'enter' || key === 'return')) {
105
- const entry = this.getSelectedSupervisorEntry();
106
- if (entry && isSupervisorEntryDegraded(entry) && entry.recovery.length > 0) {
107
- const parsed = parseCommand(entry.recovery[0].command);
108
- if (parsed) {
109
- this.pendingCommand = parsed;
110
- return true;
111
- }
112
- }
113
- }
114
- const activeConnections = this.getActiveConnections();
115
- const contracts = this.readModel?.getSnapshot().contracts ?? [];
116
- const browseCount = this.browseMode === 'connections' && activeConnections.length > 0
117
- ? activeConnections.length
118
- : contracts.length;
119
- if (key === 'tab' && contracts.length > 0) {
120
- this.browseMode = this.browseMode === 'connections' ? 'contracts' : 'connections';
121
- this.selectedIndex = 0;
122
- this.markDirty();
123
- return true;
124
- }
125
- if (browseCount === 0) return false;
126
- if (key === 'up' || key === 'k') {
127
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
128
- this.markDirty();
129
- return true;
130
- }
131
- if (key === 'down' || key === 'j') {
132
- this.selectedIndex = Math.min(browseCount - 1, this.selectedIndex + 1);
133
- this.markDirty();
134
- return true;
135
- }
136
- if (key === 'home') {
137
- this.selectedIndex = 0;
138
- this.markDirty();
139
- return true;
140
- }
141
- if (key === 'end') {
142
- this.selectedIndex = Math.max(0, browseCount - 1);
143
- this.markDirty();
144
- return true;
145
- }
146
- return false;
147
- }
148
-
149
- private getActiveConnections() {
150
- return this.readModel?.getSnapshot().acp.activeConnections ?? [];
151
- }
152
-
153
- // Runner id backing the currently browsed row, whichever browse mode is
154
- // active — shared by the Enter/r dispatch logic and render()'s own
155
- // selection lookups so they never drift apart.
156
- private getSelectedRunnerId(): string | null {
157
- const snapshot = this.readModel?.getSnapshot();
158
- if (!snapshot) return null;
159
- const activeConnections = this.getActiveConnections();
160
- const contracts = snapshot.contracts;
161
- const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
162
- // selectedIndex indexes only the mode's active list (activeConnections when
163
- // viewing connections, otherwise contracts); .at() keeps that read off any
164
- // raw source array (no-raw-selectedindex-read rule).
165
- if (viewingConnections) {
166
- return activeConnections.at(this.selectedIndex)?.agentId ?? null;
167
- }
168
- return contracts.at(this.selectedIndex)?.runnerId ?? null;
169
- }
170
-
171
- private getSelectedSupervisorEntry() {
172
- const snapshot = this.readModel?.getSnapshot();
173
- if (!snapshot) return null;
174
- const runnerId = this.getSelectedRunnerId();
175
- if (!runnerId) return null;
176
- return snapshot.supervisor.sessions.find((entry) => entry.runnerId === runnerId) ?? null;
177
- }
178
-
179
- public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
180
- if (!this.pendingCommand) return false;
181
- const { name, args } = this.pendingCommand;
182
- this.pendingCommand = null;
183
- void ctx.executeCommand?.(name, args);
184
- return true;
185
- }
186
-
187
- public render(width: number, height: number): Line[] {
188
- this.needsRender = false;
189
- const intro = 'Bridge, ACP, runner-contract, and artifact posture for self-hosted remote work.';
190
-
191
- if (!this.readModel) {
192
- const sectionLines = buildEmptyState(
193
- width,
194
- ' Runtime store not wired into this panel yet.',
195
- 'The remote control room needs the shell read model so it can display ACP state, runner contracts, and replay artifacts.',
196
- [
197
- { command: '/remote setup', summary: 'review bootstrap, env, tunnel, and bridge guidance' },
198
- { command: '/remote panel', summary: 'reopen the panel from the shell-owned runtime' },
199
- ],
200
- C,
201
- );
202
- const lines = buildPanelWorkspace(width, height, {
203
- title: 'Remote Control Room',
204
- intro,
205
- sections: [{ lines: sectionLines }],
206
- palette: C,
207
- });
208
- while (lines.length < height) lines.push(createEmptyLine(width));
209
- return lines;
210
- }
211
-
212
- const snapshot = this.readModel.getSnapshot();
213
- const daemon = snapshot.daemon;
214
- const acp = snapshot.acp;
215
- const activeConnections = this.getActiveConnections();
216
- const artifactCount = snapshot.artifacts.length;
217
- const pools = snapshot.pools;
218
- const contracts = snapshot.contracts;
219
- const supervisor = snapshot.supervisor;
220
- const distributed = {
221
- pairRequests: { pending: snapshot.distributed.pairRequests.length },
222
- peers: {
223
- total: snapshot.distributed.peers.length,
224
- connected: snapshot.distributed.peers.filter((peer) => peer.status === 'connected').length,
225
- nodes: snapshot.distributed.peers.filter((peer) => peer.kind === 'node').length,
226
- devices: snapshot.distributed.peers.filter((peer) => peer.kind === 'device').length,
227
- },
228
- work: {
229
- queued: snapshot.distributed.work.filter((item) => item.status === 'queued').length,
230
- claimed: snapshot.distributed.work.filter((item) => item.status === 'claimed').length,
231
- },
232
- };
233
-
234
- const postureLines: Line[] = [
235
- buildPanelLine(width, [
236
- [' daemon ', C.label],
237
- [daemon.transportState.toUpperCase(), stateColor(daemon.transportState)],
238
- [' running ', C.label],
239
- [daemon.isRunning ? 'yes' : 'no', daemon.isRunning ? C.good : C.dim],
240
- [' reconnects ', C.label],
241
- [String(daemon.reconnectAttempts), daemon.reconnectAttempts > 0 ? C.warn : C.good],
242
- [' jobs ', C.label],
243
- [String(daemon.runningJobCount), daemon.runningJobCount > 0 ? C.info : C.dim],
244
- ]),
245
- buildPanelLine(width, [
246
- [' ACP manager ', C.label],
247
- [acp.transportState.toUpperCase(), stateColor(acp.transportState)],
248
- [' active connections ', C.label],
249
- [String(acp.activeConnections.length), acp.activeConnections.length > 0 ? C.info : C.dim],
250
- [' total messages ', C.label],
251
- [String(acp.totalMessages), acp.totalMessages > 0 ? C.value : C.dim],
252
- ]),
253
- buildPanelLine(width, [
254
- [' runner contracts ', C.label],
255
- [String(contracts.length), C.info],
256
- [' pools ', C.label],
257
- [String(pools.length), pools.length > 0 ? C.info : C.dim],
258
- [' review artifacts ', C.label],
259
- [String(artifactCount), artifactCount > 0 ? C.good : C.dim],
260
- ]),
261
- buildPanelLine(width, [
262
- [' supervisor ', C.label],
263
- [String(supervisor.sessions.length), C.info],
264
- [' degraded ', C.label],
265
- [String(supervisor.degradedConnections), supervisor.degradedConnections > 0 ? C.warn : C.good],
266
- [' distributed peers ', C.label],
267
- [String(distributed.peers?.total ?? 0), C.info],
268
- [' connected ', C.label],
269
- [String(distributed.peers?.connected ?? 0), (distributed.peers?.connected ?? 0) > 0 ? C.good : C.dim],
270
- [' queued work ', C.label],
271
- [String(distributed.work?.queued ?? 0), (distributed.work?.queued ?? 0) > 0 ? C.info : C.dim],
272
- ]),
273
- ];
274
-
275
- if (daemon.lastError) {
276
- postureLines.push(buildPanelLine(width, [
277
- [' daemon error ', C.label],
278
- [truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.bad],
279
- ]));
280
- }
281
- const canBrowse = activeConnections.length > 0 || contracts.length > 0;
282
- const canSwitch = contracts.length > 0 && activeConnections.length > 0;
283
- const navHint = !canBrowse
284
- ? ` focus=${this.browseMode} idle - no connections or contracts to browse`
285
- : canSwitch
286
- ? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts r=recover Enter=recover selection`
287
- : ` focus=${this.browseMode} Up/Down move r=recover Enter=recover selection`;
288
- const footerLines = [
289
- buildPanelLine(width, [[navHint, C.dim]]),
290
- ] as const;
291
-
292
- if (activeConnections.length === 0 && contracts.length === 0) {
293
- const idleLines = [
294
- ...postureLines,
295
- ...buildEmptyState(
296
- width,
297
- ' No active ACP or remote subagent connections.',
298
- 'The remote control room is healthy but idle. Runner contracts, session bundles, and bridge pools will appear here once remote work starts.',
299
- [
300
- { command: '/remote setup', summary: 'review remote bootstrap and environment export' },
301
- { command: '/remote env', summary: 'emit a reusable remote shell snippet' },
302
- { command: '/bridge status', summary: 'inspect runner pools and existing remote artifacts' },
303
- ],
304
- C,
305
- ),
306
- ];
307
- const lines = buildPanelWorkspace(width, height, {
308
- title: 'Remote Control Room',
309
- intro,
310
- sections: [{ lines: buildSummaryBlock(width, 'Remote posture', idleLines, C) }],
311
- footerLines,
312
- palette: C,
313
- });
314
- while (lines.length < height) lines.push(createEmptyLine(width));
315
- return lines.slice(0, height);
316
- }
317
-
318
- const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
319
- this.selectedIndex = Math.min(
320
- this.selectedIndex,
321
- Math.max(0, (viewingConnections ? activeConnections.length : contracts.length) - 1),
322
- );
323
- const browseCount = viewingConnections ? activeConnections.length : contracts.length;
324
- const selected = viewingConnections ? activeConnections.at(this.selectedIndex) ?? null : null;
325
- const selectedContract = !viewingConnections ? contracts.at(this.selectedIndex) ?? null : null;
326
- const detailRows: Line[] = [];
327
-
328
- if (selected) {
329
- detailRows.push(buildPanelLine(width, [
330
- [' Agent: ', C.label],
331
- [selected.agentId, C.value],
332
- [' State: ', C.label],
333
- [selected.transportState, stateColor(selected.transportState)],
334
- [' Completing: ', C.label],
335
- [selected.completing ? 'yes' : 'no', selected.completing ? C.warn : C.dim],
336
- ]));
337
- detailRows.push(buildPanelLine(width, [
338
- [' Connected: ', C.label],
339
- [formatTimestamp(selected.connectedAt), C.dim],
340
- [' Messages: ', C.label],
341
- [String(selected.messageCount), selected.messageCount > 0 ? C.info : C.dim],
342
- [' Errors: ', C.label],
343
- [String(selected.errorCount), selected.errorCount > 0 ? C.warn : C.dim],
344
- ]));
345
- if (selected.lastError) {
346
- detailRows.push(buildPanelLine(width, [
347
- [' Last error: ', C.label],
348
- [selected.lastError.slice(0, Math.max(0, width - 13)), C.bad],
349
- ]));
350
- }
351
-
352
- const contract = contracts.find((entry) => entry.runnerId === selected.agentId);
353
- if (contract) {
354
- detailRows.push(buildPanelLine(width, [
355
- [' Contract: ', C.label],
356
- [`${contract.template} / ${contract.trustClass}`, C.info],
357
- [' Pool: ', C.label],
358
- [contract.poolId ?? '(none)', contract.poolId ? C.info : C.dim],
359
- ]));
360
- detailRows.push(buildPanelLine(width, [
361
- [' Depth: ', C.label],
362
- [String(contract.capabilityCeiling.orchestrationDepth), C.value],
363
- [' Pools: ', C.label],
364
- [String(pools.length), pools.length > 0 ? C.info : C.dim],
365
- ]));
366
- detailRows.push(buildPanelLine(width, [
367
- [' Protocol: ', C.label],
368
- [contract.capabilityCeiling.executionProtocol, C.value],
369
- [' Review: ', C.label],
370
- [contract.capabilityCeiling.reviewMode, C.value],
371
- [' Lane: ', C.label],
372
- [contract.capabilityCeiling.communicationLane, C.value],
373
- ]));
374
- detailRows.push(buildPanelLine(width, [
375
- [' Tools: ', C.label],
376
- [truncate(contract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
377
- ]));
378
- }
379
-
380
- if (selected.taskId) {
381
- detailRows.push(buildPanelLine(width, [
382
- [' Task: ', C.label],
383
- [selected.taskId, C.value],
384
- ]));
385
- }
386
-
387
- const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selected.agentId);
388
- if (supervisorEntry) {
389
- detailRows.push(buildPanelLine(width, [
390
- [' Heartbeat: ', C.label],
391
- [supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
392
- [' Protocol: ', C.label],
393
- [supervisorEntry.negotiation.executionProtocol, C.value],
394
- [' Review: ', C.label],
395
- [supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.good : C.dim],
396
- ]));
397
- detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${supervisorEntry.heartbeat.detail}`, width), C.dim]]));
398
- if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
399
- detailRows.push(buildPanelLine(width, [
400
- [' Enter recovers: ', C.label],
401
- [supervisorEntry.recovery[0].reason, C.warn],
402
- ]));
403
- }
404
- }
405
-
406
- const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
407
- if (recentArtifact) {
408
- detailRows.push(buildPanelLine(width, [[' Recent Review Artifact', C.label]]));
409
- detailRows.push(buildPanelLine(width, [
410
- [' Artifact: ', C.label],
411
- [recentArtifact.id, C.value],
412
- [' Status: ', C.label],
413
- [recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
414
- ]));
415
- detailRows.push(buildPanelLine(width, [
416
- [' Summary: ', C.label],
417
- [truncate(recentArtifact.task.summary, Math.max(0, width - 12)), C.dim],
418
- ]));
419
- }
420
- detailRows.push(buildPanelLine(width, [
421
- [' Tip: ', C.label],
422
- ['Use Up/Down or j/k to inspect another connection.', C.dim],
423
- ]));
424
- } else if (selectedContract) {
425
- detailRows.push(buildPanelLine(width, [
426
- [' Runner: ', C.label],
427
- [selectedContract.runnerId, C.value],
428
- [' Template: ', C.label],
429
- [selectedContract.template, C.info],
430
- [' Trust: ', C.label],
431
- [selectedContract.trustClass, C.value],
432
- ]));
433
- detailRows.push(buildPanelLine(width, [
434
- [' Pool: ', C.label],
435
- [selectedContract.poolId ?? '(none)', selectedContract.poolId ? C.info : C.dim],
436
- [' Transport: ', C.label],
437
- [selectedContract.transport.state, stateColor(selectedContract.transport.state)],
438
- ]));
439
- detailRows.push(buildPanelLine(width, [
440
- [' Protocol: ', C.label],
441
- [selectedContract.capabilityCeiling.executionProtocol, C.value],
442
- [' Review: ', C.label],
443
- [selectedContract.capabilityCeiling.reviewMode, C.value],
444
- [' Lane: ', C.label],
445
- [selectedContract.capabilityCeiling.communicationLane, C.value],
446
- ]));
447
- detailRows.push(buildPanelLine(width, [
448
- [' Tools: ', C.label],
449
- [truncate(selectedContract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
450
- ]));
451
- const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selectedContract.runnerId);
452
- if (supervisorEntry) {
453
- detailRows.push(buildPanelLine(width, [
454
- [' Heartbeat: ', C.label],
455
- [supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
456
- [' Lane: ', C.label],
457
- [supervisorEntry.negotiation.communicationLane, C.info],
458
- ]));
459
- if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
460
- detailRows.push(buildPanelLine(width, [
461
- [' Enter recovers: ', C.label],
462
- [supervisorEntry.recovery[0].reason, C.warn],
463
- ]));
464
- }
465
- }
466
- const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selectedContract.runnerId);
467
- if (recentArtifact) {
468
- detailRows.push(buildPanelLine(width, [
469
- [' Recent artifact: ', C.label],
470
- [recentArtifact.id, C.good],
471
- [' Status: ', C.label],
472
- [recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
473
- ]));
474
- }
475
- }
476
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Remote posture', postureLines, C) };
477
- const detailSection: PanelWorkspaceSection = {
478
- lines: buildDetailBlock(width, selected ? 'Selected connection' : 'Selected contract', detailRows, C),
479
- };
480
- const browseTitle = viewingConnections ? 'Active Connections' : 'Registered Remote Runner Contracts';
481
- const rawBrowseLines: Line[] = viewingConnections
482
- ? activeConnections.map((connection, absolute) => {
483
- return buildPanelListRow(width, [
484
- { text: connection.agentId.padEnd(18), fg: C.value },
485
- { text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
486
- { text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
487
- { text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
488
- { text: truncateDisplay(` ${connection.label}`, Math.max(0, width - 56)), fg: C.dim },
489
- ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
490
- })
491
- : [
492
- ...contracts.map((contract, absolute) => {
493
- return buildPanelListRow(width, [
494
- { text: contract.runnerId.padEnd(18), fg: C.value },
495
- { text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
496
- { text: truncateDisplay(` ${contract.template}`, Math.max(0, width - 42)), fg: C.dim },
497
- ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
498
- }),
499
- buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),
500
- ];
501
- const resolvedBrowseSection = resolvePrimaryScrollableSection(width, height, {
502
- intro,
503
- footerLines,
504
- palette: C,
505
- beforeSections: [postureSection],
506
- section: {
507
- title: browseTitle,
508
- scrollableLines: rawBrowseLines,
509
- selectedIndex: this.selectedIndex,
510
- scrollOffset: this.scrollOffset,
511
- guardRows: 1,
512
- minRows: 4,
513
- appendWindowSummary: viewingConnections ? { dimColor: C.dim } : undefined,
514
- },
515
- afterSections: [detailSection],
516
- });
517
- this.scrollOffset = resolvedBrowseSection.scrollOffset;
518
-
519
- const sections: PanelWorkspaceSection[] = [
520
- postureSection,
521
- resolvedBrowseSection.section,
522
- detailSection,
523
- ];
524
- const lines = buildPanelWorkspace(width, height, {
525
- title: 'Remote Control Room',
526
- intro,
527
- sections,
528
- footerLines,
529
- palette: C,
530
- });
531
- while (lines.length < height) lines.push(createEmptyLine(width));
532
- return lines.slice(0, height);
533
- }
534
- }