@pellux/goodvibes-agent 0.1.70 → 0.1.71

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 (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +42 -1
  3. package/src/agent/skill-discovery.ts +119 -0
  4. package/src/input/commands/delegation-runtime.ts +0 -8
  5. package/src/input/commands/experience-runtime.ts +0 -177
  6. package/src/input/commands/guidance-runtime.ts +0 -69
  7. package/src/input/commands/local-runtime.ts +1 -57
  8. package/src/input/commands/local-setup-review.ts +1 -1
  9. package/src/input/commands/operator-runtime.ts +1 -145
  10. package/src/input/commands/platform-access-runtime.ts +2 -195
  11. package/src/input/commands/product-runtime.ts +0 -116
  12. package/src/input/commands/security-runtime.ts +88 -0
  13. package/src/input/commands/session-content.ts +0 -97
  14. package/src/input/commands/shell-core.ts +0 -13
  15. package/src/input/commands.ts +2 -95
  16. package/src/panels/builtin/operations.ts +3 -184
  17. package/src/panels/index.ts +0 -11
  18. package/src/version.ts +1 -1
  19. package/src/input/commands/branch-runtime.ts +0 -72
  20. package/src/input/commands/control-room-runtime.ts +0 -234
  21. package/src/input/commands/discovery-runtime.ts +0 -61
  22. package/src/input/commands/hooks-runtime.ts +0 -207
  23. package/src/input/commands/incident-runtime.ts +0 -106
  24. package/src/input/commands/integration-runtime.ts +0 -437
  25. package/src/input/commands/local-setup.ts +0 -288
  26. package/src/input/commands/managed-runtime.ts +0 -240
  27. package/src/input/commands/marketplace-runtime.ts +0 -305
  28. package/src/input/commands/memory-product-runtime.ts +0 -148
  29. package/src/input/commands/operator-panel-runtime.ts +0 -146
  30. package/src/input/commands/platform-services-runtime.ts +0 -271
  31. package/src/input/commands/profile-sync-runtime.ts +0 -110
  32. package/src/input/commands/provider.ts +0 -363
  33. package/src/input/commands/remote-runtime-pool.ts +0 -89
  34. package/src/input/commands/remote-runtime-setup.ts +0 -226
  35. package/src/input/commands/remote-runtime.ts +0 -432
  36. package/src/input/commands/replay-runtime.ts +0 -25
  37. package/src/input/commands/services-runtime.ts +0 -220
  38. package/src/input/commands/settings-sync-runtime.ts +0 -197
  39. package/src/input/commands/share-runtime.ts +0 -127
  40. package/src/input/commands/skills-runtime.ts +0 -226
  41. package/src/input/commands/teleport-runtime.ts +0 -68
  42. package/src/panels/cockpit-panel.ts +0 -183
  43. package/src/panels/communication-panel.ts +0 -153
  44. package/src/panels/control-plane-panel.ts +0 -211
  45. package/src/panels/forensics-panel.ts +0 -364
  46. package/src/panels/hooks-panel.ts +0 -239
  47. package/src/panels/incident-review-panel.ts +0 -197
  48. package/src/panels/marketplace-panel.ts +0 -212
  49. package/src/panels/ops-control-panel.ts +0 -150
  50. package/src/panels/ops-strategy-panel.ts +0 -235
  51. package/src/panels/orchestration-panel.ts +0 -272
  52. package/src/panels/plugins-panel.ts +0 -178
  53. package/src/panels/remote-panel.ts +0 -449
  54. package/src/panels/routes-panel.ts +0 -178
  55. package/src/panels/services-panel.ts +0 -231
  56. package/src/panels/settings-sync-panel.ts +0 -120
  57. package/src/panels/skills-panel.ts +0 -431
  58. package/src/panels/watchers-panel.ts +0 -193
  59. package/src/verification/live-verifier.ts +0 -588
  60. 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
- }