@pellux/goodvibes-tui 2.0.17 → 2.0.18
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/CHANGELOG.md +23 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +5 -5
- package/src/core/format-user-error.ts +17 -2
- package/src/main.ts +16 -28
- package/src/renderer/deferred-render.ts +31 -0
- package/src/runtime/process-lifecycle.ts +24 -4
- package/src/runtime/session-ambience-wiring.ts +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.0.18] - 2026-08-21
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
|
|
11
|
+
- **Session start no longer crashes behind the scenes.** Every session
|
|
12
|
+
opened with "[Error] Provider error: Cannot access 'render' before
|
|
13
|
+
initialization" — a startup crash inside the TUI's own wiring, mislabeled
|
|
14
|
+
as a provider problem, that killed whatever bootstrap step was mid-flight.
|
|
15
|
+
Render requests that arrive before the renderer exists are now deferred
|
|
16
|
+
into the first paint instead of throwing.
|
|
17
|
+
- **A dead subscription login is named as such.** An OpenAI subscription
|
|
18
|
+
session ended server-side used to surface as "the provider rejected your
|
|
19
|
+
API key. Run /login to re-authenticate or check your API key", which reads
|
|
20
|
+
as "your subscription isn't there" to a logged-in subscriber. It now says
|
|
21
|
+
the subscription session has ended and to sign in again — and with
|
|
22
|
+
platform runtime 2.0.21 underneath, a token OpenAI rotated early is
|
|
23
|
+
refreshed and retried before anything is surfaced at all.
|
|
24
|
+
- **Escaped errors are captioned honestly.** An unhandled rejection with no
|
|
25
|
+
provider markers is reported as a GoodVibes bug; one carrying provider
|
|
26
|
+
markers (a 5xx outage classifies as neither auth nor network) stays
|
|
27
|
+
neutral instead of blaming either side.
|
|
28
|
+
- Platform runtime pins move to 2.0.21; the bundled daemon moves to 1.28.23.
|
|
29
|
+
|
|
7
30
|
## [2.0.17] - 2026-08-21
|
|
8
31
|
|
|
9
32
|
### Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Terminal-native GoodVibes client for coding, operations, knowledge and channel workflows; a pure client of the GoodVibes daemon.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -96,11 +96,11 @@
|
|
|
96
96
|
"@anthropic-ai/bedrock-sdk": "^0.29.1",
|
|
97
97
|
"@anthropic-ai/sdk": "^0.92.0",
|
|
98
98
|
"@ast-grep/napi": "^0.42.0",
|
|
99
|
-
"@pellux/goodvibes-sdk": "2.0.
|
|
100
|
-
"@pellux/goodvibes-terminal-shell": "2.0.
|
|
99
|
+
"@pellux/goodvibes-sdk": "2.0.21",
|
|
100
|
+
"@pellux/goodvibes-terminal-shell": "2.0.21",
|
|
101
101
|
"bash-language-server": "^5.6.0",
|
|
102
102
|
"fuse.js": "^7.1.0",
|
|
103
|
-
"@pellux/goodvibes-daemon": "1.28.
|
|
103
|
+
"@pellux/goodvibes-daemon": "1.28.23",
|
|
104
104
|
"graphql": "^16.13.2",
|
|
105
105
|
"jszip": "^3.10.1",
|
|
106
106
|
"node-edge-tts": "^1.2.10",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"zustand": "^5.0.12"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
|
-
"@pellux/goodvibes-toolchain": "2.0.
|
|
124
|
+
"@pellux/goodvibes-toolchain": "2.0.21",
|
|
125
125
|
"@types/bun": "^1.3.10",
|
|
126
126
|
"typescript": "^5.9.3"
|
|
127
127
|
},
|
|
@@ -77,12 +77,15 @@ function probeString(err: unknown): string {
|
|
|
77
77
|
export function classifyError(err: unknown): ErrorClass {
|
|
78
78
|
const probe = probeString(err);
|
|
79
79
|
|
|
80
|
-
// Auth: 401 status, key/token invalidity, Unauthorized
|
|
80
|
+
// Auth: 401 status, key/token invalidity, Unauthorized, or a subscription
|
|
81
|
+
// session the provider's backend has ended (matched by message too, because
|
|
82
|
+
// a wrapper that re-throws can drop the 401 status on the way up).
|
|
81
83
|
if (
|
|
82
84
|
/\b401\b/.test(probe) ||
|
|
83
85
|
/invalid.{0,10}(api.?key|key|token)/i.test(probe) ||
|
|
84
86
|
/unauthorized/i.test(probe) ||
|
|
85
87
|
/authentication/i.test(probe) ||
|
|
88
|
+
/subscription session has ended/i.test(probe) ||
|
|
86
89
|
/api.?key.{0,20}(missing|not.?set|required|invalid)/i.test(probe)
|
|
87
90
|
) {
|
|
88
91
|
return 'auth';
|
|
@@ -143,12 +146,24 @@ export function formatUserFacingError(err: unknown): UserFacingError {
|
|
|
143
146
|
const kind = classifyError(err);
|
|
144
147
|
|
|
145
148
|
switch (kind) {
|
|
146
|
-
case 'auth':
|
|
149
|
+
case 'auth': {
|
|
150
|
+
// A subscription-session death is not an API-key problem, and telling a
|
|
151
|
+
// subscriber to "check your API key" reads as "your subscription isn't
|
|
152
|
+
// there". The SDK's subscription providers phrase that failure
|
|
153
|
+
// distinctly; keep their meaning.
|
|
154
|
+
if (/subscription session has ended/i.test(probeString(err))) {
|
|
155
|
+
return {
|
|
156
|
+
kind,
|
|
157
|
+
message: 'Authentication failed: your provider subscription session has ended.',
|
|
158
|
+
action: 'Run /login to sign in to the provider again.',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
147
161
|
return {
|
|
148
162
|
kind,
|
|
149
163
|
message: 'Authentication failed: the provider rejected your API key.',
|
|
150
164
|
action: 'Run /login to re-authenticate or check your API key.',
|
|
151
165
|
};
|
|
166
|
+
}
|
|
152
167
|
|
|
153
168
|
case 'rate-limit':
|
|
154
169
|
return {
|
package/src/main.ts
CHANGED
|
@@ -54,9 +54,7 @@ import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
|
54
54
|
import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-startup.ts';
|
|
55
55
|
import { applyConfiguredHitlMode, applyRuntimeConfigValue, applyTerminalRuntimeConfigDefaults } from '@pellux/goodvibes-terminal-shell';
|
|
56
56
|
import { renderToolCallBlock } from './renderer/tool-call.ts';
|
|
57
|
-
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
58
57
|
import { installVoiceCapture } from './shell/voice-capture-shell.ts';
|
|
59
|
-
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
60
58
|
import { allowTerminalWrite, installFullScreenTerminalOutputGuard } from '@pellux/goodvibes-terminal-shell/terminal-output-guard';
|
|
61
59
|
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
62
60
|
import { createRenderScheduler } from '@pellux/goodvibes-terminal-shell';
|
|
@@ -68,9 +66,7 @@ import { wireStreamEventMetrics, createStreamMetrics, type StreamMetrics, type W
|
|
|
68
66
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
69
67
|
import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
|
|
70
68
|
import { isEffectiveDangerMode } from '@pellux/goodvibes-sdk/platform/config';
|
|
71
|
-
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
72
69
|
import { applyComposerCapture, applyAtModelDirective } from './input/composer-capture.ts';
|
|
73
|
-
import { createSessionAutoTitler } from './core/session-auto-titler.ts';
|
|
74
70
|
import { makeComposerEditorOpener } from './input/composer-editor.ts';
|
|
75
71
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
76
72
|
import { createCancelGeneration } from './core/turn-cancellation.ts';
|
|
@@ -80,6 +76,9 @@ import { fetchDaemonPowerState, installKeepAwakeRemoteForward } from './runtime/
|
|
|
80
76
|
import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
|
|
81
77
|
import { createTerminalNotifier } from './core/terminal-notifier.ts';
|
|
82
78
|
import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
79
|
+
import { createDeferredRender } from './renderer/deferred-render.ts';
|
|
80
|
+
import { wireSessionAmbience } from './runtime/session-ambience-wiring.ts';
|
|
81
|
+
import { createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
83
82
|
|
|
84
83
|
import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
|
|
85
84
|
import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
|
|
@@ -162,6 +161,10 @@ async function main() {
|
|
|
162
161
|
const panelManager = ctx.services.panelManager;
|
|
163
162
|
const buildSessionContinuityHints = createSessionContinuityHintsBuilder({ readModels: uiServices.readModels, panelManager });
|
|
164
163
|
|
|
164
|
+
// Callable before the scheduler exists; see deferred-render.ts for why.
|
|
165
|
+
const deferredRender = createDeferredRender();
|
|
166
|
+
const render = deferredRender.render;
|
|
167
|
+
|
|
165
168
|
let pendingPermission: PendingPermissionState | null = null;
|
|
166
169
|
// One-key jump/attach to a spawned CI fix-session: the affordance ARMS the id; the next 'j' runs the resume so the user never retypes it.
|
|
167
170
|
let fixSessionAttachArmed: string | null = null;
|
|
@@ -259,30 +262,15 @@ async function main() {
|
|
|
259
262
|
configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]),
|
|
260
263
|
});
|
|
261
264
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
configManager,
|
|
265
|
-
|
|
266
|
-
notify: (message) =>
|
|
267
|
-
});
|
|
268
|
-
// Exit-path stop: bounded drain of the audio already playing (see stopForExit).
|
|
269
|
-
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
270
|
-
unsubs.push(...spokenTurns.unsubs);
|
|
271
|
-
// Scriptable status line: runs the user's `statusline.command` at turn boundaries.
|
|
272
|
-
const scriptableStatusline = createScriptableStatusline({ configManager, cwd: workingDir, turns: uiServices.events.turns, onChange: () => render() });
|
|
273
|
-
unsubs.push(...scriptableStatusline.unsubs);
|
|
274
|
-
// Auto-title an untitled session via the weak/fast tool model (session.autoTitle, off by default).
|
|
275
|
-
const sessionAutoTitler = createSessionAutoTitler({
|
|
276
|
-
conversation, model: ctx.services.toolLLM, configManager, turns: uiServices.events.turns,
|
|
277
|
-
onTitled: (title) => { systemMessageRouter.high(`[Session] Auto-titled: "${title}"`); render(); },
|
|
265
|
+
// Spoken output, scriptable statusline, auto-titling, spoken-turn routing.
|
|
266
|
+
const ambience = wireSessionAmbience({
|
|
267
|
+
voiceService: ctx.services.voiceService, configManager, events: uiServices.events,
|
|
268
|
+
conversation, toolLLM: ctx.services.toolLLM, orchestrator, providerRegistry, workingDir,
|
|
269
|
+
notify: (message) => systemMessageRouter.high(message), render,
|
|
278
270
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
providerRegistry,
|
|
283
|
-
configManager,
|
|
284
|
-
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
285
|
-
}));
|
|
271
|
+
stopSpokenOutputForExit = ambience.stopSpokenOutputForExit;
|
|
272
|
+
const { spokenTurns, scriptableStatusline } = ambience;
|
|
273
|
+
unsubs.push(...ambience.unsubs);
|
|
286
274
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
287
275
|
input.clearModalStack();
|
|
288
276
|
scrollLocked = true; // Re-lock on any user input
|
|
@@ -649,7 +637,7 @@ async function main() {
|
|
|
649
637
|
});
|
|
650
638
|
};
|
|
651
639
|
const renderScheduler = createRenderScheduler(renderNow, undefined, () => lifecycle.isTerminalRestored()); // coalescer; no frames after terminal restore
|
|
652
|
-
|
|
640
|
+
deferredRender.set(() => renderScheduler.schedule()); // from here on, render() actually schedules
|
|
653
641
|
const terminalOutputGuard = installFullScreenTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
|
|
654
642
|
|
|
655
643
|
setRenderRequest(() => renderScheduler.flushNow()); // bootstrap's 16ms coalescer composites via the (restore-gated) scheduler
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A render function usable before the renderer exists.
|
|
3
|
+
*
|
|
4
|
+
* main() captures `render` in dozens of closures declared long before the
|
|
5
|
+
* render scheduler is built (the scheduler needs the compositor and lifecycle
|
|
6
|
+
* constructed below them). A direct `const render` declared at the scheduler
|
|
7
|
+
* put every one of those closures in its temporal dead zone, and every
|
|
8
|
+
* session start crashed with "Cannot access 'render' before initialization"
|
|
9
|
+
* surfacing as an unhandled rejection that killed whatever bootstrap step was
|
|
10
|
+
* mid-flight. The firing path has to be a callback some installer in that
|
|
11
|
+
* window invokes synchronously with the rejection crossing a promise boundary
|
|
12
|
+
* on the way out — the window contains no await, so no timer or scan callback
|
|
13
|
+
* can interleave into it — and this indirection removes the entire class
|
|
14
|
+
* rather than the one caller: a closure that fires before {@link
|
|
15
|
+
* DeferredRender.set} runs gets a no-op, and main()'s unconditional first
|
|
16
|
+
* paint after wiring covers everything deferred.
|
|
17
|
+
*/
|
|
18
|
+
export interface DeferredRender {
|
|
19
|
+
/** Safe to call at any time; a no-op until {@link set} provides the real renderer. */
|
|
20
|
+
readonly render: () => void;
|
|
21
|
+
/** Install the real renderer; every later render() call goes to it. */
|
|
22
|
+
readonly set: (impl: () => void) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createDeferredRender(): DeferredRender {
|
|
26
|
+
let impl: () => void = () => {};
|
|
27
|
+
return {
|
|
28
|
+
render: () => impl(),
|
|
29
|
+
set: (next) => { impl = next; },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync } from 'node:fs';
|
|
22
22
|
import { flushActivityLogSync, logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
23
|
import { createTerminalLifecycle, TERMINAL_ESCAPES } from '@pellux/goodvibes-terminal-shell';
|
|
24
|
-
import {
|
|
24
|
+
import { formatUserFacingError } from '../core/format-user-error.ts';
|
|
25
25
|
import { allowTerminalWrite } from '@pellux/goodvibes-terminal-shell/terminal-output-guard';
|
|
26
26
|
import { buildPersistedSessionContext, deleteRecoveryFile } from '@/runtime/index.ts';
|
|
27
27
|
import type { SessionSurface } from '@/runtime/index.ts';
|
|
@@ -154,9 +154,29 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
|
|
|
154
154
|
`[Critical] Multiple errors detected (${_unhandledRejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`
|
|
155
155
|
);
|
|
156
156
|
} else {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
// Recognized kinds (auth, rate-limit, network...) keep their specific
|
|
158
|
+
// line. A generic rejection used to be captioned "Provider error",
|
|
159
|
+
// which sent a startup UI crash out dressed as a model-backend
|
|
160
|
+
// problem — but the reverse lie matters too: the classifier has no
|
|
161
|
+
// 5xx rule, so a provider outage also classifies generic, and calling
|
|
162
|
+
// THAT a GoodVibes bug blames us for OpenAI's downtime. The
|
|
163
|
+
// discriminator is whether the reason carries provider markers
|
|
164
|
+
// (provider / statusCode, which every SDK AppError sets): with them,
|
|
165
|
+
// stay neutral; without them, it escaped from our own code and
|
|
166
|
+
// honesty says so.
|
|
167
|
+
const classified = formatUserFacingError(reason);
|
|
168
|
+
let line: string;
|
|
169
|
+
if (classified.kind !== 'generic') {
|
|
170
|
+
line = `[Error] ${classified.message} ${classified.action}`;
|
|
171
|
+
} else {
|
|
172
|
+
const record = typeof reason === 'object' && reason !== null ? reason as Record<string, unknown> : {};
|
|
173
|
+
const providerShaped = typeof record['provider'] === 'string' || typeof record['statusCode'] === 'number';
|
|
174
|
+
line = providerShaped
|
|
175
|
+
? `[Error] Unexpected error: ${msg}. Retry your last message, or switch models with /model.`
|
|
176
|
+
: `[Error] Unexpected error: ${msg}. This is a GoodVibes bug, not a provider failure; if it repeats, restart the session.`;
|
|
177
|
+
}
|
|
178
|
+
ctx.systemMessageRouter.high(line);
|
|
179
|
+
logger.error('unhandledRejection', { error: String(reason), stack: reason instanceof Error ? reason.stack : undefined });
|
|
160
180
|
}
|
|
161
181
|
render();
|
|
162
182
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-boundary ambience: spoken turn output, the user's scriptable status
|
|
3
|
+
* line, session auto-titling, and spoken-turn model routing. Extracted from
|
|
4
|
+
* main() as one seam because the four share the same inputs (turn events,
|
|
5
|
+
* config, a notify line, a repaint) and main() only needs their teardowns and
|
|
6
|
+
* the bounded exit-path audio stop.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
9
|
+
import type { Orchestrator } from '@pellux/goodvibes-sdk/platform/core';
|
|
10
|
+
import { wireSpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
|
|
11
|
+
import { attachSpokenTurnModelRouting } from '../audio/spoken-turn-model-routing.ts';
|
|
12
|
+
import { createScriptableStatusline } from '../core/scriptable-statusline.ts';
|
|
13
|
+
import { createSessionAutoTitler } from '../core/session-auto-titler.ts';
|
|
14
|
+
|
|
15
|
+
export interface SessionAmbienceDeps {
|
|
16
|
+
readonly voiceService: Parameters<typeof wireSpokenTurnRuntime>[0]['voiceService'];
|
|
17
|
+
readonly configManager: ConfigManager;
|
|
18
|
+
readonly events: Parameters<typeof wireSpokenTurnRuntime>[0]['events'];
|
|
19
|
+
readonly conversation: Parameters<typeof createSessionAutoTitler>[0]['conversation'];
|
|
20
|
+
readonly toolLLM: Parameters<typeof createSessionAutoTitler>[0]['model'];
|
|
21
|
+
readonly orchestrator: Orchestrator;
|
|
22
|
+
readonly providerRegistry: Parameters<typeof attachSpokenTurnModelRouting>[0]['providerRegistry'];
|
|
23
|
+
readonly workingDir: string;
|
|
24
|
+
readonly notify: (message: string) => void;
|
|
25
|
+
readonly render: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SessionAmbience {
|
|
29
|
+
/** Bounded drain of audio already playing; the exit path calls this. */
|
|
30
|
+
readonly stopSpokenOutputForExit: () => Promise<void>;
|
|
31
|
+
/** Spoken-turn runtime handle: submit/stop, and the cancel path consumes it whole. */
|
|
32
|
+
readonly spokenTurns: ReturnType<typeof wireSpokenTurnRuntime>;
|
|
33
|
+
/** The footer reads the statusline's current text each frame. */
|
|
34
|
+
readonly scriptableStatusline: ReturnType<typeof createScriptableStatusline>;
|
|
35
|
+
readonly unsubs: ReadonlyArray<() => void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function wireSessionAmbience(deps: SessionAmbienceDeps): SessionAmbience {
|
|
39
|
+
const { configManager, notify, render } = deps;
|
|
40
|
+
const notifyAndPaint = (message: string): void => { notify(message); render(); };
|
|
41
|
+
const spokenTurns = wireSpokenTurnRuntime({
|
|
42
|
+
voiceService: deps.voiceService,
|
|
43
|
+
configManager,
|
|
44
|
+
events: deps.events,
|
|
45
|
+
notify: notifyAndPaint,
|
|
46
|
+
});
|
|
47
|
+
const scriptableStatusline = createScriptableStatusline({
|
|
48
|
+
configManager, cwd: deps.workingDir, turns: deps.events.turns, onChange: () => render(),
|
|
49
|
+
});
|
|
50
|
+
const sessionAutoTitler = createSessionAutoTitler({
|
|
51
|
+
conversation: deps.conversation, model: deps.toolLLM, configManager, turns: deps.events.turns,
|
|
52
|
+
onTitled: (title) => notifyAndPaint(`[Session] Auto-titled: "${title}"`),
|
|
53
|
+
});
|
|
54
|
+
const routingUnsub = attachSpokenTurnModelRouting({
|
|
55
|
+
orchestrator: deps.orchestrator,
|
|
56
|
+
providerRegistry: deps.providerRegistry,
|
|
57
|
+
configManager,
|
|
58
|
+
notify: notifyAndPaint,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
stopSpokenOutputForExit: () => spokenTurns.stopForExit(),
|
|
62
|
+
spokenTurns,
|
|
63
|
+
scriptableStatusline,
|
|
64
|
+
unsubs: [...spokenTurns.unsubs, ...scriptableStatusline.unsubs, ...sessionAutoTitler.unsubs, routingUnsub],
|
|
65
|
+
};
|
|
66
|
+
}
|