@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.
- package/CHANGELOG.md +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { MODAL_TONES } from './modal-theme.ts';
|
|
2
|
+
import type {
|
|
3
|
+
DivergenceDashboardSnapshot,
|
|
4
|
+
DivergenceStats,
|
|
5
|
+
PermissionAuditEntry,
|
|
6
|
+
PolicyBundleVersion,
|
|
7
|
+
PolicyDiffResult,
|
|
8
|
+
PolicyLintFinding,
|
|
9
|
+
PolicyPreflightReview,
|
|
10
|
+
PolicySimulationSummary,
|
|
11
|
+
} from '@/runtime/index.ts';
|
|
12
|
+
import type { ModalSectionStyle } from '../../renderer/modal-factory.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ConfigModalActionContext,
|
|
15
|
+
ConfigModalRow,
|
|
16
|
+
ConfigModalSurface,
|
|
17
|
+
ConfigModalView,
|
|
18
|
+
} from '../../input/config-modal-types.ts';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Policy → config-modal surface (W6.1 group-B port). Governance CONTENT view,
|
|
22
|
+
// not a selectable list — current/candidate bundle state, diff, divergence
|
|
23
|
+
// gate, history, permission audit, lint findings, preflight review, and
|
|
24
|
+
// simulation samples render as non-selectable rows (the host windows/scrolls
|
|
25
|
+
// them). EVERY dispatchable key in the original panel (s/f/l/p/b) already
|
|
26
|
+
// routed to `/policy <sub>` via executeCommand — none were in-panel mutations;
|
|
27
|
+
// the panel's promote/rollback confirm step is dropped per charter (never fold
|
|
28
|
+
// a confirm into a modal) and the command dispatches directly. Determinism:
|
|
29
|
+
// timestamps are formatted (fmtTime/fmtRate), never read from the clock.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
interface PolicyModalSnapshot {
|
|
33
|
+
readonly current: PolicyBundleVersion | null;
|
|
34
|
+
readonly candidate: PolicyBundleVersion | null;
|
|
35
|
+
readonly history: readonly PolicyBundleVersion[];
|
|
36
|
+
readonly diff: PolicyDiffResult | null;
|
|
37
|
+
readonly divergence: DivergenceDashboardSnapshot | null;
|
|
38
|
+
readonly recentPermissionAudit: readonly PermissionAuditEntry[];
|
|
39
|
+
readonly lintFindings: readonly PolicyLintFinding[];
|
|
40
|
+
readonly lastSimulationSummary: PolicySimulationSummary | null;
|
|
41
|
+
readonly lastPreflightReview: PolicyPreflightReview | null;
|
|
42
|
+
readonly capturedAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PolicyModalDeps {
|
|
46
|
+
readonly policyRuntimeState: { getSnapshot(): PolicyModalSnapshot };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const BAD: ModalSectionStyle = { fg: MODAL_TONES.bad };
|
|
50
|
+
const WARN: ModalSectionStyle = { fg: MODAL_TONES.warn };
|
|
51
|
+
const GOOD: ModalSectionStyle = { fg: MODAL_TONES.good };
|
|
52
|
+
const DIM: ModalSectionStyle = { dim: true };
|
|
53
|
+
const MAX_ROWS = 5;
|
|
54
|
+
|
|
55
|
+
function fmtTime(value: string | undefined): string {
|
|
56
|
+
if (!value) return 'n/a';
|
|
57
|
+
return value.replace('T', ' ').slice(0, 19);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fmtRate(value: number | undefined): string {
|
|
61
|
+
if (value === undefined) return 'n/a';
|
|
62
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function gateColor(status: string | undefined): ModalSectionStyle | undefined {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case 'allowed': return GOOD;
|
|
68
|
+
case 'blocked': return BAD;
|
|
69
|
+
case 'no_data': return WARN;
|
|
70
|
+
default: return DIM;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ruleLine(rule: { id: string; type: string; effect: string }): string {
|
|
75
|
+
return `${rule.id} (${rule.type}, effect=${rule.effect})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class PolicyModalSurface implements ConfigModalSurface {
|
|
79
|
+
readonly name = 'policy-modal';
|
|
80
|
+
readonly title = 'Policy And Governance';
|
|
81
|
+
|
|
82
|
+
constructor(private readonly deps: PolicyModalDeps) {}
|
|
83
|
+
|
|
84
|
+
readonly actions = [
|
|
85
|
+
{ key: 's', id: 'simulate', label: 'simulate' },
|
|
86
|
+
{ key: 'f', id: 'preflight', label: 'preflight' },
|
|
87
|
+
{ key: 'l', id: 'lint', label: 'lint' },
|
|
88
|
+
{ key: 'p', id: 'promote', label: 'promote' },
|
|
89
|
+
{ key: 'b', id: 'rollback', label: 'rollback' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
buildView(): ConfigModalView {
|
|
93
|
+
const snapshot = this.deps.policyRuntimeState.getSnapshot();
|
|
94
|
+
const preflight = snapshot.lastPreflightReview;
|
|
95
|
+
const divergence = snapshot.divergence;
|
|
96
|
+
const lintCount = snapshot.lintFindings.length;
|
|
97
|
+
const preflightStatus = preflight ? preflight.status : 'none';
|
|
98
|
+
const gateStatus = divergence?.gate.status ?? 'n/a';
|
|
99
|
+
|
|
100
|
+
const header = [`bundles ${snapshot.current ? 1 : 0}+${snapshot.candidate ? 1 : 0}c preflight ${preflightStatus.toUpperCase()} gate ${gateStatus} lint ${lintCount}`];
|
|
101
|
+
|
|
102
|
+
const rows: ConfigModalRow[] = [];
|
|
103
|
+
let n = 0;
|
|
104
|
+
const push = (label: string, style?: ModalSectionStyle): void => { rows.push({ id: `row:${n++}`, label, selectable: false, ...(style ? { style } : {}) }); };
|
|
105
|
+
|
|
106
|
+
const nothingRecorded = !snapshot.current && !snapshot.candidate && !divergence
|
|
107
|
+
&& snapshot.history.length === 0 && snapshot.recentPermissionAudit.length === 0
|
|
108
|
+
&& snapshot.lintFindings.length === 0 && !snapshot.lastSimulationSummary && !preflight;
|
|
109
|
+
|
|
110
|
+
if (nothingRecorded) {
|
|
111
|
+
push('No policy bundles loaded.');
|
|
112
|
+
push('Bundle: none active, none candidate. Gate: n/a.', DIM);
|
|
113
|
+
push('Get started');
|
|
114
|
+
push('/policy load — load a policy bundle to begin governance review', DIM);
|
|
115
|
+
return { title: 'Policy And Governance', tabs: [{ id: 'governance', label: 'Governance', header, rows, emptyText: '' }] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (snapshot.current) {
|
|
119
|
+
push('Current');
|
|
120
|
+
push(`Bundle ${snapshot.current.bundle.bundleId} State ${snapshot.current.state}`);
|
|
121
|
+
push(`Loaded ${fmtTime(snapshot.current.loadedAt)} Active ${fmtTime(snapshot.current.activatedAt)}`, DIM);
|
|
122
|
+
}
|
|
123
|
+
if (snapshot.candidate) {
|
|
124
|
+
push('Candidate');
|
|
125
|
+
push(`Bundle ${snapshot.candidate.bundle.bundleId} State ${snapshot.candidate.state}`);
|
|
126
|
+
push(`Loaded ${fmtTime(snapshot.candidate.loadedAt)} Rules ${snapshot.candidate.rules.length}`, DIM);
|
|
127
|
+
}
|
|
128
|
+
if (snapshot.diff) {
|
|
129
|
+
const diff = snapshot.diff;
|
|
130
|
+
push('Diff');
|
|
131
|
+
push(`${diff.fromBundleId} -> ${diff.toBundleId} changes ${diff.totalChanges}`);
|
|
132
|
+
if (diff.added.length > 0) { push('Added', GOOD); for (const r of diff.added.slice(0, MAX_ROWS)) push(` + ${ruleLine(r)}`, DIM); }
|
|
133
|
+
if (diff.removed.length > 0) { push('Removed', BAD); for (const r of diff.removed.slice(0, MAX_ROWS)) push(` - ${ruleLine(r)}`, DIM); }
|
|
134
|
+
if (diff.changed.length > 0) { push('Changed', WARN); for (const c of diff.changed.slice(0, MAX_ROWS)) push(` ~ ${c.ruleId}`, DIM); }
|
|
135
|
+
}
|
|
136
|
+
if (divergence) {
|
|
137
|
+
push('Governance Gate');
|
|
138
|
+
push(`Mode ${divergence.mode} Gate ${divergence.gate.status} Divergence ${fmtRate(divergence.gate.divergenceRate ?? divergence.report.overall.divergenceRate)}`, gateColor(divergence.gate.status));
|
|
139
|
+
push(`Evaluations ${divergence.report.overall.totalEvaluations} Trend points ${divergence.trend.length}`, DIM);
|
|
140
|
+
const prefixEntries = Object.entries(divergence.report.byCommandPrefix);
|
|
141
|
+
if (prefixEntries.length > 0) {
|
|
142
|
+
push('Divergence by command prefix', DIM);
|
|
143
|
+
for (const [prefix, stats] of prefixEntries.slice(0, MAX_ROWS)) push(` ${prefix} ${fmtRate(stats.divergenceRate)} ${stats.total}/${stats.totalEvaluations}`, stats.divergenceRate > 0 ? WARN : undefined);
|
|
144
|
+
}
|
|
145
|
+
const classEntries = Object.entries(divergence.report.byToolClass).filter((e): e is [string, DivergenceStats] => e[1] !== undefined);
|
|
146
|
+
if (classEntries.length > 0) {
|
|
147
|
+
push('Divergence by tool class', DIM);
|
|
148
|
+
for (const [cls, stats] of classEntries.slice(0, MAX_ROWS)) push(` ${cls} ${fmtRate(stats.divergenceRate)} ${stats.total}/${stats.totalEvaluations}`, stats.divergenceRate > 0 ? WARN : undefined);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (snapshot.history.length > 0) {
|
|
152
|
+
push('History');
|
|
153
|
+
for (const v of snapshot.history.slice(0, MAX_ROWS)) push(`${v.bundle.bundleId} ${v.state} ${fmtTime(v.activatedAt ?? v.loadedAt)}`, DIM);
|
|
154
|
+
}
|
|
155
|
+
if (snapshot.recentPermissionAudit.length > 0) {
|
|
156
|
+
push('Permission Audit');
|
|
157
|
+
for (const entry of snapshot.recentPermissionAudit.slice(0, MAX_ROWS)) {
|
|
158
|
+
const outcome = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
159
|
+
push(`${entry.tool} ${entry.riskLevel.toUpperCase()} ${outcome}`, entry.approved === undefined ? WARN : entry.approved ? undefined : BAD);
|
|
160
|
+
push(` ${entry.summary}`, DIM);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (snapshot.lintFindings.length > 0) {
|
|
164
|
+
push('Policy Lint');
|
|
165
|
+
for (const f of snapshot.lintFindings.slice(0, MAX_ROWS)) push(`${f.severity.toUpperCase()} ${f.message}`, f.severity === 'error' ? BAD : f.severity === 'warn' ? WARN : undefined);
|
|
166
|
+
}
|
|
167
|
+
if (preflight) {
|
|
168
|
+
push('Preflight Review');
|
|
169
|
+
push(`Status ${preflight.status.toUpperCase()} Issues ${preflight.issueCount} Generated ${fmtTime(preflight.generatedAt)}`, preflight.status === 'pass' ? GOOD : preflight.status === 'warn' ? WARN : BAD);
|
|
170
|
+
push(preflight.summary, DIM);
|
|
171
|
+
for (const issue of preflight.issues.slice(0, 4)) push(`${issue.severity.toUpperCase()} ${issue.message}`, issue.severity === 'error' ? BAD : issue.severity === 'warn' ? WARN : undefined);
|
|
172
|
+
}
|
|
173
|
+
if (snapshot.lastSimulationSummary) {
|
|
174
|
+
const sim = snapshot.lastSimulationSummary;
|
|
175
|
+
push('Simulation Samples');
|
|
176
|
+
push(`Mode ${sim.mode} Diverged ${sim.divergentScenarios}/${sim.totalScenarios} Allowed(actual/sim) ${sim.allowedByActual}/${sim.allowedBySimulated}`, sim.divergentScenarios > 0 ? WARN : GOOD);
|
|
177
|
+
for (const result of sim.results.slice(0, 4)) push(`${result.scenario.label} ${(result.authoritativeDecision.allowed ? 'allow' : 'deny').toUpperCase()} ${result.diverged ? (result.divergenceType ?? 'diverged') : 'aligned'}`, result.diverged ? WARN : (result.authoritativeDecision.allowed ? GOOD : BAD));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { title: 'Policy And Governance', tabs: [{ id: 'governance', label: 'Governance', header, rows }] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
184
|
+
// "Rollout" in the WO brief maps to the registry's actual subcommand —
|
|
185
|
+
// there is no separate `/policy rollout`.
|
|
186
|
+
const sub = id === 'simulate' ? 'simulate' : id === 'preflight' ? 'preflight' : id === 'lint' ? 'lint' : id === 'promote' ? 'promote' : id === 'rollback' ? 'rollback' : null;
|
|
187
|
+
if (!sub) return;
|
|
188
|
+
void ctx.executeCommand?.('policy', [sub]);
|
|
189
|
+
ctx.setStatus(`Dispatched /policy ${sub}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createPolicyModalSurface(deps: PolicyModalDeps): ConfigModalSurface {
|
|
194
|
+
return new PolicyModalSurface(deps);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Deterministic golden fixture. A fixed `PolicyModalSnapshot` literal covering
|
|
199
|
+
* every rendered branch in one shot: an active current bundle, a simulating
|
|
200
|
+
* candidate, a 3-rule diff, a blocked divergence gate with one command-prefix
|
|
201
|
+
* and one tool-class breakdown row, one rolled-back history entry, one denied
|
|
202
|
+
* permission-audit entry, one lint warning, one preflight review with one issue,
|
|
203
|
+
* and one diverged simulation sample. No Date.now().
|
|
204
|
+
*/
|
|
205
|
+
export function policyModalGoldenSurface(): ConfigModalSurface {
|
|
206
|
+
const currentBundle: PolicyBundleVersion = {
|
|
207
|
+
bundle: { bundleId: 'bundle-current-1', issuedAt: '2023-11-01T00:00:00.000Z', payload: { version: 1, rules: [] }, issuer: 'ops' },
|
|
208
|
+
provenance: { policyBundleId: 'bundle-current-1', signatureStatus: 'unsigned', provenanceSource: 'local-file', issuedAt: '2023-11-01T00:00:00.000Z', issuer: 'ops' },
|
|
209
|
+
rules: [{ id: 'rule-base-1', type: 'prefix', origin: 'managed', effect: 'allow', toolPattern: '*', commandPrefixes: [] }],
|
|
210
|
+
state: 'active', loadedAt: '2023-11-01T00:00:00.000Z', activatedAt: '2023-11-01T00:05:00.000Z',
|
|
211
|
+
};
|
|
212
|
+
const candidateBundle: PolicyBundleVersion = {
|
|
213
|
+
bundle: { bundleId: 'bundle-candidate-1', issuedAt: '2023-11-10T00:00:00.000Z', payload: { version: 1, rules: [] }, issuer: 'ops' },
|
|
214
|
+
provenance: { policyBundleId: 'bundle-candidate-1', signatureStatus: 'unsigned', provenanceSource: 'local-file', issuedAt: '2023-11-10T00:00:00.000Z', issuer: 'ops' },
|
|
215
|
+
rules: [
|
|
216
|
+
{ id: 'rule-base-1', type: 'prefix', origin: 'managed', effect: 'allow', toolPattern: '*', commandPrefixes: [] },
|
|
217
|
+
{ id: 'rule-added-1', type: 'network-scope', origin: 'user', effect: 'deny', toolPattern: 'Fetch', hostPatterns: ['*'] },
|
|
218
|
+
],
|
|
219
|
+
state: 'simulating', loadedAt: '2023-11-10T00:00:00.000Z',
|
|
220
|
+
};
|
|
221
|
+
const historyBundle: PolicyBundleVersion = {
|
|
222
|
+
bundle: { bundleId: 'bundle-prev-1', issuedAt: '2023-10-01T00:00:00.000Z', payload: { version: 1, rules: [] } },
|
|
223
|
+
provenance: { policyBundleId: 'bundle-prev-1', signatureStatus: 'unsigned', provenanceSource: 'local-file', issuedAt: '2023-10-01T00:00:00.000Z' },
|
|
224
|
+
rules: [], state: 'rolled-back', loadedAt: '2023-10-01T00:00:00.000Z', activatedAt: '2023-10-01T00:05:00.000Z', rolledBackAt: '2023-11-01T00:00:00.000Z',
|
|
225
|
+
};
|
|
226
|
+
const removedRule = { id: 'rule-removed-1', type: 'path-scope' as const, origin: 'user' as const, effect: 'deny' as const, toolPattern: '*', pathPatterns: ['/tmp/**'] };
|
|
227
|
+
const addedRule = { id: 'rule-added-1', type: 'network-scope' as const, origin: 'user' as const, effect: 'deny' as const, toolPattern: 'Fetch', hostPatterns: ['*'] };
|
|
228
|
+
const changedFrom = { id: 'rule-base-1', type: 'prefix' as const, origin: 'managed' as const, effect: 'allow' as const, toolPattern: '*', commandPrefixes: [] };
|
|
229
|
+
const changedTo = { id: 'rule-base-1', type: 'prefix' as const, origin: 'managed' as const, effect: 'deny' as const, toolPattern: '*', commandPrefixes: [] };
|
|
230
|
+
const stats = (divergenceRate: number): DivergenceStats => ({ total: 1, byType: { 'allow-vs-deny': 1, 'deny-vs-allow': 0, 'reason-mismatch': 0 }, divergenceRate, totalEvaluations: 20 });
|
|
231
|
+
const decision = (allowed: boolean, reason: 'DEFAULT_ALLOW' | 'RULE_DENY_USER') => ({ allowed, reason, sourceLayer: 'policy' as const, toolName: 'Fetch', args: {}, timestamp: 1700000000000, evaluationTrace: [] });
|
|
232
|
+
|
|
233
|
+
const snapshot: PolicyModalSnapshot = {
|
|
234
|
+
current: currentBundle, candidate: candidateBundle, history: [historyBundle],
|
|
235
|
+
diff: { fromBundleId: 'bundle-current-1', toBundleId: 'bundle-candidate-1', removed: [removedRule], added: [addedRule], changed: [{ ruleId: 'rule-base-1', from: changedFrom, to: changedTo }], unchanged: [], totalChanges: 3 },
|
|
236
|
+
divergence: {
|
|
237
|
+
report: { overall: stats(0.1), byToolClass: { network: stats(0.1) }, byCommandPrefix: { 'fetch ': stats(0.1) }, byMode: {}, records: [] },
|
|
238
|
+
mode: 'warn-on-divergence',
|
|
239
|
+
gate: { status: 'blocked', divergenceRate: 0.1, threshold: 0.05, totalEvaluations: 20, message: 'divergence rate 10.0% exceeds 5.0% threshold' },
|
|
240
|
+
trend: [{ ts: 1700000000000, divergenceRate: 0.1, totalEvaluations: 20, totalDivergences: 2, gatePassing: false }],
|
|
241
|
+
capturedAt: 1700000000000,
|
|
242
|
+
},
|
|
243
|
+
recentPermissionAudit: [{ callId: 'call-1', tool: 'Bash', category: 'exec', approved: false, riskLevel: 'high', classification: 'destructive', summary: 'blocked rm -rf outside sandbox root', reasons: ['path escape'], requestedAt: 1700000000000, decidedAt: 1700000001000 }],
|
|
244
|
+
lintFindings: [{ severity: 'warn', ruleId: 'rule-added-1', message: 'toolPattern "Fetch" combined with a wildcard host pattern is broad; consider narrowing.' }],
|
|
245
|
+
lastSimulationSummary: {
|
|
246
|
+
simulatedAt: '2023-11-10T00:00:00.000Z', mode: 'warn-on-divergence', totalScenarios: 4, divergentScenarios: 1, allowedByActual: 3, allowedBySimulated: 2,
|
|
247
|
+
results: [{ scenario: { id: 'scenario-1', label: 'fetch external API', toolName: 'Fetch', args: {} }, actualDecision: decision(true, 'DEFAULT_ALLOW'), simulatedDecision: decision(false, 'RULE_DENY_USER'), authoritativeDecision: decision(true, 'DEFAULT_ALLOW'), diverged: true, divergenceType: 'allow-vs-deny' }],
|
|
248
|
+
},
|
|
249
|
+
lastPreflightReview: {
|
|
250
|
+
generatedAt: '2023-11-10T00:10:00.000Z', status: 'warn', summary: '1 candidate rule broadens network access beyond the current policy.', issueCount: 1,
|
|
251
|
+
issues: [{ severity: 'warn', source: 'policy', message: 'candidate rule rule-added-1 denies all outbound network access', detail: 'verify this is intentional before promotion' }],
|
|
252
|
+
},
|
|
253
|
+
capturedAt: '2023-11-10T00:15:00.000Z',
|
|
254
|
+
};
|
|
255
|
+
return createPolicyModalSurface({ policyRuntimeState: { getSnapshot: () => snapshot } });
|
|
256
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ConfigModalActionContext, ConfigModalRow, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
|
|
2
|
+
import type { UiReadModel, UiProvidersSnapshot } from '../../runtime/ui-read-models.ts';
|
|
3
|
+
import type { ProviderRuntimeSnapshot } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
|
+
import { toneStyle, statusGlyph, pad, postureLine, kv } from './modal-surface-helpers.ts';
|
|
5
|
+
|
|
6
|
+
/** The slice of ProviderRuntimeInspectionQuery this surface consumes. */
|
|
7
|
+
export interface ProviderRuntimeInspect {
|
|
8
|
+
listProviderIds(): readonly string[];
|
|
9
|
+
inspectAll(): Promise<readonly ProviderRuntimeSnapshot[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Provider-health config-modal surface (migrated from the `provider-health`
|
|
14
|
+
* panel — the charter's live-modal exemplar; also the target of the `providers`
|
|
15
|
+
* and `accounts` redirects). providerRuntime.inspectAll() is async, so its
|
|
16
|
+
* result is cached and refreshed on open, on `r`, and on a 3s live tick (the
|
|
17
|
+
* panel's display-tick cadence); buildView reads the cache synchronously so
|
|
18
|
+
* live status/model-count values update in place with a stable layout.
|
|
19
|
+
*
|
|
20
|
+
* Fidelity boundary (stated divergence): the panel's per-request latency
|
|
21
|
+
* timelines, auth-route table, and 8 repair-domain posture blocks are driven by
|
|
22
|
+
* an event-fed tracker + account-posture snapshots; those deep views remain on
|
|
23
|
+
* the `/health` and `/accounts` commands (which print them in full). The modal
|
|
24
|
+
* ports the live provider status list + the panel's one dispatch action
|
|
25
|
+
* (Enter → `/accounts repair <provider>`), across a Health and an Accounts tab.
|
|
26
|
+
*/
|
|
27
|
+
class ProviderHealthModalSurface implements ConfigModalSurface {
|
|
28
|
+
readonly name = 'providers-modal';
|
|
29
|
+
readonly title = 'Providers';
|
|
30
|
+
private requestRender: () => void = () => {};
|
|
31
|
+
private cache = new Map<string, ProviderRuntimeSnapshot>();
|
|
32
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
33
|
+
private unsub: (() => void) | null = null;
|
|
34
|
+
private loaded = false;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly providerRuntime: ProviderRuntimeInspect,
|
|
38
|
+
private readonly providersReadModel?: UiReadModel<UiProvidersSnapshot>,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
readonly actions = [
|
|
42
|
+
{ key: 'enter', id: 'repair', label: 'repair' },
|
|
43
|
+
{ key: 'r', id: 'refresh', label: 'refresh posture' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
onOpen(requestRender: () => void): void {
|
|
47
|
+
this.requestRender = requestRender;
|
|
48
|
+
void this.reinspect();
|
|
49
|
+
if (this.providersReadModel && !this.unsub) this.unsub = this.providersReadModel.subscribe(() => this.requestRender());
|
|
50
|
+
if (this.timer === null) this.timer = setInterval(() => { void this.reinspect(); }, 3_000);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onClose(): void {
|
|
54
|
+
if (this.timer !== null) { clearInterval(this.timer); this.timer = null; }
|
|
55
|
+
this.unsub?.();
|
|
56
|
+
this.unsub = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private providerIds(): string[] {
|
|
60
|
+
const ids = new Set<string>([
|
|
61
|
+
...(this.providersReadModel?.getSnapshot().providerIds ?? []),
|
|
62
|
+
...this.providerRuntime.listProviderIds(),
|
|
63
|
+
...this.cache.keys(),
|
|
64
|
+
]);
|
|
65
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
buildView(): ConfigModalView {
|
|
69
|
+
const ids = this.providerIds();
|
|
70
|
+
const active = ids.filter((id) => this.cache.get(id)?.active).length;
|
|
71
|
+
const header = [postureLine([
|
|
72
|
+
kv('providers', ids.length),
|
|
73
|
+
kv('active', active),
|
|
74
|
+
kv('inspected', this.cache.size),
|
|
75
|
+
])];
|
|
76
|
+
|
|
77
|
+
const rowFor = (id: string): ConfigModalRow => {
|
|
78
|
+
const snap = this.cache.get(id);
|
|
79
|
+
const isActive = Boolean(snap?.active);
|
|
80
|
+
return {
|
|
81
|
+
id: `provider:${id}`,
|
|
82
|
+
label: `${statusGlyph(isActive ? 'good' : 'dim')} ${pad(id, 18)} ${pad(isActive ? 'ACTIVE' : 'idle', 8)} models=${snap?.modelCount ?? '—'}`,
|
|
83
|
+
style: toneStyle(isActive ? 'good' : 'dim'),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const rows = ids.map(rowFor);
|
|
88
|
+
const emptyText = ids.length === 0
|
|
89
|
+
? (this.loaded ? 'No providers registered. Try /provider or /subscription.' : 'Inspecting providers…')
|
|
90
|
+
: undefined;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
title: 'Providers',
|
|
94
|
+
tabs: [
|
|
95
|
+
{ id: 'health', label: 'Health', header, rows, emptyText, hints: ['r refresh posture', '/health for latency & routes'] },
|
|
96
|
+
{ id: 'accounts', label: 'Accounts', header, rows: ids.map(rowFor), emptyText, hints: ['Enter repair', '/accounts routes <p> for detail'] },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
102
|
+
if (id === 'refresh') {
|
|
103
|
+
void this.reinspect();
|
|
104
|
+
ctx.setStatus('Refreshing provider posture…');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (id === 'repair') {
|
|
108
|
+
const provider = ctx.row?.id.startsWith('provider:') ? ctx.row.id.slice('provider:'.length) : null;
|
|
109
|
+
if (!provider) return;
|
|
110
|
+
void ctx.executeCommand?.('accounts', ['repair', provider]);
|
|
111
|
+
ctx.setStatus(`Dispatched /accounts repair ${provider} (see transcript).`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async reinspect(): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
const snapshots = await this.providerRuntime.inspectAll();
|
|
118
|
+
const next = new Map<string, ProviderRuntimeSnapshot>();
|
|
119
|
+
for (const s of snapshots) next.set(s.providerId, s);
|
|
120
|
+
this.cache = next;
|
|
121
|
+
} catch {
|
|
122
|
+
// Leave the last-known cache in place; a transient inspect failure should
|
|
123
|
+
// not blank the live table (honest degraded behaviour).
|
|
124
|
+
} finally {
|
|
125
|
+
this.loaded = true;
|
|
126
|
+
this.requestRender();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createProviderHealthModalSurface(
|
|
132
|
+
providerRuntime: ProviderRuntimeInspect,
|
|
133
|
+
providersReadModel?: UiReadModel<UiProvidersSnapshot>,
|
|
134
|
+
): ConfigModalSurface {
|
|
135
|
+
return new ProviderHealthModalSurface(providerRuntime, providersReadModel);
|
|
136
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ConfigModalActionContext, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
|
|
2
|
+
import type { UiReadModel, UiRemoteSnapshot } from '../../runtime/ui-read-models.ts';
|
|
3
|
+
import { toneStyle, statusGlyph, pad, postureLine, kv, type Tone } from './modal-surface-helpers.ts';
|
|
4
|
+
|
|
5
|
+
function stateTone(state: string): Tone {
|
|
6
|
+
switch (state) {
|
|
7
|
+
case 'connected':
|
|
8
|
+
case 'syncing':
|
|
9
|
+
return 'good';
|
|
10
|
+
case 'degraded':
|
|
11
|
+
case 'reconnecting':
|
|
12
|
+
case 'authenticating':
|
|
13
|
+
case 'initializing':
|
|
14
|
+
return 'warn';
|
|
15
|
+
case 'terminal_failure':
|
|
16
|
+
return 'bad';
|
|
17
|
+
default:
|
|
18
|
+
return 'dim';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Remote config-modal surface (migrated from the `remote` panel). The read-model
|
|
24
|
+
* snapshot is synchronous; a subscribe() keeps live transport/heartbeat values
|
|
25
|
+
* refreshing between key presses. The panel's `Tab` connections/contracts browse
|
|
26
|
+
* toggle becomes two real tabs. `r`/Enter dispatch `/remote recover` for the
|
|
27
|
+
* selected runner — the panel's only corrective keybinding; the richer
|
|
28
|
+
* `/remote` command set (show/supervisor/cancel/dispatch/artifact/…) stays on
|
|
29
|
+
* the command front-door.
|
|
30
|
+
*/
|
|
31
|
+
class RemoteModalSurface implements ConfigModalSurface {
|
|
32
|
+
readonly name = 'remote-modal';
|
|
33
|
+
readonly title = 'Remote';
|
|
34
|
+
private requestRender: () => void = () => {};
|
|
35
|
+
private unsub: (() => void) | null = null;
|
|
36
|
+
|
|
37
|
+
constructor(private readonly readModel?: UiReadModel<UiRemoteSnapshot>) {}
|
|
38
|
+
|
|
39
|
+
readonly actions = [
|
|
40
|
+
{ key: 'r', id: 'recover', label: 'recover' },
|
|
41
|
+
{ key: 'enter', id: 'recover', label: 'recover' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
onOpen(requestRender: () => void): void {
|
|
45
|
+
this.requestRender = requestRender;
|
|
46
|
+
if (this.readModel && !this.unsub) this.unsub = this.readModel.subscribe(() => this.requestRender());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onClose(): void {
|
|
50
|
+
this.unsub?.();
|
|
51
|
+
this.unsub = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
buildView(): ConfigModalView {
|
|
55
|
+
const snapshot = this.readModel?.getSnapshot();
|
|
56
|
+
if (!snapshot) {
|
|
57
|
+
return {
|
|
58
|
+
title: 'Remote',
|
|
59
|
+
degraded: 'Runtime store not wired into this runtime.',
|
|
60
|
+
tabs: [{ id: 'connections', label: 'Connections', rows: [], emptyText: 'Try /remote setup.' }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const { daemon, acp, contracts, artifacts, supervisor, distributed } = snapshot;
|
|
64
|
+
const peersConnected = distributed.peers.filter((p) => p.status === 'connected').length;
|
|
65
|
+
const header = [
|
|
66
|
+
postureLine([
|
|
67
|
+
`daemon ${daemon.transportState.toUpperCase()}`,
|
|
68
|
+
kv('running', daemon.isRunning ? 'yes' : 'no'),
|
|
69
|
+
kv('reconnects', daemon.reconnectAttempts),
|
|
70
|
+
kv('jobs', daemon.runningJobCount),
|
|
71
|
+
]),
|
|
72
|
+
postureLine([
|
|
73
|
+
`acp ${acp.transportState.toUpperCase()}`,
|
|
74
|
+
kv('conns', acp.activeConnections.length),
|
|
75
|
+
kv('contracts', contracts.length),
|
|
76
|
+
kv('artifacts', artifacts.length),
|
|
77
|
+
kv('sessions', supervisor.sessions.length),
|
|
78
|
+
kv('peers', `${peersConnected}/${distributed.peers.length}`),
|
|
79
|
+
]),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const connectionRows = acp.activeConnections.map((c) => ({
|
|
83
|
+
id: `conn:${c.agentId}`,
|
|
84
|
+
label: `${statusGlyph(stateTone(c.transportState))} ${pad(c.agentId, 20)} ${pad(c.transportState.toUpperCase(), 14)} msgs=${c.messageCount} errs=${c.errorCount} ${c.label ?? ''}`,
|
|
85
|
+
style: toneStyle(stateTone(c.transportState)),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const contractRows = contracts.map((ct) => ({
|
|
89
|
+
id: `contract:${ct.runnerId}`,
|
|
90
|
+
label: `${statusGlyph(stateTone(ct.transport.state))} ${pad(ct.runnerId, 20)} ${pad(ct.transport.state.toUpperCase(), 14)} ${ct.template ?? ''}`,
|
|
91
|
+
style: toneStyle(stateTone(ct.transport.state)),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
title: 'Remote',
|
|
96
|
+
tabs: [
|
|
97
|
+
{ id: 'connections', label: 'Connections', header, rows: connectionRows, emptyText: 'No active ACP or remote subagent connections.', hints: ['r recover'] },
|
|
98
|
+
{ id: 'contracts', label: 'Contracts', header, rows: contractRows, emptyText: 'No registered remote runner contracts.', hints: ['r recover'] },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
104
|
+
if (id !== 'recover') return;
|
|
105
|
+
const rowId = ctx.row?.id ?? '';
|
|
106
|
+
const runner = rowId.includes(':') ? rowId.slice(rowId.indexOf(':') + 1) : '';
|
|
107
|
+
void ctx.executeCommand?.('remote', runner ? ['recover', runner] : ['recover']);
|
|
108
|
+
ctx.setStatus(runner ? `Recovering ${runner}…` : 'Recovering…');
|
|
109
|
+
this.requestRender();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createRemoteModalSurface(readModel?: UiReadModel<UiRemoteSnapshot>): ConfigModalSurface {
|
|
114
|
+
return new RemoteModalSurface(readModel);
|
|
115
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ConfigModalAction, ConfigModalActionContext, ConfigModalRow, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
|
|
2
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
|
+
import { buildSandboxReview, listSandboxProfiles } from '@/runtime/index.ts';
|
|
4
|
+
import type { SandboxProfile, SandboxSession, SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
5
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
|
+
import { toneStyle, statusGlyph, pad, postureLine, kv, type Tone } from './modal-surface-helpers.ts';
|
|
7
|
+
|
|
8
|
+
/** SandboxSessionRegistry has no event subscription — poll at the same 3s
|
|
9
|
+
* cadence the retired panel used (POLL_INTERVAL_MS in sandbox-panel.ts). */
|
|
10
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
11
|
+
|
|
12
|
+
function sessionTone(state: SandboxSession['state']): Tone {
|
|
13
|
+
switch (state) {
|
|
14
|
+
case 'running': return 'good';
|
|
15
|
+
case 'failed': return 'bad';
|
|
16
|
+
case 'planned': return 'warn';
|
|
17
|
+
case 'stopped': return 'dim';
|
|
18
|
+
default: return 'dim';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function profileIdOf(row: ConfigModalRow | null): SandboxProfile['id'] | null {
|
|
23
|
+
return row?.id.startsWith('profile:') ? (row.id.slice('profile:'.length) as SandboxProfile['id']) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sessionIdOf(row: ConfigModalRow | null): string | null {
|
|
27
|
+
return row?.id.startsWith('session:') ? row.id.slice('session:'.length) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sandbox config-modal surface (migrated from the `sandbox` panel). All reads
|
|
32
|
+
* (buildSandboxReview, listSandboxProfiles, sessions.list) are synchronous, so
|
|
33
|
+
* buildView reads them live each tick. The panel's single profile+session
|
|
34
|
+
* selectable list becomes two real tabs. s/x/e call the SandboxSessionRegistry
|
|
35
|
+
* directly (these were never slash commands) — start/execute are wrapped so a
|
|
36
|
+
* rejection surfaces via ctx.setStatus instead of an unhandled rejection,
|
|
37
|
+
* matching the panel's try/catch + setError behaviour.
|
|
38
|
+
*/
|
|
39
|
+
class SandboxModalSurface implements ConfigModalSurface {
|
|
40
|
+
readonly name = 'sandbox-modal';
|
|
41
|
+
readonly title = 'Sandbox';
|
|
42
|
+
private requestRender: () => void;
|
|
43
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly config: ConfigManager,
|
|
47
|
+
private readonly sessions: SandboxSessionRegistry,
|
|
48
|
+
requestRender: () => void = () => {},
|
|
49
|
+
) {
|
|
50
|
+
this.requestRender = requestRender;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
readonly actions: ConfigModalAction[] = [
|
|
54
|
+
{ key: 's', id: 'start', label: 'start', enabledFor: (row, tabId) => tabId === 'profiles' && profileIdOf(row) !== null },
|
|
55
|
+
{ key: 'x', id: 'stop', label: 'stop', confirm: true, enabledFor: (row, tabId) => tabId === 'sessions' && sessionIdOf(row) !== null },
|
|
56
|
+
{ key: 'e', id: 'execute', label: 'execute probe', enabledFor: (row, tabId) => tabId === 'sessions' && sessionIdOf(row) !== null },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
onOpen(requestRender: () => void): void {
|
|
60
|
+
this.requestRender = requestRender;
|
|
61
|
+
if (this.timer === null) this.timer = setInterval(() => this.requestRender(), POLL_INTERVAL_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onClose(): void {
|
|
65
|
+
if (this.timer !== null) { clearInterval(this.timer); this.timer = null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
buildView(): ConfigModalView {
|
|
69
|
+
try {
|
|
70
|
+
const review = buildSandboxReview(this.config);
|
|
71
|
+
const profiles = listSandboxProfiles(this.config);
|
|
72
|
+
const sessions = this.sessions.list();
|
|
73
|
+
|
|
74
|
+
const header = [postureLine([
|
|
75
|
+
kv('platform', review.host.platform),
|
|
76
|
+
kv('backend', review.config.vmBackend),
|
|
77
|
+
kv('sessions', sessions.length),
|
|
78
|
+
kv('ready', review.host.secureSandboxReady ? 'yes' : 'no'),
|
|
79
|
+
])];
|
|
80
|
+
|
|
81
|
+
const profileRows: ConfigModalRow[] = profiles.map((profile) => ({
|
|
82
|
+
id: `profile:${profile.id}`,
|
|
83
|
+
label: `${pad(profile.id, 14)} ${pad(profile.kind, 6)} ${pad(profile.isolation, 10)} vm=${profile.requiresVm ? 'yes' : 'no'}`,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const sessionRows: ConfigModalRow[] = sessions.map((session) => ({
|
|
87
|
+
id: `session:${session.id}`,
|
|
88
|
+
label: `${statusGlyph(sessionTone(session.state))} ${pad(session.id, 20)} ${pad(session.state.toUpperCase(), 9)} backend=${pad(session.resolvedBackend ?? session.backend, 6)} runs=${session.executionCount ?? 0}`,
|
|
89
|
+
style: toneStyle(sessionTone(session.state)),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
title: 'Sandbox',
|
|
94
|
+
tabs: [
|
|
95
|
+
{ id: 'profiles', label: 'Profiles', header, rows: profileRows, emptyText: 'No sandbox profiles available.', hints: ['s start'] },
|
|
96
|
+
{ id: 'sessions', label: 'Sessions', header, rows: sessionRows, emptyText: 'No active sandbox sessions.', hints: ['x stop', 'e execute probe'] },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
title: 'Sandbox',
|
|
102
|
+
degraded: `Sandbox posture unavailable: ${summarizeError(error)}`,
|
|
103
|
+
tabs: [
|
|
104
|
+
{ id: 'profiles', label: 'Profiles', rows: [] },
|
|
105
|
+
{ id: 'sessions', label: 'Sessions', rows: [] },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
112
|
+
switch (id) {
|
|
113
|
+
case 'start': {
|
|
114
|
+
const profileId = profileIdOf(ctx.row);
|
|
115
|
+
if (!profileId) return;
|
|
116
|
+
void this.sessions.start(profileId, undefined, this.config)
|
|
117
|
+
.catch((error: unknown) => ctx.setStatus(summarizeError(error)))
|
|
118
|
+
.finally(() => this.requestRender());
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case 'stop': {
|
|
122
|
+
const sessionId = sessionIdOf(ctx.row);
|
|
123
|
+
if (!sessionId) return;
|
|
124
|
+
this.sessions.stop(sessionId);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'execute': {
|
|
128
|
+
const sessionId = sessionIdOf(ctx.row);
|
|
129
|
+
if (!sessionId) return;
|
|
130
|
+
try {
|
|
131
|
+
this.sessions.execute(sessionId, process.execPath, ['-e', "console.log('sandbox modal probe ok')"], this.config, { timeoutMs: 5_000 });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
ctx.setStatus(summarizeError(error));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.requestRender();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createSandboxModalSurface(
|
|
145
|
+
config: ConfigManager,
|
|
146
|
+
sessions: SandboxSessionRegistry,
|
|
147
|
+
requestRender: () => void = () => {},
|
|
148
|
+
): ConfigModalSurface {
|
|
149
|
+
return new SandboxModalSurface(config, sessions, requestRender);
|
|
150
|
+
}
|