@pellux/goodvibes-agent 0.1.70 → 0.1.72

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 (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -3
  3. package/docs/README.md +2 -2
  4. package/docs/getting-started.md +1 -1
  5. package/docs/runtime-connection.md +37 -0
  6. package/package.json +43 -2
  7. package/src/agent/skill-discovery.ts +119 -0
  8. package/src/cli/config-overrides.ts +1 -5
  9. package/src/cli/entrypoint.ts +0 -6
  10. package/src/cli/help.ts +0 -43
  11. package/src/cli/index.ts +0 -2
  12. package/src/cli/management-commands.ts +1 -109
  13. package/src/cli/management.ts +1 -32
  14. package/src/cli/package-verification.ts +12 -4
  15. package/src/cli/parser.ts +0 -16
  16. package/src/cli/status.ts +1 -1
  17. package/src/cli/types.ts +0 -8
  18. package/src/input/commands/delegation-runtime.ts +0 -8
  19. package/src/input/commands/experience-runtime.ts +0 -177
  20. package/src/input/commands/guidance-runtime.ts +0 -69
  21. package/src/input/commands/local-runtime.ts +1 -57
  22. package/src/input/commands/local-setup-review.ts +1 -1
  23. package/src/input/commands/operator-runtime.ts +1 -145
  24. package/src/input/commands/platform-access-runtime.ts +2 -195
  25. package/src/input/commands/product-runtime.ts +0 -116
  26. package/src/input/commands/security-runtime.ts +88 -0
  27. package/src/input/commands/session-content.ts +0 -97
  28. package/src/input/commands/shell-core.ts +0 -13
  29. package/src/input/commands.ts +2 -95
  30. package/src/panels/builtin/operations.ts +3 -184
  31. package/src/panels/confirm-state.ts +1 -1
  32. package/src/panels/index.ts +0 -11
  33. package/src/version.ts +1 -1
  34. package/docs/deployment-and-services.md +0 -52
  35. package/src/cli/service-command.ts +0 -26
  36. package/src/cli/surface-command.ts +0 -247
  37. package/src/input/commands/branch-runtime.ts +0 -72
  38. package/src/input/commands/control-room-runtime.ts +0 -234
  39. package/src/input/commands/discovery-runtime.ts +0 -61
  40. package/src/input/commands/hooks-runtime.ts +0 -207
  41. package/src/input/commands/incident-runtime.ts +0 -106
  42. package/src/input/commands/integration-runtime.ts +0 -437
  43. package/src/input/commands/local-setup.ts +0 -288
  44. package/src/input/commands/managed-runtime.ts +0 -240
  45. package/src/input/commands/marketplace-runtime.ts +0 -305
  46. package/src/input/commands/memory-product-runtime.ts +0 -148
  47. package/src/input/commands/operator-panel-runtime.ts +0 -146
  48. package/src/input/commands/platform-services-runtime.ts +0 -271
  49. package/src/input/commands/profile-sync-runtime.ts +0 -110
  50. package/src/input/commands/provider.ts +0 -363
  51. package/src/input/commands/remote-runtime-pool.ts +0 -89
  52. package/src/input/commands/remote-runtime-setup.ts +0 -226
  53. package/src/input/commands/remote-runtime.ts +0 -432
  54. package/src/input/commands/replay-runtime.ts +0 -25
  55. package/src/input/commands/services-runtime.ts +0 -220
  56. package/src/input/commands/settings-sync-runtime.ts +0 -197
  57. package/src/input/commands/share-runtime.ts +0 -127
  58. package/src/input/commands/skills-runtime.ts +0 -226
  59. package/src/input/commands/teleport-runtime.ts +0 -68
  60. package/src/panels/cockpit-panel.ts +0 -183
  61. package/src/panels/communication-panel.ts +0 -153
  62. package/src/panels/control-plane-panel.ts +0 -211
  63. package/src/panels/forensics-panel.ts +0 -364
  64. package/src/panels/hooks-panel.ts +0 -239
  65. package/src/panels/incident-review-panel.ts +0 -197
  66. package/src/panels/marketplace-panel.ts +0 -212
  67. package/src/panels/ops-control-panel.ts +0 -150
  68. package/src/panels/ops-strategy-panel.ts +0 -235
  69. package/src/panels/orchestration-panel.ts +0 -272
  70. package/src/panels/plugins-panel.ts +0 -178
  71. package/src/panels/remote-panel.ts +0 -449
  72. package/src/panels/routes-panel.ts +0 -178
  73. package/src/panels/services-panel.ts +0 -231
  74. package/src/panels/settings-sync-panel.ts +0 -120
  75. package/src/panels/skills-panel.ts +0 -431
  76. package/src/panels/watchers-panel.ts +0 -193
  77. package/src/verification/live-verifier.ts +0 -588
  78. package/src/verification/verification-ledger.ts +0 -239
@@ -1,449 +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 { UiReadModel, UiRemoteSnapshot } from '../runtime/ui-read-models.ts';
5
- import {
6
- buildDetailBlock,
7
- buildEmptyState,
8
- buildGuidanceLine,
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
- const C = {
21
- ...DEFAULT_PANEL_PALETTE,
22
- header: '#94a3b8',
23
- headerBg: '#1e293b',
24
- dim: '#475569',
25
- ok: '#22c55e',
26
- warn: '#eab308',
27
- error: '#ef4444',
28
- } as const;
29
-
30
- function stateColor(state: string): string {
31
- switch (state) {
32
- case 'connected':
33
- case 'syncing':
34
- return C.ok;
35
- case 'degraded':
36
- case 'reconnecting':
37
- case 'authenticating':
38
- case 'initializing':
39
- return C.warn;
40
- case 'terminal_failure':
41
- return C.error;
42
- default:
43
- return C.dim;
44
- }
45
- }
46
-
47
- function formatTimestamp(value?: number): string {
48
- return value ? new Date(value).toLocaleTimeString() : 'n/a';
49
- }
50
-
51
- function truncate(text: string, width: number): string {
52
- return truncateDisplay(text, width);
53
- }
54
-
55
- export class RemotePanel extends BasePanel {
56
- private readonly readModel?: UiReadModel<UiRemoteSnapshot>;
57
- private readonly unsub: (() => void) | null;
58
- private selectedIndex = 0;
59
- private scrollOffset = 0;
60
- private browseMode: 'connections' | 'contracts' = 'connections';
61
-
62
- public constructor(readModel?: UiReadModel<UiRemoteSnapshot>) {
63
- super('remote', 'Remote', 'R', 'monitoring');
64
- this.readModel = readModel;
65
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
66
- }
67
-
68
- public override onDestroy(): void {
69
- this.unsub?.();
70
- }
71
-
72
- public handleInput(key: string): boolean {
73
- const activeConnections = this.getActiveConnections();
74
- const contracts = this.readModel?.getSnapshot().contracts ?? [];
75
- const browseCount = this.browseMode === 'connections' && activeConnections.length > 0
76
- ? activeConnections.length
77
- : contracts.length;
78
- if (key === 'tab' && contracts.length > 0) {
79
- this.browseMode = this.browseMode === 'connections' ? 'contracts' : 'connections';
80
- this.selectedIndex = 0;
81
- this.markDirty();
82
- return true;
83
- }
84
- if (browseCount === 0) return false;
85
- if (key === 'up' || key === 'k') {
86
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
87
- this.markDirty();
88
- return true;
89
- }
90
- if (key === 'down' || key === 'j') {
91
- this.selectedIndex = Math.min(browseCount - 1, this.selectedIndex + 1);
92
- this.markDirty();
93
- return true;
94
- }
95
- if (key === 'home') {
96
- this.selectedIndex = 0;
97
- this.markDirty();
98
- return true;
99
- }
100
- if (key === 'end') {
101
- this.selectedIndex = Math.max(0, browseCount - 1);
102
- this.markDirty();
103
- return true;
104
- }
105
- return false;
106
- }
107
-
108
- private getActiveConnections() {
109
- return this.readModel?.getSnapshot().acp.activeConnections ?? [];
110
- }
111
-
112
- public render(width: number, height: number): Line[] {
113
- this.needsRender = false;
114
- const intro = 'Remote worker, bridge, and review-artifact posture for delegated work.';
115
-
116
- if (!this.readModel) {
117
- const sectionLines = buildEmptyState(
118
- width,
119
- ' Runtime store not wired into this panel yet.',
120
- 'The remote review workspace needs the shell read model so it can display worker state and review artifacts.',
121
- [
122
- { command: '/remote setup', summary: 'review bootstrap, env, tunnel, and bridge guidance' },
123
- { command: '/remote panel', summary: 'reopen the panel from the shell-owned runtime' },
124
- ],
125
- C,
126
- );
127
- const lines = buildPanelWorkspace(width, height, {
128
- title: 'Remote Work Review',
129
- intro,
130
- sections: [{ lines: sectionLines }],
131
- palette: C,
132
- });
133
- while (lines.length < height) lines.push(createEmptyLine(width));
134
- return lines;
135
- }
136
-
137
- const snapshot = this.readModel.getSnapshot();
138
- const daemon = snapshot.daemon;
139
- const acp = snapshot.acp;
140
- const activeConnections = this.getActiveConnections();
141
- const artifactCount = snapshot.artifacts.length;
142
- const pools = snapshot.pools;
143
- const contracts = snapshot.contracts;
144
- const supervisor = snapshot.supervisor;
145
- const distributed = {
146
- pairRequests: { pending: snapshot.distributed.pairRequests.length },
147
- peers: {
148
- total: snapshot.distributed.peers.length,
149
- connected: snapshot.distributed.peers.filter((peer) => peer.status === 'connected').length,
150
- nodes: snapshot.distributed.peers.filter((peer) => peer.kind === 'node').length,
151
- devices: snapshot.distributed.peers.filter((peer) => peer.kind === 'device').length,
152
- },
153
- work: {
154
- queued: snapshot.distributed.work.filter((item) => item.status === 'queued').length,
155
- claimed: snapshot.distributed.work.filter((item) => item.status === 'claimed').length,
156
- },
157
- };
158
-
159
- const postureLines: Line[] = [
160
- buildPanelLine(width, [
161
- [' runtime ', C.label],
162
- [daemon.transportState.toUpperCase(), stateColor(daemon.transportState)],
163
- [' running ', C.label],
164
- [daemon.isRunning ? 'yes' : 'no', daemon.isRunning ? C.ok : C.dim],
165
- [' reconnects ', C.label],
166
- [String(daemon.reconnectAttempts), daemon.reconnectAttempts > 0 ? C.warn : C.ok],
167
- [' jobs ', C.label],
168
- [String(daemon.runningJobCount), daemon.runningJobCount > 0 ? C.info : C.dim],
169
- ]),
170
- buildPanelLine(width, [
171
- [' ACP manager ', C.label],
172
- [acp.transportState.toUpperCase(), stateColor(acp.transportState)],
173
- [' active connections ', C.label],
174
- [String(acp.activeConnections.length), acp.activeConnections.length > 0 ? C.info : C.dim],
175
- [' total messages ', C.label],
176
- [String(acp.totalMessages), acp.totalMessages > 0 ? C.value : C.dim],
177
- ]),
178
- buildPanelLine(width, [
179
- [' worker contracts ', C.label],
180
- [String(contracts.length), C.info],
181
- [' pools ', C.label],
182
- [String(pools.length), pools.length > 0 ? C.info : C.dim],
183
- [' review artifacts ', C.label],
184
- [String(artifactCount), artifactCount > 0 ? C.ok : C.dim],
185
- ]),
186
- buildPanelLine(width, [
187
- [' supervisor ', C.label],
188
- [String(supervisor.sessions.length), C.info],
189
- [' degraded ', C.label],
190
- [String(supervisor.degradedConnections), supervisor.degradedConnections > 0 ? C.warn : C.ok],
191
- [' distributed peers ', C.label],
192
- [String(distributed.peers?.total ?? 0), C.info],
193
- [' connected ', C.label],
194
- [String(distributed.peers?.connected ?? 0), (distributed.peers?.connected ?? 0) > 0 ? C.ok : C.dim],
195
- [' queued work ', C.label],
196
- [String(distributed.work?.queued ?? 0), (distributed.work?.queued ?? 0) > 0 ? C.info : C.dim],
197
- ]),
198
- ];
199
-
200
- if (daemon.lastError) {
201
- postureLines.push(buildPanelLine(width, [
202
- [' runtime error ', C.label],
203
- [daemon.lastError.slice(0, Math.max(0, width - 14)), C.error],
204
- ]));
205
- }
206
- postureLines.push(
207
- buildGuidanceLine(width, '/remote recover', 'inspect remote state with worker support and disconnect recovery hints', C),
208
- buildGuidanceLine(width, '/remote support', 'inspect transport support before routing remote work or reattaching a session', C),
209
- );
210
-
211
- const footerLines = [
212
- buildGuidanceLine(width, '/remote setup', 'review bridge, tunnel, env, and bootstrap flows for self-hosted remote work', C),
213
- buildPanelLine(width, [[` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`, C.dim]]),
214
- ] as const;
215
-
216
- if (activeConnections.length === 0 && contracts.length === 0) {
217
- const idleLines = [
218
- ...postureLines,
219
- ...buildEmptyState(
220
- width,
221
- ' No active ACP or remote subagent connections.',
222
- 'Remote review is healthy but idle. Worker contracts, session bundles, and bridge pools will appear here once delegated work exists.',
223
- [
224
- { command: '/remote setup', summary: 'review remote bootstrap and environment export' },
225
- { command: '/remote env', summary: 'emit a reusable remote shell snippet' },
226
- { command: '/bridge status', summary: 'inspect worker pools and existing remote artifacts' },
227
- ],
228
- C,
229
- ),
230
- ];
231
- const lines = buildPanelWorkspace(width, height, {
232
- title: 'Remote Work Review',
233
- intro,
234
- sections: [{ lines: buildSummaryBlock(width, 'Remote posture', idleLines, C) }],
235
- footerLines,
236
- palette: C,
237
- });
238
- while (lines.length < height) lines.push(createEmptyLine(width));
239
- return lines.slice(0, height);
240
- }
241
-
242
- const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
243
- this.selectedIndex = Math.min(
244
- this.selectedIndex,
245
- Math.max(0, (viewingConnections ? activeConnections.length : contracts.length) - 1),
246
- );
247
- const browseCount = viewingConnections ? activeConnections.length : contracts.length;
248
- const selected = viewingConnections ? activeConnections[this.selectedIndex] ?? null : null;
249
- const selectedContract = !viewingConnections ? contracts[this.selectedIndex] ?? null : null;
250
- const detailRows: Line[] = [];
251
-
252
- if (selected) {
253
- detailRows.push(buildPanelLine(width, [
254
- [' Agent: ', C.label],
255
- [selected.agentId, C.value],
256
- [' State: ', C.label],
257
- [selected.transportState, stateColor(selected.transportState)],
258
- [' Completing: ', C.label],
259
- [selected.completing ? 'yes' : 'no', selected.completing ? C.warn : C.dim],
260
- ]));
261
- detailRows.push(buildPanelLine(width, [
262
- [' Connected: ', C.label],
263
- [formatTimestamp(selected.connectedAt), C.dim],
264
- [' Messages: ', C.label],
265
- [String(selected.messageCount), selected.messageCount > 0 ? C.info : C.dim],
266
- [' Errors: ', C.label],
267
- [String(selected.errorCount), selected.errorCount > 0 ? C.warn : C.dim],
268
- ]));
269
- if (selected.lastError) {
270
- detailRows.push(buildPanelLine(width, [
271
- [' Last error: ', C.label],
272
- [selected.lastError.slice(0, Math.max(0, width - 13)), C.error],
273
- ]));
274
- }
275
-
276
- const contract = contracts.find((entry) => entry.runnerId === selected.agentId);
277
- if (contract) {
278
- detailRows.push(buildPanelLine(width, [
279
- [' Contract: ', C.label],
280
- [`${contract.template} / ${contract.trustClass}`, C.info],
281
- [' Pool: ', C.label],
282
- [contract.poolId ?? '(none)', contract.poolId ? C.info : C.dim],
283
- ]));
284
- detailRows.push(buildPanelLine(width, [
285
- [' Depth: ', C.label],
286
- [String(contract.capabilityCeiling.orchestrationDepth), C.value],
287
- [' Pools: ', C.label],
288
- [String(pools.length), pools.length > 0 ? C.info : C.dim],
289
- ]));
290
- detailRows.push(buildPanelLine(width, [
291
- [' Protocol: ', C.label],
292
- [contract.capabilityCeiling.executionProtocol, C.value],
293
- [' Review: ', C.label],
294
- [contract.capabilityCeiling.reviewMode, C.value],
295
- [' Lane: ', C.label],
296
- [contract.capabilityCeiling.communicationLane, C.value],
297
- ]));
298
- detailRows.push(buildPanelLine(width, [
299
- [' Tools: ', C.label],
300
- [truncate(contract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
301
- ]));
302
- }
303
-
304
- if (selected.taskId) {
305
- detailRows.push(buildPanelLine(width, [
306
- [' Task: ', C.label],
307
- [selected.taskId, C.value],
308
- ]));
309
- }
310
-
311
- const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selected.agentId);
312
- if (supervisorEntry) {
313
- detailRows.push(buildPanelLine(width, [
314
- [' Heartbeat: ', C.label],
315
- [supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.ok : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.error],
316
- [' Protocol: ', C.label],
317
- [supervisorEntry.negotiation.executionProtocol, C.value],
318
- [' Review: ', C.label],
319
- [supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.ok : C.dim],
320
- ]));
321
- detailRows.push(buildPanelLine(width, [[` ${supervisorEntry.heartbeat.detail}`.slice(0, width), C.dim]]));
322
- }
323
-
324
- const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
325
- if (recentArtifact) {
326
- detailRows.push(buildPanelLine(width, [[' Recent Review Artifact', C.label]]));
327
- detailRows.push(buildPanelLine(width, [
328
- [' Artifact: ', C.label],
329
- [recentArtifact.id, C.value],
330
- [' Status: ', C.label],
331
- [recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
332
- ]));
333
- detailRows.push(buildPanelLine(width, [
334
- [' Summary: ', C.label],
335
- [truncate(recentArtifact.task.summary, Math.max(0, width - 12)), C.dim],
336
- ]));
337
- }
338
- detailRows.push(buildPanelLine(width, [
339
- [' Tip: ', C.label],
340
- ['Use Up/Down or j/k to inspect another connection.', C.dim],
341
- ]));
342
- } else if (selectedContract) {
343
- detailRows.push(buildPanelLine(width, [
344
- [' Worker: ', C.label],
345
- [selectedContract.runnerId, C.value],
346
- [' Template: ', C.label],
347
- [selectedContract.template, C.info],
348
- [' Trust: ', C.label],
349
- [selectedContract.trustClass, C.value],
350
- ]));
351
- detailRows.push(buildPanelLine(width, [
352
- [' Pool: ', C.label],
353
- [selectedContract.poolId ?? '(none)', selectedContract.poolId ? C.info : C.dim],
354
- [' Transport: ', C.label],
355
- [selectedContract.transport.state, stateColor(selectedContract.transport.state)],
356
- ]));
357
- detailRows.push(buildPanelLine(width, [
358
- [' Protocol: ', C.label],
359
- [selectedContract.capabilityCeiling.executionProtocol, C.value],
360
- [' Review: ', C.label],
361
- [selectedContract.capabilityCeiling.reviewMode, C.value],
362
- [' Lane: ', C.label],
363
- [selectedContract.capabilityCeiling.communicationLane, C.value],
364
- ]));
365
- detailRows.push(buildPanelLine(width, [
366
- [' Tools: ', C.label],
367
- [truncate(selectedContract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
368
- ]));
369
- const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selectedContract.runnerId);
370
- if (supervisorEntry) {
371
- detailRows.push(buildPanelLine(width, [
372
- [' Heartbeat: ', C.label],
373
- [supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.ok : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.error],
374
- [' Lane: ', C.label],
375
- [supervisorEntry.negotiation.communicationLane, C.info],
376
- ]));
377
- for (const action of supervisorEntry.recovery.slice(0, 2)) {
378
- detailRows.push(buildGuidanceLine(width, action.command, action.reason, C));
379
- }
380
- }
381
- const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selectedContract.runnerId);
382
- if (recentArtifact) {
383
- detailRows.push(buildPanelLine(width, [
384
- [' Recent artifact: ', C.label],
385
- [recentArtifact.id, C.ok],
386
- [' Status: ', C.label],
387
- [recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
388
- ]));
389
- }
390
- }
391
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Remote posture', postureLines, C) };
392
- const detailSection: PanelWorkspaceSection = {
393
- lines: buildDetailBlock(width, selected ? 'Selected connection' : 'Selected contract', detailRows, C),
394
- };
395
- const browseTitle = viewingConnections ? 'Active Connections' : 'Registered Remote Worker Contracts';
396
- const rawBrowseLines: Line[] = viewingConnections
397
- ? activeConnections.map((connection, absolute) => {
398
- return buildPanelListRow(width, [
399
- { text: connection.agentId.padEnd(18), fg: C.value },
400
- { text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
401
- { text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
402
- { text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
403
- { text: ` ${connection.label}`.slice(0, Math.max(0, width - 56)), fg: C.dim },
404
- ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
405
- })
406
- : [
407
- ...contracts.map((contract, absolute) => {
408
- return buildPanelListRow(width, [
409
- { text: contract.runnerId.padEnd(18), fg: C.value },
410
- { text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
411
- { text: ` ${contract.template}`.slice(0, Math.max(0, width - 42)), fg: C.dim },
412
- ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
413
- }),
414
- buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),
415
- ];
416
- const resolvedBrowseSection = resolvePrimaryScrollableSection(width, height, {
417
- intro,
418
- footerLines,
419
- palette: C,
420
- beforeSections: [postureSection],
421
- section: {
422
- title: browseTitle,
423
- scrollableLines: rawBrowseLines,
424
- selectedIndex: this.selectedIndex,
425
- scrollOffset: this.scrollOffset,
426
- guardRows: 1,
427
- minRows: 4,
428
- appendWindowSummary: viewingConnections ? { dimColor: C.dim } : undefined,
429
- },
430
- afterSections: [detailSection],
431
- });
432
- this.scrollOffset = resolvedBrowseSection.scrollOffset;
433
-
434
- const sections: PanelWorkspaceSection[] = [
435
- postureSection,
436
- resolvedBrowseSection.section,
437
- detailSection,
438
- ];
439
- const lines = buildPanelWorkspace(width, height, {
440
- title: 'Remote Work Review',
441
- intro,
442
- sections,
443
- footerLines,
444
- palette: C,
445
- });
446
- while (lines.length < height) lines.push(createEmptyLine(width));
447
- return lines.slice(0, height);
448
- }
449
- }
@@ -1,178 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import type { UiReadModel, UiRoutesSnapshot } from '../runtime/ui-read-models.ts';
5
- import { truncateDisplay } from '../utils/terminal-width.ts';
6
- import {
7
- buildEmptyState,
8
- buildGuidanceLine,
9
- buildKeyValueLine,
10
- buildPanelLine,
11
- buildPanelWorkspace,
12
- buildStatusPill,
13
- DEFAULT_PANEL_PALETTE,
14
- type PanelPalette,
15
- } from './polish.ts';
16
-
17
- const C = {
18
- ...DEFAULT_PANEL_PALETTE,
19
- header: '#94a3b8',
20
- headerBg: '#1e293b',
21
- ok: '#22c55e',
22
- warn: '#eab308',
23
- error: '#ef4444',
24
- info: '#38bdf8',
25
- selectBg: '#0f172a',
26
- } as const;
27
-
28
- function formatTime(value?: number): string {
29
- if (!value) return 'n/a';
30
- return new Date(value).toLocaleString();
31
- }
32
-
33
- type RouteBinding = UiRoutesSnapshot['bindings'][number];
34
-
35
- export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
36
- private readonly readModel?: UiReadModel<UiRoutesSnapshot>;
37
- private readonly unsub: (() => void) | null;
38
-
39
- public constructor(readModel?: UiReadModel<UiRoutesSnapshot>) {
40
- super('routes', 'Routes', 'R', 'monitoring');
41
- this.showSelectionGutter = true; // I5: non-color selection affordance
42
- this.readModel = readModel;
43
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
44
- }
45
-
46
- public override onDestroy(): void {
47
- this.unsub?.();
48
- }
49
-
50
- protected override getPalette(): PanelPalette {
51
- return C;
52
- }
53
-
54
- protected getItems(): readonly RouteBinding[] {
55
- if (!this.readModel) return [];
56
- return this.readModel.getSnapshot().bindings;
57
- }
58
-
59
- protected renderItem(binding: RouteBinding, _index: number, selected: boolean, width: number): Line {
60
- const bg = selected ? C.selectBg : undefined;
61
- return buildPanelLine(width, [
62
- [' ', C.label, bg],
63
- [binding.surfaceKind.padEnd(9), C.info, bg],
64
- [` ${truncateDisplay(binding.title ?? binding.externalId, 22).padEnd(22)}`, C.value, bg],
65
- ...buildStatusPill(binding.sessionId ? 'good' : 'warn', ` ${truncateDisplay(binding.sessionId ?? binding.runId ?? 'unbound', 18).padEnd(18)}`, { bg }),
66
- [` ${truncateDisplay(formatTime(binding.lastSeenAt), Math.max(0, width - 54))}`, C.dim, bg],
67
- ]);
68
- }
69
-
70
- protected override getEmptyStateMessage(): string {
71
- return ' No route bindings recorded.';
72
- }
73
-
74
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
75
- return [
76
- { command: '/schedule list', summary: 'run jobs and triggers that create route bindings' },
77
- { command: '/communication', summary: 'inspect routed communication once a surface is active' },
78
- ];
79
- }
80
-
81
- public render(width: number, height: number): Line[] {
82
- const intro = 'External route bindings that preserve thread, session, and reply context across Slack, Discord, ntfy, webhook, web, and TUI surfaces.';
83
-
84
- if (!this.readModel) {
85
- const workspace = buildPanelWorkspace(width, height, {
86
- title: 'Route Bindings',
87
- intro,
88
- sections: [{
89
- lines: buildEmptyState(
90
- width,
91
- ' Runtime store not wired.',
92
- 'This panel needs the shared runtime store to inspect omnichannel route bindings.',
93
- [{ command: '/communication', summary: 'review communication posture while route state is unavailable' }],
94
- C,
95
- ),
96
- }],
97
- palette: C,
98
- });
99
- while (workspace.length < height) workspace.push(createEmptyLine(width));
100
- return workspace;
101
- }
102
-
103
- const snapshot = this.readModel.getSnapshot();
104
- const bindings = this.getItems();
105
- const surfaceEntries = Object.entries(snapshot.bindingIdsBySurface)
106
- .filter(([, ids]) => ids.length > 0)
107
- .sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]));
108
-
109
- const headerLines: Line[] = [
110
- buildKeyValueLine(width, [
111
- { label: 'bindings', value: String(snapshot.totalBindings), valueColor: snapshot.totalBindings > 0 ? C.info : C.dim },
112
- { label: 'active', value: String(snapshot.activeBindingIds.length), valueColor: snapshot.activeBindingIds.length > 0 ? C.ok : C.dim },
113
- { label: 'resolved', value: String(snapshot.totalResolved), valueColor: snapshot.totalResolved > 0 ? C.ok : C.dim },
114
- { label: 'failures', value: String(snapshot.totalFailures), valueColor: snapshot.totalFailures > 0 ? C.error : C.dim },
115
- ], C),
116
- buildGuidanceLine(width, '/communication', 'inspect routed message flow and delivery behavior across bound surfaces', C),
117
- ];
118
-
119
- if (bindings.length === 0) {
120
- return this.renderList(width, height, {
121
- title: 'Route Bindings',
122
- header: headerLines,
123
- emptyMessage: ' No route bindings recorded.',
124
- });
125
- }
126
-
127
- this.clampSelection();
128
- const selected = bindings[this.selectedIndex]!;
129
-
130
- const footerLines: Line[] = [
131
- buildPanelLine(width, [
132
- [' Binding: ', C.label],
133
- [selected.id, C.value],
134
- [' Surface: ', C.label],
135
- [selected.surfaceKind, C.info],
136
- ]),
137
- buildPanelLine(width, [
138
- [' External: ', C.label],
139
- [truncateDisplay(selected.externalId, 28), C.value],
140
- [' Kind: ', C.label],
141
- [selected.kind, C.dim],
142
- ]),
143
- buildPanelLine(width, [
144
- [' Session: ', C.label],
145
- [selected.sessionId ?? 'n/a', C.value],
146
- [' Run: ', C.label],
147
- [selected.runId ?? 'n/a', C.dim],
148
- ]),
149
- buildPanelLine(width, [
150
- [' Channel: ', C.label],
151
- [selected.channelId ?? 'n/a', C.dim],
152
- [' Thread: ', C.label],
153
- [selected.threadId ?? 'n/a', C.dim],
154
- ]),
155
- buildPanelLine(width, [
156
- [' Last seen: ', C.label],
157
- [formatTime(selected.lastSeenAt), C.dim],
158
- ]),
159
- ];
160
-
161
- if (surfaceEntries.length > 0) {
162
- footerLines.push(
163
- ...surfaceEntries.slice(0, 6).map(([surface, ids]) => buildPanelLine(width, [
164
- [' ', C.label],
165
- [surface.padEnd(10), C.info],
166
- [` ${String(ids.length)} binding(s)`, C.value],
167
- ])),
168
- );
169
- }
170
- footerLines.push(buildPanelLine(width, [[' Up/Down move through route bindings', C.dim]]));
171
-
172
- return this.renderList(width, height, {
173
- title: 'Route Bindings',
174
- header: headerLines,
175
- footer: footerLines,
176
- });
177
- }
178
- }