@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,197 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import type { ForensicsRegistry } from '@/runtime/index.ts';
3
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import {
5
- buildBodyText,
6
- buildEmptyState,
7
- buildGuidanceLine,
8
- buildKeyValueLine,
9
- buildPanelLine,
10
- buildPanelWorkspace,
11
- buildStatusPill,
12
- DEFAULT_PANEL_PALETTE,
13
- type PanelPalette,
14
- } from './polish.ts';
15
- import type { FailureReport } from '@/runtime/index.ts';
16
-
17
- const C = {
18
- ...DEFAULT_PANEL_PALETTE,
19
- header: '#cbd5e1',
20
- headerBg: '#0f172a',
21
- warn: '#f59e0b',
22
- bad: '#ef4444',
23
- selectBg: '#111827',
24
- } as const;
25
-
26
- function classificationColor(value: string): string {
27
- switch (value) {
28
- case 'cancelled':
29
- return C.dim;
30
- case 'max_tokens':
31
- case 'unknown':
32
- return C.warn;
33
- default:
34
- return C.bad;
35
- }
36
- }
37
-
38
- export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
39
- private readonly registry?: ForensicsRegistry;
40
- private readonly unsub: (() => void) | null;
41
-
42
- public constructor(registry?: ForensicsRegistry) {
43
- super('incident', 'Incident Review', 'N', 'monitoring');
44
- this.showSelectionGutter = true; // I5: non-color selection affordance
45
- this.registry = registry;
46
- this.unsub = registry ? registry.subscribe(() => this.markDirty()) : null;
47
- }
48
-
49
- public override onDestroy(): void {
50
- this.unsub?.();
51
- }
52
-
53
- protected override getPalette(): PanelPalette {
54
- return C;
55
- }
56
-
57
- protected getItems(): readonly FailureReport[] {
58
- return this.registry?.getAll() ?? [];
59
- }
60
-
61
- protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
62
- const bg = selected ? C.selectBg : undefined;
63
- return buildPanelLine(width, [
64
- [' ', C.label, bg],
65
- [report.id.slice(0, 8).padEnd(9), C.dim, bg],
66
- [report.classification.padEnd(20), classificationColor(report.classification), bg],
67
- [report.summary.slice(0, Math.max(0, width - 31)), C.value, bg],
68
- ]);
69
- }
70
-
71
- protected override getEmptyStateMessage(): string {
72
- return ' No incidents recorded yet.';
73
- }
74
-
75
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
76
- return [
77
- { command: '/incident latest', summary: 'inspect the latest report once one exists' },
78
- { command: '/recall capture incident latest', summary: 'promote incident evidence into project knowledge' },
79
- ];
80
- }
81
-
82
- public render(width: number, height: number): Line[] {
83
- const intro = 'Failure bundles, replay mismatches, permission fallout, and exportable review evidence.';
84
-
85
- if (!this.registry) {
86
- return buildPanelWorkspace(width, height, {
87
- title: 'Incident Review Workspace',
88
- intro,
89
- sections: [{
90
- lines: buildEmptyState(
91
- width,
92
- ' Forensics registry not wired into this panel yet.',
93
- 'Incident review needs the live forensics registry so it can inspect failure bundles, replay mismatches, and causal evidence.',
94
- [
95
- { command: '/incident latest', summary: 'inspect the latest incident from the command surface' },
96
- { command: '/security', summary: 'open the broader trust and incident posture control room' },
97
- ],
98
- C,
99
- ),
100
- }],
101
- palette: C,
102
- });
103
- }
104
-
105
- const reports = this.getItems();
106
- if (reports.length === 0) {
107
- return this.renderList(width, height, { title: 'Incident Review Workspace' });
108
- }
109
-
110
- this.clampSelection();
111
- const selected = reports[this.selectedIndex]!;
112
- const bundle = this.registry.buildBundle(selected.id);
113
-
114
- const headerLines: Line[] = [
115
- buildKeyValueLine(width, [
116
- { label: 'incidents', value: String(reports.length), valueColor: C.value },
117
- { label: 'selected', value: `${this.selectedIndex + 1}/${reports.length}`, valueColor: C.info },
118
- { label: 'classification', value: selected.classification, valueColor: classificationColor(selected.classification) },
119
- ], C),
120
- buildPanelLine(width, [[' Up/Down move Home/End jump selected incident drives the action rail below', C.dim]]),
121
- ];
122
-
123
- const footerLines: Line[] = [];
124
- if (bundle) {
125
- footerLines.push(buildKeyValueLine(width, [
126
- { label: 'id', value: selected.id, valueColor: C.dim },
127
- { label: 'trace', value: selected.traceId, valueColor: C.dim },
128
- ], C));
129
- footerLines.push(...buildBodyText(width, `Root cause: ${bundle.evidence.rootCause ?? 'n/a'}`, C, C.value));
130
- footerLines.push(buildKeyValueLine(width, [
131
- { label: 'Permissions denied', value: String(bundle.evidence.deniedPermissionCount), valueColor: bundle.evidence.deniedPermissionCount > 0 ? C.warn : C.dim },
132
- { label: 'Budget breaches', value: String(bundle.evidence.budgetBreachCount), valueColor: bundle.evidence.budgetBreachCount > 0 ? C.warn : C.dim },
133
- { label: 'Replay mismatches', value: String(bundle.replay.mismatchCount), valueColor: bundle.replay.mismatchCount > 0 ? C.bad : C.dim },
134
- ], C));
135
- footerLines.push(buildPanelLine(width, [
136
- [' Related IDs: ', C.label],
137
- [`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`.slice(0, Math.max(0, width - 14)), C.info],
138
- ]));
139
- if (bundle.evidence.slowPhases.length > 0) {
140
- footerLines.push(buildPanelLine(width, [
141
- [' Slow phases: ', C.label],
142
- ...buildStatusPill('warn', bundle.evidence.slowPhases.join(', ').slice(0, Math.max(0, width - 15))),
143
- ]));
144
- }
145
- const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
146
- if (rootCause) {
147
- footerLines.push(buildPanelLine(width, [
148
- [' Root event: ', C.label],
149
- [`${rootCause.sourceEventType} - ${rootCause.description}`.slice(0, Math.max(0, width - 14)), C.dim],
150
- ]));
151
- }
152
- const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
153
- if (denied) {
154
- footerLines.push(buildPanelLine(width, [
155
- [' Permission: ', C.label],
156
- [`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`.slice(0, Math.max(0, width - 14)), C.warn],
157
- ]));
158
- }
159
- if (bundle.replay.relatedMismatches.length > 0) {
160
- const mismatch = bundle.replay.relatedMismatches[0]!;
161
- const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
162
- .filter(([, count]) => count > 0)
163
- .slice(0, 3)
164
- .map(([domain, count]) => `${domain}:${count}`)
165
- .join(', ');
166
- const replayDetail = ownerBreakdown.length > 0
167
- ? `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description} Replay owners: ${ownerBreakdown}`
168
- : `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
169
- footerLines.push(buildPanelLine(width, [
170
- [' ', C.label],
171
- ...buildStatusPill('bad', replayDetail.slice(0, Math.max(0, width - 2))),
172
- ]));
173
- } else {
174
- const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
175
- .filter(([, count]) => count > 0)
176
- .slice(0, 3)
177
- .map(([domain, count]) => `${domain}:${count}`)
178
- .join(', ');
179
- if (ownerBreakdown.length > 0) {
180
- footerLines.push(buildPanelLine(width, [
181
- [' Replay owners: ', C.label],
182
- [ownerBreakdown.slice(0, Math.max(0, width - 17)), C.info],
183
- ]));
184
- }
185
- }
186
- }
187
- footerLines.push(buildPanelLine(width, [[' Action Rail', C.label]]));
188
- footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} <path> --yes /incident capture ${selected.id} --yes`, C.info]]));
189
- footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
190
-
191
- return this.renderList(width, height, {
192
- title: 'Incident Review Workspace',
193
- header: headerLines,
194
- footer: footerLines,
195
- });
196
- }
197
- }
@@ -1,212 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
- import {
4
- buildEmptyState,
5
- buildGuidanceLine,
6
- buildKeyValueLine,
7
- buildPanelLine,
8
- buildPanelWorkspace,
9
- DEFAULT_PANEL_PALETTE,
10
- type PanelWorkspaceSection,
11
- } from './polish.ts';
12
- import {
13
- type EcosystemCatalogPathOptions,
14
- listInstalledEcosystemEntries,
15
- loadEcosystemCatalog,
16
- reviewEcosystemCatalogEntry,
17
- type EcosystemCatalogEntry,
18
- type EcosystemEntryKind,
19
- } from '@/runtime/index.ts';
20
- import type { UiMarketplaceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
21
-
22
- const C = {
23
- ...DEFAULT_PANEL_PALETTE,
24
- header: '#e2e8f0',
25
- headerBg: '#1f2937',
26
- } as const;
27
-
28
- type MarketplaceRow = {
29
- kind: EcosystemEntryKind;
30
- entry: EcosystemCatalogEntry;
31
- installed: boolean;
32
- };
33
-
34
- function statusColor(installed: boolean): string {
35
- return installed ? C.good : C.dim;
36
- }
37
-
38
- export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
39
- private rows: MarketplaceRow[] = [];
40
- private readonly unsub: (() => void) | null;
41
-
42
- public constructor(
43
- private readonly readModel?: UiReadModel<UiMarketplaceSnapshot>,
44
- private readonly ecosystemPaths?: EcosystemCatalogPathOptions,
45
- ) {
46
- super('marketplace', 'Marketplace', 'M', 'monitoring');
47
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
48
- }
49
-
50
- public override onDestroy(): void {
51
- this.unsub?.();
52
- }
53
-
54
- public override onActivate(): void {
55
- super.onActivate();
56
- this.refresh();
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // ScrollableListPanel implementation
61
- // ---------------------------------------------------------------------------
62
-
63
- protected getItems(): readonly MarketplaceRow[] {
64
- return this.rows;
65
- }
66
-
67
- protected renderItem(row: MarketplaceRow, index: number, selected: boolean, width: number): Line {
68
- const bg = selected ? C.selectBg : undefined;
69
- const provenance = row.entry.provenance ?? 'local';
70
- return buildPanelLine(width, [
71
- [' ', C.label, bg],
72
- [row.kind.padEnd(11), C.info, bg],
73
- [row.entry.name.slice(0, 20).padEnd(20), C.value, bg],
74
- [` ${provenance.slice(0, 16).padEnd(16)}`, provenance === 'local' ? C.dim : C.info, bg],
75
- [` ${(row.installed ? 'INSTALLED' : 'CURATED').padEnd(9)} `, statusColor(row.installed), bg],
76
- [` ${row.entry.version ?? 'n/a'}`, C.dim, bg],
77
- ]);
78
- }
79
-
80
- protected override getPalette() { return C; }
81
- protected override getEmptyStateMessage() {
82
- return this.ecosystemPaths
83
- ? ' No curated marketplace entries found yet.'
84
- : ' Marketplace catalog paths are not wired into this panel yet.';
85
- }
86
- protected override getEmptyStateActions() {
87
- return [
88
- { command: '/marketplace bundle import <path>', summary: 'import a curated marketplace bundle' },
89
- { command: '/marketplace catalog review', summary: 'inspect the current local catalog posture' },
90
- { command: '/marketplace publish <kind> <path>', summary: 'publish local ecosystem entries back into the curated catalog' },
91
- ];
92
- }
93
-
94
- private refresh(): void {
95
- if (!this.ecosystemPaths) {
96
- this.rows = [];
97
- this.clampSelection();
98
- return;
99
- }
100
- try {
101
- const installedPlugins = new Set(listInstalledEcosystemEntries('plugin', this.ecosystemPaths).map((receipt) => receipt.entry.id));
102
- const installedSkills = new Set(listInstalledEcosystemEntries('skill', this.ecosystemPaths).map((receipt) => receipt.entry.id));
103
- const installedHookPacks = new Set(listInstalledEcosystemEntries('hook-pack', this.ecosystemPaths).map((receipt) => receipt.entry.id));
104
- const installedPolicyPacks = new Set(listInstalledEcosystemEntries('policy-pack', this.ecosystemPaths).map((receipt) => receipt.entry.id));
105
- const rows: MarketplaceRow[] = [
106
- ...loadEcosystemCatalog('plugin', this.ecosystemPaths).map((entry) => ({ kind: 'plugin' as const, entry, installed: installedPlugins.has(entry.id) })),
107
- ...loadEcosystemCatalog('skill', this.ecosystemPaths).map((entry) => ({ kind: 'skill' as const, entry, installed: installedSkills.has(entry.id) })),
108
- ...loadEcosystemCatalog('hook-pack', this.ecosystemPaths).map((entry) => ({ kind: 'hook-pack' as const, entry, installed: installedHookPacks.has(entry.id) })),
109
- ...loadEcosystemCatalog('policy-pack', this.ecosystemPaths).map((entry) => ({ kind: 'policy-pack' as const, entry, installed: installedPolicyPacks.has(entry.id) })),
110
- ];
111
- this.rows = rows.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
112
- this.clampSelection();
113
- // I2: clear any previous catalog load error on successful refresh
114
- this.clearError();
115
- } catch (e) {
116
- // I2: surface catalog load failure
117
- this.setError(`Catalog load failed: ${e instanceof Error ? e.message : String(e)}`);
118
- }
119
- }
120
-
121
- public render(width: number, height: number): Line[] {
122
- this.clampSelection();
123
- this.refresh();
124
-
125
- const intro = 'Curated local-first ecosystem with provenance, compatibility, rollback history, and receipt-aware lifecycle review.';
126
- const installedCount = this.rows.filter((row) => row.installed).length;
127
- const snapshot = this.readModel?.getSnapshot();
128
- const recommendations = snapshot?.recommendations ?? [];
129
- const startupIssues = snapshot?.startupIssues ?? [];
130
-
131
- if (this.rows.length === 0) {
132
- return buildPanelWorkspace(width, height, {
133
- title: 'Marketplace Control Room',
134
- intro,
135
- sections: [{
136
- lines: buildEmptyState(
137
- width,
138
- this.ecosystemPaths ? ' No curated marketplace entries found yet.' : ' Marketplace catalog paths are not wired into this panel yet.',
139
- this.ecosystemPaths
140
- ? 'The marketplace is ready, but no plugin, skill, hook-pack, or policy-pack catalogs are available in this workspace.'
141
- : 'The shell needs explicit marketplace catalog roots before this panel can inspect curated plugin, skill, hook-pack, or policy-pack entries.',
142
- [
143
- { command: '/marketplace bundle import <path>', summary: 'import a curated marketplace bundle' },
144
- { command: '/marketplace catalog review', summary: 'inspect the current local catalog posture' },
145
- { command: '/marketplace publish <kind> <path>', summary: 'publish local ecosystem entries back into the curated catalog' },
146
- ],
147
- C,
148
- ),
149
- }],
150
- palette: C,
151
- });
152
- }
153
-
154
- const postureLines = [
155
- buildKeyValueLine(width, [
156
- { label: 'curated', value: String(this.rows.length), valueColor: C.value },
157
- { label: 'installed', value: String(installedCount), valueColor: installedCount > 0 ? C.good : C.dim },
158
- { label: 'plugins', value: String(this.rows.filter((row) => row.kind === 'plugin').length), valueColor: C.info },
159
- { label: 'skills', value: String(this.rows.filter((row) => row.kind === 'skill').length), valueColor: C.info },
160
- { label: 'hooks', value: String(this.rows.filter((row) => row.kind === 'hook-pack').length), valueColor: C.info },
161
- { label: 'policies', value: String(this.rows.filter((row) => row.kind === 'policy-pack').length), valueColor: C.info },
162
- ], C),
163
- buildGuidanceLine(width, '/marketplace open', 'browse curated entries and inspect compatibility, provenance, and receipts', C),
164
- ];
165
-
166
- const recommendationLines = recommendations.length > 0
167
- ? recommendations.slice(0, 4).map((recommendation) => buildPanelLine(width, [
168
- [' ', C.label],
169
- [`${recommendation.kind} ${recommendation.entry.id}`.slice(0, 28).padEnd(28), C.info],
170
- [` ${recommendation.title}`.slice(0, Math.max(0, width - 31)), C.dim],
171
- ]))
172
- : [buildPanelLine(width, [[' No contextual marketplace recommendations right now.', C.dim]])];
173
-
174
- const startupIssueLines = startupIssues.length > 0
175
- ? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [issue.slice(0, Math.max(0, width - 2)), C.warn]]))
176
- : [buildPanelLine(width, [[' No startup or lifecycle issues are currently pushing marketplace repair recommendations.', C.dim]])];
177
-
178
- const selectedRow = this.rows[this.selectedIndex];
179
- const selectedLines: Line[] = [];
180
- if (selectedRow) {
181
- const review = reviewEcosystemCatalogEntry(selectedRow.entry, this.ecosystemPaths!);
182
- selectedLines.push(buildPanelLine(width, [
183
- [' Provenance: ', C.label],
184
- [(selectedRow.entry.provenance ?? '(none)').slice(0, Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
185
- ]));
186
- selectedLines.push(buildPanelLine(width, [
187
- [' Source: ', C.label],
188
- [selectedRow.entry.source.slice(0, Math.max(0, width - 11)), C.value],
189
- ]));
190
- selectedLines.push(buildKeyValueLine(width, [
191
- { label: 'Compatibility', value: review.compatibility.status, valueColor: review.compatibility.status === 'supported' ? C.good : C.warn },
192
- { label: 'Risk', value: review.riskLevel, valueColor: review.riskLevel === 'low' ? C.good : C.warn },
193
- { label: 'State', value: selectedRow.installed ? 'installed' : 'curated', valueColor: statusColor(selectedRow.installed) },
194
- ], C));
195
- selectedLines.push(buildGuidanceLine(width, '/marketplace review <id>', 'inspect full compatibility and receipt detail for the selected entry', C));
196
- }
197
-
198
- const postureSection: PanelWorkspaceSection = { title: 'Marketplace posture', lines: postureLines };
199
- const startupIssuesSection: PanelWorkspaceSection = { title: 'Startup Issues', lines: startupIssueLines };
200
- const recommendationsSection: PanelWorkspaceSection = { title: 'Recommendations', lines: recommendationLines };
201
-
202
- return this.renderList(width, height, {
203
- title: 'Marketplace Control Room',
204
- header: [
205
- ...postureLines,
206
- ...startupIssueLines,
207
- ...recommendationLines,
208
- ],
209
- footer: selectedLines.length > 0 && height >= 20 ? selectedLines : [],
210
- });
211
- }
212
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * OpsControlPanel — operator intervention log panel.
3
- *
4
- * Renders the ops audit log sourced from the OpsPanel diagnostics subscriber.
5
- * Each entry shows: seq, timestamp, action, target, outcome, and optional note.
6
- *
7
- * Requires the `operator-control-plane` feature flag to be enabled.
8
- * Open via Ctrl+O keybind or `/ops view` command.
9
- */
10
- import type { Line } from '../types/grid.ts';
11
- import type { OpsEvent } from '@/runtime/index.ts';
12
- import type { UiEventFeed } from '../runtime/ui-events.ts';
13
- import type { OpsAuditEntry } from '../runtime/diagnostics/panels/ops.ts';
14
- import { OpsPanel } from '../runtime/diagnostics/panels/ops.ts';
15
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
16
- import {
17
- buildPanelLine,
18
- DEFAULT_PANEL_PALETTE,
19
- type PanelPalette,
20
- } from './polish.ts';
21
-
22
- // ── Colour palette ──────────────────────────────────────────────────────────
23
- const C = {
24
- ...DEFAULT_PANEL_PALETTE,
25
- header: '#94a3b8',
26
- headerBg: '#1e293b',
27
- success: '#22c55e',
28
- rejected: '#f97316',
29
- error: '#ef4444',
30
- dim: '#4b5563',
31
- label: '#64748b',
32
- value: '#cbd5e1',
33
- note: '#eab308',
34
- seq: '#475569',
35
- taskColor: '#22d3ee',
36
- agentColor: '#a78bfa',
37
- empty: '#334155',
38
- selectBg: '#0f172a',
39
- } as const;
40
-
41
- // ── Helpers ──────────────────────────────────────────────────────────────────
42
-
43
- function fmtTime(ts: number): string {
44
- const d = new Date(ts);
45
- const hh = String(d.getHours()).padStart(2, '0');
46
- const mm = String(d.getMinutes()).padStart(2, '0');
47
- const ss = String(d.getSeconds()).padStart(2, '0');
48
- return `${hh}:${mm}:${ss}`;
49
- }
50
-
51
- function outcomeColor(outcome: OpsAuditEntry['outcome']): string {
52
- switch (outcome) {
53
- case 'success': return C.success;
54
- case 'rejected': return C.rejected;
55
- case 'error': return C.error;
56
- }
57
- }
58
-
59
- function outcomeLabel(outcome: OpsAuditEntry['outcome']): string {
60
- switch (outcome) {
61
- case 'success': return 'OK ';
62
- case 'rejected': return 'REJECT';
63
- case 'error': return 'ERR ';
64
- }
65
- }
66
-
67
- function targetColor(kind: OpsAuditEntry['targetKind']): string {
68
- return kind === 'task' ? C.taskColor : C.agentColor;
69
- }
70
-
71
- // ── OpsControlPanel ──────────────────────────────────────────────────────────
72
-
73
- export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
74
- private readonly _opsPanel: OpsPanel;
75
- private _unsub: (() => void) | null = null;
76
-
77
- public constructor(eventFeed: UiEventFeed<OpsEvent>) {
78
- super('ops-control', 'Ops Control', 'Q', 'agent');
79
- this.showSelectionGutter = true; // I5: non-color selection affordance
80
- this._opsPanel = new OpsPanel(eventFeed);
81
- this._unsub = this._opsPanel.subscribe(() => this.markDirty());
82
- }
83
-
84
- public override onActivate(): void {
85
- super.onActivate();
86
- this.selectedIndex = 0;
87
- }
88
-
89
- public override onDestroy(): void {
90
- if (this._unsub) {
91
- this._unsub();
92
- this._unsub = null;
93
- }
94
- this._opsPanel.dispose();
95
- }
96
-
97
- protected override getPalette(): PanelPalette {
98
- return C;
99
- }
100
-
101
- protected getItems(): readonly OpsAuditEntry[] {
102
- // Return reversed so newest entries appear at top
103
- return [...this._opsPanel.getSnapshot()].reverse();
104
- }
105
-
106
- protected renderItem(entry: OpsAuditEntry, _index: number, _selected: boolean, width: number): Line {
107
- const seqStr = String(entry.seq).padStart(4, ' ');
108
- const timeStr = fmtTime(entry.ts);
109
- const action = entry.action.slice(0, 15).padEnd(15, ' ');
110
- const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
111
- // Truncation is intentional: TUI column width limits target ID display to 14 chars
112
- const shortId = entry.targetId.slice(-10);
113
- const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
114
- const outLabel = outcomeLabel(entry.outcome);
115
- const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
116
-
117
- const segs: Array<[string, string, string?]> = [
118
- [` ${seqStr} `, C.seq],
119
- [`${timeStr} `, C.dim],
120
- [`${action} `, C.value],
121
- [`${target} `, targetColor(entry.targetKind)],
122
- [outLabel, outcomeColor(entry.outcome)],
123
- ];
124
- if (noteRaw) segs.push([` ${noteRaw}`, C.note]);
125
- return buildPanelLine(width, segs);
126
- }
127
-
128
- protected override getEmptyStateMessage(): string {
129
- return ' No operator interventions recorded.';
130
- }
131
-
132
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
133
- return [{ command: '/cockpit', summary: 'open the cockpit and review runtime interventions' }];
134
- }
135
-
136
- public render(width: number, height: number): Line[] {
137
- const headerLines: Line[] = [
138
- buildPanelLine(width, [[' SEQ TIME ACTION TARGET OUT NOTE', C.label]]),
139
- ];
140
- const footerLines: Line[] = [
141
- buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]]),
142
- ];
143
-
144
- return this.renderList(width, height, {
145
- title: 'Operator Interventions',
146
- header: headerLines,
147
- footer: footerLines,
148
- });
149
- }
150
- }