@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,266 @@
1
+ // ---------------------------------------------------------------------------
2
+ // fleet-spawn.ts
3
+ //
4
+ // The Fleet panel's ACP spawn affordance: 'n' on the fleet surface lists the
5
+ // third-party coding agents the daemon discovered (acp.agents.list — read-only,
6
+ // quiet when none), you pick one, then pick a working directory from the known
7
+ // candidates (the registered workspaces + the current dir — no free-text path
8
+ // retyping where a known dir exists), and acp.sessions.create hosts it as a
9
+ // long-lived daemon session. The new row shows up on the next fleet snapshot as
10
+ // kind 'acp-agent' and steers/stops/flags-for-attention like any other row.
11
+ //
12
+ // A structured spawn failure ({binary, stage, message}) is surfaced verbatim —
13
+ // never a hung row: the daemon bounds the handshake and returns a 'failed'
14
+ // record, and this controller reports its three honest fields.
15
+ //
16
+ // The controller owns the fleet view + input while a pick is in flight (mirrors
17
+ // FleetActs' pick mode), so fleet-panel.ts stays a thin delegator under the
18
+ // architecture line cap. The gateway is injectable so the flow round-trips
19
+ // against a mocked daemon in tests; the live builder is createAcpSpawnGateway.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
23
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
24
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
25
+ import type { Line } from '../types/grid.ts';
26
+ import { resolveOperatorRpc } from '../input/commands/operator-rpc.ts';
27
+ import {
28
+ buildKeyboardHints,
29
+ buildPanelWorkspace,
30
+ buildPanelLine,
31
+ DEFAULT_PANEL_PALETTE,
32
+ type PanelPalette,
33
+ } from './polish.ts';
34
+
35
+ /** One discovered third-party ACP agent (acp.agents.list). */
36
+ export type AcpDiscoveredAgent = OperatorMethodOutput<'acp.agents.list'>['agents'][number];
37
+ /** acp.sessions.create output — the hosted record + whether it started. */
38
+ export type AcpSpawnResult = OperatorMethodOutput<'acp.sessions.create'>;
39
+ /** One registered workspace root (workspaces.registrations.list). */
40
+ export type AcpWorkspaceRegistration = OperatorMethodOutput<'workspaces.registrations.list'>['workspaces'][number];
41
+
42
+ /** A working-directory candidate offered in the picker: a known dir, never retyped. */
43
+ export interface AcpDirCandidate {
44
+ readonly path: string;
45
+ /** A short human label (a workspace label, or "current directory"). */
46
+ readonly label: string;
47
+ }
48
+
49
+ /**
50
+ * The async verb surface the spawn flow drives — real daemon round-trips in
51
+ * production (createAcpSpawnGateway), a mocked shape in tests.
52
+ */
53
+ export interface AcpSpawnGateway {
54
+ /** Discovered third-party agents (read-only; empty is a quiet, honest absence). */
55
+ listAgents(): Promise<readonly AcpDiscoveredAgent[]>;
56
+ /** The registered workspace roots — the known-dir candidates for the picker. */
57
+ listWorkspaces(): Promise<readonly AcpWorkspaceRegistration[]>;
58
+ /** Host a discovered agent in a directory as a long-lived daemon session. */
59
+ createSession(input: { readonly agentId: string; readonly cwd: string }): Promise<AcpSpawnResult>;
60
+ }
61
+
62
+ export type AcpSpawnGatewayResolution =
63
+ | { readonly available: true; readonly gateway: AcpSpawnGateway }
64
+ | { readonly available: false; readonly reason: string };
65
+
66
+ export interface AcpSpawnGatewayDeps {
67
+ readonly configManager: ConfigManager;
68
+ readonly homeDirectory: string | (() => string);
69
+ }
70
+
71
+ /**
72
+ * Build the live ACP spawn gateway over the generic operator invoke path — the
73
+ * same daemon resolution the fleet acts use. Honest unavailable reason when no
74
+ * daemon is reachable, so the spawn key refuses cleanly instead of throwing.
75
+ */
76
+ export function createAcpSpawnGateway(deps: AcpSpawnGatewayDeps): AcpSpawnGatewayResolution {
77
+ const rpc = resolveOperatorRpc(deps);
78
+ if (!rpc.available) return { available: false, reason: rpc.reason };
79
+ const { sdk } = rpc;
80
+ const gateway: AcpSpawnGateway = {
81
+ listAgents: async () => (await sdk.operator.invoke('acp.agents.list', {})).agents,
82
+ listWorkspaces: async () => (await sdk.operator.invoke('workspaces.registrations.list', {})).workspaces,
83
+ createSession: (input) => sdk.operator.invoke('acp.sessions.create', input),
84
+ };
85
+ return { available: true, gateway };
86
+ }
87
+
88
+ export interface FleetSpawnDeps {
89
+ /** Resolve a live gateway per action (a daemon that comes up mid-session is seen). */
90
+ readonly resolveGateway: () => AcpSpawnGatewayResolution;
91
+ /** The current working directory — always a candidate dir (labeled "current directory"). */
92
+ readonly currentDirectory: () => string;
93
+ /** Surface a result/receipt/error line to the operator (system message). */
94
+ readonly notify: (message: string) => void;
95
+ /** Repaint request when the controller's own mode state changes. */
96
+ readonly markDirty: () => void;
97
+ }
98
+
99
+ /** Pick a third-party agent to host. */
100
+ interface AgentStep {
101
+ readonly step: 'agent';
102
+ readonly agents: readonly AcpDiscoveredAgent[];
103
+ index: number;
104
+ }
105
+ /** Pick the working directory the hosted agent runs in. */
106
+ interface DirStep {
107
+ readonly step: 'dir';
108
+ readonly agent: AcpDiscoveredAgent;
109
+ readonly candidates: readonly AcpDirCandidate[];
110
+ index: number;
111
+ }
112
+ type SpawnMode = AgentStep | DirStep | null;
113
+
114
+ const C = DEFAULT_PANEL_PALETTE;
115
+
116
+ export class FleetSpawn {
117
+ private mode: SpawnMode = null;
118
+ /** True while a create round-trip is in flight (absorb keys, show a spinner line). */
119
+ private creating = false;
120
+
121
+ public constructor(private readonly deps: FleetSpawnDeps) {}
122
+
123
+ /** True while the spawn picker owns the fleet view + input. */
124
+ public spawnModeActive(): boolean {
125
+ return this.mode !== null;
126
+ }
127
+
128
+ /**
129
+ * Begin the spawn flow: list discovered agents. Quiet, honest absence when the
130
+ * daemon reports none (no mode entered) rather than an empty picker.
131
+ */
132
+ public async begin(): Promise<void> {
133
+ if (this.mode !== null || this.creating) return;
134
+ const gateway = this.requireGateway();
135
+ if (!gateway) return;
136
+ let agents: readonly AcpDiscoveredAgent[];
137
+ try {
138
+ agents = await gateway.listAgents();
139
+ } catch (err) {
140
+ this.deps.notify(`Could not list ACP agents: ${summarizeError(err)}`);
141
+ return;
142
+ }
143
+ if (agents.length === 0) {
144
+ this.deps.notify('No third-party ACP agents discovered (Claude Code, Codex, opencode). Install one on PATH to host it here.');
145
+ return;
146
+ }
147
+ this.mode = { step: 'agent', agents, index: 0 };
148
+ this.deps.markDirty();
149
+ }
150
+
151
+ /** Input while the spawn picker is active. Absorbs every key it owns. */
152
+ public handleSpawnInput(key: string): boolean {
153
+ if (!this.mode || this.creating) return true;
154
+ if (key === 'escape' || key === 'esc') { this.mode = null; this.deps.markDirty(); return true; }
155
+ const list = this.mode.step === 'agent' ? this.mode.agents : this.mode.candidates;
156
+ if (key === 'up' || key === 'k') { this.mode.index = (this.mode.index - 1 + list.length) % list.length; this.deps.markDirty(); return true; }
157
+ if (key === 'down' || key === 'j') { this.mode.index = (this.mode.index + 1) % list.length; this.deps.markDirty(); return true; }
158
+ if (key === 'enter' || key === 'return') { void this.advance(); return true; }
159
+ return true;
160
+ }
161
+
162
+ /** Enter on the current step: agent → dir picker; dir → create the session. */
163
+ private async advance(): Promise<void> {
164
+ if (!this.mode) return;
165
+ if (this.mode.step === 'agent') {
166
+ const agent = this.mode.agents[this.mode.index];
167
+ if (!agent) return;
168
+ const candidates = await this.resolveDirCandidates();
169
+ this.mode = { step: 'dir', agent, candidates, index: 0 };
170
+ this.deps.markDirty();
171
+ return;
172
+ }
173
+ const cwd = this.mode.candidates[this.mode.index]?.path;
174
+ const agentId = this.mode.agent.id;
175
+ if (!cwd) return;
176
+ await this.createSession(agentId, cwd);
177
+ }
178
+
179
+ /** The known-dir candidates: the current directory first, then registered workspaces (deduped). */
180
+ private async resolveDirCandidates(): Promise<AcpDirCandidate[]> {
181
+ const current = this.deps.currentDirectory();
182
+ const candidates: AcpDirCandidate[] = [{ path: current, label: 'current directory' }];
183
+ const gateway = this.requireGateway();
184
+ if (gateway) {
185
+ try {
186
+ for (const ws of await gateway.listWorkspaces()) {
187
+ if (ws.root === current) continue; // already offered as "current directory"
188
+ candidates.push({ path: ws.root, label: ws.label ?? 'workspace' });
189
+ }
190
+ } catch {
191
+ // A workspaces read failure is non-fatal: the current dir is still offered.
192
+ }
193
+ }
194
+ return candidates;
195
+ }
196
+
197
+ /** Drive acp.sessions.create and surface the honest outcome (success or structured failure). */
198
+ private async createSession(agentId: string, cwd: string): Promise<void> {
199
+ const gateway = this.requireGateway();
200
+ if (!gateway) { this.mode = null; this.deps.markDirty(); return; }
201
+ this.creating = true;
202
+ this.deps.markDirty();
203
+ try {
204
+ const result = await gateway.createSession({ agentId, cwd });
205
+ const hosted = result.hosted;
206
+ if (hosted.error) {
207
+ // Structured failure rendered verbatim — never a hung row.
208
+ this.deps.notify(`[Fleet] Could not host ${hosted.title || agentId}: ${hosted.error.stage} stage failed for ${hosted.error.binary} — ${hosted.error.message}`);
209
+ } else {
210
+ this.deps.notify(`[Fleet] Hosting ${hosted.title || agentId} in ${cwd} — it appears as an acp-agent row; steer and stop it like any agent.`);
211
+ }
212
+ } catch (err) {
213
+ this.deps.notify(`ACP session create failed: ${summarizeError(err)}`);
214
+ } finally {
215
+ this.creating = false;
216
+ this.mode = null;
217
+ this.deps.markDirty();
218
+ }
219
+ }
220
+
221
+ /** The spawn picker view (replaces the tree while a pick is in flight). */
222
+ public renderSpawnMode(width: number, height: number, palette: PanelPalette = C): Line[] {
223
+ const P = palette;
224
+ if (!this.mode) return [];
225
+ const lines: Line[] = [];
226
+ if (this.creating) {
227
+ lines.push(buildPanelLine(width, [[' Hosting the agent…', P.dim]]));
228
+ return buildPanelWorkspace(width, height, { title: 'Fleet — Host an agent', sections: [{ lines }], footerLines: [], palette: P });
229
+ }
230
+ if (this.mode.step === 'agent') {
231
+ lines.push(buildPanelLine(width, [[' Host a third-party coding agent — pick one, then a directory.', P.dim]]));
232
+ lines.push(buildPanelLine(width, [['', P.dim]]));
233
+ this.mode.agents.forEach((agent, i) => {
234
+ const selected = i === (this.mode as AgentStep).index;
235
+ lines.push(buildPanelLine(width, [
236
+ [selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
237
+ [agent.title, selected ? P.value : P.dim],
238
+ [` — ${agent.binaryPath}`, P.dim],
239
+ ]));
240
+ });
241
+ } else {
242
+ lines.push(buildPanelLine(width, [[` Directory for ${this.mode.agent.title} — a known dir, no path to type.`, P.dim]]));
243
+ lines.push(buildPanelLine(width, [['', P.dim]]));
244
+ this.mode.candidates.forEach((cand, i) => {
245
+ const selected = i === (this.mode as DirStep).index;
246
+ lines.push(buildPanelLine(width, [
247
+ [selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
248
+ [cand.label, selected ? P.value : P.dim],
249
+ [` — ${cand.path}`, P.dim],
250
+ ]));
251
+ });
252
+ }
253
+ const footerLines = [buildKeyboardHints(width, [
254
+ { keys: '↑↓', label: 'choose' },
255
+ { keys: 'Enter', label: this.mode.step === 'agent' ? 'pick agent' : 'host here' },
256
+ { keys: 'Esc', label: 'cancel' },
257
+ ], P)];
258
+ return buildPanelWorkspace(width, height, { title: 'Fleet — Host an agent', sections: [{ lines }], footerLines, palette: P });
259
+ }
260
+
261
+ private requireGateway(): AcpSpawnGateway | null {
262
+ const resolution = this.deps.resolveGateway();
263
+ if (!resolution.available) { this.deps.notify(`[Fleet] ${resolution.reason}`); return null; }
264
+ return resolution.gateway;
265
+ }
266
+ }
@@ -34,7 +34,7 @@ export function steerBadgeTone(status: SteerBadgeStatus, palette: PanelPalette):
34
34
  }
35
35
  }
