@pellux/goodvibes-tui 1.7.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 (137) hide show
  1. package/README.md +10 -9
  2. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  3. package/package.json +2 -2
  4. package/src/audio/spoken-turn-controller.ts +12 -2
  5. package/src/audio/spoken-turn-wiring.ts +2 -1
  6. package/src/cli/help.ts +8 -1
  7. package/src/cli/service-posture.ts +2 -1
  8. package/src/cli/status.ts +2 -1
  9. package/src/cli/surface-command.ts +2 -2
  10. package/src/core/composer-state.ts +11 -3
  11. package/src/core/conversation-line-cache.ts +27 -3
  12. package/src/core/conversation-rendering.ts +71 -14
  13. package/src/core/stream-event-wiring.ts +20 -1
  14. package/src/core/system-message-noise.ts +87 -0
  15. package/src/core/system-message-router.ts +68 -1
  16. package/src/core/turn-cancellation.ts +7 -2
  17. package/src/core/turn-event-wiring.ts +10 -2
  18. package/src/daemon/cli.ts +29 -2
  19. package/src/daemon/handlers/register.ts +8 -1
  20. package/src/daemon/service-commands.ts +329 -0
  21. package/src/input/autocomplete.ts +27 -1
  22. package/src/input/command-registry.ts +46 -4
  23. package/src/input/commands/codebase-runtime.ts +46 -6
  24. package/src/input/commands/config.ts +43 -3
  25. package/src/input/commands/health-runtime.ts +9 -1
  26. package/src/input/commands/memory.ts +68 -35
  27. package/src/input/commands/operator-panel-runtime.ts +31 -7
  28. package/src/input/commands/planning-runtime.ts +95 -6
  29. package/src/input/commands/qrcode-runtime.ts +25 -5
  30. package/src/input/commands/remote-runtime-setup.ts +5 -3
  31. package/src/input/commands/session-content.ts +20 -9
  32. package/src/input/commands/settings-sync-runtime.ts +15 -3
  33. package/src/input/commands/shell-core.ts +10 -1
  34. package/src/input/commands/workstream-runtime.ts +168 -18
  35. package/src/input/config-modal-types.ts +15 -1
  36. package/src/input/config-modal.ts +227 -12
  37. package/src/input/feed-context-factory.ts +3 -0
  38. package/src/input/handler-command-route.ts +10 -3
  39. package/src/input/handler-content-actions.ts +17 -2
  40. package/src/input/handler-feed-routes.ts +43 -121
  41. package/src/input/handler-feed.ts +32 -4
  42. package/src/input/handler-modal-routes.ts +78 -13
  43. package/src/input/handler-modal-stack.ts +11 -2
  44. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  45. package/src/input/handler-onboarding.ts +71 -59
  46. package/src/input/handler-picker-routes.ts +15 -8
  47. package/src/input/handler-shortcuts.ts +59 -9
  48. package/src/input/handler.ts +6 -1
  49. package/src/input/keybindings.ts +6 -5
  50. package/src/input/model-picker.ts +19 -2
  51. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  52. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  53. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  54. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  55. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  56. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  57. package/src/input/panel-mouse-geometry.ts +97 -0
  58. package/src/input/panel-paste-flood-guard.ts +86 -0
  59. package/src/input/selection-modal.ts +11 -0
  60. package/src/input/session-picker-modal.ts +44 -4
  61. package/src/input/settings-modal-data.ts +51 -2
  62. package/src/input/settings-modal-types.ts +4 -2
  63. package/src/main.ts +28 -27
  64. package/src/panels/builtin/shared.ts +9 -3
  65. package/src/panels/fleet-deep-link.ts +31 -0
  66. package/src/panels/fleet-panel-format.ts +62 -0
  67. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  68. package/src/panels/fleet-panel.ts +89 -131
  69. package/src/panels/fleet-read-model.ts +43 -0
  70. package/src/panels/fleet-steer.ts +35 -2
  71. package/src/panels/fleet-stop.ts +29 -1
  72. package/src/panels/modals/keybindings-modal.ts +16 -1
  73. package/src/panels/modals/modal-theme.ts +35 -29
  74. package/src/panels/modals/pairing-modal.ts +25 -6
  75. package/src/panels/modals/planning-modal.ts +43 -21
  76. package/src/panels/modals/work-plan-modal.ts +28 -0
  77. package/src/panels/panel-manager.ts +15 -4
  78. package/src/panels/polish-core.ts +38 -25
  79. package/src/panels/project-planning-answer-actions.ts +8 -13
  80. package/src/panels/types.ts +24 -0
  81. package/src/permissions/prompt.ts +160 -13
  82. package/src/renderer/autocomplete-overlay.ts +28 -2
  83. package/src/renderer/compositor.ts +2 -3
  84. package/src/renderer/config-modal.ts +19 -5
  85. package/src/renderer/footer-tips.ts +5 -1
  86. package/src/renderer/fullscreen-primitives.ts +32 -22
  87. package/src/renderer/git-status.ts +3 -1
  88. package/src/renderer/layout.ts +0 -4
  89. package/src/renderer/markdown.ts +7 -3
  90. package/src/renderer/modal-factory.ts +25 -20
  91. package/src/renderer/model-workspace.ts +18 -3
  92. package/src/renderer/overlay-box.ts +21 -17
  93. package/src/renderer/process-indicator.ts +14 -3
  94. package/src/renderer/selection-modal-overlay.ts +6 -1
  95. package/src/renderer/session-picker-modal.ts +196 -3
  96. package/src/renderer/settings-modal-helpers.ts +2 -0
  97. package/src/renderer/settings-modal.ts +7 -0
  98. package/src/renderer/shell-surface.ts +8 -1
  99. package/src/renderer/status-glyphs.ts +14 -15
  100. package/src/renderer/system-message.ts +15 -3
  101. package/src/renderer/terminal-bg-probe.ts +339 -0
  102. package/src/renderer/terminal-escapes.ts +20 -0
  103. package/src/renderer/theme-mode-config.ts +67 -0
  104. package/src/renderer/theme.ts +91 -1
  105. package/src/renderer/thinking.ts +11 -3
  106. package/src/renderer/tool-call.ts +15 -9
  107. package/src/renderer/tool-result-summary.ts +148 -0
  108. package/src/renderer/turn-injection.ts +22 -3
  109. package/src/renderer/ui-factory.ts +154 -85
  110. package/src/renderer/ui-primitives.ts +30 -129
  111. package/src/runtime/bootstrap-command-context.ts +6 -0
  112. package/src/runtime/bootstrap-command-parts.ts +8 -4
  113. package/src/runtime/bootstrap-core.ts +33 -11
  114. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  115. package/src/runtime/bootstrap-shell.ts +19 -1
  116. package/src/runtime/bootstrap.ts +118 -5
  117. package/src/runtime/code-index-services.ts +25 -2
  118. package/src/runtime/legacy-daemon-migration.ts +516 -0
  119. package/src/runtime/memory-fold.ts +26 -0
  120. package/src/runtime/onboarding/derivation.ts +7 -2
  121. package/src/runtime/onboarding/snapshot.ts +27 -1
  122. package/src/runtime/onboarding/types.ts +31 -1
  123. package/src/runtime/operator-token-cleanup.ts +82 -1
  124. package/src/runtime/orchestrator-core-services.ts +10 -0
  125. package/src/runtime/resume-notice.ts +209 -0
  126. package/src/runtime/services.ts +12 -8
  127. package/src/runtime/session-inbound-inputs.ts +252 -0
  128. package/src/runtime/session-spine-transport.ts +64 -0
  129. package/src/runtime/terminal-output-guard.ts +15 -8
  130. package/src/runtime/ui-services.ts +19 -3
  131. package/src/runtime/workstream-services.ts +160 -28
  132. package/src/runtime/wrfc-persistence.ts +124 -17
  133. package/src/shell/blocking-input.ts +46 -3
  134. package/src/shell/recovery-input-helpers.ts +170 -1
  135. package/src/shell/ui-openers.ts +42 -9
  136. package/src/utils/terminal-width.ts +52 -0
  137. package/src/version.ts +1 -1
