@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.
- package/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +33 -11
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- 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
|
-
|
|
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
|
|
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: (
|
|
218
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
},
|