@pellux/goodvibes-tui 1.16.1 → 1.18.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 (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -0,0 +1,161 @@
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 type { MintedPairingToken, PublicPairingToken } from '@pellux/goodvibes-sdk/platform/pairing';
10
+ import { formatDeviceLine, shortTokenId } from '../../core/pairing-devices.ts';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Paired Devices — the settings security-domain device/token management surface.
14
+ // Lists every per-device pairing token (name · created · last-seen · id) and
15
+ // mirrors the pairing.tokens.* gateway verbs: revoke a device, migrate off the
16
+ // legacy shared token, revoke the shared token. Rename needs a name, which the
17
+ // stable-layout modal cannot capture inline, so it routes to the /devices rename
18
+ // command with the row's id pre-filled in the guidance. buildView reads list()
19
+ // fresh every render, so a revoke/migrate reflects immediately.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** The token-store capability this surface needs — satisfied by PairingTokenManager. */
23
+ export interface DevicesModalPairingTokens {
24
+ list(): PublicPairingToken[];
25
+ rename(id: string, name: string): boolean;
26
+ revoke(id: string): boolean;
27
+ mintForMigration(input: { readonly name: string }): MintedPairingToken;
28
+ isLegacyRevoked(): boolean;
29
+ revokeLegacyShared(): void;
30
+ }
31
+
32
+ export interface DevicesModalDeps {
33
+ readonly pairingTokens: DevicesModalPairingTokens;
34
+ /** Injectable clock so last-seen formatting is deterministic in tests. */
35
+ readonly now?: () => number;
36
+ readonly copyToClipboard?: (text: string) => void;
37
+ }
38
+
39
+ const DEVICE_ROW_PREFIX = 'device:';
40
+
41
+ function deviceIdOf(row: ConfigModalRow | null): string | null {
42
+ return row && row.id.startsWith(DEVICE_ROW_PREFIX) ? row.id.slice(DEVICE_ROW_PREFIX.length) : null;
43
+ }
44
+
45
+ class DevicesModalSurface implements ConfigModalSurface {
46
+ readonly name = 'devices-modal';
47
+ readonly title = 'Paired Devices';
48
+ private requestRender: () => void = () => {};
49
+
50
+ constructor(private readonly deps: DevicesModalDeps) {}
51
+
52
+ readonly actions = [
53
+ { key: 'r', id: 'rename', label: 'rename', enabledFor: (row: ConfigModalRow | null) => deviceIdOf(row) !== null },
54
+ { key: 'x', id: 'revoke', label: 'revoke', confirm: true, enabledFor: (row: ConfigModalRow | null) => deviceIdOf(row) !== null },
55
+ { key: 'm', id: 'migrateShared', label: 'migrate off shared token' },
56
+ { key: 's', id: 'revokeShared', label: 'revoke shared token', confirm: true, enabledFor: () => !this.deps.pairingTokens.isLegacyRevoked() },
57
+ ];
58
+
59
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; }
60
+
61
+ buildView(): ConfigModalView {
62
+ const now = this.deps.now?.() ?? Date.now();
63
+ const tokens = this.deps.pairingTokens.list();
64
+ const rows: ConfigModalRow[] = [];
65
+ rows.push(infoRow('intro', 'Devices paired to this daemon. Each has its own token — revoke one without affecting the others.', { dim: true }));
66
+
67
+ if (tokens.length === 0) {
68
+ rows.push(infoRow('empty', 'No devices paired yet — run /pair (or open the pairing QR) to add one.'));
69
+ } else {
70
+ for (const token of tokens) {
71
+ rows.push({ id: `${DEVICE_ROW_PREFIX}${token.id}`, label: formatDeviceLine(token, now) });
72
+ }
73
+ }
74
+
75
+ const legacyRevoked = this.deps.pairingTokens.isLegacyRevoked();
76
+ rows.push(infoRow(
77
+ 'legacy',
78
+ legacyRevoked
79
+ ? 'Legacy shared token: revoked.'
80
+ : 'Legacy shared token: active — migrate a device off it (m), then revoke it (s).',
81
+ legacyRevoked ? { dim: true } : { fg: MODAL_TONES.reasoning },
82
+ ));
83
+
84
+ return {
85
+ title: 'Paired Devices',
86
+ tabs: [{ id: 'devices', label: 'Devices', rows, hints: ['r rename', 'x revoke', 'm migrate off shared', 's revoke shared'] }],
87
+ };
88
+ }
89
+
90
+ onAction(id: string, ctx: ConfigModalActionContext): void {
91
+ const tokens = this.deps.pairingTokens;
92
+
93
+ if (id === 'rename') {
94
+ const deviceId = deviceIdOf(ctx.row);
95
+ if (!deviceId) { ctx.setStatus('Select a device row first.'); return; }
96
+ // The stable-layout modal cannot capture a name inline — route to the
97
+ // command with the id pre-filled so the user only types the new name.
98
+ ctx.print(`To rename this device, run: /devices rename ${shortTokenId(deviceId)} <new name>`);
99
+ return;
100
+ }
101
+
102
+ if (id === 'revoke') {
103
+ const deviceId = deviceIdOf(ctx.row);
104
+ if (!deviceId) { ctx.setStatus('Select a device row first.'); return; }
105
+ const record = tokens.list().find((t) => t.id === deviceId);
106
+ const revoked = tokens.revoke(deviceId);
107
+ ctx.setStatus(revoked ? `Revoked "${record?.name ?? shortTokenId(deviceId)}" — its token stops working now.` : 'That device was already absent.');
108
+ this.requestRender();
109
+ return;
110
+ }
111
+
112
+ if (id === 'migrateShared') {
113
+ const minted = tokens.mintForMigration({ name: 'migrated device' });
114
+ if (this.deps.copyToClipboard) {
115
+ this.deps.copyToClipboard(minted.token);
116
+ ctx.print(`Minted "${minted.name}" and copied its one-time token to the clipboard. Pair the device, then revoke the shared token (s).`);
117
+ } else {
118
+ ctx.print(`Minted "${minted.name}" (one-time token): ${minted.token}\nPair the device with it, then revoke the shared token (s).`);
119
+ }
120
+ this.requestRender();
121
+ return;
122
+ }
123
+
124
+ if (id === 'revokeShared') {
125
+ if (tokens.isLegacyRevoked()) { ctx.setStatus('The legacy shared token is already revoked.'); return; }
126
+ tokens.revokeLegacyShared();
127
+ ctx.setStatus('Revoked the legacy shared token — devices on it must re-pair with their own token.');
128
+ this.requestRender();
129
+ return;
130
+ }
131
+ }
132
+ }
133
+
134
+ export function createDevicesModalSurface(deps: DevicesModalDeps): ConfigModalSurface {
135
+ return new DevicesModalSurface(deps);
136
+ }
137
+
138
+ /**
139
+ * Deterministic golden/render fixture: frozen token list + clock so last-seen
140
+ * formatting is stable.
141
+ */
142
+ export function devicesModalGoldenSurface(): ConfigModalSurface {
143
+ const now = Date.UTC(2026, 6, 13, 12, 0, 0);
144
+ const tokens: PublicPairingToken[] = [
145
+ { id: 'aaaa1111bbbb2222', name: 'my laptop', createdAt: Date.UTC(2026, 6, 10, 9, 0, 0), lastSeenAt: now - 3 * 60_000 },
146
+ { id: 'cccc3333dddd4444', name: 'kitchen tablet', createdAt: Date.UTC(2026, 6, 1, 9, 0, 0), lastSeenAt: now - 5 * 60 * 60_000 },
147
+ { id: 'eeee5555ffff6666', name: 'phone (never scanned)', createdAt: Date.UTC(2026, 6, 13, 8, 0, 0) },
148
+ ];
149
+ let legacyRevoked = false;
150
+ return createDevicesModalSurface({
151
+ now: () => now,
152
+ pairingTokens: {
153
+ list: () => tokens,
154
+ rename: () => true,
155
+ revoke: () => true,
156
+ mintForMigration: () => ({ id: 'new', name: 'migrated device', token: 'tok', createdAt: now }),
157
+ isLegacyRevoked: () => legacyRevoked,
158
+ revokeLegacyShared: () => { legacyRevoked = true; },
159
+ },
160
+ });
161
+ }
@@ -16,7 +16,7 @@ import type {
16
16
  } from '../../input/config-modal-types.ts';