@@ -1,134 +1,35 @@
1
- export const GLYPHS = {
2
- frame: {
3
- topLeft: '┌',
4
- topRight: '┐',
5
- bottomLeft: '└',
6
- bottomRight: '┘',
7
- horizontal: '─',
8
- vertical: '│',
9
- teeLeft: '├',
10
- teeRight: '┤',
11
- teeTop: '┬',
12
- teeBottom: '┴',
13
- cross: '┼',
14
- },
15
- surface: {
16
- top: '▄',
17
- bottom: '▀',
18
- cursor: '█',
19
- altCursor: '▌',
20
- },
21
- navigation: {
22
- selected: '▸',
23
- collapsed: '▸',
24
- expanded: '▾',
25
- up: '',
26
- down: '↓',
27
- moreAbove: '▲',
28
- moreBelow: '▼',
29
- next: '→',
30
- back: '←',
31
- pipeSeparator: '│',
32
- },
33
- status: {
34
- success: '✓',
35
- failure: '✕',
36
- pending: '•',
37
- active: '●',
38
- idle: '◌',
39
- info: '○',
40
- warn: '⚠',
41
- blocked: '⊘',
42
- skipped: '◇',
43
- review: '◈',
44
- retry: '↻',
45
- handoff: '⇢',
46
- reference: '↗',
47
- partial: '◐',
48
- dualPane: '◆',
49
- star: '★',
50
- },
51
- meter: {
52
- filled: '█',
53
- medium: '▓',
54
- light: '▒',
55
- empty: '░',
56
- spark: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
57
- },
58
- } as const;
1
+ // ---------------------------------------------------------------------------
2
+ // ui-primitives.ts — glyph registry + tone-token table.
3
+ //
4
+ // W6-P1: these four tables (GLYPHS, UI_TONES, DIFF_TONES, SPINNER_FRAMES) are
5
+ // no longer minted locally. They are the SDK presentation contract
6
+ // (@pellux/goodvibes-sdk/platform/presentation, landed by W4-S1 and already
7
+ // adopted by the agent in W4-R4), consumed here so the TUI and the agent
8
+ // share ONE source (Mike's move-to-SDK ruling — machinery needed by 2+
9
+ // surfaces => SDK). The TUI was the reference these values were lifted from
10
+ // verbatim, so this swap is byte-identical. See
11
+ // docs/decisions/2026-07-05-presentation-contract-sdk-extraction.md in the SDK.
12
+ //
13
+ // Re-exported under the historical names (GLYPHS, UI_TONES) so every existing
14
+ // importer keeps working with no call-site churn. UI_TONES is the dark-mode
15
+ // tone table (== resolveTones('dark')); light is resolved via theme.ts's
16
+ // activeUiTones() / resolveUiTones(), which composes the mode dimension over
17
+ // this dark constant unchanged.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import {
21
+ GLYPHS,
22
+ TONE_TOKENS,
23
+ DIFF_TONES,
24
+ SPINNER_FRAMES,
25
+ } from '@pellux/goodvibes-sdk/platform/presentation';
26
+
27
+ export { GLYPHS, DIFF_TONES, SPINNER_FRAMES };
59
28
 
