@pellux/goodvibes-tui 1.1.0 → 1.9.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
+ }