17
17
 
18
18
  // ---------------------------------------------------------------------------
19
- // Hooks → config-modal surface (W6.1 group-B port). Two tabs: 'Hooks' (active
19
+ // Hooks → config-modal surface (group-B port). Two tabs: 'Hooks' (active
20
20
  // registrations + contract posture) and 'Activity' (recent runtime activity +
21
21
  // managed authoring + last simulation — the panel's expand-activity view, now a
22
22
  // dedicated tab). All sources are in-memory (dispatcher/workbench/tracker hold
@@ -9,7 +9,7 @@ import type {
9
9
  import { KeybindingsManager } from '../../input/keybindings.ts';
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // Docs + Shortcuts → 'keybindings' config-modal surface (W6.1 group-B port).
12
+ // Docs + Shortcuts → 'keybindings' config-modal surface (group-B port).
13
13
  // A MERGE of the retired DocsPanel (tools/models/shortcuts) and the
14
14
  // shortcuts-overlay reference. Three tabs: 'Tools', 'Models', and 'Shortcuts'
15
15
  // (the categorized keyboard reference — driven live off keybindingsManager so
@@ -112,7 +112,7 @@ class KeybindingsModalSurface implements ConfigModalSurface {
112
112
  if (id === 'refresh') { ctx.setStatus('Docs & shortcuts are read live.'); ctx.requestRender(); return; }
113
113
  if (id !== 'activate') return;
114
114
  if (ctx.tabId === 'tools') {
115
- // DEBT-5 item 4: pass the tool name through as a deep-link target.
115
+ // item 4: pass the tool name through as a deep-link target.
116
116
  // Honest caveat (documented, not a bug to silently paper over): fleet's
117
117
  // ProcessKind set has no 'tool' node — a docs Tools row names a static
118
118
  // tool DEFINITION, not a live process, so FleetPanel.receiveDeepLink
@@ -9,7 +9,7 @@ import type {
9
9
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // Knowledge (graph) → config-modal surface (W6.1 group-B port). Two tabs:
12
+ // Knowledge (graph) → config-modal surface (group-B port). Two tabs:
13
13
  // 'Browse' (sources/nodes/issues + schedules) and 'Review' (the open-issue
14
14
  // queue) — the panel's browse/review toggle, now host tabs. Issue review
15
15
  // mutations (accept/reject/resolve/reopen) route to the `/knowledge
@@ -38,14 +38,14 @@ interface MarketplaceRow {
38
38
  const KINDS: readonly EcosystemEntryKind[] = ['plugin', 'skill', 'hook-pack', 'policy-pack'];
39
39
 
40
40
  /**
41
- * Marketplace → config-modal surface (W6.1 group-B port). Local-first
41
+ * Marketplace → config-modal surface (group-B port). Local-first
42
42
  * publish/import catalog (NOT a remote store) with provenance, compatibility,
43
43
  * and install posture. Disk catalog loads happen in refresh() (never in
44
44
  * buildView), mirroring the panel's explicit render()/refresh() split.
45
45
  * Install/uninstall route to the `/marketplace` command path (charter: no
46
46
  * destructive confirm folded into a modal).
47
47
  *
48
- * B30 (honest empty-state): the panel's original "No curated marketplace
48
+ * Honest empty-state: the panel's original "No curated marketplace
49
49
  * entries found yet." copy implied a curated remote catalog. It isn't one —
50
50
  * loadEcosystemCatalog only ever reads .goodvibes/ecosystem/<kind>s.json under
51
51
  * the project/home, populated solely by `/marketplace publish` and
@@ -118,7 +118,7 @@ class MarketplaceModalSurface implements ConfigModalSurface {
118
118
  const snapshot = this.deps.readModel?.getSnapshot();
119
119
  const rows: ConfigModalRow[] = [];
120
120
 
121
- // Honest empty / degraded state (B30). loadError → degraded banner.
121
+ // Honest empty / degraded state. loadError → degraded banner.
122
122
  if (this.loadError) {
123
123
  return { title: 'Marketplace', degraded: this.loadError, tabs: [{ id: 'catalog', label: 'Catalog', rows: [] }] };
124
124
  }
@@ -205,7 +205,7 @@ export function createMarketplaceModalSurface(deps: MarketplaceModalDeps): Confi
205
205
  * Deterministic golden fixture: catalog roots wired at a fresh tmp path that is
206
206
  * removed immediately (loadEcosystemCatalog/listInstalledEcosystemEntries guard
207
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
208
+ * honest empty-state copy). The random tmp path never appears in the
209
209
  * rendered lines (the empty-state copy is static), so the golden is byte-stable.
210
210
  */
211
211
  export function marketplaceModalGoldenSurface(): ConfigModalSurface {
@@ -7,9 +7,11 @@ import type {
7
7
  ConfigModalView,
8
8
  } from '../../input/config-modal-types.ts';
9
9
  import { memoryRecordTemporalStatus } from '@pellux/goodvibes-sdk/platform/state';
10
+ import type { MemoryConsolidationGatewayResolution, MemoryConsolidationProposal } from '../memory-consolidation-gateway.ts';
11
+ import { classifyConsolidationFetchError } from '../memory-consolidation-gateway.ts';
10
12
 
11
13
  // ---------------------------------------------------------------------------
12
- // Memory → config-modal surface (W6.1 group-B port). Two tabs: 'All Records'
14
+ // Memory → config-modal surface (group-B port). Two tabs: 'All Records'
13
15
  // (the full record list) and 'Review Queue' (the operator review queue) — the
14
16
  // same split the panel toggled with Tab, now real host tabs. Review-state
15
17
  // mutations (reviewed/stale/contradicted/fresh) and record delete route to the
@@ -63,8 +65,23 @@ export interface MemoryModalDeps {
63
65
  readonly indexUnavailableReason?: string | null | undefined;
64
66
  }>;
65
67
  };
68
+ /**
69
+ * Resolve the memory-consolidation-receipts gateway (memory-consolidation-gateway.ts),
70
+ * lazily — called fresh each time the Proposals tab (re)fetches, exactly like
71
+ * the Fleet panel's `resolveGateway` (fleet-gateway.ts / fleet-acts.ts). Absent
72
+ * in a session with no daemon-backed control plane wired: the Proposals tab
73
+ * then renders its "unavailable" honest state without ever attempting a fetch.
74
+ */
75
+ readonly resolveConsolidationGateway?: () => MemoryConsolidationGatewayResolution;
66
76
  }
67
77
 
78
+ /** The Proposals tab's fetch state — one honest rendering per state, never a blend. */
79
+ type ProposalsStatus =
80
+ | { readonly kind: 'loading' }
81
+ | { readonly kind: 'unavailable'; readonly reason: string }
82
+ | { readonly kind: 'error'; readonly message: string }
83
+ | { readonly kind: 'ready'; readonly proposals: readonly MemoryConsolidationProposal[] };
84
+
68
85
  /**
69
86
  * Client-side port of the SDK's MemoryStore.reviewQueue ranking
70
87
  * (memory-store-helpers.ts reviewQueueScore/isReviewCandidate): all four
@@ -104,11 +121,14 @@ class MemoryModalSurface implements ConfigModalSurface {
104
121
  private reviewRecords: MemoryRecordLike[] = [];
105
122
  /** Honest note on the last read: a wire failure (client mode) or the index-unavailable fallback reason — never silently dropped. */
106
123
  private loadNote: string | null = null;
124
+ /** The Proposals tab's fetch state — starts 'loading' whenever a gateway is wired (see refreshProposals). */
125
+ private proposalsStatus: ProposalsStatus = { kind: 'loading' };
107
126
  private requestRender: (() => void) | null = null;
108
127
 
109
128
  constructor(private readonly deps: MemoryModalDeps) {}
110
129
 
111
130
  private readonly reviewGate = (row: ConfigModalRow | null, tabId: string): boolean => tabId === 'review' && row !== null;
131
+ private readonly proposalsGate = (row: ConfigModalRow | null, tabId: string): boolean => tabId === 'proposals' && row !== null;
112
132
 
113
133
  readonly actions = [
114
134
  { key: 'enter', id: 'markReviewed', label: 'reviewed', enabledFor: this.reviewGate },
@@ -117,6 +137,13 @@ class MemoryModalSurface implements ConfigModalSurface {
117
137
  { key: 'f', id: 'markFresh', label: 'fresh', enabledFor: this.reviewGate },
118
138
  { key: 'd', id: 'remove', label: 'delete', enabledFor: (row: ConfigModalRow | null) => row !== null },
119
139
  { key: 'r', id: 'refresh', label: 'refresh' },
140
+ // Jump from a selected proposal to one of its affected records in the
141
+ // Review Queue tab. A DISTINCT key from 'enter' (not shared with
142
+ // markReviewed): ConfigModal.resolveAction matches the FIRST action bound
143
+ // to a key and returns null if THAT one's enabledFor fails, rather than
144
+ // trying the next action bound to the same key — so gating two different
145
+ // 'enter' actions by tabId alone would silently break one of them.
146
+ { key: 'v', id: 'jumpToReviewQueue', label: 'view in review queue', enabledFor: this.proposalsGate },
120
147
  ];
121
148
 
122
149
  onOpen(requestRender: () => void): void {
@@ -129,6 +156,10 @@ class MemoryModalSurface implements ConfigModalSurface {
129
156
  }
130
157
 
131
158
  private async refresh(): Promise<void> {
159
+ await Promise.all([this.refreshRecords(), this.refreshProposals()]);
160
+ }
161
+
162
+ private async refreshRecords(): Promise<void> {
132
163
  if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; this.loadNote = null; return; }
133
164
  try {
134
165
  const result = await this.deps.memoryRegistry.honestSearch({ limit: 100 });
@@ -142,10 +173,50 @@ class MemoryModalSurface implements ConfigModalSurface {
142
173
  this.requestRender?.();
143
174
  }
144
175
 
176
+ /**
177
+ * Lazy fetch of memory.consolidation.receipts (memory-consolidation-gateway.ts)
178
+ * — only attempted when this surface opens or refreshes, never polled. No
179
+ * gateway resolver wired at all (session has no daemon-backed control
180
+ * plane) renders the same honest "unavailable" state as a resolved-but-
181
+ * refused gateway (daemon disabled / no control-plane URL) or a 501/404
182
+ * from an older daemon with no consolidation scheduler — three different
183
+ * causes, one bucket, because from the operator's seat they mean the same
184
+ * thing: nothing to show, and no fault of theirs.
185
+ */
186
+ private async refreshProposals(): Promise<void> {
187
+ if (!this.deps.resolveConsolidationGateway) {
188
+ this.proposalsStatus = { kind: 'unavailable', reason: 'No consolidation gateway wired for this session.' };
189
+ return;
190
+ }
191
+ this.proposalsStatus = { kind: 'loading' };
192
+ this.requestRender?.();
193
+ const resolution = this.deps.resolveConsolidationGateway();
194
+ if (!resolution.available) {
195
+ this.proposalsStatus = { kind: 'unavailable', reason: resolution.reason };
196
+ return;
197
+ }
198
+ try {
199
+ const result = await resolution.gateway.fetchReceipts();
200
+ this.proposalsStatus = { kind: 'ready', proposals: result.pendingProposals };
201
+ } catch (error) {
202
+ this.proposalsStatus = classifyConsolidationFetchError(error);
203
+ }
204
+ }
205
+
145
206
  private recordFrom(id: string): MemoryRecordLike | undefined {
146
207
  return this.allRecords.find((r) => r.id === id) ?? this.reviewRecords.find((r) => r.id === id);
147
208
  }
148
209
 
210
+ /** First pending proposal that names a given record id, keyed for the Review Queue's reason correlation (item 3). */
211
+ private proposalsByRecordId(): Map<string, MemoryConsolidationProposal> {
212
+ const map = new Map<string, MemoryConsolidationProposal>();
213
+ if (this.proposalsStatus.kind !== 'ready') return map;
214
+ for (const proposal of this.proposalsStatus.proposals) {
215
+ for (const id of proposal.ids) if (!map.has(id)) map.set(id, proposal);
216
+ }
217
+ return map;
218
+ }
219
+
149
220
  private allTab(): ConfigModalTab {
150
221
  const rows: ConfigModalRow[] = this.allRecords.map((record) => ({
151
222
  id: record.id,
@@ -162,10 +233,25 @@ class MemoryModalSurface implements ConfigModalSurface {
162
233
  }
163
234
 
164
235
  private reviewTab(): ConfigModalTab {
165
- const rows: ConfigModalRow[] = this.reviewRecords.map((record) => ({
166
- id: record.id,
167
- label: `${record.reviewState.padEnd(13)} ${String(record.confidence).padStart(3)}% ${record.summary}${record.staleReason ? ` (stale: ${record.staleReason})` : ''}${temporalSuffix(record)}`,
168
- }));
236
+ const proposalByRecord = this.proposalsByRecordId();
237
+ const rows: ConfigModalRow[] = this.reviewRecords.map((record) => {
238
+ // A contradiction proposal already carries its reason on the record
239
+ // itself (the daemon sets reviewState 'contradicted' + staleReason); a
240
+ // cross-scope-duplicate proposal marks the record 'fresh' with NO
241
+ // staleReason, so without this correlation those rows are bare and
242
+ // unexplained (item 3) — fall back to the fetched proposal's own
243
+ // reason only when the record didn't already carry one.
244
+ const proposal = record.staleReason ? undefined : proposalByRecord.get(record.id);
245
+ const reasonSuffix = record.staleReason
246
+ ? ` (stale: ${record.staleReason})`
247
+ : proposal
248
+ ? ` (${proposal.kind}: ${proposal.reason})`
249
+ : '';
250
+ return {
251
+ id: record.id,
252
+ label: `${record.reviewState.padEnd(13)} ${String(record.confidence).padStart(3)}% ${record.summary}${reasonSuffix}${temporalSuffix(record)}`,
253
+ };
254
+ });
169
255
  return {
170
256
  id: 'review',
171
257
  label: 'Review Queue',
@@ -176,6 +262,40 @@ class MemoryModalSurface implements ConfigModalSurface {
176
262
  };
177
263
  }
178
264
 
265
+ /**
266
+ * The Proposals tab — WHAT the daemon's idle-time consolidation pass
267
+ * proposed (contradiction / cross-scope-duplicate / stale-delete) and is
268
+ * holding for human judgment, read lazily over memory.consolidation.receipts
269
+ * (memory-consolidation-gateway.ts). Three distinct honest states render as
270
+ * three distinct lines — 'loading' / 'unavailable' / 'error' never collapse
271
+ * into a shared "nothing to show" — plus the true empty state ('ready' with
272
+ * zero proposals) which is a fourth, equally honest line.
273
+ */
274
+ private proposalsTab(): ConfigModalTab {
275
+ const status = this.proposalsStatus;
276
+ const header = [`pending proposals ${status.kind === 'ready' ? status.proposals.length : 0}`];
277
+ if (status.kind === 'loading') {
278
+ return { id: 'proposals', label: 'Proposals', header, rows: [], emptyText: 'Loading pending proposals…' };
279
+ }
280
+ if (status.kind === 'unavailable') {
281
+ return { id: 'proposals', label: 'Proposals', header, rows: [], emptyText: `Pending proposals unavailable: ${status.reason}` };
282
+ }
283
+ if (status.kind === 'error') {
284
+ return { id: 'proposals', label: 'Proposals', header, rows: [], emptyText: `Could not fetch pending proposals: ${status.message}` };
285
+ }
286
+ const rows: ConfigModalRow[] = status.proposals.map((proposal, index) => ({
287
+ id: `proposal:${index}`,
288
+ label: `[${proposal.kind}] ${proposal.reason} — ${proposal.ids.length} record${proposal.ids.length === 1 ? '' : 's'}: ${proposal.ids.join(', ')}`,
289
+ }));
290
+ return {
291
+ id: 'proposals',
292
+ label: 'Proposals',
293
+ header,
294
+ rows,
295
+ emptyText: 'No pending proposals — nothing awaiting judgment right now.',
296
+ };
297
+ }
298
+
179
299
  buildView(): ConfigModalView {
180
300
  if (!this.deps.memoryRegistry) {
181
301
  return {
@@ -187,12 +307,13 @@ class MemoryModalSurface implements ConfigModalSurface {
187
307
  return {
188
308
  title: 'Memory',
189
309
  ...(this.loadNote ? { degraded: this.loadNote } : {}),
190
- tabs: [this.allTab(), this.reviewTab()],
310
+ tabs: [this.allTab(), this.reviewTab(), this.proposalsTab()],
191
311
  };
192
312
  }
193
313
 
194
314
  onAction(id: string, ctx: ConfigModalActionContext): void {
195
315
  if (id === 'refresh') { void this.refresh(); ctx.setStatus('Reloading memory records...'); return; }
316
+ if (id === 'jumpToReviewQueue') { this.handleJumpToReviewQueue(ctx); return; }
196
317
  const record = ctx.row ? this.recordFrom(ctx.row.id) : undefined;
197
318
  if (!record) return;
198
319
  const review = (state: string, confidence: number, reason?: string): void => {
@@ -209,6 +330,29 @@ class MemoryModalSurface implements ConfigModalSurface {
209
330
  case 'remove': void ctx.executeCommand?.('recall', ['remove', record.id]); ctx.setStatus(`Dispatched /recall remove ${record.id}.`); break;
210
331
  }
211
332
  }
333
+
334
+ /**
335
+ * The Proposals tab's "jump": move to the Review Queue tab with one of the
336
+ * selected proposal's affected records selected — the same host mechanism
337
+ * (`ctx.jumpToRow`) any future in-surface jump would use. A proposal's
338
+ * `ids` are not guaranteed to all be inside the review queue's top-24
339
+ * window, so this picks the first id that IS present there rather than
340
+ * assuming the first id always is; when none are, it says so honestly
341
+ * instead of jumping nowhere and leaving the user to wonder why.
342
+ */
343
+ private handleJumpToReviewQueue(ctx: ConfigModalActionContext): void {
344
+ if (this.proposalsStatus.kind !== 'ready' || !ctx.row) return;
345
+ const index = Number(ctx.row.id.slice('proposal:'.length));
346
+ const proposal = this.proposalsStatus.proposals[index];
347
+ if (!proposal) return;
348
+ const target = proposal.ids.find((id) => this.reviewRecords.some((r) => r.id === id));
349
+ if (!target) {
350
+ ctx.setStatus(`None of this proposal's ${proposal.ids.length} record(s) are in the current review-queue window.`);
351
+ return;
352
+ }
353
+ ctx.jumpToRow?.('review', target);
354
+ ctx.setStatus(`Jumped to ${target} in the Review Queue.`);
355
+ }
212
356
  }
213
357
 
214
358
  export function createMemoryModalSurface(deps: MemoryModalDeps): ConfigModalSurface {
@@ -227,9 +371,9 @@ export async function memoryModalGoldenSurface(): Promise<ConfigModalSurface> {
227
371
  const records: readonly MemoryRecordLike[] = [
228
372
  {
229
373
  id: 'mem-0000001a', scope: 'project', cls: 'decision',
230
- summary: 'Wave-6 batches panel retirements behind modal builders.',
231
- detail: 'Applies to KNOWLEDGE, MEMORY, WORK-PLAN in WO-B.',
232
- tags: ['wave-6', 'modals'], reviewState: 'reviewed', confidence: 90,
374
+ summary: 'Panel retirements are batched behind modal builders.',
375
+ detail: 'Applies to the knowledge, memory, and work-plan modals.',
376
+ tags: ['panels', 'modals'], reviewState: 'reviewed', confidence: 90,
233
377
  reviewedAt: FIXED_CREATED_AT + 3600000, reviewedBy: 'operator', createdAt: FIXED_CREATED_AT,
234
378
  provenance: [{ kind: 'session', ref: 'session-fixed-1' }],
235
379
  },
@@ -3,7 +3,7 @@ import type { ModalSectionStyle } from '../../renderer/modal-factory.ts';
3
3
  import type { ConfigModalRow } from '../../input/config-modal-types.ts';
4
4
 
5
5
  /**
6
- * Shared formatting helpers for W6.1 config-modal surfaces. Row styling reuses
6
+ * Shared formatting helpers for config-modal surfaces. Row styling reuses
7
7
  * DEFAULT_PANEL_PALETTE (the retired panels' palette) so a migrated modal reads
8
8
  * as the same surface it replaced — the goldens stay panel-consistent.
9
9
  */
@@ -1,7 +1,7 @@
1
1
  import { activeUiTones, registerThemeRefresh } from '../../renderer/theme.ts';
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // Semantic tone tokens for the W6.1 group-B config modals.
4
+ // Semantic tone tokens for the group-B config modals.
5
5
  //
6
6
  // Derived entirely from the chrome token set so the builders carry NO raw hex
7
7
  // color literals (architecture: no-raw-hex-literal-growth). Rebuilt IN PLACE