60
29
  /**
61
30
  * UI_TONES — the single chrome/color-token source for src/renderer and
62
31
  * src/panels. Concrete (dark-mode) values; resolveUiTones(mode) in theme.ts
63
- * is the single mode-resolved read path — this object is the 'dark' entry.
64
- */
65
- export const UI_TONES = {
66
- fg: {
67
- primary: '#e2e8f0',
68
- secondary: '#cbd5e1',
69
- muted: '#94a3b8',
70
- dim: '#475569',
71
- inverse: '#0f172a',
72
- /** Empty-state / placeholder foreground (formerly DEFAULT_PANEL_PALETTE.empty literal). */
73
- empty: '#334155',
74
- },
75
- bg: {
76
- base: '#11131a',
77
- surface: '#161a22',
78
- title: '#0f172a',
79
- section: '#18202b',
80
- summary: '#1b2430',
81
- selected: '#223049',
82
- input: '#1e293b',
83
- warning: '#2b2116',
84
- error: '#2a161b',
85
- success: '#14241b',
86
- /** Fullscreen/shell footer background. */
87
- footer: '#111827',
88
- },
89
- state: {
90
- info: '#38bdf8',
91
- good: '#22c55e',
92
- warn: '#f59e0b',
93
- bad: '#ef4444',
94
- blocked: '#f97316',
95
- active: '#60a5fa',
96
- /** Single canonical reasoning/thinking purple (replaces #9945FF and ad-hoc purples). */
97
- reasoning: '#a855f7',
98
- },
99
- accent: {
100
- browser: '#7dd3fc',
101
- control: '#22d3ee',
102
- inspector: '#c4b5fd',
103
- workflow: '#fbbf24',
104
- conversation: '#93c5fd',
105
- /** Neon brand accent — header/splash/thinking gradient only. */
106
- brand: '#00ffff',
107
- gradientStart: '#00ffff',
108
- gradientEnd: '#d000ff',
109
- },
110
- /** Canonical border stroke color for fullscreen/panel chrome. */
111
- border: '#64748b',
112
- } as const;
113
-
114
- /**
115
- * Diff surface accent color shared across the three diff-rendering surfaces —
116
- * the conversation diff view (diff-view.ts), the file diff panel
117
- * (diff-panel.ts), and the git panel's inline diff (git-panel.ts). Hunk-header
118
- * blue has no matching UI_TONES.state/accent role, so WO-204 gives it one
119
- * named token here (value converged on diff-panel.ts's pre-existing hunk
120
- * color, the only one of the three surfaces that predates this token; add/del
121
- * on all three surfaces already converge on UI_TONES.state.good/bad).
32
+ * is the single mode-resolved read path — this object is the 'dark' entry
33
+ * (== resolveTones('dark') from the SDK presentation contract).
122
34
  */