36
36
 
37
- /** Labels of the currently live, steerable agent nodes other than `excludeNodeId` — offered when a steer target has gone inactive (WO UX-A item 4). */
37
+ /** Labels of the currently live, steerable agent nodes other than `excludeNodeId` — offered when a steer target has gone inactive (WO item 4). */
38
38
  export function liveSteerableLabels(nodes: readonly ProcessNode[], excludeNodeId: string): string[] {
39
39
  return nodes
40
40
  .filter((node) => node.id !== excludeNodeId && node.kind === 'agent' && node.capabilities.steerable && !isTerminalProcessState(node.state))
@@ -49,7 +49,7 @@ export function steerRefusalMessage(reason: string, siblingLabels: readonly stri
49
49
  return `${reason}.${suggestion}`;
50
50
  }
51
51
 
52
- /** One-line honest status for a tab's steer badge; the queued line names the target and points at the ⧗ delivery badge (WO UX-A item 4). */
52
+ /** One-line honest status for a tab's steer badge; the queued line names the target and points at the ⧗ delivery badge (WO item 4). */
53
53
  export function renderSteerBadgeLine(badge: SteerBadge, width: number, palette: PanelPalette, targetLabel?: string): Line {
54
54
  const glyph = steerBadgeGlyph(badge.status);
55
55
  const tone = steerBadgeTone(badge.status, palette);
@@ -1,14 +1,20 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-stop.ts
3
3
  //
4
- // d1 (W6.2) — the "stopping…" write-window overlay, split out of fleet-panel.ts
4
+ // d1 — the "stopping…" write-window overlay, split out of fleet-panel.ts
5
5
  // to keep that file under the architecture 800-line gate (same rationale as
6
6
  // fleet-steer.ts). FleetPanel owns nothing here beyond a FleetStopTracker
7
7
  // instance; all the state->display logic lives in these pure helpers.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  import type { ProcessNode, ProcessState } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
11
- import { fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone, type FleetTreeRow } from './fleet-read-model.ts';
11
+ import type { WorkItem } from '@pellux/goodvibes-sdk/platform/orchestration';
12
+ import { fleetNodeAttention, fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone, type FleetTreeRow } from './fleet-read-model.ts';
13
+
14
+ /** True when a work-item node owns a worktree that worktrees.discard can act on (D from the tree). */
15
+ function ownsWorktree(node: ProcessNode): boolean {
16
+ return Boolean((node.raw as { item?: WorkItem } | undefined)?.item?.worktreePath);
17
+ }
12
18
 
13
19
  /** How long 'stopping…' lingers after a stop keypress before the true state is shown regardless (never masks a stuck kill). */
14
20
  export const STOP_SETTLE_MS = 1500;
@@ -80,7 +86,7 @@ export interface FleetPauseDeps {
80
86
  }
81
87
 
82
88
  /**
83
- * d2 (W6.2): 'p' toggles pause<->resume by the node's state. A `paused`
89
+ * d2: 'p' toggles pause<->resume by the node's state. A `paused`
84
90
  * resumable node is resumed (interrupt's inverse); a live pausable node is
85
91
  * paused via the registry's interrupt() (its disable path). Honest refusal for
86
92
  * non-resumable/non-pausable kinds. Returns true when the key is consumed,
@@ -123,9 +129,15 @@ export function buildFleetTreeHints(
123
129
  ): FleetHint[] {
124
130
  const live = selected !== undefined && !isTerminalProcessState(selected.state);
125
131
  const isPaused = selected !== undefined && selected.state === 'paused';
132
+ // Enter is context-sensitive: on a flagged pick/conflict row it acts, else it
133
+ // attaches. Name the act so the key is discoverable from the tree.
134
+ const attention = selected ? fleetNodeAttention(selected) : null;
135
+ const enterLabel = attention?.reason === 'pick' ? 'pick'
136
+ : attention?.reason === 'conflict' ? 'resolve conflict'
137
+ : 'attach';
126
138
  const hints: FleetHint[] = [
127
139
  { keys: 'j/k', label: 'navigate' },
128
- { keys: 'Enter', label: 'attach' },
140
+ { keys: 'Enter', label: enterLabel },
129
141
  ];
130
142
  // Jump-to-blocked is offered whenever something is waiting on the operator,
131
143
  // in either view (the blocked nodes live in the active fleet, and the jump
@@ -144,15 +156,17 @@ export function buildFleetTreeHints(
144
156
  if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
145
157
  if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
146
158
  if (isPaused && selected.capabilities.resumable) hints.push({ keys: 'p', label: 'resume' });
159
+ if (selected && ownsWorktree(selected)) hints.push({ keys: 'D', label: 'discard worktree' });
147
160
  if (selected && !live) hints.push({ keys: 'a', label: 'archive' });
148
161
  hints.push({ keys: 'A', label: 'archive finished' });
149
162
  hints.push({ keys: 'v', label: 'archived' });
163
+ hints.push({ keys: 'n', label: 'host agent' });
150
164
  hints.push({ keys: 'f', label: follow ? 'follow:on' : 'follow' });
151
165
  if (hasTabs) hints.push({ keys: '[ ]', label: 'tabs' });
152
166
  return hints;
153
167
  }
154
168
 
155
- /** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" in an earlier version, undercounting a mixed subtree. */
169
+ /** K-confirm descendant stats (item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" in an earlier version, undercounting a mixed subtree. */
156
170
  export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
157
171
  const byParent = new Map<string, string[]>();
158
172
  for (const row of rows) {
@@ -178,3 +192,25 @@ export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: stri
178
192
  }
179
193
  return { total, active };
180
194
  }
195
+
196
+ /**
197
+ * Build the confirm-overlay arm options for a cascade Kill of one node — the
198
+ * label carries the descendant count so the operator sees what a confirm takes
199
+ * down. Extracted from FleetPanel to keep that file under the 800-line cap.
200
+ */
201
+ export function fleetKillConfirmArgs(
202
+ node: ProcessNode,
203
+ items: readonly FleetTreeRow[],
204
+ deps: { readonly kill: (id: string) => void; readonly markStopping: (id: string) => void },
205
+ ): { id: string; label: string; verb: string; onConfirm: () => void } {
206
+ const shortId = node.id.length > 8 ? node.id.slice(-8) : node.id;
207
+ const stats = countDescendantStats(items, node.id);
208
+ const suffix = stats.total > 0 ? ` (+${stats.total} descendant${stats.total === 1 ? '' : 's'}, ${stats.active} active)` : '';
209
+ return {
210
+ id: node.id,
211
+ label: `${node.kind} ${shortId}${suffix}`,
212
+ verb: 'Kill',
213
+ // Mark 'stopping…' only once the kill is confirmed, not while merely armed.
214
+ onConfirm: () => { deps.kill(node.id); deps.markStopping(node.id); },
215
+ };
216
+ }
@@ -25,10 +25,10 @@ import { MessageLineCache } from '../core/conversation-line-cache.ts';
25
25
  import { fleetKindTag } from './fleet-read-model.ts';
26
26
 
27
27
  /** Node kinds that can be attached as a session tab. */
28
- export type FleetAttachableKind = 'agent' | 'wrfc-chain';
28
+ export type FleetAttachableKind = 'agent' | 'wrfc-chain' | 'acp-agent';
29
29
 
30
30
  export function isAttachableFleetKind(kind: ProcessKind): kind is FleetAttachableKind {
31
- return kind === 'agent' || kind === 'wrfc-chain';
31
+ return kind === 'agent' || kind === 'wrfc-chain' || kind === 'acp-agent';
32
32
  }
33
33
 
34
34
  /**
@@ -91,17 +91,16 @@ export function appendSteerText(draft: string, ch: string): string {
91
91
  * One attached session tab. `agentId` is the SDK attach handle for
92
92
  * `AgentManager.getConversationSnapshot(agentId)` — populated only for
93
93
  * 'agent' tabs (agent.ts sets ProcessNode.id = record.id, so node.id IS the
94
- * agentId; see the W3.1 brief's C2 note). 'wrfc-chain' tabs render a live
95
- * member-summary instead of a transcript (a chain has no single conversation
96
- * of its own — see fleet-transcript.ts renderFleetChainSummary) and carry an
97
- * empty agentId, unused.
94
+ * agentId). 'wrfc-chain' tabs render a live member-summary instead of a
95
+ * transcript (a chain has no single conversation of its own — see
96
+ * fleet-transcript.ts renderFleetChainSummary) and carry an empty agentId,
97
+ * unused.
98
98
  *
99
- * `lineCache` is a PER-TAB `MessageLineCache` (C5 backpressure): switching
100
- * tabs must never invalidate or thrash the main session's own cache, and a
101
- * background (non-focused) tab must render nothing beyond its existing
102
- * tree-row, so its cache simply stays empty (size 0) until the tab is
103
- * focused. Disposed (`.clear()`) on detach so a closed tab's rendered
104
- * Line[] are not retained (brief risk #8).
99
+ * `lineCache` is a PER-TAB `MessageLineCache`: switching tabs must never
100
+ * invalidate or thrash the main session's own cache, and a background
101
+ * (non-focused) tab must render nothing beyond its existing tree-row, so its
102
+ * cache simply stays empty (size 0) until the tab is focused. Disposed
103
+ * (`.clear()`) on detach so a closed tab's rendered Line[] are not retained.
105
104
  */
106
105
  export interface FleetTab {
107
106
  readonly nodeId: string;
@@ -111,10 +110,10 @@ export interface FleetTab {
111
110
  readonly lineCache: MessageLineCache;
112
111
  /**
113
112
  * Cached parsed ledger fallback for a terminal agent whose conversation
114
- * snapshot has been evicted from the SDK's retention ring (C6 degraded
115
- * path). `null` = not yet loaded/attempted; `[]` = loaded and empty (or
116
- * the load failed) — both render the same honest "no transcript" state
117
- * once loaded is true.
113
+ * snapshot has been evicted from the SDK's retention ring (the degraded
114
+ * fallback path). `null` = not yet loaded/attempted; `[]` = loaded and
115
+ * empty (or the load failed) — both render the same honest "no transcript"
116
+ * state once loaded is true.
118
117
  */
119
118
  ledgerEntries: Record<string, unknown>[] | null;
120
119
  ledgerLoadStarted: boolean;
@@ -59,7 +59,7 @@ function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
59
59
  }
60
60
 
61
61
  // ---------------------------------------------------------------------------
62
- // Agent transcript (C6 live/frozen split)
62
+ // Agent transcript (live/frozen split)
63
63
  // ---------------------------------------------------------------------------
64
64
 
65
65
  export type FleetTranscriptMode = 'live' | 'frozen' | 'unavailable';
@@ -70,7 +70,7 @@ export interface FleetTranscriptRender {
70
70
  }
71
71
 
72
72
  /**
73
- * W3.3 (design point 4) — a 'frozen' transcript is a static capture of a
73
+ * (design point 4) — a 'frozen' transcript is a static capture of a
74
74
  * completed process, not something that will change on the next render. The
75
75
  * distinction matters because this wave also introduced kill/interrupt
76
76
  * display bugs (elapsed/usage briefly climbing after a terminal state
@@ -157,7 +157,7 @@ export function renderFleetAgentTranscript(
157
157
  }
158
158
 
159
159
  // ---------------------------------------------------------------------------
160
- // Chain summary — 'wrfc-chain' tabs have no single conversation (C2/C6)
160
+ // Chain summary — 'wrfc-chain' tabs have no single conversation
161
161
  // ---------------------------------------------------------------------------
162
162
 
163
163
  /**
@@ -197,7 +197,7 @@ export function renderFleetChainSummary(
197
197
 
198
198
  // ---------------------------------------------------------------------------
199
199
  // Ledger fallback — degraded activity view for a terminal agent whose
200
- // full-fidelity snapshot is unavailable (C6 "honest fallback")
200
+ // full-fidelity snapshot is unavailable (an honest fallback)
201
201
  // ---------------------------------------------------------------------------
202
202
 
203
203
  /**
@@ -257,7 +257,7 @@ function renderLedgerEntry(width: number, entry: Record<string, unknown>, palett
257
257
  const toolName = typeof entry['toolName'] === 'string' ? entry['toolName'] : 'tool';
258
258
  const success = entry['success'] !== false;
259
259
  const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
260
- // W3.3: the writer already truncates this to 500 chars (session.ts's
260
+ // the writer already truncates this to 500 chars (session.ts's
261
261
  // resultPreview field) — passed through verbatim (collapsed to one
262
262
  // line), never re-summarized or fabricated, then tail-truncated again
263
263
  // to fit the row.
@@ -77,7 +77,7 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
77
77
  commitAuthor: '244',
78
78
  selected: '#1c1c1c',
79
79
  selectedFg: '#ffffff',
80
- diffMeta: DIFF_TONES.hunk, // WO-204: shared diff-hunk token, was a local literal
80
+ diffMeta: DIFF_TONES.hunk, // shared diff-hunk token, was a local literal
81
81
  diffNeutral: '250',
82
82
  // Reuses the existing workflow accent token rather than adding a new hex
83
83
  // literal (architecture gate ratchets the raw-hex-literal count).
@@ -626,7 +626,7 @@ export class GitPanel extends BasePanel {
626
626
  { keys: 'r', label: 'refresh' },
627
627
  ], DEFAULT_PANEL_PALETTE),
628
628
  ];
629
- const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); // WO-160: was '/git status' (chat-only, never touches this panel)
629
+ const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); // was '/git status' (chat-only, never touches this panel)
630
630
  const rows: Line[] = [];
631
631
  for (let i = 0; i < this.items.length; i++) {
632
632
  const item = this.items[i];
@@ -7,7 +7,7 @@ export { CostTrackerPanel } from './cost-tracker-panel.ts';
7
7
  export { GitPanel } from './git-panel.ts';
8
8
  export { registerBuiltinPanels } from './builtin-panels.ts';
9
9
  export type { BuiltinPanelDeps } from './builtin-panels.ts';
10
- // W6.1 (the purge) — group B: plugins/skills panels are register-retired but
10
+ // (the purge) — group B: plugins/skills panels are register-retired but
11
11
  // their modules are retained for shared non-class exports (PluginManagerControls;
12
12
  // discoverSkills + SkillRecord/SkillOrigin, consumed by command runtimes and the
13
13
  // skills modal). The other group-A and group-B panel classes are deleted outright
@@ -16,3 +16,6 @@ export { PluginsPanel } from './plugins-panel.ts';
16
16
  export { SkillsPanel } from './skills-panel.ts';
17
17
  export { FleetPanel } from './fleet-panel.ts';
18
18
  export type { FleetActionCallbacks } from './fleet-panel.ts';
19
+ export { NotificationsPanel } from './notifications-panel.ts';
20
+ export { PanelNotificationFeed, getSharedNotificationFeed } from './notifications-feed.ts';
21
+ export type { PanelFeedEntry } from './notifications-feed.ts';
@@ -21,7 +21,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
21
  import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
22
22
 
23
23
  // Base chrome only — state colors and text tokens come straight from
24
- // DEFAULT_PANEL_PALETTE (WO-002).
24
+ // DEFAULT_PANEL_PALETTE.
25
25
  const C = DEFAULT_PANEL_PALETTE;
26
26
 
27
27
  function formatRoles(roles: readonly string[]): string {
@@ -0,0 +1,94 @@
1
+ // ---------------------------------------------------------------------------
2
+ // memory-consolidation-gateway.ts
3
+ //
4
+ // The async, daemon-backed verb the Memory modal's Proposals tab reads:
5
+ // memory.consolidation.receipts (GET /api/memory/consolidation/receipts) — the
6
+ // retained memory-consolidation run receipts plus the flattened
7
+ // pendingProposals (contradiction / cross-scope-duplicate / stale-delete)
8
+ // awaiting human review. The records a proposal references are already
9
+ // marked into the review queue by the daemon (reviewState 'contradicted' with
10
+ // a staleReason, or 'fresh' for cross-scope duplicates); this gateway is only
11
+ // how the Memory modal learns WHAT was proposed and WHY, so it can show that
12
+ // reason next to the affected records and offer a jump to them.
13
+ //
14
+ // No named facade exists on the in-process OperatorClient for this verb, so —
15
+ // exactly like the Fleet gateway (fleet-gateway.ts) — it goes over the
16
+ // generic operator invoke path (operator-rpc.ts's resolveOperatorRpc ->
17
+ // sdk.operator.invoke), reaching the SAME daemon the command layer does. A
18
+ // runtime without the consolidation scheduler answers an honest 501; an older
19
+ // daemon that has not wired this route yet answers 404 — both are "verb
20
+ // unavailable", not "no proposals" (see classifyConsolidationFetchError).
21
+ //
22
+ // The interface is injectable so the modal's proposals fetch round-trips
23
+ // against a mocked daemon in tests; the live builder
24
+ // (createMemoryConsolidationGateway) is wired in builtin-modals.ts, resolved
25
+ // lazily (a fresh factory call per fetch) exactly like the Fleet gateway.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
29
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
30
+ import { GoodVibesSdkError } from '@pellux/goodvibes-sdk';
31
+ import { resolveOperatorRpc, describeOperatorRpcError } from '../input/commands/operator-rpc.ts';
32
+
33
+ /** memory.consolidation.receipts output — retained receipts + the flattened pending proposals. */
34
+ export type MemoryConsolidationReceiptsResult = OperatorMethodOutput<'memory.consolidation.receipts'>;
35
+ /** One pending judgment proposal (contradiction / cross-scope-duplicate / stale-delete) awaiting human review. */
36
+ export type MemoryConsolidationProposal = MemoryConsolidationReceiptsResult['pendingProposals'][number];
37
+
38
+ /** The narrow async verb surface the Memory modal's Proposals tab drives. */
39
+ export interface MemoryConsolidationGateway {
40
+ /** Fetch the retained consolidation receipts + flattened pending proposals. */
41
+ fetchReceipts(): Promise<MemoryConsolidationReceiptsResult>;
42
+ }
43
+
44
+ /**
45
+ * Why the gateway could not be built (daemon disabled / no control-plane URL),
46
+ * surfaced verbatim so the modal can print an honest "unavailable" line
47
+ * rather than guessing — mirrors FleetGatewayResolution.
48
+ */
49
+ export type MemoryConsolidationGatewayResolution =
50
+ | { readonly available: true; readonly gateway: MemoryConsolidationGateway }
51
+ | { readonly available: false; readonly reason: string };
52
+
53
+ export interface MemoryConsolidationGatewayDeps {
54
+ readonly configManager: ConfigManager;
55
+ readonly homeDirectory: string;
56
+ }
57
+
58
+ /**
59
+ * Build the live consolidation-receipts gateway over the generic operator
60
+ * invoke path — the same daemon resolution the command layer and the Fleet
61
+ * gateway use. Returns an honest unavailable reason when no daemon is
62
+ * reachable, so the modal can refuse cleanly instead of throwing mid-render.
63
+ */
64
+ export function createMemoryConsolidationGateway(deps: MemoryConsolidationGatewayDeps): MemoryConsolidationGatewayResolution {
65
+ const rpc = resolveOperatorRpc({ configManager: deps.configManager, homeDirectory: deps.homeDirectory });
66
+ if (!rpc.available) return { available: false, reason: rpc.reason };
67
+ const { sdk } = rpc;
68
+ return {
69
+ available: true,
70
+ gateway: { fetchReceipts: () => sdk.operator.invoke('memory.consolidation.receipts', {}) },
71
+ };
72
+ }
73
+
74
+ /** How a `fetchReceipts()` rejection should render in the Proposals tab. */
75
+ export type MemoryConsolidationFetchFailure =
76
+ | { readonly kind: 'unavailable'; readonly reason: string }
77
+ | { readonly kind: 'error'; readonly message: string };
78
+
79
+ /**
80
+ * Classify a `fetchReceipts()` rejection: a 501 (the documented "no
81
+ * consolidation scheduler on this runtime" response) or a 404 (an older
82
+ * daemon that has not wired this route yet) are both "verb unavailable" —
83
+ * distinct from a generic request failure (network error, 401/403, 500),
84
+ * which renders as an honest fetch error instead. Reuses
85
+ * describeOperatorRpcError's wording so the two call sites (command layer,
86
+ * Fleet gateway, this gateway) never diverge on how they describe the same
87
+ * daemon response.
88
+ */
89
+ export function classifyConsolidationFetchError(error: unknown): MemoryConsolidationFetchFailure {
90
+ if (error instanceof GoodVibesSdkError && (error.status === 501 || error.status === 404)) {
91
+ return { kind: 'unavailable', reason: describeOperatorRpcError(error) };
92
+ }
93
+ return { kind: 'error', message: describeOperatorRpcError(error) };
94
+ }