@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
@@ -36,12 +36,17 @@ import {
36
36
  import { scheduleBackgroundMcpDiscovery, startBackgroundProviderRegistration } from '@/runtime/index.ts';
37
37
  import { restoreSavedModel } from '@/runtime/index.ts';
38
38
  import { startExternalServices, type ExternalServicesHandle, type HostServiceStatus } from '@/runtime/index.ts';
39
- import { getOrCreateCompanionToken, pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing';
40
- import { workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
39
+ import { createHttpTransport } from '@/runtime/index.ts';
40
+ import { foldLegacySpineStore, deriveSpineFooterStatus } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
41
+ import { createTuiSpineTransport, type SpineSessionsClient } from './session-spine-transport.ts';
42
+ import { pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing';
43
+ import { resolveDaemonCompanionToken, workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
41
44
  import type { UiRuntimeServices } from './ui-services.ts';
42
45
  import { createDeferredStartupCoordinator } from '@/runtime/index.ts';
43
46
  import { initializeBootstrapCore } from './bootstrap-core.ts';
44
47
  import { createBootstrapShell } from './bootstrap-shell.ts';
48
+ import { announceResumeState } from './resume-notice.ts';
49
+ import { buildSharedOrchestratorCoreServices } from './orchestrator-core-services.ts';
45
50
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
46
51
  import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon';
47
52
  import { HttpListener } from '@pellux/goodvibes-sdk/platform/daemon';
@@ -110,19 +115,15 @@ export type BootstrapContext = RuntimeContext & {
110
115
  /** Command registry used by InputHandler. main.ts needs this to wire input. */
111
116
  commandRegistry: import('../input/command-registry.ts').CommandRegistry;
112
117
  /**
113
- * System message router instantiated at startup, wired to conversation and panel manager.
118
+ * System message router instantiated at startup, wired to conversation.
114
119
  *
115
120
  * @remarks
116
121
  * Route operational messages through this rather than calling
117
- * conversation.addSystemMessage() directly so that low-priority messages
118
- * stay out of the main conversation and go to the SystemMessagesPanel instead.
122
+ * conversation.addSystemMessage() directly so routing-target config
123
+ * (panel/conversation/both) and the forced-inline critical prefixes stay
124
+ * centralized in one place.
119
125
  */
120
126
  systemMessageRouter: SystemMessageRouter;
121
- /**
122
- * Wire the agent detail modal opener after InputHandler is constructed in main.ts.
123
- * Call with `(id) => input.agentDetailModal.open(id)` once the InputHandler is ready.
124
- */
125
- setOpenAgentDetail: (fn: (agentId: string) => void) => void;
126
127
  };
127
128
 
128
129
  // ── Bootstrap function ────────────────────────────────────────────────────
@@ -186,6 +187,9 @@ export async function bootstrapRuntime(
186
187
  setRenderRequest,
187
188
  runtimeSessionIdRef,
188
189
  wrfcPersistence,
190
+ sessionSpine,
191
+ sessionInboundInputs,
192
+ sessionUnionCache,
189
193
  } = await initializeBootstrapCore(stdout, options, (limit) => controlPlaneRecentEventsRef.value(limit));
190
194
  const providerRegistry = services.providerRegistry;
191
195
  const {
@@ -195,6 +199,11 @@ export async function bootstrapRuntime(
195
199
  pluginManager,
196
200
  } = services;
197
201
 
202
+ // D4b: a liveness flip is only PAINTED once something calls requestRender();
203
+ // without this, the footer's spine segment sat correct-but-undrawn until
204
+ // incidental activity redrew it (minutes, during an idle stretch).
205
+ sessionUnionCache.setOnTransition(() => requestRender());
206
+
198
207
  // ── Phase 6: Orchestrator + AcpManager ───────────────────────────────────
199
208
 
200
209
  // Mutable function refs so main.ts can patch these after constructing the scroll/viewport state.
@@ -235,15 +244,11 @@ export async function bootstrapRuntime(
235
244
  logger.debug('companion handleUserInput safety catch', { error: String(err) });
236
245
  });
237
246
  };
247
+ // Shared payload (single source of truth, includes wo805's memoryRegistry —
248
+ // see orchestrator-core-services.ts) plus this site's cacheHitTracker.
238
249
  orchestrator.setCoreServices({
239
- configManager,
240
- providerRegistry,
250
+ ...buildSharedOrchestratorCoreServices({ services, configManager, providerRegistry }),
241
251
  cacheHitTracker: services.cacheHitTracker,
242
- planManager: services.planManager,
243
- adaptivePlanner: services.adaptivePlanner,
244
- sessionMemoryStore: services.sessionMemoryStore,
245
- sessionLineageTracker: services.sessionLineageTracker,
246
- idempotencyStore: services.idempotencyStore,
247
252
  });
248
253
  conversation.setSessionLineageTracker(services.sessionLineageTracker);
249
254
 
@@ -269,6 +274,7 @@ export async function bootstrapRuntime(
269
274
  runtimeBus,
270
275
  runtimeStore: store,
271
276
  services,
277
+ sessionSpine,
272
278
  conversation,
273
279
  runtime,
274
280
  orchestrator,
@@ -294,10 +300,31 @@ export async function bootstrapRuntime(
294
300
  wrfcPersistence.rehydrate();
295
301
  const commandRegistry = shell.commandRegistry;
296
302
  const commandContext = shell.commandContext;
303
+ // Boot resume notice (UX-D item 1): after rehydrate() so chain history is
304
+ // ready, before the operator can type anything. Fire-and-forget — the
305
+ // checkpoint-count lookup is async (WorkspaceCheckpointManager.list() awaits
306
+ // its own init()), and this must not block the rest of bootstrap the way
307
+ // main.ts's `void workspaceCheckpointManager.init().catch(() => {})`
308
+ // deliberately doesn't either. Local file I/O only; resolves well before a
309
+ // human can react to the first rendered frame.
310
+ void announceResumeState({
311
+ workingDirectory: services.workingDirectory,
312
+ homeDirectory: services.homeDirectory,
313
+ surfaceRoot: 'tui',
314
+ sessionManager: services.sessionManager,
315
+ checkpointManager: services.workspaceCheckpointManager,
316
+ chainHistory: wrfcPersistence.knownChains,
317
+ memoryAvailable: Boolean(commandContext.clients?.knowledgeApi?.memory),
318
+ router: systemMessageRouter,
319
+ }).catch(() => {
320
+ // Best-effort — never let the resume notice block or crash boot.
321
+ });
297
322
  const gitStatusProvider = shell.gitStatusProvider;
298
323
  const inputHistory = shell.inputHistory;
299
324
  const lastGitInfoRef = shell.lastGitInfoRef;
300
- const setOpenAgentDetail = shell.setOpenAgentDetail;
325
+ // W1.6 FIX 2: dispose the header's live-repo-state poll (git-status.ts
326
+ // startPolling) on shutdown, same pattern as acpTaskSyncInterval above.
327
+ bootstrapUnsubs.push(() => gitStatusProvider.stopPolling());
301
328
  const pluginCommandRegistry = {
302
329
  register(command: {
303
330
  readonly name: string;
@@ -348,7 +375,70 @@ export async function bootstrapRuntime(
348
375
 
349
376
  const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
350
377
 
351
- const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked';
378
+ // 'blocked' (occupied by an unverified process) and 'incompatible' (occupied
379
+ // by a GoodVibes daemon we refused to adopt on a wire-version mismatch) both
380
+ // mean the configured port is held and unusable by this TUI instance.
381
+ const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked' || status.mode === 'incompatible';
382
+
383
+ // PERMANENT DESIGN (docs/decisions/2026-07-06-session-spine-mode-branch-is-permanent.md):
384
+ // 'embedded'/'external' are two distinct, both-supported daemon topologies, not
385
+ // migration stages — the ONE client-selection point for the session spine, driven by
386
+ // the SAME authoritative HostServiceMode adopt-or-start already computed above.
387
+ // 'embedded' (daemon.embedInProcess=true) means THIS process's own SharedSessionBroker
388
+ // already IS the daemon's broker — nothing to mirror TO, so the spine stays
389
+ // permanently dormant. Every other mode ('disabled'/'blocked'/'incompatible'/
390
+ // 'unavailable') also stays local-only and honest. Only 'external' (a separately-
391
+ // running daemon this TUI adopted) activates the wire mirror.
392
+ let spineActiveForBaseUrl: string | null = null;
393
+ const syncSessionSpineToHostStatus = (daemonStatus: HostServiceStatus, sharedDaemonToken: string): void => {
394
+ if (daemonStatus.mode !== 'external') {
395
+ if (spineActiveForBaseUrl !== null) {
396
+ sessionSpine.deactivate(`daemon mode changed to '${daemonStatus.mode}'`);
397
+ sessionInboundInputs.deactivate(`daemon mode changed to '${daemonStatus.mode}'`);
398
+ spineActiveForBaseUrl = null;
399
+ }
400
+ // Keep the read facade honest per topology (permanent, not staged): 'embedded'
401
+ // means this process's broker IS the daemon's broker (local reads are the whole
402
+ // truth); every other non-external mode is local-only/dormant.
403
+ if (daemonStatus.mode === 'embedded') sessionUnionCache.markEmbedded();
404
+ else sessionUnionCache.deactivate(`daemon mode '${daemonStatus.mode}'`);
405
+ logger.info(`[bootstrap] session spine: daemon mode '${daemonStatus.mode}' — local-only (no spine mirror)`);
406
+ return;
407
+ }
408
+ const baseUrl = daemonStatus.baseUrl;
409
+ if (spineActiveForBaseUrl === baseUrl) return; // already wired to this exact adopted daemon
410
+ const httpTransport = createHttpTransport({ baseUrl, authToken: sharedDaemonToken });
411
+ const sessionsClient: SpineSessionsClient = {
412
+ register: (input) => httpTransport.operator.sessions.register(input),
413
+ close: (sessionId) => httpTransport.operator.sessions.close(sessionId),
414
+ };
415
+ sessionSpine.activate(createTuiSpineTransport(sessionsClient));
416
+ // D3: adopt the same daemon's wire for the INBOUND steer path — collect
417
+ // steer/follow-up inputs another live surface queued for this session and
418
+ // inject them into the turn machinery (acking delivery on the wire).
419
+ sessionInboundInputs.activate({
420
+ listInputs: async (sessionId, opts) => ({
421
+ inputs: await httpTransport.operator.sessions.inputs(sessionId, opts.limit, { state: opts.state, since: opts.since }),
422
+ }),
423
+ deliverInput: (sessionId, inputId, opts) => httpTransport.operator.sessions.deliverInput(sessionId, inputId, opts),
424
+ });
425
+ // 'external' only: adopt the same daemon's wire as the read facade's cross-surface
426
+ // union source (interval-refreshed; served synchronously to panels).
427
+ sessionUnionCache.activate({ list: (limit) => httpTransport.operator.sessions.list(limit) });
428
+ spineActiveForBaseUrl = baseUrl;
429
+ logger.info(`[bootstrap] session spine: adopted external daemon at ${baseUrl} — mirroring session identity`);
430
+ // Legacy fold: one-time (marker-guarded) import of this project's own
431
+ // pre-spine control-plane sessions.json into the now-adopted daemon.
432
+ const legacyStorePath = services.shellPaths.resolveProjectPath('tui', 'control-plane', 'sessions.json');
433
+ const fold = foldLegacySpineStore(sessionSpine, {
434
+ storePath: legacyStorePath,
435
+ markerPath: `${legacyStorePath}.spine-migrated`,
436
+ project: services.workingDirectory,
437
+ });
438
+ if (fold.folded > 0) {
439
+ logger.info(`[bootstrap] session spine: folded ${fold.folded} legacy local session(s) into the adopted daemon`);
440
+ }
441
+ };
352
442
 
353
443
  const inspectExternalServices = () => {
354
444
  const daemonStatus = externalServices.daemonStatus;
@@ -360,6 +450,16 @@ export async function bootstrapRuntime(
360
450
  httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
361
451
  daemonStatus,
362
452
  httpListenerStatus,
453
+ // Honest session-spine posture, independent of daemonRunning —
454
+ // 'external'-adopted-but-currently-unreachable degrades to 'offline' here
455
+ // even though daemonRunning might still read true from a stale handle.
456
+ // D4: sessionSpine.status() alone is ACTIVITY-gated (only updates on a
457
+ // register/heartbeat/close), so after the daemon dies mid-idle it would
458
+ // keep reading 'online'. Derive the footer status from the union cache's
459
+ // 5s liveness probe too — one signal, no new timer — so offline surfaces
460
+ // within one refresh interval of the daemon dying.
461
+ sessionSpineActive: sessionSpine.active,
462
+ sessionSpineStatus: deriveSpineFooterStatus(sessionSpine.status(), sessionUnionCache.crossSurfaceView),
363
463
  };
364
464
  };
365
465
 
@@ -418,7 +518,7 @@ export async function bootstrapRuntime(
418
518
  await waitForConfigDrivenRestarts(externalServices);
419
519
  await externalServices.stop();
420
520
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
421
- const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
521
+ const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
422
522
  externalServicesPromise = startExternalServices(
423
523
  configManager,
424
524
  runtimeBus,
@@ -428,6 +528,7 @@ export async function bootstrapRuntime(
428
528
  );
429
529
  externalServices = await externalServicesPromise;
430
530
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
531
+ syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
431
532
  requestRender();
432
533
  return inspectExternalServices();
433
534
  },
@@ -466,7 +567,7 @@ export async function bootstrapRuntime(
466
567
  // bearer, so tokens scanned from the /qrcode panel's QR actually
467
568
  // authenticate against the embedded daemon this surface starts.
468
569
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
469
- const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
570
+ const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
470
571
  // F3 resolution (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
471
572
  // token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
472
573
  // The prune is best-effort — it silently skips missing files, no-ops when tokens
@@ -491,6 +592,7 @@ export async function bootstrapRuntime(
491
592
  );
492
593
  externalServices = await externalServicesPromise;
493
594
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
595
+ syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
494
596
  requestRender();
495
597
  },
496
598
  onError: (error) => {
@@ -628,7 +730,6 @@ export async function bootstrapRuntime(
628
730
  _getConfiguredProviderIds: () => services.providerRegistry.getConfiguredProviderIds(),
629
731
  commandRegistry,
630
732
  systemMessageRouter,
631
- setOpenAgentDetail,
632
733
  shutdown: async (sessionData) => {
633
734
  // Clear bootstrap-owned subscriptions
634
735
  bootstrapUnsubs.forEach(fn => fn());
@@ -636,6 +737,12 @@ export async function bootstrapRuntime(
636
737
  runtimeUnsubs.forEach((fn) => fn());
637
738
  runtimeUnsubs.length = 0;
638
739
  forensicsCollector.dispose();
740
+ // Honest close on exit — fire-and-forget (never blocks shutdown);
741
+ // a no-op when the spine was never activated (embedded/local-only topology).
742
+ sessionSpine.close(runtime.sessionId);
743
+ sessionSpine.dispose();
744
+ sessionInboundInputs.dispose(); // D3: stop the inbound steer poll interval on exit
745
+ sessionUnionCache.dispose(); // stop the wire-refresh interval on exit
639
746
  await deferredStartup.drain(100);
640
747
  if (externalServicesPromise) {
641
748
  try {
@@ -0,0 +1,135 @@
1
+ // ---------------------------------------------------------------------------
2
+ // code-index-services.ts — Wave 5 (wo804, W5.3 Stage A TUI wiring)
3
+ //
4
+ // Constructs the TUI's repo source-tree code index: CodeIndexStore
5
+ // (@pellux/goodvibes-sdk/platform/state, landed on SDK main as wo802/W5.3
6
+ // Stage A). Extracted into its own module rather than built inline in
7
+ // services.ts: services.ts sits at the architecture check's 800-line cap
8
+ // (scripts/check-architecture.ts), so any new service gets its own
9
+ // construction module and a single wiring call there (mirrors
10
+ // createWorkstreamServices's one-function-bundle shape).
11
+ //
12
+ // Schema-initialized eagerly (mirrors memoryStore: construction + init()
13
+ // happen unconditionally so the store is queryable — /codebase status, the
14
+ // fleet node — even before any build has ever run). The actual walk/chunk/
15
+ // embed build is NEVER auto-triggered by default: the hundreds of existing
16
+ // test fixtures (and every headless invocation) construct RuntimeServices
17
+ // without asking for a full source-tree walk, and init() alone never runs
18
+ // one (mirrors the SDK's own RuntimeServices wiring in
19
+ // platform/runtime/services.ts, which gates its equivalent scheduleBuild()
20
+ // call behind an explicit autoStartCodeIndex option).
21
+ //
22
+ // REALITY-WINS DIVERGENCE from the SDK's own shape: the SDK's
23
+ // RuntimeServicesOptions exposes autoStartCodeIndex as a constructor-time
24
+ // boolean because that call site is a library entrypoint threaded by each
25
+ // embedder. This TUI's createRuntimeServices has no such per-call knob —
26
+ // auto-start is decided from a TUI-local config key instead
27
+ // (CODE_INDEX_ENABLED_CONFIG_KEY, default OFF), so every construction path
28
+ // (interactive main.ts, the daemon, and every test fixture) shares one
29
+ // honest, user-visible on/off switch rather than needing to thread a new
30
+ // boolean through every call site. With it off (the default), `/codebase
31
+ // build` is the explicit, visible trigger — exactly the shape wo804's brief
32
+ // asks for.
33
+ //
34
+ // Shares memoryEmbeddingRegistry with MemoryStore (constructed in
35
+ // services.ts) so code + memory retrieval use one embedding provider and one
36
+ // dimensionality, per the SDK design doc (code-index-store.ts's constructor
37
+ // takes the SAME MemoryEmbeddingProviderRegistry instance memory uses).
38
+ // ---------------------------------------------------------------------------
39
+
40
+ import { join } from 'node:path';
41
+ import { CodeIndexStore, CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
42
+ import type { MemoryEmbeddingProviderRegistry } from '@pellux/goodvibes-sdk/platform/state';
43
+ import type { ConfigKey, ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
44
+ import { readBooleanConfig } from '../core/alert-gating.ts';
45
+
46
+ /**
47
+ * TUI-local synthetic config key (not yet in the SDK's ConfigKey union) that
48
+ * gates the code index's auto-build on construction. Default OFF — see this
49
+ * module's header doc. Surfaced in /config via a synthetic entry
50
+ * (settings-modal-data.ts) exactly like behavior.notifyAfterSeconds.
51
+ */
52
+ export const CODE_INDEX_ENABLED_CONFIG_KEY = 'storage.codeIndexEnabled';
53
+
54
+ /**
55
+ * Honest, generous bounds passed EXPLICITLY to CodeIndexStore (values equal
56
+ * to its own internal defaults) so /codebase status and the settings
57
+ * description can state them in one place without reaching into store
58
+ * internals or duplicating magic numbers.
59
+ */
60
+ export const CODE_INDEX_MAX_FILES = 5000;
61
+ export const CODE_INDEX_MAX_FILE_BYTES = 512 * 1024;
62
+ export const CODE_INDEX_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
63
+
64
+ export interface CodeIndexServicesDeps {
65
+ readonly workingDirectory: string;
66
+ readonly configManager: Pick<ConfigManager, 'get'>;
67
+ readonly memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry;
68
+ }
69
+
70
+ /**
71
+ * Whether Stage B code auto-injection / tool-site reindex may run: the SAME
72
+ * storage.codeIndexEnabled switch that gates auto-build. Read live so a runtime
73
+ * /config toggle takes effect without restart. (The separate, default-off
74
+ * `agent-passive-code-injection` feature flag gates injection additionally; this
75
+ * is only the storage-side setting the SDK asks embedders to respect.)
76
+ */
77
+ export function isCodeInjectionSettingEnabled(configManager: Pick<ConfigManager, 'get'>): boolean {
78
+ return isCodeIndexAutoStartEnabled(configManager);
79
+ }
80
+
81
+ export interface CodeIndexServices {
82
+ readonly codeIndexStore: CodeIndexStore;
83
+ /**
84
+ * Wave-5 Stage B tool-site incremental reindex scheduler, bound to codeIndexStore and
85
+ * gated live on storage.codeIndexEnabled. Threaded into both SDK orchestrators (main +
86
+ * agent) so a successful write/edit debounces an incremental reindex, and into the command
87
+ * context so /codebase status can report the last reindex activity honestly.
88
+ */
89
+ readonly codeIndexReindexScheduler: CodeIndexReindexScheduler;
90
+ }
91
+
92
+ /** Absolute path to the TUI's code-index sqlite file, sibling to memory.sqlite under .goodvibes/tui/. */
93
+ export function codeIndexDbPath(workingDirectory: string): string {
94
+ return join(workingDirectory, '.goodvibes', 'tui', 'code-index.sqlite');
95
+ }
96
+
97
+ /**
98
+ * Whether the code index's initial build should auto-start on construction.
99
+ * Exported so /codebase status and the settings modal read the exact same
100
+ * config key + default this module decides auto-start from.
101
+ */
102
+ export function isCodeIndexAutoStartEnabled(configManager: Pick<ConfigManager, 'get'>): boolean {
103
+ return readBooleanConfig(
104
+ (key) => configManager.get(key as ConfigKey),
105
+ CODE_INDEX_ENABLED_CONFIG_KEY,
106
+ false,
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Constructs the TUI's CodeIndexStore. Schema-init runs unconditionally
112
+ * (init() never throws — it degrades to an honest `available: false` +
113
+ * recorded error on failure, mirrored by CodeIndexStats/describeDegradation);
114
+ * the initial full build only fires when isCodeIndexAutoStartEnabled() is
115
+ * true, via the SAME fire-and-forget scheduleBuild() an explicit
116
+ * `/codebase build` invocation uses.
117
+ */
118
+ export function createCodeIndexServices(deps: CodeIndexServicesDeps): CodeIndexServices {
119
+ const codeIndexStore = new CodeIndexStore(
120
+ deps.workingDirectory,
121
+ codeIndexDbPath(deps.workingDirectory),
122
+ deps.memoryEmbeddingRegistry,
123
+ { maxFiles: CODE_INDEX_MAX_FILES, maxFileBytes: CODE_INDEX_MAX_FILE_BYTES, maxTotalBytes: CODE_INDEX_MAX_TOTAL_BYTES },
124
+ );
125
+ codeIndexStore.init();
126
+ if (isCodeIndexAutoStartEnabled(deps.configManager)) {
127
+ codeIndexStore.scheduleBuild();
128
+ }
129
+ const codeIndexReindexScheduler = new CodeIndexReindexScheduler({
130
+ target: codeIndexStore,
131
+ workingDirectory: deps.workingDirectory,
132
+ isEnabled: () => isCodeInjectionSettingEnabled(deps.configManager),
133
+ });
134
+ return { codeIndexStore, codeIndexReindexScheduler };
135
+ }