123
- export const DIFF_TONES = {
124
- // add/del carry diff-panel.ts's shipped colors: the diff panel is the only
125
- // diff surface users have ever seen (diff-view.ts was unwired from v0.9.6
126
- // until WO-204), so its look is the reference — the conversation surface
127
- // adopts it, never the other way around.
128
- add: '#00ff88',
129
- del: '#ff4444',
130
- hunk: '#88aaff',
131
- } as const;
132
-
133
- /** Single spinner-frame source — layout.ts and progress.ts both re-export this. */
134
- export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
35
+ export const UI_TONES = TONE_TOKENS;
@@ -123,6 +123,8 @@ export type CreateBootstrapCommandContextOptions = {
123
123
  hydrateSessionUsage?: () => void;
124
124
  workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
125
125
  codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
126
+ codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
127
+ isPassiveCodeInjectionFlagEnabled?: () => boolean;
126
128
  getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
127
129
  };
128
130
 
@@ -184,6 +186,8 @@ export function createBootstrapCommandContext(
184
186
  hydrateSessionUsage,
185
187
  workstreamEngine,
186
188
  codeIndexStore,
189
+ codeIndexReindexScheduler,
190
+ isPassiveCodeInjectionFlagEnabled,
187
191
  getMainSessionTurnInjections,
188
192
  planManager,
189
193
  adaptivePlanner,
@@ -244,6 +248,8 @@ export function createBootstrapCommandContext(
244
248
  hydrateSessionUsage,
245
249
  workstreamEngine,
246
250
  codeIndexStore,
251
+ codeIndexReindexScheduler,
252
+ isPassiveCodeInjectionFlagEnabled,
247
253
  getMainSessionTurnInjections,
248
254
  });
