@pellux/goodvibes-tui 1.7.0 → 1.9.1

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 (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +10 -9
  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/config/credential-availability.ts +122 -0
  12. package/src/config/index.ts +15 -0
  13. package/src/core/composer-state.ts +11 -3
  14. package/src/core/conversation-line-cache.ts +27 -3
  15. package/src/core/conversation-rendering.ts +71 -14
  16. package/src/core/stream-event-wiring.ts +20 -1
  17. package/src/core/system-message-noise.ts +87 -0
  18. package/src/core/system-message-router.ts +68 -1
  19. package/src/core/turn-cancellation.ts +7 -2
  20. package/src/core/turn-event-wiring.ts +10 -2
  21. package/src/daemon/cli.ts +29 -2
  22. package/src/daemon/handlers/register.ts +8 -1
  23. package/src/daemon/service-commands.ts +329 -0
  24. package/src/input/autocomplete.ts +27 -1
  25. package/src/input/command-registry.ts +46 -4
  26. package/src/input/commands/codebase-runtime.ts +46 -6
  27. package/src/input/commands/config.ts +43 -3
  28. package/src/input/commands/health-runtime.ts +9 -1
  29. package/src/input/commands/memory.ts +68 -35
  30. package/src/input/commands/operator-panel-runtime.ts +31 -7
  31. package/src/input/commands/planning-runtime.ts +95 -6
  32. package/src/input/commands/qrcode-runtime.ts +25 -5
  33. package/src/input/commands/remote-runtime-setup.ts +5 -3
  34. package/src/input/commands/session-content.ts +20 -9
  35. package/src/input/commands/settings-sync-runtime.ts +15 -3
  36. package/src/input/commands/shell-core.ts +10 -1
  37. package/src/input/commands/workstream-runtime.ts +168 -18
  38. package/src/input/config-modal-types.ts +15 -1
  39. package/src/input/config-modal.ts +227 -12
  40. package/src/input/feed-context-factory.ts +3 -0
  41. package/src/input/handler-command-route.ts +10 -3
  42. package/src/input/handler-content-actions.ts +17 -2
  43. package/src/input/handler-feed-routes.ts +43 -121
  44. package/src/input/handler-feed.ts +32 -4
  45. package/src/input/handler-modal-routes.ts +78 -13
  46. package/src/input/handler-modal-stack.ts +11 -2
  47. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  48. package/src/input/handler-onboarding.ts +71 -59
  49. package/src/input/handler-picker-routes.ts +15 -8
  50. package/src/input/handler-shortcuts.ts +59 -9
  51. package/src/input/handler.ts +6 -1
  52. package/src/input/keybindings.ts +6 -5
  53. package/src/input/model-picker.ts +19 -2
  54. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  55. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  56. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  57. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  58. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  59. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  60. package/src/input/panel-mouse-geometry.ts +97 -0
  61. package/src/input/panel-paste-flood-guard.ts +86 -0
  62. package/src/input/selection-modal.ts +11 -0
  63. package/src/input/session-picker-modal.ts +44 -4
  64. package/src/input/settings-modal-data.ts +51 -2
  65. package/src/input/settings-modal-types.ts +4 -2
  66. package/src/main.ts +28 -27
  67. package/src/panels/builtin/shared.ts +9 -3
  68. package/src/panels/fleet-deep-link.ts +31 -0
  69. package/src/panels/fleet-panel-format.ts +62 -0
  70. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  71. package/src/panels/fleet-panel.ts +89 -131
  72. package/src/panels/fleet-read-model.ts +43 -0
  73. package/src/panels/fleet-steer.ts +35 -2
  74. package/src/panels/fleet-stop.ts +29 -1
  75. package/src/panels/modals/keybindings-modal.ts +16 -1
  76. package/src/panels/modals/modal-theme.ts +35 -29
  77. package/src/panels/modals/pairing-modal.ts +25 -6
  78. package/src/panels/modals/planning-modal.ts +43 -21
  79. package/src/panels/modals/work-plan-modal.ts +28 -0
  80. package/src/panels/panel-manager.ts +15 -4
  81. package/src/panels/polish-core.ts +38 -25
  82. package/src/panels/project-planning-answer-actions.ts +8 -13
  83. package/src/panels/types.ts +24 -0
  84. package/src/permissions/prompt.ts +160 -13
  85. package/src/renderer/autocomplete-overlay.ts +28 -2
  86. package/src/renderer/compositor.ts +2 -3
  87. package/src/renderer/config-modal.ts +19 -5
  88. package/src/renderer/footer-tips.ts +5 -1
  89. package/src/renderer/fullscreen-primitives.ts +32 -22
  90. package/src/renderer/git-status.ts +3 -1
  91. package/src/renderer/layout.ts +0 -4
  92. package/src/renderer/markdown.ts +7 -3
  93. package/src/renderer/modal-factory.ts +25 -20
  94. package/src/renderer/model-workspace.ts +18 -3
  95. package/src/renderer/overlay-box.ts +21 -17
  96. package/src/renderer/process-indicator.ts +14 -3
  97. package/src/renderer/selection-modal-overlay.ts +6 -1
  98. package/src/renderer/session-picker-modal.ts +196 -3
  99. package/src/renderer/settings-modal-helpers.ts +2 -0
  100. package/src/renderer/settings-modal.ts +7 -0
  101. package/src/renderer/shell-surface.ts +8 -1
  102. package/src/renderer/status-glyphs.ts +14 -15
  103. package/src/renderer/system-message.ts +15 -3
  104. package/src/renderer/terminal-bg-probe.ts +339 -0
  105. package/src/renderer/terminal-escapes.ts +20 -0
  106. package/src/renderer/theme-mode-config.ts +67 -0
  107. package/src/renderer/theme.ts +91 -1
  108. package/src/renderer/thinking.ts +11 -3
  109. package/src/renderer/tool-call.ts +15 -9
  110. package/src/renderer/tool-result-summary.ts +148 -0
  111. package/src/renderer/turn-injection.ts +22 -3
  112. package/src/renderer/ui-factory.ts +154 -85
  113. package/src/renderer/ui-primitives.ts +30 -129
  114. package/src/runtime/bootstrap-command-context.ts +6 -0
  115. package/src/runtime/bootstrap-command-parts.ts +8 -4
  116. package/src/runtime/bootstrap-core.ts +46 -24
  117. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  118. package/src/runtime/bootstrap-shell.ts +19 -1
  119. package/src/runtime/bootstrap.ts +118 -5
  120. package/src/runtime/code-index-services.ts +25 -2
  121. package/src/runtime/legacy-daemon-migration.ts +516 -0
  122. package/src/runtime/memory-fold.ts +55 -0
  123. package/src/runtime/onboarding/derivation.ts +7 -2
  124. package/src/runtime/onboarding/snapshot.ts +27 -1
  125. package/src/runtime/onboarding/types.ts +31 -1
  126. package/src/runtime/operator-token-cleanup.ts +82 -1
  127. package/src/runtime/orchestrator-core-services.ts +10 -0
  128. package/src/runtime/resume-notice.ts +209 -0
  129. package/src/runtime/services.ts +12 -8
  130. package/src/runtime/session-inbound-inputs.ts +252 -0
  131. package/src/runtime/session-spine-transport.ts +64 -0
  132. package/src/runtime/terminal-output-guard.ts +15 -8
  133. package/src/runtime/ui-services.ts +19 -3
  134. package/src/runtime/workstream-services.ts +160 -28
  135. package/src/runtime/wrfc-persistence.ts +124 -17
  136. package/src/shell/blocking-input.ts +46 -3
  137. package/src/shell/recovery-input-helpers.ts +170 -1
  138. package/src/shell/ui-openers.ts +42 -9
  139. package/src/utils/terminal-width.ts +52 -0
  140. package/src/version.ts +1 -1
@@ -0,0 +1,252 @@
1
+ /**
2
+ * session-inbound-inputs.ts
3
+ *
4
+ * D3 (One-Platform Wave 2, live-surface steer delivery — the inbound half of the
5
+ * session spine): the TUI's INBOUND path for steer/follow-up inputs that another
6
+ * live surface (e.g. the webui) queued against THIS TUI's session.
7
+ *
8
+ * Charter: when the owning surface is LIVE (heartbeating within the daemon's
9
+ * SURFACE_ROUTE_FRESHNESS window), a steer is DELIVERED to that surface rather
10
+ * than spawning a daemon-side executor (which would fail on the daemon's empty
11
+ * model registry). The daemon already does its half — `sessions.steer` /
12
+ * `sessions.followUp` to a surface-managed session with a fresh participant
13
+ * QUEUES the input for the surface (mode 'queued-for-surface') instead of
14
+ * spawning. This poller is the surface completing that contract:
15
+ *
16
+ * 1. poll `sessions.inputs.list` for QUEUED inputs on ITS OWN sessionId
17
+ * (the query is scoped by sessionId, so a steer for another session can
18
+ * never land here — the per-session isolation the concurrent-sessions
19
+ * charter requires);
20
+ * 2. hand each steer/follow-up to `onSteer` (the surface-side injection into
21
+ * the TUI's turn machinery — narrate "steer received from <surface>" and
22
+ * fire the next-turn boundary), skipping the TUI's own submissions;
23
+ * 3. acknowledge on the wire via `sessions.inputs.deliver` (queued → delivered)
24
+ * so every surface sees delivery truth live and the input is not re-picked.
25
+ *
26
+ * Discipline (mirrors SessionSpineClient): every wire call is best-effort and
27
+ * never throws into the render/keystroke path; a failed poll leaves the cursor
28
+ * where it was so the input is retried on the next tick (deliver is the only
29
+ * de-dup — an input already advanced past 'queued' is not returned again).
30
+ */
31
+
32
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
33
+ import type {
34
+ SharedSessionInputIntent,
35
+ SharedSessionInputRecord,
36
+ } from '@pellux/goodvibes-sdk/platform/control-plane';
37
+
38
+ /** The TUI's own participant surfaceId — inputs it authored are never re-injected. */
39
+ export const TUI_SURFACE_ID = 'surface:tui';
40
+
41
+ /** The narrow inbound wire surface this poller needs. Structurally satisfied by
42
+ * the http operator client's `sessions.inputs` (list + deliver). Typed against
43
+ * this shape rather than the concrete client so tests inject a stub. */
44
+ export interface SpineInboundInputsClient {
45
+ listInputs(
46
+ sessionId: string,
47
+ options: { readonly state?: string; readonly since?: number; readonly limit?: number },
48
+ ): Promise<{ readonly inputs: readonly SharedSessionInputRecord[] }>;
49
+ deliverInput(
50
+ sessionId: string,
51
+ inputId: string,
52
+ options?: { readonly consumed?: boolean },
53
+ ): Promise<unknown>;
54
+ }
55
+
56
+ /** A steer/follow-up collected from the wire for surface-side injection. */
57
+ export interface InboundSteer {
58
+ readonly inputId: string;
59
+ readonly sessionId: string;
60
+ readonly intent: SharedSessionInputIntent;
61
+ readonly body: string;
62
+ readonly surfaceKind: string | undefined;
63
+ readonly surfaceId: string | undefined;
64
+ readonly displayName: string | undefined;
65
+ }
66
+
67
+ type InboundLogger = Pick<typeof logger, 'debug' | 'info'>;
68
+
69
+ export interface SessionInboundInputPollerOptions {
70
+ /** Returns the TUI's current live sessionId, or null when there is none yet.
71
+ * Read on every tick so a session swap re-targets the poll automatically. */
72
+ readonly sessionId: () => string | null;
73
+ /** Surface-side injection: narrate + deliver the steer into the turn machinery. */
74
+ readonly onSteer: (steer: InboundSteer) => void;
75
+ readonly intervalMs?: number;
76
+ readonly now?: () => number;
77
+ readonly log?: InboundLogger;
78
+ /** The poller's own surfaceId — inputs it authored are skipped (default TUI). */
79
+ readonly ownSurfaceId?: string;
80
+ }
81
+
82
+ const DEFAULT_POLL_INTERVAL_MS = 4_000;
83
+
84
+ export class SessionInboundInputPoller {
85
+ private readonly sessionId: () => string | null;
86
+ private readonly onSteer: (steer: InboundSteer) => void;
87
+ private readonly intervalMs: number;
88
+ private readonly now: () => number;
89
+ private readonly log: InboundLogger;
90
+ private readonly ownSurfaceId: string;
91
+
92
+ private client: SpineInboundInputsClient | null = null;
93
+ private timer: ReturnType<typeof setInterval> | null = null;
94
+ private polling = false;
95
+ /** Exclusive createdAt cursor, per session — a session swap resets it. */
96
+ private cursor = 0;
97
+ private cursorSessionId: string | null = null;
98
+
99
+ constructor(options: SessionInboundInputPollerOptions) {
100
+ this.sessionId = options.sessionId;
101
+ this.onSteer = options.onSteer;
102
+ this.intervalMs = Math.max(250, options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS);
103
+ this.now = options.now ?? (() => Date.now());
104
+ this.log = options.log ?? logger;
105
+ this.ownSurfaceId = options.ownSurfaceId ?? TUI_SURFACE_ID;
106
+ }
107
+
108
+ /** Whether a backend is attached (external daemon adopted). */
109
+ get active(): boolean {
110
+ return this.client !== null;
111
+ }
112
+
113
+ /**
114
+ * Attach the wire client and begin polling (external daemon adopted). Idempotent:
115
+ * re-activating swaps the client without stacking timers.
116
+ */
117
+ activate(client: SpineInboundInputsClient): void {
118
+ this.client = client;
119
+ if (this.timer === null) {
120
+ this.timer = setInterval(() => { void this.pollOnce(); }, this.intervalMs);
121
+ this.timer.unref?.();
122
+ }
123
+ this.log.info('session inbound-input poller activated — collecting queued steers for this surface', {});
124
+ }
125
+
126
+ /** Detach the backend and stop polling (daemon mode resolved to non-external, or lost). */
127
+ deactivate(reason: string): void {
128
+ this.client = null;
129
+ if (this.timer !== null) {
130
+ clearInterval(this.timer);
131
+ this.timer = null;
132
+ }
133
+ this.log.info('session inbound-input poller deactivated', { reason });
134
+ }
135
+
136
+ /** Stop the timer (shutdown). */
137
+ dispose(): void {
138
+ if (this.timer !== null) {
139
+ clearInterval(this.timer);
140
+ this.timer = null;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * One poll pass: collect queued steer/follow-up inputs for the current session,
146
+ * inject each, and acknowledge delivery. Returns the number of steers delivered.
147
+ * Best-effort: any wire error is absorbed and the pass is retried next tick.
148
+ */
149
+ async pollOnce(): Promise<number> {
150
+ const client = this.client;
151
+ if (!client || this.polling) return 0;
152
+ const sessionId = this.sessionId();
153
+ if (!sessionId) return 0;
154
+
155
+ // A session swap resets the cursor so we never skip the new session's backlog.
156
+ if (sessionId !== this.cursorSessionId) {
157
+ this.cursor = 0;
158
+ this.cursorSessionId = sessionId;
159
+ }
160
+
161
+ this.polling = true;
162
+ let delivered = 0;
163
+ try {
164
+ const result = await client.listInputs(sessionId, { state: 'queued', since: this.cursor });
165
+ const inputs = result?.inputs ?? [];
166
+ for (const input of inputs) {
167
+ // Advance the cursor across every queued input we observe (even skipped
168
+ // ones) so it is monotonic; deliver() is the real de-dup for injected ones.
169
+ if (input.createdAt > this.cursor) this.cursor = input.createdAt;
170
+
171
+ if (input.state !== 'queued') continue;
172
+ if (input.intent !== 'steer' && input.intent !== 'follow-up') continue;
173
+ // Never re-inject this surface's OWN submissions.
174
+ if (input.surfaceId === this.ownSurfaceId) continue;
175
+
176
+ const steer: InboundSteer = {
177
+ inputId: input.id,
178
+ sessionId,
179
+ intent: input.intent,
180
+ body: input.body,
181
+ surfaceKind: input.surfaceKind,
182
+ surfaceId: input.surfaceId,
183
+ displayName: input.displayName,
184
+ };
185
+ try {
186
+ this.onSteer(steer);
187
+ } catch (error) {
188
+ // A failing injection must not wedge the poll loop or drop the ack —
189
+ // narrate-and-continue; the input is still acked so it is not re-picked.
190
+ this.log.debug('session inbound steer injection threw — continuing', {
191
+ error: error instanceof Error ? error.message : String(error),
192
+ });
193
+ }
194
+ try {
195
+ // consumed:false → queued → delivered (surface has collected it).
196
+ await client.deliverInput(sessionId, input.id, { consumed: false });
197
+ delivered += 1;
198
+ } catch (error) {
199
+ this.log.debug('session inbound deliver ack failed — will retry', {
200
+ error: error instanceof Error ? error.message : String(error),
201
+ });
202
+ }
203
+ }
204
+ } catch (error) {
205
+ // Poll failed entirely (daemon gone, transient): keep the cursor, retry next tick.
206
+ this.log.debug('session inbound-input poll failed — retry next tick', {
207
+ error: error instanceof Error ? error.message : String(error),
208
+ });
209
+ } finally {
210
+ this.polling = false;
211
+ }
212
+ return delivered;
213
+ }
214
+ }
215
+
216
+ /** Format the operator-facing narration for an inbound steer, e.g.
217
+ * "steer received from webui (Alice): resize the panel". */
218
+ export function narrateInboundSteer(steer: InboundSteer): string {
219
+ const surface = steer.surfaceKind ?? steer.surfaceId ?? 'another surface';
220
+ const who = steer.displayName ? ` (${steer.displayName})` : '';
221
+ const verb = steer.intent === 'follow-up' ? 'follow-up received from' : 'steer received from';
222
+ return `${verb} ${surface}${who}: ${steer.body}`;
223
+ }
224
+
225
+ /**
226
+ * Build the standard bootstrap-time poller: sessionId tracks the live runtime
227
+ * session ref, and onSteer narrates the steer to the operator then injects it
228
+ * via the SAME path COMPANION_MESSAGE_RECEIVED uses (orchestrator.handleUserInput
229
+ * when wired, else a bare conversation append + render request as a fallback
230
+ * before the orchestrator ref is patched). Extracted from bootstrap-core.ts to
231
+ * keep this module (not the bootstrap god-function) the owner of the wiring.
232
+ */
233
+ export function createBootstrapInboundInputPoller(deps: {
234
+ readonly runtimeSessionIdRef: { readonly value: string };
235
+ readonly routeOrBuffer: (text: string, priority: 'low' | 'high') => void;
236
+ readonly orchestratorHandleUserInputRef: { readonly value: ((text: string) => void) | null };
237
+ readonly conversation: { readonly addUserMessage: (text: string) => void };
238
+ readonly requestRender: () => void;
239
+ }): SessionInboundInputPoller {
240
+ return new SessionInboundInputPoller({
241
+ sessionId: () => deps.runtimeSessionIdRef.value || null,
242
+ onSteer: (steer) => {
243
+ deps.routeOrBuffer(narrateInboundSteer(steer), 'low');
244
+ if (deps.orchestratorHandleUserInputRef.value) {
245
+ deps.orchestratorHandleUserInputRef.value(steer.body);
246
+ } else {
247
+ deps.conversation.addUserMessage(steer.body);
248
+ deps.requestRender();
249
+ }
250
+ },
251
+ });
252
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * session-spine-transport.ts
3
+ *
4
+ * W3-T1 (One-Platform Wave 3, TUI session-spine cutover): the thin typed-client
5
+ * adapter that lets the TUI's bootstrap drive the SDK's
6
+ * `@pellux/goodvibes-sdk/platform/runtime/session-spine` `SessionSpineClient`
7
+ * (extracted in W3-S4) via its injected `SpineTransport` seam.
8
+ *
9
+ * The TUI is SDK-clean here: `SpineSessionsClient` is exactly the narrow wire
10
+ * surface the old TUI-local `session-spine-client.ts` declared (structurally
11
+ * satisfied by `HttpTransport.operator.sessions`), and `createTuiSpineTransport`
12
+ * folds it into the SDK's `SpineTransport` the SAME way the SDK's own
13
+ * consumability proof (`session-spine-daemon-integration.test.ts`) does:
14
+ * resolve -> `'ok'`, throw -> `'offline'`. The TUI never emits `'rejected'` — it
15
+ * has no durable-refusal vocabulary to fold (that distinction is the agent's
16
+ * REST mirror's job, per the SDK's divergence ruling #6/#7).
17
+ */
18
+ import type {
19
+ RegisterSharedSessionInput,
20
+ SharedSessionRecord,
21
+ SharedSessionRegisterResult,
22
+ } from '@pellux/goodvibes-sdk/platform/control-plane';
23
+ import type { SpineResult, SpineTransport } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
24
+
25
+ /**
26
+ * The minimal wire surface this adapter needs. Structurally satisfied by BOTH
27
+ * the SDK's in-process `OperatorClient['sessions']` (DirectTransport) and the
28
+ * wire `HttpTransportOperatorClient['sessions']` (HttpTransport) — bootstrap.ts
29
+ * only ever constructs an Http-backed instance in practice, but this is
30
+ * deliberately typed against the narrow shape rather than either concrete
31
+ * transport type so it has no compile-time dependency on which transport
32
+ * backs it.
33
+ */
34
+ export interface SpineSessionsClient {
35
+ register(input: RegisterSharedSessionInput): Promise<SharedSessionRegisterResult>;
36
+ close(sessionId: string): Promise<SharedSessionRecord | null>;
37
+ }
38
+
39
+ /**
40
+ * Adapt the TUI's typed `SpineSessionsClient` into the SDK `SessionSpineClient`'s
41
+ * injected `SpineTransport`. A rejecting wire call is treated as a transient
42
+ * connectivity fault (`'offline'`) rather than a durable refusal — the same
43
+ * binary reachability model the pre-extraction TUI-local client used.
44
+ */
45
+ export function createTuiSpineTransport(sessionsClient: SpineSessionsClient): SpineTransport {
46
+ return {
47
+ register: async (input: RegisterSharedSessionInput): Promise<SpineResult> => {
48
+ try {
49
+ await sessionsClient.register(input);
50
+ return { outcome: 'ok' };
51
+ } catch (error) {
52
+ return { outcome: 'offline', error: error instanceof Error ? error.message : String(error) };
53
+ }
54
+ },
55
+ close: async (sessionId: string): Promise<SpineResult> => {
56
+ try {
57
+ await sessionsClient.close(sessionId);
58
+ return { outcome: 'ok' };
59
+ } catch (error) {
60
+ return { outcome: 'offline', error: error instanceof Error ? error.message : String(error) };
61
+ }
62
+ },
63
+ };
64
+ }
@@ -40,7 +40,14 @@ export type TuiTerminalOutputGuardOptions = {
40
40
  readonly stdout: WritableStreamLike;
41
41
  readonly stderr?: WritableStreamLike;
42
42
  readonly active?: boolean;
43
- readonly notify: (message: string) => void;
43
+ /**
44
+ * Called (rate-limited) after intercepts with the cumulative count of direct
45
+ * writes captured this session, so an honest quiet counter (e.g. /debug) can
46
+ * refresh. The per-write detail is already recorded to the activity log via
47
+ * logger.warn — this callback intentionally does NOT push transcript lines.
48
+ * (UX-B item 1a.)
49
+ */
50
+ readonly onCapture?: (total: number) => void;
44
51
  };
45
52
 
46
53
  const MAX_LOG_TEXT = 4_000;
@@ -208,21 +215,21 @@ export function installTerminalOutputGuard(options: TerminalOutputGuardOptions):
208
215
  }
209
216
 
210
217
  export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOptions): TerminalOutputGuard {
211
- let capturedWriteCount = 0;
218
+ let totalInterceptedWrites = 0;
212
219
  let lastNoticeAt = 0;
213
220
  return installTerminalOutputGuard({
214
221
  stdout: options.stdout,
215
222
  stderr: options.stderr,
216
223
  active: options.active,
217
- onIntercept: (event) => {
218
- capturedWriteCount++;
224
+ onIntercept: () => {
225
+ // Each intercept is already logged (logger.warn in record()). Here we only
226
+ // maintain a cumulative counter and, rate-limited, refresh a quiet counter
227
+ // surface — no repeated transcript lines. (UX-B item 1a.)
228
+ totalInterceptedWrites++;
219
229
  const now = Date.now();
220
230
  if (now - lastNoticeAt < 5_000) return;
221
- const count = capturedWriteCount;
222
- capturedWriteCount = 0;
223
231
  lastNoticeAt = now;
224
- const plural = count === 1 ? '' : 's';
225
- options.notify(`[Terminal] Captured ${count} direct ${event.source} write${plural} that would have corrupted the TUI: ${event.preview}`);
232
+ options.onCapture?.(totalInterceptedWrites);
226
233
  },
227
234
  });
228
235
  }
@@ -6,7 +6,7 @@ import { createUiReadModels, type UiReadModels, type UiReadModelOptions } from '
6
6
  import type { ForensicsRegistry } from '@/runtime/index.ts';
7
7
  import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
8
8
  import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
9
- import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
9
+ import { SessionUnionCache, type SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
10
10
  import type { ShellPathService } from '@/runtime/index.ts';
11
11
  import type { HostServiceStatus } from '@/runtime/index.ts';
12
12
  import type { SecretsManager } from '../config/secrets.ts';
@@ -39,7 +39,13 @@ export interface UiProviderServices {
39
39
 
40
40
  export interface UiSessionServices {
41
41
  readonly sessionManager: RuntimeServices['sessionManager'];
42
- readonly sessionBroker: SharedSessionBroker;
42
+ /**
43
+ * The panel-facing session READ source is the cross-surface union
44
+ * facade, not the raw local broker — in adopted-daemon ('external') mode the
45
+ * local broker misses sessions hosted for other surfaces. Sync signature
46
+ * preserved; the facade's cache makes that honest (see session-union-cache.ts).
47
+ */
48
+ readonly sessionBroker: SessionReadFacade;
43
49
  readonly sessionOrchestration: RuntimeServices['sessionOrchestration'];
44
50
  readonly sessionMemoryStore: RuntimeServices['sessionMemoryStore'];
45
51
  }
@@ -65,6 +71,10 @@ export interface UiPlatformServices {
65
71
  readonly httpListenerPortInUse?: boolean;
66
72
  readonly daemonStatus?: HostServiceStatus;
67
73
  readonly httpListenerStatus?: HostServiceStatus;
74
+ // Honest session-spine posture for the footer segment, driven by
75
+ // the spine client's own live wire attempts (not the one-shot adopt probe).
76
+ readonly sessionSpineActive?: boolean;
77
+ readonly sessionSpineStatus?: 'unknown' | 'online' | 'offline';
68
78
  };
69
79
  restart(): Promise<{
70
80
  readonly daemonRunning: boolean;
@@ -126,6 +136,12 @@ export interface UiRuntimeServices {
126
136
  export interface UiRuntimeServicesOptions extends UiReadModelOptions {
127
137
  readonly forensicsRegistry?: ForensicsRegistry;
128
138
  readonly getControlPlaneRecentEvents?: (limit: number) => readonly ControlPlaneRecentEvent[];
139
+ /**
140
+ * The shared session read facade constructed in bootstrap-core (so
141
+ * bootstrap.ts can drive its adopted-external/embedded/local mode). When omitted
142
+ * (test callers), a local-only passthrough facade over the broker is auto-created.
143
+ */
144
+ readonly sessionUnionCache?: SessionUnionCache;
129
145
  }
130
146
 
131
147
  export function createUiRuntimeServices(
@@ -157,7 +173,7 @@ export function createUiRuntimeServices(
157
173
  },
158
174
  sessions: {
159
175
  sessionManager: runtimeServices.sessionManager,
160
- sessionBroker: runtimeServices.sessionBroker,
176
+ sessionBroker: options.sessionUnionCache ?? new SessionUnionCache({ local: runtimeServices.sessionBroker }),
161
177
  sessionOrchestration: runtimeServices.sessionOrchestration,
162
178
  sessionMemoryStore: runtimeServices.sessionMemoryStore,
163
179
  },