@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,218 @@
1
+ import type {
2
+ ConfigModalActionContext,
3
+ ConfigModalRow,
4
+ ConfigModalSurface,
5
+ ConfigModalView,
6
+ } from '../../input/config-modal-types.ts';
7
+ import { MODAL_TONES } from './modal-theme.ts';
8
+ import { infoRow } from './modal-surface-helpers.ts';
9
+ import {
10
+ loadEcosystemCatalog,
11
+ listInstalledEcosystemEntries,
12
+ reviewEcosystemCatalogEntry,
13
+ type EcosystemCatalogEntry,
14
+ type EcosystemCatalogPathOptions,
15
+ type EcosystemEntryKind,
16
+ } from '@/runtime/index.ts';
17
+ import type { UiMarketplaceSnapshot, UiReadModel } from '../../runtime/ui-read-models.ts';
18
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
19
+ import { mkdtempSync, rmSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { join } from 'node:path';
22
+
23
+ /** Live deps the marketplace modal captures (mirrors the panel factory). */
24
+ export interface MarketplaceModalDeps {
25
+ readonly readModel?: UiReadModel<UiMarketplaceSnapshot>;
26
+ readonly ecosystemPaths?: EcosystemCatalogPathOptions;
27
+ }
28
+
29
+ type MarketplaceReview = ReturnType<typeof reviewEcosystemCatalogEntry>;
30
+
31
+ interface MarketplaceRow {
32
+ readonly kind: EcosystemEntryKind;
33
+ readonly entry: EcosystemCatalogEntry;
34
+ readonly installed: boolean;
35
+ readonly review: MarketplaceReview;
36
+ }
37
+
38
+ const KINDS: readonly EcosystemEntryKind[] = ['plugin', 'skill', 'hook-pack', 'policy-pack'];
39
+
40
+ /**
41
+ * Marketplace → config-modal surface (W6.1 group-B port). Local-first
42
+ * publish/import catalog (NOT a remote store) with provenance, compatibility,
43
+ * and install posture. Disk catalog loads happen in refresh() (never in
44
+ * buildView), mirroring the panel's explicit render()/refresh() split.
45
+ * Install/uninstall route to the `/marketplace` command path (charter: no
46
+ * destructive confirm folded into a modal).
47
+ *
48
+ * B30 (honest empty-state): the panel's original "No curated marketplace
49
+ * entries found yet." copy implied a curated remote catalog. It isn't one —
50
+ * loadEcosystemCatalog only ever reads .goodvibes/ecosystem/<kind>s.json under
51
+ * the project/home, populated solely by `/marketplace publish` and
52
+ * `/marketplace bundle import`. The empty-state copy below (locked byte-for-byte
53
+ * in the golden) names that reality. Selection-blind port: the panel's
54
+ * selected-entry compatibility/risk/state detail is folded into each row label.
55
+ */
56
+ class MarketplaceModalSurface implements ConfigModalSurface {
57
+ readonly name = 'marketplace-modal';
58
+ readonly title = 'Marketplace';
59
+ private rows: MarketplaceRow[] = [];
60
+ private loadError: string | null = null;
61
+ private requestRender: () => void = () => {};
62
+ private unsub: (() => void) | null = null;
63
+
64
+ constructor(private readonly deps: MarketplaceModalDeps) {}
65
+
66
+ readonly actions = [
67
+ { key: 'i', id: 'install', label: 'install', enabledFor: (row: ConfigModalRow | null) => this.canInstall(row) },
68
+ { key: 'u', id: 'uninstall', label: 'uninstall', enabledFor: (row: ConfigModalRow | null) => this.canUninstall(row) },
69
+ { key: 'r', id: 'refresh', label: 'refresh' },
70
+ ];
71
+
72
+ onOpen(requestRender: () => void): void {
73
+ this.requestRender = requestRender;
74
+ this.refresh();
75
+ if (this.deps.readModel && !this.unsub) this.unsub = this.deps.readModel.subscribe(() => this.requestRender());
76
+ }
77
+
78
+ onClose(): void {
79
+ this.unsub?.();
80
+ this.unsub = null;
81
+ }
82
+
83
+ private refresh(): void {
84
+ const paths = this.deps.ecosystemPaths;
85
+ if (!paths) { this.rows = []; this.loadError = null; return; }
86
+ try {
87
+ const built: MarketplaceRow[] = [];
88
+ for (const kind of KINDS) {
89
+ const installed = new Set(listInstalledEcosystemEntries(kind, paths).map((receipt) => receipt.entry.id));
90
+ for (const entry of loadEcosystemCatalog(kind, paths)) {
91
+ built.push({ kind, entry, installed: installed.has(entry.id), review: reviewEcosystemCatalogEntry(entry, paths) });
92
+ }
93
+ }
94
+ this.rows = built.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
95
+ this.loadError = null;
96
+ } catch (e) {
97
+ this.rows = [];
98
+ this.loadError = `Catalog load failed: ${summarizeError(e)}`;
99
+ }
100
+ }
101
+
102
+ private entryFor(row: ConfigModalRow | null): MarketplaceRow | undefined {
103
+ if (!row) return undefined;
104
+ return this.rows.find((r) => `${r.kind}:${r.entry.id}` === row.id);
105
+ }
106
+
107
+ private canInstall(row: ConfigModalRow | null): boolean {
108
+ const entry = this.entryFor(row);
109
+ return Boolean(entry && !entry.installed);
110
+ }
111
+
112
+ private canUninstall(row: ConfigModalRow | null): boolean {
113
+ const entry = this.entryFor(row);
114
+ return Boolean(entry && entry.installed);
115
+ }
116
+
117
+ buildView(): ConfigModalView {
118
+ const snapshot = this.deps.readModel?.getSnapshot();
119
+ const rows: ConfigModalRow[] = [];
120
+
121
+ // Honest empty / degraded state (B30). loadError → degraded banner.
122
+ if (this.loadError) {
123
+ return { title: 'Marketplace', degraded: this.loadError, tabs: [{ id: 'catalog', label: 'Catalog', rows: [] }] };
124
+ }
125
+ if (this.rows.length === 0) {
126
+ if (!this.deps.ecosystemPaths) {
127
+ rows.push(infoRow('empty:unwired', 'Marketplace catalog roots aren’t wired into this session, so there’s nothing to read yet.'));
128
+ } else {
129
+ rows.push(infoRow('empty:0', 'This is your local plugin, skill, hook-pack, and policy-pack catalog — not a remote store.'));
130
+ rows.push(infoRow('empty:1', 'It’s empty because nothing has been published or imported into this workspace yet. Entries appear here once you publish a local component or import a bundle.'));
131
+ }
132
+ rows.push(infoRow('empty:title', 'Populate it', { bold: true }));
133
+ rows.push(infoRow('empty:publish', '/marketplace publish <kind> <path> — publish local plugins/skills into the catalog', { dim: true }));
134
+ rows.push(infoRow('empty:import', '/marketplace bundle import <path> — import a catalog bundle from disk', { dim: true }));
135
+ rows.push(infoRow('empty:review', '/marketplace catalog review — inspect the current local catalog posture', { dim: true }));
136
+ return {
137
+ title: 'Marketplace',
138
+ tabs: [{ id: 'catalog', label: 'Catalog', rows, emptyText: '' }],
139
+ hints: ['local publish/import catalog'],
140
+ };
141
+ }
142
+
143
+ // Posture summary header.
144
+ const installedCount = this.rows.filter((r) => r.installed).length;
145
+ const count = (k: EcosystemEntryKind): number => this.rows.filter((r) => r.kind === k).length;
146
+ const header = [
147
+ `catalog ${this.rows.length} installed ${installedCount} plugins ${count('plugin')} skills ${count('skill')} hooks ${count('hook-pack')} policies ${count('policy-pack')}`,
148
+ ];
149
+
150
+ const startupIssues = snapshot?.startupIssues ?? [];
151
+ for (const [i, issue] of startupIssues.slice(0, 3).entries()) {
152
+ rows.push(infoRow(`issue:${i}`, `⚠ ${issue}`, { fg: MODAL_TONES.warn }));
153
+ }
154
+
155
+ // Catalog list — selection-blind: fold provenance/compat/risk/state into the label.
156
+ for (const row of this.rows) {
157
+ const review = row.review;
158
+ const provenance = row.entry.provenance ?? 'local';
159
+ rows.push({
160
+ id: `${row.kind}:${row.entry.id}`,
161
+ label: `${row.kind.padEnd(11)} ${row.entry.name.padEnd(22)} ${provenance.padEnd(14)} ${row.installed ? 'INSTALLED' : 'local '} ${row.entry.version ?? 'n/a'} · compat ${review.compatibility.status} risk ${review.riskLevel}`,
162
+ });
163
+ }
164
+
165
+ // Recommendations (displayed with their command; digit-jump dropped).
166
+ const recommendations = snapshot?.recommendations ?? [];
167
+ if (recommendations.length > 0) {
168
+ rows.push(infoRow('rec:title', 'Recommended', { bold: true }));
169
+ for (const [i, rec] of recommendations.slice(0, 3).entries()) {
170
+ rows.push(infoRow(`rec:${i}`, `${rec.title} — ${rec.command}`, { dim: true }));
171
+ }
172
+ }
173
+
174
+ return {
175
+ title: 'Marketplace',
176
+ tabs: [{
177
+ id: 'catalog',
178
+ label: 'Catalog',
179
+ header,
180
+ rows,
181
+ hints: ['local publish/import catalog'],
182
+ }],
183
+ };
184
+ }
185
+
186
+ onAction(id: string, ctx: ConfigModalActionContext): void {
187
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Refreshing catalog…'); return; }
188
+ const entry = this.entryFor(ctx.row);
189
+ if (!entry) return;
190
+ if (id === 'install' && !entry.installed) {
191
+ void ctx.executeCommand?.('marketplace', ['install', entry.kind, entry.entry.id]);
192
+ ctx.setStatus(`Dispatched /marketplace install ${entry.kind} ${entry.entry.id} (see transcript).`);
193
+ } else if (id === 'uninstall' && entry.installed) {
194
+ void ctx.executeCommand?.('marketplace', ['uninstall', entry.kind, entry.entry.id]);
195
+ ctx.setStatus(`Dispatched /marketplace uninstall ${entry.kind} ${entry.entry.id} (see transcript).`);
196
+ }
197
+ }
198
+ }
199
+
200
+ export function createMarketplaceModalSurface(deps: MarketplaceModalDeps): ConfigModalSurface {
201
+ return new MarketplaceModalSurface(deps);
202
+ }
203
+
204
+ /**
205
+ * Deterministic golden fixture: catalog roots wired at a fresh tmp path that is
206
+ * removed immediately (loadEcosystemCatalog/listInstalledEcosystemEntries guard
207
+ * missing paths with existsSync → [], so refresh() finds nothing and renders the
208
+ * B30 honest empty-state copy). The random tmp path never appears in the
209
+ * rendered lines (the empty-state copy is static), so the golden is byte-stable.
210
+ */
211
+ export function marketplaceModalGoldenSurface(): ConfigModalSurface {
212
+ const root = mkdtempSync(join(tmpdir(), 'gv-marketplace-golden-'));
213
+ const surface = createMarketplaceModalSurface({
214
+ ecosystemPaths: { cwd: root, homeDir: root, projectCatalogRoot: join(root, 'ecosystem') },
215
+ });
216
+ rmSync(root, { recursive: true, force: true });
217
+ return surface;
218
+ }
@@ -0,0 +1,180 @@
1
+ import { infoRow } from './modal-surface-helpers.ts';
2
+ import type {
3
+ ConfigModalActionContext,
4
+ ConfigModalRow,
5
+ ConfigModalSurface,
6
+ ConfigModalTab,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Memory → config-modal surface (W6.1 group-B port). Two tabs: 'All Records'
12
+ // (the full record list) and 'Review Queue' (the operator review queue) — the
13
+ // same split the panel toggled with Tab, now real host tabs. Review-state
14
+ // mutations (reviewed/stale/contradicted/fresh) and record delete route to the
15
+ // existing `/recall review` / `/recall remove` command path (charter: no
16
+ // destructive mutation direct-called from a modal). Selection-blind port: the
17
+ // panel's selected-record scope/class/tags/provenance detail is folded into
18
+ // each row label. `memoryRegistry` absent → the retired MemoryPanel's
19
+ // "not configured" copy renders as an honest degraded state.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Minimal read shape of a `MemoryRecord` this modal renders. */
23
+ interface MemoryRecordLike {
24
+ readonly id: string;
25
+ readonly scope: string;
26
+ readonly cls: string;
27
+ readonly summary: string;
28
+ readonly detail?: string | undefined;
29
+ readonly tags: readonly string[];
30
+ readonly reviewState: string;
31
+ readonly confidence: number;
32
+ readonly staleReason?: string | undefined;
33
+ readonly reviewedAt?: number | undefined;
34
+ readonly reviewedBy?: string | undefined;
35
+ readonly createdAt: number;
36
+ readonly provenance: readonly { readonly kind: string; readonly ref: string }[];
37
+ }
38
+
39
+ export interface MemoryModalDeps {
40
+ readonly memoryRegistry?: {
41
+ search(filter?: { limit?: number }): readonly MemoryRecordLike[];
42
+ reviewQueue(limit?: number): readonly MemoryRecordLike[];
43
+ };
44
+ }
45
+
46
+ // Mirrors registerKnowledgePanels's withUnconfiguredFallback copy for the
47
+ // retired MemoryPanel (src/panels/builtin/knowledge.ts) verbatim.
48
+ const NOT_CONFIGURED_TITLE = 'Memory registry not configured for this session.';
49
+ const NOT_CONFIGURED_BODY = 'This runtime was not wired with a project memory registry at bootstrap, so no memory data is available.';
50
+
51
+ function fmtTime(ts: number): string {
52
+ return new Date(ts).toISOString().slice(0, 16).replace('T', ' ');
53
+ }
54
+
55
+ class MemoryModalSurface implements ConfigModalSurface {
56
+ readonly name = 'memory-modal';
57
+ readonly title = 'Memory';
58
+ private allRecords: MemoryRecordLike[] = [];
59
+ private reviewRecords: MemoryRecordLike[] = [];
60
+
61
+ constructor(private readonly deps: MemoryModalDeps) {}
62
+
63
+ private readonly reviewGate = (row: ConfigModalRow | null, tabId: string): boolean => tabId === 'review' && row !== null;
64
+
65
+ readonly actions = [
66
+ { key: 'enter', id: 'markReviewed', label: 'reviewed', enabledFor: this.reviewGate },
67
+ { key: 's', id: 'markStale', label: 'stale', enabledFor: this.reviewGate },
68
+ { key: 'c', id: 'markContradicted', label: 'contradicted', enabledFor: this.reviewGate },
69
+ { key: 'f', id: 'markFresh', label: 'fresh', enabledFor: this.reviewGate },
70
+ { key: 'd', id: 'remove', label: 'delete', enabledFor: (row: ConfigModalRow | null) => row !== null },
71
+ { key: 'r', id: 'refresh', label: 'refresh' },
72
+ ];
73
+
74
+ onOpen(): void { this.refresh(); }
75
+
76
+ private refresh(): void {
77
+ if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; return; }
78
+ this.allRecords = [...this.deps.memoryRegistry.search({ limit: 100 })];
79
+ this.reviewRecords = [...this.deps.memoryRegistry.reviewQueue(24)];
80
+ }
81
+
82
+ private recordFrom(id: string): MemoryRecordLike | undefined {
83
+ return this.allRecords.find((r) => r.id === id) ?? this.reviewRecords.find((r) => r.id === id);
84
+ }
85
+
86
+ private allTab(): ConfigModalTab {
87
+ const rows: ConfigModalRow[] = this.allRecords.map((record) => ({
88
+ id: record.id,
89
+ label: `[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] ${record.id.slice(-8)} ${fmtTime(record.createdAt)} ${record.summary}${record.tags.length > 0 ? ` #${record.tags.join(' #')}` : ''}`,
90
+ }));
91
+ return {
92
+ id: 'all',
93
+ label: 'All Records',
94
+ header: [`records ${this.allRecords.length} review queue ${this.reviewRecords.length}`],
95
+ rows,
96
+ emptyText: 'No memory records.',
97
+ hints: ['d delete'],
98
+ };
99
+ }
100
+
101
+ private reviewTab(): ConfigModalTab {
102
+ const rows: ConfigModalRow[] = this.reviewRecords.map((record) => ({
103
+ id: record.id,
104
+ label: `${record.reviewState.padEnd(13)} ${String(record.confidence).padStart(3)}% ${record.summary}${record.staleReason ? ` (stale: ${record.staleReason})` : ''}`,
105
+ }));
106
+ return {
107
+ id: 'review',
108
+ label: 'Review Queue',
109
+ header: [`records ${this.allRecords.length} review queue ${this.reviewRecords.length}`],
110
+ rows,
111
+ emptyText: 'No records in the review queue.',
112
+ hints: ['enter reviewed', 's stale', 'c contradicted', 'f fresh', 'd delete'],
113
+ };
114
+ }
115
+
116
+ buildView(): ConfigModalView {
117
+ if (!this.deps.memoryRegistry) {
118
+ return {
119
+ title: 'Memory',
120
+ degraded: `${NOT_CONFIGURED_TITLE} ${NOT_CONFIGURED_BODY}`,
121
+ tabs: [{ id: 'all', label: 'All Records', rows: [] }],
122
+ };
123
+ }
124
+ return { title: 'Memory', tabs: [this.allTab(), this.reviewTab()] };
125
+ }
126
+
127
+ onAction(id: string, ctx: ConfigModalActionContext): void {
128
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloaded memory records.'); return; }
129
+ const record = ctx.row ? this.recordFrom(ctx.row.id) : undefined;
130
+ if (!record) return;
131
+ const review = (state: string, confidence: number, reason?: string): void => {
132
+ const args = ['review', record.id, state, '--confidence', String(confidence), '--by', 'operator'];
133
+ if (reason) args.push('--reason', reason);
134
+ void ctx.executeCommand?.('recall', args);
135
+ ctx.setStatus(`Dispatched /recall review ${record.id} ${state}.`);
136
+ };
137
+ switch (id) {
138
+ case 'markReviewed': review('reviewed', Math.max(record.confidence, 85)); break;
139
+ case 'markStale': review('stale', Math.min(record.confidence, 40), 'marked stale from the memory panel'); break;
140
+ case 'markContradicted': review('contradicted', 0, 'marked contradicted from the memory panel'); break;
141
+ case 'markFresh': review('fresh', Math.max(record.confidence, 60)); break;
142
+ case 'remove': void ctx.executeCommand?.('recall', ['remove', record.id]); ctx.setStatus(`Dispatched /recall remove ${record.id}.`); break;
143
+ }
144
+ }
145
+ }
146
+
147
+ export function createMemoryModalSurface(deps: MemoryModalDeps): ConfigModalSurface {
148
+ return new MemoryModalSurface(deps);
149
+ }
150
+
151
+ /**
152
+ * Deterministic golden fixture: fixed memory records with frozen createdAt
153
+ * timestamps — no live registry, no wall-clock, no random ids.
154
+ */
155
+ export function memoryModalGoldenSurface(): ConfigModalSurface {
156
+ const FIXED_CREATED_AT = 1735689600000; // 2025-01-01T00:00:00.000Z
157
+ const records: readonly MemoryRecordLike[] = [
158
+ {
159
+ id: 'mem-0000001a', scope: 'project', cls: 'decision',
160
+ summary: 'Wave-6 batches panel retirements behind modal builders.',
161
+ detail: 'Applies to KNOWLEDGE, MEMORY, WORK-PLAN in WO-B.',
162
+ tags: ['wave-6', 'modals'], reviewState: 'reviewed', confidence: 90,
163
+ reviewedAt: FIXED_CREATED_AT + 3600000, reviewedBy: 'operator', createdAt: FIXED_CREATED_AT,
164
+ provenance: [{ kind: 'session', ref: 'session-fixed-1' }],
165
+ },
166
+ {
167
+ id: 'mem-0000002b', scope: 'session', cls: 'risk',
168
+ summary: 'Modal review actions must not call mutation APIs directly.',
169
+ tags: ['charter'], reviewState: 'stale', confidence: 35,
170
+ staleReason: 'needs re-verification against the charter doc',
171
+ createdAt: FIXED_CREATED_AT + 86400000, provenance: [],
172
+ },
173
+ ];
174
+ return createMemoryModalSurface({
175
+ memoryRegistry: {
176
+ search: () => records,
177
+ reviewQueue: () => records.filter((record) => record.reviewState !== 'reviewed'),
178
+ },
179
+ });
180
+ }
@@ -0,0 +1,54 @@
1
+ import { DEFAULT_PANEL_PALETTE } from '../polish.ts';
2
+ import type { ModalSectionStyle } from '../../renderer/modal-factory.ts';
3
+ import type { ConfigModalRow } from '../../input/config-modal-types.ts';
4
+
5
+ /**
6
+ * Shared formatting helpers for W6.1 config-modal surfaces. Row styling reuses
7
+ * DEFAULT_PANEL_PALETTE (the retired panels' palette) so a migrated modal reads
8
+ * as the same surface it replaced — the goldens stay panel-consistent.
9
+ */
10
+ export const PALETTE = DEFAULT_PANEL_PALETTE;
11
+
12
+ export type Tone = 'good' | 'bad' | 'warn' | 'info' | 'dim' | 'value';
13
+
14
+ /** Map a semantic tone to a ModalSectionStyle (foreground only). */
15
+ export function toneStyle(tone: Tone): ModalSectionStyle {
16
+ return { fg: PALETTE[tone] };
17
+ }
18
+
19
+ /** A single-glyph status dot matching the provider-console convention
20
+ * (● online · ◐ warming · ✕ error · ○ idle). */
21
+ export function statusGlyph(tone: Tone): string {
22
+ switch (tone) {
23
+ case 'good': return '●';
24
+ case 'warn': return '◐';
25
+ case 'bad': return '✕';
26
+ default: return '○';
27
+ }
28
+ }
29
+
30
+ /** Fixed-width left-justify by display columns (plain padEnd is fine for the
31
+ * ASCII/latin labels these surfaces use; wide glyphs are the status dot only,
32
+ * which we place at a fixed column). */
33
+ export function pad(text: string, width: number): string {
34
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
35
+ }
36
+
37
+ /** Build a `label value` posture cell for the header line. */
38
+ export function kv(label: string, value: string | number): string {
39
+ return `${label} ${value}`;
40
+ }
41
+
42
+ /** Join posture cells into one header line with a consistent separator. */
43
+ export function postureLine(cells: string[]): string {
44
+ return cells.join(' ');
45
+ }
46
+
47
+ /**
48
+ * A stable non-selectable informational row (section title, empty-state copy,
49
+ * or a content line the surface renders but the cursor never lands on). `id`
50
+ * must be unique within its tab so the host's live-value overlay keys off it.
51
+ */
52
+ export function infoRow(id: string, label: string, style?: ModalSectionStyle): ConfigModalRow {
53
+ return { id, label, selectable: false, ...(style ? { style } : {}) };
54
+ }
@@ -0,0 +1,42 @@
1
+ import { activeUiTones, registerThemeRefresh } from '../../renderer/theme.ts';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Semantic tone tokens for the W6.1 group-B config modals.
5
+ //
6
+ // Derived entirely from the chrome token set so the builders carry NO raw hex
7
+ // color literals (architecture: no-raw-hex-literal-growth). Rebuilt IN PLACE
8
+ // on theme flips via registerThemeRefresh — the group-B surfaces read
9
+ // MODAL_TONES by reference at render time, so a dark-pinned module const here
10
+ // left them rendering dark accents on light chrome (batch refutation
11
+ // finding 2). Mirrors the polish-core/overlay-box/modal-factory pattern.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function buildModalTones() {
15
+ const T = activeUiTones();
16
+ return {
17
+ /** Informational / accent (blue). */
18
+ info: T.state.info,
19
+ /** Positive / healthy (green). */
20
+ good: T.state.good,
21
+ /** Caution (amber). */
22
+ warn: T.state.warn,
23
+ /** Error / danger (red). */
24
+ bad: T.state.bad,
25
+ /** Reasoning / secondary accent (purple). */
26
+ reasoning: T.state.reasoning,
27
+ /** Muted secondary text. */
28
+ muted: T.fg.muted,
29
+ /** Dim tertiary text. */
30
+ dim: T.fg.dim,
31
+ /** Primary text. */
32
+ primary: T.fg.primary,
33
+ /** QR module foreground — high-contrast, mode-appropriate. */
34
+ qrDark: T.fg.inverse,
35
+ /** QR field background — high-contrast, mode-appropriate. */
36
+ qrLight: T.fg.primary,
37
+ };
38
+ }
39
+
40
+ export const MODAL_TONES: Readonly<ReturnType<typeof buildModalTones>> = buildModalTones();
41
+
42
+ registerThemeRefresh(() => Object.assign(MODAL_TONES as Record<string, string>, buildModalTones()));
@@ -0,0 +1,163 @@
1
+ import { MODAL_TONES } from './modal-theme.ts';
2
+ import { infoRow } from './modal-surface-helpers.ts';
3
+ import type {
4
+ ConfigModalActionContext,
5
+ ConfigModalRow,
6
+ ConfigModalSurface,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+ import { encodeConnectionPayload, generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // QR Code → 'pairing' config-modal surface (W6.1 group-B port). Connection
13
+ // URL/token/username(/password) plus a scannable QR block — read/navigate only.
14
+ // Token regeneration is a destructive mutation (it invalidates any live
15
+ // companion session) that the panel gated behind an in-panel confirm; per
16
+ // charter that confirm is never folded into a modal, so regenerate routes to a
17
+ // command instead. `getConnectionInfo` is a lazy thunk (loaded once on first
18
+ // buildView, cached) so a missing daemon home degrades honestly rather than
19
+ // throwing at modal-registration time.
20
+ //
21
+ // DEBT-3: the regenerate action now dispatches the real `/qrcode regenerate`
22
+ // verb (which rotates the shared operator-token store) and, once it resolves,
23
+ // RE-PULLS the lazy connection-info thunk so the modal shows the rotated token +
24
+ // QR in place without being re-opened. `confirm: true` keeps the two-press
25
+ // guard so a stray keystroke never rotates a live companion's token.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface PairingModalConnectionInfo {
29
+ readonly url: string;
30
+ readonly token: string;
31
+ readonly username: string;
32
+ readonly password?: string;
33
+ readonly version?: string;
34
+ readonly surface?: string;
35
+ }
36
+ export interface PairingModalControlPlaneSnapshot { readonly activeClientIds: readonly string[]; }
37
+ export interface PairingModalReadModel<T> { getSnapshot(): T; }
38
+
39
+ export interface PairingModalDeps {
40
+ /** Lazy connection-info provider — returns null when the daemon/companion
41
+ * token cannot be resolved (honest degraded state instead of a throw). */
42
+ readonly getConnectionInfo: () => PairingModalConnectionInfo | null;
43
+ readonly controlPlaneReadModel?: PairingModalReadModel<PairingModalControlPlaneSnapshot>;
44
+ readonly copyToClipboard?: (text: string) => void;
45
+ }
46
+
47
+ // Fixed-width placeholder — deliberately NOT derived from the real secret's
48
+ // length (mirrors QrPanel.SECRET_MASK), so masking doesn't leak length.
49
+ const SECRET_MASK = '••••••••••••';
50
+
51
+ class PairingModalSurface implements ConfigModalSurface {
52
+ readonly name = 'pairing-modal';
53
+ readonly title = 'Companion Pairing';
54
+ private info: PairingModalConnectionInfo | null | undefined = undefined;
55
+ private revealed = false;
56
+ private requestRender: () => void = () => {};
57
+
58
+ constructor(private readonly deps: PairingModalDeps) {}
59
+
60
+ readonly actions = [
61
+ { key: 'v', id: 'toggleReveal', label: 'reveal token' },
62
+ { key: 'c', id: 'copyToken', label: 'copy token', enabledFor: () => Boolean(this.deps.copyToClipboard) },
63
+ { key: 'r', id: 'regenerate', label: 'regenerate token', confirm: true },
64
+ ];
65
+
66
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; }
67
+
68
+ private ensureInfo(): PairingModalConnectionInfo | null {
69
+ if (this.info === undefined) {
70
+ try { this.info = this.deps.getConnectionInfo(); } catch { this.info = null; }
71
+ }
72
+ return this.info;
73
+ }
74
+
75
+ buildView(): ConfigModalView {
76
+ const info = this.ensureInfo();
77
+ if (!info) {
78
+ return { title: 'Companion Pairing', degraded: 'Companion pairing is unavailable — the daemon connection info could not be resolved for this session.', tabs: [{ id: 'pairing', label: 'Pairing', rows: [] }] };
79
+ }
80
+ const { url, token, username, password } = info;
81
+ const displayToken = this.revealed ? token : SECRET_MASK;
82
+ const displayPassword = password !== undefined ? (this.revealed ? password : SECRET_MASK) : undefined;
83
+
84
+ const rows: ConfigModalRow[] = [];
85
+ rows.push(infoRow('intro', 'Scan with the GoodVibes companion app to pair this session.', { dim: true }));
86
+ rows.push(infoRow('url', `URL ${url}`));
87
+ rows.push(infoRow('token', `Token ${displayToken}`, { fg: MODAL_TONES.reasoning }));
88
+ rows.push(infoRow('username', `Username ${username}`));
89
+ if (displayPassword !== undefined) rows.push(infoRow('password', `Password ${displayPassword}`));
90
+ if (this.deps.controlPlaneReadModel) {
91
+ const connected = this.deps.controlPlaneReadModel.getSnapshot().activeClientIds.length;
92
+ rows.push(infoRow('companions', `Companions connected: ${connected}`, connected > 0 ? { fg: MODAL_TONES.good } : { dim: true }));
93
+ }
94
+
95
+ // ASCII QR block — same payload shape as QrPanel.render(); the multi-line
96
+ // preformatted string is split into one non-selectable row per block row.
97
+ const payload = encodeConnectionPayload({
98
+ url, token, username,
99
+ ...(password !== undefined ? { password } : {}),
100
+ version: info.version ?? '0.0.0',
101
+ surface: info.surface ?? 'tui',
102
+ });
103
+ const qr = renderQrToString(generateQrMatrix(payload));
104
+ qr.split('\n').forEach((line, i) => rows.push(infoRow(`qr:${i}`, line, { fg: MODAL_TONES.qrDark, bg: MODAL_TONES.qrLight })));
105
+
106
+ return {
107
+ title: 'Companion Pairing',
108
+ tabs: [{ id: 'pairing', label: 'Pairing', rows, hints: [this.revealed ? 'v hide token' : 'v reveal token'] }],
109
+ };
110
+ }
111
+
112
+ onAction(id: string, ctx: ConfigModalActionContext): void {
113
+ const info = this.ensureInfo();
114
+ if (id === 'toggleReveal') { this.revealed = !this.revealed; this.requestRender(); return; }
115
+ if (id === 'copyToken') {
116
+ if (!this.deps.copyToClipboard || !info) { ctx.print('Clipboard not available.'); return; }
117
+ this.deps.copyToClipboard(info.token);
118
+ ctx.print('Token copied to clipboard.');
119
+ return;
120
+ }
121
+ if (id === 'regenerate') {
122
+ const dispatched = ctx.executeCommand?.('qrcode', ['regenerate']);
123
+ if (!dispatched) {
124
+ ctx.setStatus('Token rotation is unavailable in this runtime.');
125
+ return;
126
+ }
127
+ ctx.setStatus('Rotating pairing token…');
128
+ void dispatched
129
+ .then((ok) => {
130
+ if (ok === false) { ctx.setStatus('Token rotation did not complete.'); return; }
131
+ // Re-pull the lazy connection-info thunk: /qrcode regenerate rewrote
132
+ // the shared token store, so a fresh getConnectionInfo() reflects the
133
+ // rotated token + QR. ensureInfo caches on first build — clear it.
134
+ this.info = undefined;
135
+ this.revealed = false;
136
+ this.ensureInfo();
137
+ this.requestRender();
138
+ ctx.setStatus('Pairing token rotated — new token and QR loaded.');
139
+ })
140
+ .catch(() => { ctx.setStatus('Token rotation failed.'); });
141
+ }
142
+ }
143
+ }
144
+
145
+ export function createPairingModalSurface(deps: PairingModalDeps): ConfigModalSurface {
146
+ return new PairingModalSurface(deps);
147
+ }
148
+
149
+ /**
150
+ * Deterministic golden fixture: a frozen connectionInfo literal (no real
151
+ * token/QR generation call — encodeConnectionPayload/generateQrMatrix are pure
152
+ * functions of this literal) and a frozen control-plane snapshot.
153
+ */
154
+ export function pairingModalGoldenSurface(): ConfigModalSurface {
155
+ const connectionInfo: PairingModalConnectionInfo = {
156
+ url: 'http://192.168.1.50:3141', token: 'golden-token-0123456789abcdef', username: 'golden-user', password: 'golden-pass', version: '0.0.0', surface: 'tui',
157
+ };
158
+ return createPairingModalSurface({
159
+ getConnectionInfo: () => connectionInfo,
160
+ controlPlaneReadModel: { getSnapshot: () => ({ activeClientIds: ['golden-client-1'] }) },
161
+ copyToClipboard: () => {},
162
+ });
163
+ }