249
255
  const provider = createBootstrapCommandProviderSection({
@@ -5,7 +5,7 @@ import type { ConversationManager } from '../core/conversation';
5
5
  import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
6
6
  import type { HookApi } from '@pellux/goodvibes-sdk/platform/hooks';
7
7
  import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp';
8
- import type { PanelManager } from '../panels/panel-manager.ts';
8
+ import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
9
9
  import type { ProviderApi } from '@pellux/goodvibes-sdk/platform/providers';
10
10
  import type { OpsApi } from '@/runtime/index.ts';
11
11
  import type { MutableRuntimeState } from '@/runtime/index.ts';
@@ -118,6 +118,8 @@ export interface BootstrapCommandSectionOptions {
118
118
  readonly hydrateSessionUsage?: () => void;
119
119
  readonly workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
120
120
  readonly codeIndexStore?: CodeIndexStore;
121
+ readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
122
+ readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
121
123
  readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
122
124
  readonly agentManager?: ShellAgentManagerService;
123
125
  readonly modeManager?: ShellModeManagerService;
@@ -190,7 +192,7 @@ export function createBootstrapCommandActions(
190
192
  localUserAuthManager,
191
193
  } = options;
192
194
 
193
- const showPanel = (panelId: string, pane?: 'top' | 'bottom') => {
195
+ const showPanel = (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget) => {
194
196
  // W6.1 (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
195
197
  // panelManager.open() fires the injected openModal callback and returns a
196
198
  // no-op sentinel — so skip panelManager.show() (which would reveal an empty
@@ -198,7 +200,7 @@ export function createBootstrapCommandActions(
198
200
  // showPanel-based front-door (openHooksPanel/openSecurityPanel/… and the
199
201
  // migrated command runtimes) opening the modal cleanly.
200
202
  const redirected = panelManager.getModalRedirect(panelId) !== undefined;
201
- panelManager.open(panelId, pane);
203
+ panelManager.open(panelId, pane, target);
202
204
  if (!redirected) panelManager.show();
203
205
  requestRender();
204
206
  };
@@ -321,7 +323,7 @@ export function createBootstrapCommandActions(
321
323
  export function createBootstrapCommandSessionSection(
322
324
  options: Pick<
323
325
  BootstrapCommandSectionOptions,
324
- 'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'getMainSessionTurnInjections'
326
+ 'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'codeIndexReindexScheduler' | 'isPassiveCodeInjectionFlagEnabled' | 'getMainSessionTurnInjections'
325
327
  >,
326
328
  ): BootstrapCommandSessionSection {
327
329
  return {
@@ -335,6 +337,8 @@ export function createBootstrapCommandSessionSection(
335
337
  hydrateSessionUsage: options.hydrateSessionUsage,
336
338
  workstreamEngine: options.workstreamEngine,
337
339
  codeIndexStore: options.codeIndexStore,
340
+ codeIndexReindexScheduler: options.codeIndexReindexScheduler,
341
+ isPassiveCodeInjectionFlagEnabled: options.isPassiveCodeInjectionFlagEnabled,
338
342
  getMainSessionTurnInjections: options.getMainSessionTurnInjections,
339
343
  };
340
344
  }
@@ -33,6 +33,8 @@ import { join } from 'node:path';
33
33
  import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
34
34
  import { createWrfcPersistence, type WrfcPersistence } from './wrfc-persistence.ts';
35
35
  import type { SystemMessagePriority } from '../core/system-message-router.ts';
36
+ import { SessionSpineClient, SessionUnionCache, TUI_SPINE_PARTICIPANT } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
37
+ import { SessionInboundInputPoller, createBootstrapInboundInputPoller } from './session-inbound-inputs.ts';
36
38
 
37
39
  // ---------------------------------------------------------------------------
38
40
  // Pre-router buffer
@@ -114,6 +116,12 @@ export interface BootstrapCoreState {
114
116
  readonly requestRender: () => void;
115
117
  readonly setRenderRequest: (fn: () => void) => void;
116
118
  readonly runtimeSessionIdRef: { value: string };
119
+ /** Cross-surface identity mirror; permanently dormant for embedded/local-only (docs/decisions/2026-07-06-session-spine-mode-branch-is-permanent.md), activated by bootstrap.ts only for an adopted 'external' daemon. */
120
+ readonly sessionSpine: SessionSpineClient;
121
+ /** D3: inbound steer/follow-up delivery; dormant until bootstrap.ts activates it. */
122
+ readonly sessionInboundInputs: SessionInboundInputPoller;
123
+ /** Cache-backed read facade; bootstrap.ts drives its mode (embedded/external/local-only) from the same HostServiceMode as the spine above. */
124
+ readonly sessionUnionCache: SessionUnionCache;
117
125
  /**
118
126
  * WRFC chain persistence — call `rehydrate()` once after the SystemMessageRouter
119
127
  * is wired so interrupted chains from a previous process are surfaced to the operator.
@@ -127,17 +135,8 @@ export type CompanionMessagePayload = Extract<SessionEvent, { type: 'COMPANION_M
127
135
  // Operator narration of inbound channel events
128
136
  // ---------------------------------------------------------------------------
129
137
 
130
- /**
131
- * Narrate an inbound channel event to the operator via the SystemMessageRouter.
132
- *
133
- * When an external surface (GitHub, Slack, ntfy, etc.) triggers an agent turn,
134
- * this function produces a human-readable system message so the operator can
135
- * observe which event caused the turn. Returns null for internal/companion
136
- * sources that do not need operator narration.
137
- *
138
- * @param event - The normalized inbound event descriptor.
139
- * @returns A narration string, or null if no narration is appropriate.
140
- */
138
+ /** Narrate an inbound channel event (GitHub, Slack, ntfy, etc.) that triggered an
139
+ * agent turn, via the SystemMessageRouter null for internal/companion sources. */
141
140
  export function narrateInboundEvent(event: {
142
141
  source: string;
143
142
  metadata: Readonly<Record<string, unknown>> | undefined;
@@ -293,6 +292,12 @@ export async function initializeBootstrapCore(
293
292
  surfaceRegistry,
294
293
  watcherRegistry,
295
294
  } = services;
295
+ // Permanently dormant for embedded/local-only (nothing to mirror to); bootstrap.ts activates it only for an adopted 'external' daemon.
296
+ const sessionSpine = new SessionSpineClient({ participant: TUI_SPINE_PARTICIPANT, recordKind: 'tui' });
297
+ // Cache-backed read facade over the local broker (passthrough until bootstrap.ts marks it
298
+ // embedded, or activates the adopted-daemon wire union for 'external').
299
+ // selfSessionIds keeps local authoritative for our own wire-mirrored session even when local/wire ids diverge (D-TUI-1).
300
+ const sessionUnionCache = new SessionUnionCache({ local: sharedSessionBroker, selfSessionIds: () => sessionSpine.mirroredSessionIds });
296
301
 
297
302
  routeBindings.attachRuntime({ runtimeBus, runtimeStore: store });
298
303
  surfaceRegistry.attachRuntime(store);
@@ -324,6 +329,7 @@ export async function initializeBootstrapCore(
324
329
  const uiServices = createUiRuntimeServices(services, {
325
330
  forensicsRegistry,
326
331
  getControlPlaneRecentEvents,
332
+ sessionUnionCache,
327
333
  });
328
334
 
329
335
  const conversation = new ConversationManager(() => {
@@ -427,6 +433,10 @@ export async function initializeBootstrapCore(
427
433
  } catch (err) {
428
434
  logger.error('Render threw; next requestRender will reschedule', { error: String(err) });
429
435
  }
436
+ // Debounced spine heartbeat on turn/render activity (no-op while dormant, i.e.
437
+ // embedded/local-only): a cheap synchronous no-op unless its own internal window has
438
+ // elapsed (at most one wire call per heartbeatMinIntervalMs) — safe on the hot path.
439
+ sessionSpine.heartbeat(runtimeSessionIdRef.value);
430
440
  };
431
441
  const requestRender = (): void => {
432
442
  if (renderScheduled) return;
@@ -634,6 +644,11 @@ export async function initializeBootstrapCore(
634
644
  },
635
645
  ));
636
646
 
647
+ // D3: inbound steer delivery — see createBootstrapInboundInputPoller's doc comment.
648
+ const sessionInboundInputs = createBootstrapInboundInputPoller({
649
+ runtimeSessionIdRef, routeOrBuffer, orchestratorHandleUserInputRef, conversation, requestRender,
650
+ });
651
+
637
652
  providerRegistry.startWatching(runtimeBus);
638
653
 
639
654
  // W2.3: attach the SAME WebhookNotifier instance that `/notify add|remove|clear`
@@ -724,6 +739,10 @@ export async function initializeBootstrapCore(
724
739
  lastSeenAt: Date.now(),
725
740
  },
726
741
  }).catch((err) => { logger.debug('session broker create session failed at bootstrap', { err }); });
742
+ // Fire-and-forget spine mirror — a permanent parallel-write posture alongside (never
743
+ // instead of) the still-authoritative local SharedSessionBroker above. Dormant/queued
744
+ // until bootstrap.ts's deferred task activates it for an adopted 'external' daemon.
745
+ sessionSpine.register({ sessionId: runtime.sessionId, project: services.workingDirectory, title: 'Terminal UI session' });
727
746
 
728
747
  domainDispatch.syncSessionState({
729
748
  id: userSessionId,
@@ -774,5 +793,8 @@ export async function initializeBootstrapCore(
774
793
  },
775
794
  runtimeSessionIdRef,
776
795
  wrfcPersistence,
796
+ sessionSpine,
797
+ sessionInboundInputs,
798
+ sessionUnionCache,
777
799
  };
778
800
  }
@@ -9,6 +9,7 @@ import { HelperModel } from '@pellux/goodvibes-sdk/platform/config';
9
9
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
10
10
  import { formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
11
11
  import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
12
+ import type { SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
12
13
  import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
13
14
  import type { PanelManager } from '../panels/panel-manager.ts';
14
15
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
@@ -22,6 +23,10 @@ export interface ResumeSessionOptions {
22
23
  readonly requestRender: () => void;
23
24
  readonly onSessionIdChanged?: (sessionId: string) => void;
24
25
  readonly sharedSessionBroker: Pick<SharedSessionBroker, 'reopenSession'>;
26
+ /** S3c: fire-and-forget daemon-spine mirror. Reopen (not register) is the
27
+ * ONLY resume-time verb — see the SDK session-spine client.ts header doc. */
28
+ readonly sessionSpine: Pick<SessionSpineClient, 'reopen'>;
29
+ readonly project: string;
25
30
  readonly writeLastSessionPointer: (sessionId: string) => void;
26
31
  readonly hookDispatcher: HookDispatcher;
27
32
  readonly sessionManager: SessionManager;
@@ -75,6 +80,8 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
75
80
  if (meta?.provider) options.runtime.provider = meta.provider;
76
81
  options.writeLastSessionPointer(sessionId);
77
82
  void options.sharedSessionBroker.reopenSession(sessionId).catch((err) => { logger.debug('session broker reopen session failed', { err }); });
83
+ // S3c: fire-and-forget spine mirror (reopen:true — the user resume verb).
84
+ options.sessionSpine.reopen({ sessionId, project: options.project, title: options.conversation.title || meta.title });
78
85
  options.conversation.log(`Resumed session: ${sessionId}`, { fg: '135' });
79
86
  const reopenedPanels: string[] = [];
80
87
  if (meta.returnContext?.openPanels?.length) {
@@ -32,6 +32,7 @@ import type { PolicyRuntimeState } from '@/runtime/index.ts';
32
32
  import type { TaskManager } from '@/runtime/index.ts';
33
33
  import type { UiRuntimeServices } from './ui-services.ts';
34
34
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
35
+ import type { SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
35
36
 
36
37
  export interface BootstrapShellState {
37
38
  readonly commandRegistry: CommandRegistry;
@@ -47,6 +48,8 @@ export interface BootstrapShellOptions {
47
48
  readonly runtimeBus: RuntimeEventBus;
48
49
  readonly runtimeStore: RuntimeStore;
49
50
  readonly services: RuntimeServices;
51
+ /** S3c: dormant until bootstrap.ts activates it for an adopted 'external' daemon. */
52
+ readonly sessionSpine: SessionSpineClient;
50
53
  readonly conversation: ConversationManager;
51
54
  readonly runtime: MutableRuntimeState;
52
55
  readonly orchestrator: Orchestrator;
@@ -70,6 +73,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
70
73
  runtimeBus,
71
74
  runtimeStore,
72
75
  services,
76
+ sessionSpine,
73
77
  conversation,
74
78
  runtime,
75
79
  orchestrator,
@@ -103,6 +107,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
103
107
  requestRender,
104
108
  onSessionIdChanged,
105
109
  sharedSessionBroker: services.sessionBroker,
110
+ sessionSpine,
111
+ project: services.workingDirectory,
106
112
  writeLastSessionPointer,
107
113
  hookDispatcher: services.hookDispatcher,
108
114
  sessionManager: services.sessionManager,
@@ -175,7 +181,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
175
181
  forensicsRegistry,
176
182
  policyRuntimeState,
177
183
  approvalBroker: services.approvalBroker,
178
- sessionBroker: services.sessionBroker,
184
+ // S3d: panels read the cross-surface union facade, not the raw local broker.
185
+ sessionBroker: uiServices.sessions.sessionBroker,
179
186
  automationManager: services.automationManager,
180
187
  getControlPlaneRecentEvents,
181
188
  tokenAuditor: services.tokenAuditor,
@@ -207,6 +214,15 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
207
214
  if (kind === 'operational') return ui.operationalMessages;
208
215
  return ui.systemMessages;
209
216
  },
217
+ {
218
+ // Suppress stale WRFC replay re-notifications for chains that can no
219
+ // longer act — gone (killed/removed → getChain null) or terminal
220
+ // (passed/failed). (UX-B item 1c.)
221
+ isChainTerminal: (chainId) => {
222
+ const chain = services.wrfcController.getChain(chainId);
223
+ return chain === null || chain.state === 'passed' || chain.state === 'failed';
224
+ },
225
+ },
210
226
  );
211
227
  orchestrator.setSystemMessageRouter(systemMessageRouter);
212
228
 
@@ -271,6 +287,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
271
287
  wrfcController: services.wrfcController,
272
288
  workstreamEngine: services.workstreamCommands,
273
289
  codeIndexStore: services.codeIndexStore,
290
+ codeIndexReindexScheduler: services.codeIndexReindexScheduler,
291
+ isPassiveCodeInjectionFlagEnabled: () => services.featureFlags.isEnabled('agent-passive-code-injection'),
274
292
  // Wave 5 (wo805): expose the MAIN session's per-turn passive-injection ring
275
293
  // so `/recall injections` (no agent id) renders it — see recall-review.ts.
276
294
  getMainSessionTurnInjections: () => orchestrator.getTurnInjections(),
@@ -36,12 +36,16 @@ 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';
45
49
  import { buildSharedOrchestratorCoreServices } from './orchestrator-core-services.ts';
46
50
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
47
51
  import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon';
@@ -183,6 +187,9 @@ export async function bootstrapRuntime(
183
187
  setRenderRequest,
184
188
  runtimeSessionIdRef,
185
189
  wrfcPersistence,
190
+ sessionSpine,
191
+ sessionInboundInputs,
192
+ sessionUnionCache,
186
193
  } = await initializeBootstrapCore(stdout, options, (limit) => controlPlaneRecentEventsRef.value(limit));
187
194
  const providerRegistry = services.providerRegistry;
188
195
  const {
@@ -192,6 +199,11 @@ export async function bootstrapRuntime(
192
199
  pluginManager,
193
200
  } = services;
194
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
+
195
207
  // ── Phase 6: Orchestrator + AcpManager ───────────────────────────────────
196
208
 
197
209
  // Mutable function refs so main.ts can patch these after constructing the scroll/viewport state.
@@ -262,6 +274,7 @@ export async function bootstrapRuntime(
262
274
  runtimeBus,
263
275
  runtimeStore: store,
264
276
  services,
277
+ sessionSpine,
265
278
  conversation,
266
279
  runtime,
267
280
  orchestrator,
@@ -287,6 +300,25 @@ export async function bootstrapRuntime(
287
300
  wrfcPersistence.rehydrate();
288
301
  const commandRegistry = shell.commandRegistry;
289
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
+ });
290
322
  const gitStatusProvider = shell.gitStatusProvider;
291
323
  const inputHistory = shell.inputHistory;
292
324
  const lastGitInfoRef = shell.lastGitInfoRef;
@@ -343,7 +375,70 @@ export async function bootstrapRuntime(
343
375
 
344
376
  const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
345
377
 
346
- 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
+ };
347
442
 
348
443
  const inspectExternalServices = () => {
349
444
  const daemonStatus = externalServices.daemonStatus;
@@ -355,6 +450,16 @@ export async function bootstrapRuntime(
355
450
  httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
356
451
  daemonStatus,
357
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),
358
463
  };
359
464
  };
360
465
 
@@ -413,7 +518,7 @@ export async function bootstrapRuntime(
413
518
  await waitForConfigDrivenRestarts(externalServices);
414
519
  await externalServices.stop();
415
520
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
416
- const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
521
+ const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
417
522
  externalServicesPromise = startExternalServices(
418
523
  configManager,
419
524
  runtimeBus,
@@ -423,6 +528,7 @@ export async function bootstrapRuntime(
423
528
  );
424
529
  externalServices = await externalServicesPromise;
425
530
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
531
+ syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
426
532
  requestRender();
427
533
  return inspectExternalServices();
428
534
  },
@@ -461,7 +567,7 @@ export async function bootstrapRuntime(
461
567
  // bearer, so tokens scanned from the /qrcode panel's QR actually
462
568
  // authenticate against the embedded daemon this surface starts.
463
569
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
464
- const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
570
+ const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
465
571
  // F3 resolution (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
466
572
  // token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
467
573
  // The prune is best-effort — it silently skips missing files, no-ops when tokens
@@ -486,6 +592,7 @@ export async function bootstrapRuntime(
486
592
  );
487
593
  externalServices = await externalServicesPromise;
488
594
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
595
+ syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
489
596
  requestRender();
490
597
  },
491
598
  onError: (error) => {
@@ -630,6 +737,12 @@ export async function bootstrapRuntime(
630
737
  runtimeUnsubs.forEach((fn) => fn());
631
738
  runtimeUnsubs.length = 0;
632
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
633
746
  await deferredStartup.drain(100);
634
747
  if (externalServicesPromise) {
635
748
  try {