@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,217 @@
|
|
|
1
|
+
import { MODAL_TONES } from './modal-theme.ts';
|
|
2
|
+
import { infoRow } from './modal-surface-helpers.ts';
|
|
3
|
+
import type { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security';
|
|
4
|
+
import type { ModalSectionStyle } from '../../renderer/modal-factory.ts';
|
|
5
|
+
import type { UiReadModel, UiSecuritySnapshot } from '../../runtime/ui-read-models.ts';
|
|
6
|
+
import type {
|
|
7
|
+
ConfigModalActionContext,
|
|
8
|
+
ConfigModalRow,
|
|
9
|
+
ConfigModalSurface,
|
|
10
|
+
ConfigModalTab,
|
|
11
|
+
ConfigModalView,
|
|
12
|
+
} from '../../input/config-modal-types.ts';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Security → config-modal surface (W6.1 group-B port). Two tabs: 'Tokens' (the
|
|
16
|
+
// token scope/rotation audit list under the governance summary) and
|
|
17
|
+
// 'Governance' (policy preflight, MCP/plugin quarantine, latest incident, and
|
|
18
|
+
// the MCP attack-path review). Read-model-backed, so refresh() is a no-op —
|
|
19
|
+
// buildView reads getSnapshot() fresh every render. 'f' (preflight) routes to
|
|
20
|
+
// its `/policy preflight` command path; 'i' jumps to the incident surface
|
|
21
|
+
// (fleet) via the command path. Selection-blind port: the panel's
|
|
22
|
+
// selected-token scope/rotation detail is folded into each token row label.
|
|
23
|
+
// Determinism: absolute ISO timestamps (never Date.now()).
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface SecurityModalDeps {
|
|
27
|
+
readonly readModel: UiReadModel<UiSecuritySnapshot>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BAD: ModalSectionStyle = { fg: MODAL_TONES.bad };
|
|
31
|
+
const WARN: ModalSectionStyle = { fg: MODAL_TONES.warn };
|
|
32
|
+
const MAX_FINDINGS_SHOWN = 5;
|
|
33
|
+
|
|
34
|
+
function resultColor(result: TokenAuditResult): ModalSectionStyle | undefined {
|
|
35
|
+
if (result.blocked) return BAD;
|
|
36
|
+
if (result.scope.outcome === 'violation') return BAD;
|
|
37
|
+
if (result.rotation.outcome === 'overdue') return BAD;
|
|
38
|
+
if (result.rotation.outcome === 'warning') return WARN;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resultSummary(result: TokenAuditResult): string {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (result.scope.outcome === 'violation') parts.push(`scope:${result.scope.excessScopes.join(',')}`);
|
|
45
|
+
if (result.rotation.outcome === 'warning') parts.push('rotation warning');
|
|
46
|
+
if (result.rotation.outcome === 'overdue') parts.push('rotation overdue');
|
|
47
|
+
if (result.blocked) parts.push('blocked');
|
|
48
|
+
return parts.length > 0 ? parts.join(' | ') : 'in policy';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmtDue(msUntilDue: number): string {
|
|
52
|
+
const overdue = msUntilDue < 0;
|
|
53
|
+
const sec = Math.floor(Math.abs(msUntilDue) / 1000);
|
|
54
|
+
const unit = sec < 3600 ? `${Math.max(1, Math.floor(sec / 60))}m` : sec < 86400 ? `${Math.floor(sec / 3600)}h` : `${Math.floor(sec / 86400)}d`;
|
|
55
|
+
return overdue ? `overdue by ${unit}` : `in ${unit}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fmtIso(ts: number | null | undefined): string {
|
|
59
|
+
return ts === null || ts === undefined ? 'never' : new Date(ts).toISOString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findingColor(severity: string): ModalSectionStyle | undefined {
|
|
63
|
+
return severity === 'critical' || severity === 'high' ? BAD : severity === 'medium' ? WARN : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class SecurityModalSurface implements ConfigModalSurface {
|
|
67
|
+
readonly name = 'security-modal';
|
|
68
|
+
readonly title = 'Security Control Room';
|
|
69
|
+
private requestRender: () => void = () => {};
|
|
70
|
+
private unsub: (() => void) | null = null;
|
|
71
|
+
|
|
72
|
+
constructor(private readonly deps: SecurityModalDeps) {}
|
|
73
|
+
|
|
74
|
+
readonly actions = [
|
|
75
|
+
{ key: 'f', id: 'preflight', label: 'preflight' },
|
|
76
|
+
{ key: 'i', id: 'jumpToIncident', label: 'jump to incident', enabledFor: () => Boolean(this.deps.readModel.getSnapshot().latestIncident) },
|
|
77
|
+
{ key: 'r', id: 'refresh', label: 'refresh audit' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
onOpen(requestRender: () => void): void {
|
|
81
|
+
this.requestRender = requestRender;
|
|
82
|
+
if (!this.unsub) this.unsub = this.deps.readModel.subscribe(() => this.requestRender());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onClose(): void { this.unsub?.(); this.unsub = null; }
|
|
86
|
+
|
|
87
|
+
private governanceHeader(snapshot: UiSecuritySnapshot): string[] {
|
|
88
|
+
const audit = snapshot.audit;
|
|
89
|
+
const quarantinedMcp = snapshot.mcpServers.filter((s) => s.schemaFreshness === 'quarantined');
|
|
90
|
+
const elevatedMcp = snapshot.mcpServers.filter((s) => s.trustMode === 'allow-all');
|
|
91
|
+
return [
|
|
92
|
+
`mode ${audit.managed ? 'MANAGED' : 'ADVISORY'} tokens ${audit.totalTokens} blocked ${audit.blocked.length} scope violations ${audit.scopeViolations.length} overdue ${audit.rotationOverdue.length} warnings ${audit.rotationWarnings.length}`,
|
|
93
|
+
`preflight ${snapshot.policy.preflightStatus.toUpperCase()} issues ${snapshot.policy.preflightIssueCount} lint ${snapshot.policy.lintFindingCount} denied permissions ${snapshot.deniedPermissions}`,
|
|
94
|
+
`quarantined MCP ${quarantinedMcp.length} elevated MCP ${elevatedMcp.length} quarantined plugins ${snapshot.quarantinedPlugins.length} untrusted plugins ${snapshot.untrustedPlugins.length}`,
|
|
95
|
+
`incidents ${snapshot.incidents.length}`,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private tokensTab(snapshot: UiSecuritySnapshot): ConfigModalTab {
|
|
100
|
+
const audit = snapshot.audit;
|
|
101
|
+
const header = this.governanceHeader(snapshot);
|
|
102
|
+
const rows: ConfigModalRow[] = [];
|
|
103
|
+
|
|
104
|
+
if (audit.results.length === 0) {
|
|
105
|
+
const quarantinedMcp = snapshot.mcpServers.filter((s) => s.schemaFreshness === 'quarantined');
|
|
106
|
+
rows.push(infoRow('empty:0', 'No API tokens are registered with the security auditor yet.'));
|
|
107
|
+
rows.push(infoRow('empty:1', 'The security control room can already review policy, MCP, plugin, and incident posture, but token-specific scope and rotation audit data has not been registered.', { dim: true }));
|
|
108
|
+
if (quarantinedMcp.length > 0) rows.push(infoRow('empty:mcp', 'MCP quarantine still active despite no registered tokens.', WARN));
|
|
109
|
+
rows.push(infoRow('empty:title', 'Inspect further'));
|
|
110
|
+
rows.push(infoRow('empty:storage', '/storage review — inspect secure secret storage and environment overrides', { dim: true }));
|
|
111
|
+
rows.push(infoRow('empty:mcptrust', '/mcp trust — inspect active MCP trust and quarantine posture', { dim: true }));
|
|
112
|
+
return { id: 'tokens', label: 'Tokens', header, rows, emptyText: '' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const result of audit.results) {
|
|
116
|
+
const ageDays = Math.floor(result.rotation.ageMs / (24 * 60 * 60 * 1000));
|
|
117
|
+
rows.push({
|
|
118
|
+
id: result.tokenId,
|
|
119
|
+
label: `${result.label.padEnd(22)} ${result.tokenId.padEnd(14)} ${result.scope.policyId.padEnd(12)} ${resultSummary(result)} · rot ${result.rotation.outcome}/${fmtDue(result.rotation.msUntilDue)} age ${ageDays}d${result.blocked ? ' · BLOCKED' : ''}`,
|
|
120
|
+
...(resultColor(result) ? { style: resultColor(result)! } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
rows.push(infoRow('audit:when', `Last audit ${fmtIso(audit.lastAuditAt)} — press r to refresh`, { dim: true }));
|
|
124
|
+
return { id: 'tokens', label: 'Tokens', header, rows, hints: ['f preflight'] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private governanceTab(snapshot: UiSecuritySnapshot): ConfigModalTab {
|
|
128
|
+
const rows: ConfigModalRow[] = [];
|
|
129
|
+
const quarantinedMcp = snapshot.mcpServers.filter((s) => s.schemaFreshness === 'quarantined');
|
|
130
|
+
if (quarantinedMcp.length > 0) {
|
|
131
|
+
const server = quarantinedMcp[0]!;
|
|
132
|
+
rows.push(infoRow('mcp:q', `MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` — ${server.quarantineDetail}` : ''}`, BAD));
|
|
133
|
+
}
|
|
134
|
+
if (snapshot.quarantinedPlugins.length > 0) {
|
|
135
|
+
const plugin = snapshot.quarantinedPlugins[0]!;
|
|
136
|
+
rows.push(infoRow('plugin:q', `Plugin quarantine: ${plugin.name} (${plugin.trustTier})`, BAD));
|
|
137
|
+
} else if (snapshot.untrustedPlugins.length > 0) {
|
|
138
|
+
const plugin = snapshot.untrustedPlugins[0]!;
|
|
139
|
+
rows.push(infoRow('plugin:u', `Plugin trust warning: ${plugin.name} remains untrusted.`, WARN));
|
|
140
|
+
}
|
|
141
|
+
if (snapshot.latestIncident) {
|
|
142
|
+
rows.push(infoRow('incident', `Latest incident: ${snapshot.latestIncident.classification} — ${snapshot.latestIncident.summary} (${fmtIso(snapshot.latestIncident.generatedAt)})`, WARN));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const review = snapshot.attackPathReview;
|
|
146
|
+
rows.push(infoRow('atk:title', 'MCP Attack-Path Review'));
|
|
147
|
+
rows.push(infoRow('atk:counts', `critical ${review.criticalFindings} incoherent ${review.incoherentFindings}`, review.criticalFindings > 0 ? BAD : { dim: true }));
|
|
148
|
+
rows.push(infoRow('atk:summary', review.summary, { dim: true }));
|
|
149
|
+
const shown = review.findings.slice(0, MAX_FINDINGS_SHOWN);
|
|
150
|
+
shown.forEach((finding, i) => {
|
|
151
|
+
rows.push(infoRow(`atk:${i}:h`, `${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`, findingColor(finding.severity)));
|
|
152
|
+
rows.push(infoRow(`atk:${i}:r`, ` ${finding.reason}`, { dim: true }));
|
|
153
|
+
rows.push(infoRow(`atk:${i}:e`, ` evidence: ${finding.evidence.join(' | ')}`, { dim: true }));
|
|
154
|
+
});
|
|
155
|
+
if (review.findings.length > shown.length) {
|
|
156
|
+
const extra = review.findings.length - shown.length;
|
|
157
|
+
rows.push(infoRow('atk:more', `+${extra} more finding${extra === 1 ? '' : 's'} not shown`, { dim: true }));
|
|
158
|
+
}
|
|
159
|
+
return { id: 'governance', label: 'Governance', rows, emptyText: 'No governance findings.' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
buildView(): ConfigModalView {
|
|
163
|
+
const snapshot = this.deps.readModel.getSnapshot();
|
|
164
|
+
return { title: 'Security Control Room', tabs: [this.tokensTab(snapshot), this.governanceTab(snapshot)] };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
168
|
+
if (id === 'refresh') { ctx.setStatus('Security posture is read live.'); ctx.requestRender(); return; }
|
|
169
|
+
if (id === 'preflight') { void ctx.executeCommand?.('policy', ['preflight']); ctx.setStatus('Dispatched /policy preflight.'); return; }
|
|
170
|
+
if (id === 'jumpToIncident' && this.deps.readModel.getSnapshot().latestIncident) {
|
|
171
|
+
void ctx.executeCommand?.('panel', ['open', 'incident']);
|
|
172
|
+
ctx.setStatus('Opened the incident surface (fleet).');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function createSecurityModalSurface(deps: SecurityModalDeps): ConfigModalSurface {
|
|
178
|
+
return new SecurityModalSurface(deps);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Deterministic golden fixture. A fixed `UiSecuritySnapshot` literal covering
|
|
183
|
+
* the meaningful states in one shot: a clean token, a blocked/overdue/scope-
|
|
184
|
+
* violating token, a quarantined allow-all MCP server, a quarantined plugin,
|
|
185
|
+
* one recorded incident, and one critical attack-path finding. No Date.now().
|
|
186
|
+
*/
|
|
187
|
+
export function securityModalGoldenSurface(): ConfigModalSurface {
|
|
188
|
+
const FIXED_INCIDENT = {
|
|
189
|
+
id: 'inc-0001', traceId: 'trace-0001', sessionId: 'sess-0001', generatedAt: 1700000000000,
|
|
190
|
+
classification: 'permission_denied' as const, summary: 'blocked write outside sandbox root',
|
|
191
|
+
phaseTimings: [], phaseLedger: [], causalChain: [], cascadeEvents: [], permissionEvidence: [], budgetBreaches: [], jumpLinks: [],
|
|
192
|
+
};
|
|
193
|
+
const snapshot: UiSecuritySnapshot = {
|
|
194
|
+
audit: {
|
|
195
|
+
managed: true, totalTokens: 2,
|
|
196
|
+
results: [
|
|
197
|
+
{ tokenId: 'tok-openai', label: 'OPENAI_API_KEY', scope: { tokenId: 'tok-openai', outcome: 'ok', excessScopes: [], policyId: 'openai' }, rotation: { tokenId: 'tok-openai', outcome: 'ok', ageMs: 5 * 86400000, cadenceMs: 90 * 86400000, msUntilDue: 85 * 86400000, dueAt: 1707600000000 }, blocked: false },
|
|
198
|
+
{ tokenId: 'tok-slack', label: 'SLACK_BOT_TOKEN', scope: { tokenId: 'tok-slack', outcome: 'violation', excessScopes: ['admin'], policyId: 'slack' }, rotation: { tokenId: 'tok-slack', outcome: 'overdue', ageMs: 120 * 86400000, cadenceMs: 90 * 86400000, msUntilDue: -30 * 86400000, dueAt: 1697328000000 }, blocked: true },
|
|
199
|
+
],
|
|
200
|
+
blocked: ['tok-slack'], scopeViolations: ['tok-slack'], rotationWarnings: [], rotationOverdue: ['tok-slack'],
|
|
201
|
+
lastAuditAt: 1700000000000, capturedAt: '2023-11-14T22:13:20.000Z',
|
|
202
|
+
},
|
|
203
|
+
policy: { preflightStatus: 'warn', preflightIssueCount: 2, lintFindingCount: 1 },
|
|
204
|
+
deniedPermissions: 3, incidents: [FIXED_INCIDENT], latestIncident: FIXED_INCIDENT,
|
|
205
|
+
mcpServers: [{ name: 'fs-server', role: 'filesystem', trustMode: 'allow-all', connected: true, allowedPaths: [], allowedHosts: [], schemaFreshness: 'quarantined', quarantineReason: 'stale_threshold', quarantineDetail: 'ttl expired 2 days ago' }],
|
|
206
|
+
recentMcpDecisions: [],
|
|
207
|
+
attackPathReview: {
|
|
208
|
+
reviewedAt: 1700000000000, totalServers: 1, connectedServers: 1, allowAllServers: 1, askOnRiskServers: 0, constrainedServers: 0, blockedServers: 0, quarantinedServers: 1, incoherentFindings: 1, criticalFindings: 1,
|
|
209
|
+
findings: [{ kind: 'server-posture', serverName: 'fs-server', route: 'fs-server -> write_fs', verdict: 'ask', severity: 'critical', incoherent: true, reason: 'allow-all trust combined with a quarantined schema', evidence: ['schemaFreshness=quarantined', 'trustMode=allow-all'] }],
|
|
210
|
+
summary: '1 server in allow-all mode with a quarantined schema',
|
|
211
|
+
},
|
|
212
|
+
plugins: [],
|
|
213
|
+
quarantinedPlugins: [{ name: 'legacy-formatter', version: '0.1.0', description: 'old formatter plugin', enabled: false, active: false, trustTier: 'untrusted', quarantined: true }],
|
|
214
|
+
untrustedPlugins: [],
|
|
215
|
+
};
|
|
216
|
+
return createSecurityModalSurface({ readModel: { getSnapshot: () => snapshot, subscribe: () => () => {} } });
|
|
217
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ConfigModalActionContext, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
|
|
2
|
+
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../../runtime/ui-service-queries.ts';
|
|
3
|
+
import type { ServiceConfig, ServiceInspection, ServiceConnectionTestResult } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
+
import { toneStyle, statusGlyph, pad, postureLine, kv, type Tone } from './modal-surface-helpers.ts';
|
|
6
|
+
|
|
7
|
+
interface Entry {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly inspection: ServiceInspection | null;
|
|
10
|
+
lastTest?: ServiceConnectionTestResult;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function statusLabel(e: Entry): string {
|
|
14
|
+
if (!e.inspection) return 'INSPECT FAILED';
|
|
15
|
+
if (!e.inspection.hasPrimaryCredential) return 'UNCONFIGURED';
|
|
16
|
+
if (e.lastTest?.ok) return 'HEALTHY';
|
|
17
|
+
if (e.lastTest && !e.lastTest.ok) return 'ERROR';
|
|
18
|
+
return 'CONFIGURED';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function statusTone(e: Entry): Tone {
|
|
22
|
+
const label = statusLabel(e);
|
|
23
|
+
if (label === 'HEALTHY') return 'good';
|
|
24
|
+
if (label === 'ERROR' || label === 'INSPECT FAILED') return 'bad';
|
|
25
|
+
if (label === 'CONFIGURED') return 'warn';
|
|
26
|
+
return 'dim';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function statusDotTone(e: Entry): Tone {
|
|
30
|
+
const label = statusLabel(e);
|
|
31
|
+
if (label === 'HEALTHY') return 'good';
|
|
32
|
+
if (label === 'ERROR' || label === 'INSPECT FAILED') return 'bad';
|
|
33
|
+
return 'dim';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ported verbatim from ServicesPanel.authSummary (the retired panel's derivation).
|
|
37
|
+
function authSummary(config: ServiceConfig, manager: SubscriptionAccessQuery): string {
|
|
38
|
+
const provider = config.providerId ?? config.name;
|
|
39
|
+
const hasOverride = manager.getAccessToken(provider) != null;
|
|
40
|
+
switch (config.authType) {
|
|
41
|
+
case 'bearer': return hasOverride ? 'bearer+subscription' : 'bearer';
|
|
42
|
+
case 'basic': return 'basic';
|
|
43
|
+
case 'api-key': return hasOverride ? 'oauth-override' : config.apiKeyHeader ? `api-key:${config.apiKeyHeader}` : 'api-key';
|
|
44
|
+
case 'oauth': return manager.get(provider) != null ? 'oauth(active)' : 'oauth';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Services config-modal surface (migrated from the `services` panel). Reads the
|
|
50
|
+
* same ServiceInspectionQuery — getAll() is sync but inspect()/testConnection()
|
|
51
|
+
* are async, so an inspection cache is refreshed on open / `r` and read
|
|
52
|
+
* synchronously by buildView (mirrors the panel's own refresh contract).
|
|
53
|
+
* Actions map onto their existing behaviours; `s` jumps to the subscription
|
|
54
|
+
* surface, `t`/`T` run the same live connection tests, `r` re-inspects.
|
|
55
|
+
*/
|
|
56
|
+
class ServicesModalSurface implements ConfigModalSurface {
|
|
57
|
+
readonly name = 'services-modal';
|
|
58
|
+
readonly title = 'Services';
|
|
59
|
+
private entries: Entry[] = [];
|
|
60
|
+
private loading = false;
|
|
61
|
+
private error = '';
|
|
62
|
+
private requestRender: () => void = () => {};
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private readonly registry: ServiceInspectionQuery,
|
|
66
|
+
private readonly manager: SubscriptionAccessQuery,
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
readonly actions = [
|
|
70
|
+
{ key: 'r', id: 'refresh', label: 'refresh' },
|
|
71
|
+
{ key: 't', id: 'test', label: 'test' },
|
|
72
|
+
{ key: 'T', id: 'test-all', label: 'test all' },
|
|
73
|
+
{ key: 's', id: 'subscription', label: 'subscription' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
onOpen(requestRender: () => void): void {
|
|
77
|
+
this.requestRender = requestRender;
|
|
78
|
+
if (this.entries.length === 0 && !this.loading) void this.refresh();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
buildView(): ConfigModalView {
|
|
82
|
+
const counts = { healthy: 0, error: 0, unconfigured: 0 };
|
|
83
|
+
for (const e of this.entries) {
|
|
84
|
+
const label = statusLabel(e);
|
|
85
|
+
if (label === 'HEALTHY') counts.healthy++;
|
|
86
|
+
else if (label === 'ERROR') counts.error++;
|
|
87
|
+
else if (label !== 'CONFIGURED') counts.unconfigured++;
|
|
88
|
+
}
|
|
89
|
+
const header = [postureLine([
|
|
90
|
+
kv('services', this.entries.length),
|
|
91
|
+
kv('healthy', counts.healthy),
|
|
92
|
+
kv('errors', counts.error),
|
|
93
|
+
kv('unconfigured', counts.unconfigured),
|
|
94
|
+
])];
|
|
95
|
+
|
|
96
|
+
const rows = this.entries.map((e) => {
|
|
97
|
+
const auth = e.inspection ? authSummary(e.inspection.config, this.manager) : 'n/a';
|
|
98
|
+
const base = e.inspection ? (e.inspection.config.baseUrl ?? '(no baseUrl)') : '(inspection failed)';
|
|
99
|
+
return {
|
|
100
|
+
id: `svc:${e.name}`,
|
|
101
|
+
label: `${statusGlyph(statusDotTone(e))} ${pad(e.name, 16)} ${pad(statusLabel(e), 13)} ${pad(auth, 18)} ${base}`,
|
|
102
|
+
style: toneStyle(statusTone(e)),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
title: this.loading && this.entries.length === 0 ? 'Services (loading…)' : 'Services',
|
|
108
|
+
degraded: this.error || undefined,
|
|
109
|
+
tabs: [{
|
|
110
|
+
id: 'services',
|
|
111
|
+
label: 'Services',
|
|
112
|
+
header,
|
|
113
|
+
rows,
|
|
114
|
+
emptyText: this.loading ? 'Loading configured services…' : 'No services configured. Try /services auth-review.',
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
120
|
+
if (id === 'refresh') void this.refresh();
|
|
121
|
+
else if (id === 'test') void this.testOne(ctx);
|
|
122
|
+
else if (id === 'test-all') void this.testAll();
|
|
123
|
+
else if (id === 'subscription') ctx.openModal?.('subscription-modal');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private nameOf(rowId: string | undefined): string | null {
|
|
127
|
+
return rowId?.startsWith('svc:') ? rowId.slice('svc:'.length) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async refresh(): Promise<void> {
|
|
131
|
+
this.loading = true;
|
|
132
|
+
this.error = '';
|
|
133
|
+
this.requestRender();
|
|
134
|
+
try {
|
|
135
|
+
const configs = this.registry.getAll();
|
|
136
|
+
const names = Object.keys(configs).sort((a, b) => a.localeCompare(b));
|
|
137
|
+
const inspections = await Promise.all(names.map(async (name) => ({ name, inspection: await this.registry.inspect(name) })));
|
|
138
|
+
const prev = new Map(this.entries.map((e) => [e.name, e.lastTest] as const));
|
|
139
|
+
this.entries = inspections.map((e) => ({ ...e, lastTest: prev.get(e.name) }));
|
|
140
|
+
} catch (e) {
|
|
141
|
+
this.error = `Refresh failed: ${summarizeError(e)}`;
|
|
142
|
+
} finally {
|
|
143
|
+
this.loading = false;
|
|
144
|
+
this.requestRender();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async testOne(ctx: ConfigModalActionContext): Promise<void> {
|
|
149
|
+
const name = this.nameOf(ctx.row?.id);
|
|
150
|
+
if (!name) return;
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.registry.testConnection(name);
|
|
153
|
+
this.entries = this.entries.map((e) => (e.name === name ? { ...e, lastTest: result } : e));
|
|
154
|
+
ctx.setStatus(`${name}: ${result.ok ? 'ok' : `failed (${result.status ?? 'n/a'})`}`);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
ctx.setStatus(`Test failed: ${summarizeError(e)}`);
|
|
157
|
+
}
|
|
158
|
+
this.requestRender();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async testAll(): Promise<void> {
|
|
162
|
+
for (const entry of this.entries) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await this.registry.testConnection(entry.name);
|
|
165
|
+
this.entries = this.entries.map((e) => (e.name === entry.name ? { ...e, lastTest: result } : e));
|
|
166
|
+
} catch (e) {
|
|
167
|
+
this.error = `Test failed for ${entry.name}: ${summarizeError(e)}`;
|
|
168
|
+
}
|
|
169
|
+
this.requestRender();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createServicesModalSurface(
|
|
175
|
+
registry: ServiceInspectionQuery,
|
|
176
|
+
manager: SubscriptionAccessQuery,
|
|
177
|
+
): ConfigModalSurface {
|
|
178
|
+
return new ServicesModalSurface(registry, manager);
|
|
179
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ConfigModalAction, ConfigModalActionContext, ConfigModalRow, ConfigModalSurface, ConfigModalView } from '../../input/config-modal-types.ts';
|
|
2
|
+
import { getSettingsControlPlaneSnapshot } from '@/runtime/index.ts';
|
|
3
|
+
import type { ConfigManager } from '../../config/index.ts';
|
|
4
|
+
import { toneStyle, pad, postureLine, kv, type Tone } from './modal-surface-helpers.ts';
|
|
5
|
+
|
|
6
|
+
type Snapshot = ReturnType<typeof getSettingsControlPlaneSnapshot>;
|
|
7
|
+
|
|
8
|
+
function sourceTone(source: string, conflict: boolean, locked: boolean): Tone {
|
|
9
|
+
if (conflict) return 'bad';
|
|
10
|
+
if (locked || source === 'managed') return 'warn';
|
|
11
|
+
if (source === 'synced') return 'good';
|
|
12
|
+
if (source === 'local') return 'info';
|
|
13
|
+
return 'dim';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Settings-sync config-modal surface (migrated from the `settings-sync` panel).
|
|
18
|
+
* All reads are synchronous (getSettingsControlPlaneSnapshot). The panel's six
|
|
19
|
+
* Tab-cycled browse modes become six real tabs. Conflict resolution keeps the
|
|
20
|
+
* panel's two-step shape: Enter on a conflicted key arms a resolve prompt, then
|
|
21
|
+
* l/s dispatch the existing `/settings-sync resolve <key> <local|synced>`
|
|
22
|
+
* command (the command owns the mutation + live-session refresh). `m` dispatches
|
|
23
|
+
* `/managed review`. push/pull remain CLI-only (they were never panel actions).
|
|
24
|
+
*/
|
|
25
|
+
class SettingsSyncModalSurface implements ConfigModalSurface {
|
|
26
|
+
readonly name = 'settings-sync-modal';
|
|
27
|
+
readonly title = 'Settings Sync';
|
|
28
|
+
private requestRender: () => void = () => {};
|
|
29
|
+
/** Conflicted key awaiting an l/s choice (surface-managed sub-flow). */
|
|
30
|
+
private resolvePrompt: string | null = null;
|
|
31
|
+
private conflictedKeys = new Set<string>();
|
|
32
|
+
private hasStaged = false;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly configManager: ConfigManager) {}
|
|
35
|
+
|
|
36
|
+
readonly actions: ConfigModalAction[] = [
|
|
37
|
+
{ key: 'enter', id: 'resolve-open', label: 'resolve conflict', enabledFor: (row, tab) => tab === 'keys' && this.isConflicted(row) && this.resolvePrompt === null },
|
|
38
|
+
{ key: 'l', id: 'resolve-local', label: 'keep local', enabledFor: () => this.resolvePrompt !== null },
|
|
39
|
+
{ key: 's', id: 'resolve-synced', label: 'use synced', enabledFor: () => this.resolvePrompt !== null },
|
|
40
|
+
{ key: 'n', id: 'resolve-cancel', label: 'cancel', enabledFor: () => this.resolvePrompt !== null },
|
|
41
|
+
{ key: 'm', id: 'managed-review', label: 'managed review', enabledFor: () => this.hasStaged },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
onOpen(requestRender: () => void): void {
|
|
45
|
+
this.requestRender = requestRender;
|
|
46
|
+
this.resolvePrompt = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onClose(): void {
|
|
50
|
+
this.resolvePrompt = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private isConflicted(row: ConfigModalRow | null): boolean {
|
|
54
|
+
return !!row && row.id.startsWith('key:') && this.conflictedKeys.has(row.id.slice('key:'.length));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
buildView(): ConfigModalView {
|
|
58
|
+
const s = this.configManager ? this.trySnapshot() : null;
|
|
59
|
+
if (!s) {
|
|
60
|
+
return { title: 'Settings Sync', degraded: 'settings control plane unavailable', tabs: [{ id: 'keys', label: 'Keys', rows: [] }] };
|
|
61
|
+
}
|
|
62
|
+
this.conflictedKeys = new Set(s.conflicts.map((c) => c.key));
|
|
63
|
+
this.hasStaged = Boolean(s.stagedManagedBundle);
|
|
64
|
+
|
|
65
|
+
const header = [
|
|
66
|
+
postureLine([kv('resolved', s.resolvedEntries.length), kv('conflicts', s.conflicts.length), kv('failures', s.recentFailures.length), kv('locks', s.managedLockCount)]),
|
|
67
|
+
postureLine([kv('local', s.resolvedCounts.local), kv('synced', s.resolvedCounts.synced), kv('managed', s.resolvedCounts.managed), kv('last-sync', s.lastSync ? `${s.lastSync.surface}/${s.lastSync.direction}` : 'none'), kv('staged', s.stagedManagedBundle ? s.stagedManagedBundle.profileName : 'none')]),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const keysRows: ConfigModalRow[] = s.resolvedEntries.map((e) => ({
|
|
71
|
+
id: `key:${e.key}`,
|
|
72
|
+
label: `${pad(e.key, 32)} ${pad(String(e.effectiveSource), 10)} ${String(e.effectiveValue)}`,
|
|
73
|
+
style: toneStyle(sourceTone(String(e.effectiveSource), Boolean(e.conflict), Boolean(e.locked))),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const eventRows: ConfigModalRow[] = s.recentEvents.map((ev, i) => ({ id: `event:${i}`, label: `${pad(`${ev.surface}/${ev.direction}`, 18)} ${ev.detail}`, style: toneStyle('info') }));
|
|
77
|
+
const lockRows: ConfigModalRow[] = s.managedLocks.map((l) => ({ id: `lock:${l.key}`, label: `${pad(l.key, 30)} source=${pad(l.source, 12)} ${l.reason}`, style: toneStyle('warn') }));
|
|
78
|
+
const failureRows: ConfigModalRow[] = s.recentFailures.map((f, i) => ({ id: `failure:${i}`, label: `${pad(f.surface, 10)} ${f.message}`, style: toneStyle('bad') }));
|
|
79
|
+
const conflictRows: ConfigModalRow[] = s.conflicts.map((c) => ({ id: `conflict:${c.key}`, label: `${pad(c.key, 30)} ${pad(c.source, 10)} local=${String(c.localValue)} incoming=${String(c.incomingValue)}`, style: toneStyle('warn') }));
|
|
80
|
+
const rollbackRows: ConfigModalRow[] = s.rollbackHistory.map((r) => ({ id: `rollback:${r.token}`, label: `${pad(r.token, 18)} ${pad(r.profileName, 18)} restored=${r.restoredKeys.length} ${new Date(r.appliedAt).toLocaleString()}`, style: toneStyle('dim') }));
|
|
81
|
+
|
|
82
|
+
const keyHints = this.resolvePrompt
|
|
83
|
+
? [`resolving "${this.resolvePrompt}"`, 'l keep local', 's use synced', 'n cancel']
|
|
84
|
+
: ['Enter resolve conflict', 'm managed review'];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
title: 'Settings Sync',
|
|
88
|
+
hints: ['←/→ tab'],
|
|
89
|
+
tabs: [
|
|
90
|
+
{ id: 'keys', label: 'Keys', header, rows: keysRows, emptyText: 'No resolved settings entries.', hints: keyHints },
|
|
91
|
+
{ id: 'events', label: 'Events', header, rows: eventRows, emptyText: 'No sync or managed-setting events recorded yet.' },
|
|
92
|
+
{ id: 'locks', label: 'Locks', header, rows: lockRows, emptyText: 'No managed locks are currently active.' },
|
|
93
|
+
{ id: 'failures', label: 'Failures', header, rows: failureRows, emptyText: 'No recent sync or managed-setting failures.' },
|
|
94
|
+
{ id: 'conflicts', label: 'Conflicts', header, rows: conflictRows, emptyText: 'No settings conflicts detected.' },
|
|
95
|
+
{ id: 'rollback', label: 'Rollback', header, rows: rollbackRows, emptyText: 'No managed rollback records yet.' },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
101
|
+
switch (id) {
|
|
102
|
+
case 'resolve-open': {
|
|
103
|
+
const key = ctx.row?.id.startsWith('key:') ? ctx.row.id.slice('key:'.length) : null;
|
|
104
|
+
if (key && this.conflictedKeys.has(key)) {
|
|
105
|
+
this.resolvePrompt = key;
|
|
106
|
+
ctx.setStatus(`Resolve "${key}": l keep local · s use synced · n cancel`);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'resolve-local':
|
|
111
|
+
case 'resolve-synced': {
|
|
112
|
+
if (!this.resolvePrompt) break;
|
|
113
|
+
const choice = id === 'resolve-local' ? 'local' : 'synced';
|
|
114
|
+
void ctx.executeCommand?.('settings-sync', ['resolve', this.resolvePrompt, choice]);
|
|
115
|
+
ctx.setStatus(`Resolving ${this.resolvePrompt} → ${choice}…`);
|
|
116
|
+
this.resolvePrompt = null;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'resolve-cancel':
|
|
120
|
+
this.resolvePrompt = null;
|
|
121
|
+
ctx.setStatus('');
|
|
122
|
+
break;
|
|
123
|
+
case 'managed-review':
|
|
124
|
+
void ctx.executeCommand?.('managed', ['review']);
|
|
125
|
+
ctx.setStatus('Opening managed review…');
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
this.requestRender();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private trySnapshot(): Snapshot | null {
|
|
132
|
+
try {
|
|
133
|
+
return getSettingsControlPlaneSnapshot(this.configManager);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createSettingsSyncModalSurface(configManager: ConfigManager): ConfigModalSurface {
|
|
141
|
+
return new SettingsSyncModalSurface(configManager);
|
|
142
|
+
}
|