@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.15
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 +48 -0
- package/dist/cli.js +5615 -2833
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +64 -14
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/parallel.d.ts +6 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +74 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/share.ts +51 -28
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +29 -1
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +5 -5
- package/src/prompts/tools/bash.md +2 -3
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/sdk.ts +13 -7
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +42 -9
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
- package/src/web/search/providers/perplexity.ts +18 -6
package/src/tts/tts-client.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import {
|
|
3
|
+
createUnavailableWorker,
|
|
4
|
+
createWorkerHandle,
|
|
5
|
+
createWorkerSubprocess,
|
|
6
|
+
logWorkerMessage,
|
|
7
|
+
type RefCountedWorkerHandle,
|
|
8
|
+
resolveWorkerSpawnCmd,
|
|
9
|
+
SMOKE_TEST_TIMEOUT_MS,
|
|
10
|
+
type SpawnedSubprocess,
|
|
11
|
+
smokeTestWorker,
|
|
12
|
+
spawnWorkerOrUnavailable,
|
|
13
|
+
} from "../subprocess/worker-client";
|
|
14
|
+
import { tinyWorkerEnv } from "../tiny/title-client";
|
|
6
15
|
import { safeSend } from "../utils/ipc";
|
|
7
16
|
import { isTtsLocalModelKey, type TtsLocalModelKey } from "./models";
|
|
8
17
|
import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
|
|
@@ -13,22 +22,6 @@ export interface TtsAudio {
|
|
|
13
22
|
sampleRate: number;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
/**
|
|
17
|
-
* Abstraction over the TTS subprocess. The runtime implementation is a Bun child
|
|
18
|
-
* process so `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
|
|
19
|
-
* address space — that destructor segfaults Bun during shutdown (issue #1606).
|
|
20
|
-
*/
|
|
21
|
-
interface WorkerHandle {
|
|
22
|
-
send(message: TtsWorkerInbound): void;
|
|
23
|
-
onMessage(handler: (message: TtsWorkerOutbound) => void): () => void;
|
|
24
|
-
onError(handler: (error: Error) => void): () => void;
|
|
25
|
-
/** Re-reference the subprocess so a pending request keeps the parent event loop alive. */
|
|
26
|
-
ref(): void;
|
|
27
|
-
/** Drop the reference once the worker is idle so it never blocks process exit. */
|
|
28
|
-
unref(): void;
|
|
29
|
-
terminate(): Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
type PendingRequest =
|
|
33
26
|
| { kind: "synthesize"; modelKey: TtsLocalModelKey; resolve: (audio: TtsAudio | null) => void }
|
|
34
27
|
| { kind: "download"; modelKey: TtsLocalModelKey; resolve: (ok: boolean) => void }
|
|
@@ -134,128 +127,30 @@ class AudioChunkChannel {
|
|
|
134
127
|
}
|
|
135
128
|
}
|
|
136
129
|
|
|
137
|
-
// Cold-starting the worker from a compiled binary (decompress + module graph load)
|
|
138
|
-
// is slow on contended CI runners; the probe only proves the worker spawns and
|
|
139
|
-
// ponges, so a generous bound removes flakes without weakening the check.
|
|
140
|
-
const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
141
|
-
|
|
142
130
|
/**
|
|
143
131
|
* Hidden subcommand on the main CLI that boots the TTS worker in the spawned
|
|
144
132
|
* subprocess. Kept in sync with the dispatch in `cli.ts` (Main-owned).
|
|
145
133
|
*/
|
|
146
134
|
export const TTS_WORKER_ARG = "__omp_worker_tts";
|
|
147
135
|
|
|
148
|
-
function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
149
|
-
try {
|
|
150
|
-
const value = settings.get(path);
|
|
151
|
-
return typeof value === "string" ? value : undefined;
|
|
152
|
-
} catch {
|
|
153
|
-
// Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Env handed to the TTS subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env
|
|
160
|
-
* vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
161
|
-
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
162
|
-
* subprocess's env-based resolution governs speech the same way it governs the
|
|
163
|
-
* tiny LLM worker.
|
|
164
|
-
*/
|
|
165
|
-
function ttsWorkerEnv(): Record<string, string> {
|
|
166
|
-
const overlay = tinyWorkerEnvOverlay(
|
|
167
|
-
$env,
|
|
168
|
-
readTinyModelSetting("providers.tinyModelDevice"),
|
|
169
|
-
readTinyModelSetting("providers.tinyModelDtype"),
|
|
170
|
-
);
|
|
171
|
-
const base = $env as Record<string, string | undefined>;
|
|
172
|
-
const merged: Record<string, string> = {};
|
|
173
|
-
for (const key in base) {
|
|
174
|
-
const value = base[key];
|
|
175
|
-
if (typeof value === "string") merged[key] = value;
|
|
176
|
-
}
|
|
177
|
-
for (const key in overlay) merged[key] = overlay[key];
|
|
178
|
-
return merged;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
interface TtsWorkerSpawnCommand {
|
|
182
|
-
cmd: string[];
|
|
183
|
-
cwd?: string;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Resolve the command used to relaunch the agent CLI into TTS-worker mode. In a
|
|
188
|
-
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
189
|
-
* declared worker-host entry (cwd-relative for reliable Bun IPC), falling back
|
|
190
|
-
* to this package's own `src/cli.ts` when no host entry is declared (bun test).
|
|
191
|
-
*/
|
|
192
|
-
function ttsWorkerSpawnCmd(): TtsWorkerSpawnCommand {
|
|
193
|
-
if (isCompiledBinary()) return { cmd: [process.execPath, TTS_WORKER_ARG] };
|
|
194
|
-
const hostEntry = workerHostEntry();
|
|
195
|
-
if (hostEntry) {
|
|
196
|
-
return { cmd: [process.execPath, path.basename(hostEntry), TTS_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
197
|
-
}
|
|
198
|
-
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
199
|
-
return { cmd: [process.execPath, "src/cli.ts", TTS_WORKER_ARG], cwd: packageRoot };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
interface SpawnedSubprocess {
|
|
203
|
-
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
204
|
-
inbound: Set<(message: TtsWorkerOutbound) => void>;
|
|
205
|
-
errors: Set<(error: Error) => void>;
|
|
206
|
-
/** Flipped to `true` right before the deliberate SIGKILL so `onExit` can tell it apart from a crash. */
|
|
207
|
-
intentionalExit: { value: boolean };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
136
|
/**
|
|
211
137
|
* Spawn the TTS worker as a subprocess. Exported for tests and the smoke probe;
|
|
212
138
|
* production callers go through {@link spawnTtsWorker}.
|
|
213
139
|
*/
|
|
214
|
-
export function createTtsSubprocess(): SpawnedSubprocess {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const proc = Bun.spawn({
|
|
220
|
-
cmd: spawnCommand.cmd,
|
|
221
|
-
cwd: spawnCommand.cwd,
|
|
222
|
-
env: ttsWorkerEnv(),
|
|
223
|
-
stdin: "ignore",
|
|
224
|
-
stdout: "ignore",
|
|
225
|
-
stderr: "ignore",
|
|
226
|
-
serialization: "advanced",
|
|
227
|
-
windowsHide: true,
|
|
228
|
-
ipc(message) {
|
|
229
|
-
for (const handler of inbound) handler(message as TtsWorkerOutbound);
|
|
230
|
-
},
|
|
231
|
-
onExit(_proc, exitCode, signalCode) {
|
|
232
|
-
if (exitCode === 0) return;
|
|
233
|
-
if (exitCode === null && intentionalExit.value) return;
|
|
234
|
-
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
235
|
-
const err = new Error(`tts subprocess exited with ${reason}`);
|
|
236
|
-
for (const handler of errors) handler(err);
|
|
237
|
-
},
|
|
140
|
+
export function createTtsSubprocess(): SpawnedSubprocess<TtsWorkerOutbound> {
|
|
141
|
+
return createWorkerSubprocess<TtsWorkerOutbound>({
|
|
142
|
+
spawnCommand: resolveWorkerSpawnCmd(TTS_WORKER_ARG),
|
|
143
|
+
env: tinyWorkerEnv(),
|
|
144
|
+
exitLabel: "tts subprocess",
|
|
238
145
|
});
|
|
239
|
-
// Don't keep the parent event loop alive on an idle worker; the dispose path
|
|
240
|
-
// calls `terminate()` explicitly. Bun's test runner starves IPC for unref'd
|
|
241
|
-
// subprocesses, so keep it referenced only under tests.
|
|
242
|
-
if (!isBunTestRuntime()) proc.unref();
|
|
243
|
-
return { proc, inbound, errors, intentionalExit };
|
|
244
146
|
}
|
|
245
147
|
|
|
246
|
-
function wrapSubprocess(
|
|
148
|
+
function wrapSubprocess(
|
|
149
|
+
spawned: SpawnedSubprocess<TtsWorkerOutbound>,
|
|
150
|
+
): RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> {
|
|
151
|
+
const { proc } = spawned;
|
|
247
152
|
return {
|
|
248
|
-
|
|
249
|
-
safeSend(proc, message, "tts");
|
|
250
|
-
},
|
|
251
|
-
onMessage(handler) {
|
|
252
|
-
inbound.add(handler);
|
|
253
|
-
return () => inbound.delete(handler);
|
|
254
|
-
},
|
|
255
|
-
onError(handler) {
|
|
256
|
-
errors.add(handler);
|
|
257
|
-
return () => errors.delete(handler);
|
|
258
|
-
},
|
|
153
|
+
...createWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound>(spawned, message => safeSend(proc, message, "tts")),
|
|
259
154
|
ref() {
|
|
260
155
|
try {
|
|
261
156
|
proc.ref();
|
|
@@ -270,79 +165,36 @@ function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubpr
|
|
|
270
165
|
// Already gone.
|
|
271
166
|
}
|
|
272
167
|
},
|
|
273
|
-
async terminate() {
|
|
274
|
-
// SIGKILL: the point of subprocess isolation is that the parent never
|
|
275
|
-
// runs `onnxruntime-node`'s NAPI finalizer (it crashes Bun on Windows).
|
|
276
|
-
// Hard-kill instead; the OS reclaims the model memory.
|
|
277
|
-
intentionalExit.value = true;
|
|
278
|
-
try {
|
|
279
|
-
proc.kill("SIGKILL");
|
|
280
|
-
} catch {
|
|
281
|
-
// Already gone.
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
168
|
};
|
|
285
169
|
}
|
|
286
170
|
|
|
287
|
-
function spawnInlineUnavailableWorker(error: unknown):
|
|
288
|
-
const listeners = new Set<(message: TtsWorkerOutbound) => void>();
|
|
289
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
290
|
-
const emit = (message: TtsWorkerOutbound): void => {
|
|
291
|
-
for (const listener of listeners) listener(message);
|
|
292
|
-
};
|
|
171
|
+
function spawnInlineUnavailableWorker(error: unknown): RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> {
|
|
293
172
|
return {
|
|
294
|
-
|
|
295
|
-
queueMicrotask(() => {
|
|
296
|
-
if (message.type === "ping") {
|
|
297
|
-
emit({ type: "pong", id: message.id });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
emit({ type: "error", id: message.id, error: errorMessage });
|
|
301
|
-
});
|
|
302
|
-
},
|
|
303
|
-
onMessage(handler) {
|
|
304
|
-
listeners.add(handler);
|
|
305
|
-
return () => listeners.delete(handler);
|
|
306
|
-
},
|
|
307
|
-
onError() {
|
|
308
|
-
return () => {};
|
|
309
|
-
},
|
|
173
|
+
...createUnavailableWorker<TtsWorkerInbound, TtsWorkerOutbound>(error),
|
|
310
174
|
ref() {},
|
|
311
175
|
unref() {},
|
|
312
|
-
async terminate() {
|
|
313
|
-
listeners.clear();
|
|
314
|
-
},
|
|
315
176
|
};
|
|
316
177
|
}
|
|
317
178
|
|
|
318
|
-
function spawnTtsWorker():
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
325
|
-
return spawnInlineUnavailableWorker(error);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function logWorkerMessage(message: Extract<TtsWorkerOutbound, { type: "log" }>): void {
|
|
330
|
-
if (message.level === "debug") logger.debug(message.msg, message.meta);
|
|
331
|
-
else if (message.level === "warn") logger.warn(message.msg, message.meta);
|
|
332
|
-
else logger.error(message.msg, message.meta);
|
|
179
|
+
function spawnTtsWorker(): RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> {
|
|
180
|
+
return spawnWorkerOrUnavailable(
|
|
181
|
+
() => wrapSubprocess(createTtsSubprocess()),
|
|
182
|
+
spawnInlineUnavailableWorker,
|
|
183
|
+
"TTS worker spawn failed; local TTS disabled",
|
|
184
|
+
);
|
|
333
185
|
}
|
|
334
186
|
|
|
335
187
|
export class TtsClient {
|
|
336
|
-
#worker:
|
|
188
|
+
#worker: RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> | null = null;
|
|
337
189
|
#unsubscribeMessage: (() => void) | null = null;
|
|
338
190
|
#unsubscribeError: (() => void) | null = null;
|
|
339
191
|
#pending = new Map<string, PendingRequest>();
|
|
340
192
|
#progressListeners = new Set<(event: TtsProgressEvent) => void>();
|
|
341
193
|
#nextRequestId = 0;
|
|
342
194
|
#refed = false;
|
|
343
|
-
#spawnWorker: () =>
|
|
195
|
+
#spawnWorker: () => RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound>;
|
|
344
196
|
|
|
345
|
-
constructor(spawnWorker: () =>
|
|
197
|
+
constructor(spawnWorker: () => RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> = spawnTtsWorker) {
|
|
346
198
|
this.#spawnWorker = spawnWorker;
|
|
347
199
|
}
|
|
348
200
|
|
|
@@ -400,7 +252,7 @@ export class TtsClient {
|
|
|
400
252
|
return { push: () => {}, end: () => {}, chunks: channel.iterator() };
|
|
401
253
|
}
|
|
402
254
|
|
|
403
|
-
let worker:
|
|
255
|
+
let worker: RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound>;
|
|
404
256
|
try {
|
|
405
257
|
worker = this.#ensureWorker();
|
|
406
258
|
} catch (error) {
|
|
@@ -505,7 +357,7 @@ export class TtsClient {
|
|
|
505
357
|
}
|
|
506
358
|
}
|
|
507
359
|
|
|
508
|
-
#ensureWorker():
|
|
360
|
+
#ensureWorker(): RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> {
|
|
509
361
|
if (this.#worker) return this.#worker;
|
|
510
362
|
const worker = this.#spawnWorker();
|
|
511
363
|
this.#worker = worker;
|
|
@@ -618,25 +470,5 @@ export async function smokeTestTtsWorker({
|
|
|
618
470
|
}: {
|
|
619
471
|
timeoutMs?: number;
|
|
620
472
|
} = {}): Promise<void> {
|
|
621
|
-
|
|
622
|
-
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
623
|
-
const timer = setTimeout(() => reject(new Error(`tts worker did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
624
|
-
const unsubscribeMessage = handle.onMessage(message => {
|
|
625
|
-
if (message.type === "pong") {
|
|
626
|
-
resolve();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (message.type === "log") return;
|
|
630
|
-
reject(new Error(`tts worker: expected pong, got ${JSON.stringify(message)}`));
|
|
631
|
-
});
|
|
632
|
-
const unsubscribeError = handle.onError(reject);
|
|
633
|
-
try {
|
|
634
|
-
handle.send({ type: "ping", id: "smoke" } satisfies TtsWorkerInbound);
|
|
635
|
-
await promise;
|
|
636
|
-
} finally {
|
|
637
|
-
clearTimeout(timer);
|
|
638
|
-
unsubscribeMessage();
|
|
639
|
-
unsubscribeError();
|
|
640
|
-
await handle.terminate();
|
|
641
|
-
}
|
|
473
|
+
await smokeTestWorker(wrapSubprocess(createTtsSubprocess()), "tts worker", timeoutMs);
|
|
642
474
|
}
|
package/src/tts/tts-worker.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import * as path from "node:path";
|
|
3
2
|
import type { ProgressInfo, RawAudio } from "@huggingface/transformers";
|
|
3
|
+
import { ensureRuntimeInstalled, getTinyModelsCacheDir, resolveRuntimeModule } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
errorMessage,
|
|
6
|
+
errorText,
|
|
7
|
+
installSharpStubResolver,
|
|
8
|
+
MemoizedRuntime,
|
|
9
|
+
replayCachedReady,
|
|
10
|
+
sendLog,
|
|
11
|
+
sendProgress,
|
|
12
|
+
TRANSFORMERS_PACKAGE,
|
|
13
|
+
} from "../subprocess/worker-runtime";
|
|
10
14
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "../tiny/device";
|
|
11
15
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "../tiny/dtype";
|
|
12
16
|
import { getTtsLocalModelSpec, resolveTtsVoice, type TtsLocalModelKey, type TtsLocalModelSpec } from "./models";
|
|
@@ -17,10 +21,9 @@ import {
|
|
|
17
21
|
ONNXRUNTIME_NODE_PACKAGE,
|
|
18
22
|
ONNXRUNTIME_NODE_VERSION,
|
|
19
23
|
} from "./runtime";
|
|
20
|
-
import type {
|
|
24
|
+
import type { TtsTransport, TtsWorkerInbound } from "./tts-protocol";
|
|
21
25
|
|
|
22
26
|
const TTS_TASK = "text-to-speech";
|
|
23
|
-
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
24
27
|
// kokoro-js is NEVER a dependency of the main tree: its transformers@3.8.1 +
|
|
25
28
|
// onnxruntime-node@1.21 graph must not pollute it (1.21 segfaults Bun on session
|
|
26
29
|
// creation). It is lazily `bun install`ed into a side runtime dir on first use,
|
|
@@ -89,7 +92,7 @@ interface TransformersEnv {
|
|
|
89
92
|
|
|
90
93
|
const models = new Map<TtsLocalModelKey, Promise<KokoroTtsInstance>>();
|
|
91
94
|
let synthesizeQueue = Promise.resolve();
|
|
92
|
-
|
|
95
|
+
const kokoroRuntime = new MemoizedRuntime<KokoroRuntime>();
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
98
|
* In-flight streaming sessions keyed by request id. A session is created on
|
|
@@ -107,36 +110,6 @@ interface StreamSession {
|
|
|
107
110
|
}
|
|
108
111
|
const streamSessions = new Map<string, StreamSession>();
|
|
109
112
|
|
|
110
|
-
function errorText(error: unknown): string {
|
|
111
|
-
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function errorMessage(error: unknown): string {
|
|
115
|
-
return error instanceof Error ? error.message : String(error);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function sendLog(
|
|
119
|
-
transport: TtsTransport,
|
|
120
|
-
level: "debug" | "warn" | "error",
|
|
121
|
-
msg: string,
|
|
122
|
-
meta?: Record<string, unknown>,
|
|
123
|
-
): void {
|
|
124
|
-
transport.send({ type: "log", level, msg, meta });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function sendRuntimeInstallProgress(
|
|
128
|
-
transport: TtsTransport,
|
|
129
|
-
requestId: string,
|
|
130
|
-
modelKey: TtsLocalModelKey,
|
|
131
|
-
status: "initiate" | "download" | "done",
|
|
132
|
-
): void {
|
|
133
|
-
transport.send({
|
|
134
|
-
type: "progress",
|
|
135
|
-
id: requestId,
|
|
136
|
-
event: { modelKey, status, name: `${KOKORO_PACKAGE}@${KOKORO_VERSION}` },
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
113
|
/**
|
|
141
114
|
* Map a tiny-model device onto the narrow set `kokoro-js` accepts. The worker
|
|
142
115
|
* always runs `kokoro-js` on Node, where `cpu` (onnxruntime-node) is the only
|
|
@@ -165,13 +138,12 @@ function configureTransformers(transformers: TransformersEnv): void {
|
|
|
165
138
|
* the TTS pipeline is audio-only, so the native image codec transformers eagerly
|
|
166
139
|
* requires is dead weight. Memoized so the runtime loads once per process.
|
|
167
140
|
*/
|
|
168
|
-
|
|
141
|
+
function loadKokoroRuntime(
|
|
169
142
|
transport: TtsTransport,
|
|
170
143
|
requestId: string,
|
|
171
144
|
modelKey: TtsLocalModelKey,
|
|
172
145
|
): Promise<KokoroRuntime> {
|
|
173
|
-
|
|
174
|
-
kokoroRuntime = (async () => {
|
|
146
|
+
return kokoroRuntime.load(async () => {
|
|
175
147
|
const runtimeDir = await ensureRuntimeInstalled({
|
|
176
148
|
runtimeDir: getTtsRuntimeDir(),
|
|
177
149
|
install: {
|
|
@@ -180,12 +152,14 @@ async function loadKokoroRuntime(
|
|
|
180
152
|
trustedDependencies: [ONNXRUNTIME_NODE_PACKAGE],
|
|
181
153
|
},
|
|
182
154
|
probePackage: KOKORO_PACKAGE,
|
|
183
|
-
onPhase: phase =>
|
|
155
|
+
onPhase: phase =>
|
|
156
|
+
transport.send({
|
|
157
|
+
type: "progress",
|
|
158
|
+
id: requestId,
|
|
159
|
+
event: { modelKey, status: phase, name: `${KOKORO_PACKAGE}@${KOKORO_VERSION}` },
|
|
160
|
+
}),
|
|
184
161
|
});
|
|
185
|
-
const nodeModules =
|
|
186
|
-
const sharpStub = path.join(runtimeDir, "omp-sharp-stub.cjs");
|
|
187
|
-
await Bun.write(sharpStub, "module.exports = {};\n");
|
|
188
|
-
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
|
|
162
|
+
const nodeModules = await installSharpStubResolver(runtimeDir);
|
|
189
163
|
const kokoroEntry = resolveRuntimeModule(nodeModules, KOKORO_PACKAGE);
|
|
190
164
|
if (!kokoroEntry) throw new Error(`Unable to resolve ${KOKORO_PACKAGE} in runtime at ${nodeModules}`);
|
|
191
165
|
const transformersEntry = resolveRuntimeModule(nodeModules, TRANSFORMERS_PACKAGE);
|
|
@@ -193,44 +167,7 @@ async function loadKokoroRuntime(
|
|
|
193
167
|
const runtimeRequire = createRequire(kokoroEntry);
|
|
194
168
|
configureTransformers(runtimeRequire(transformersEntry) as TransformersEnv);
|
|
195
169
|
return runtimeRequire(kokoroEntry) as KokoroRuntime;
|
|
196
|
-
})().catch(error => {
|
|
197
|
-
kokoroRuntime = null;
|
|
198
|
-
throw error;
|
|
199
170
|
});
|
|
200
|
-
return kokoroRuntime;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function toProgressEvent(modelKey: TtsLocalModelKey, info: ProgressInfo): TtsProgressEvent {
|
|
204
|
-
if (info.status === "ready") {
|
|
205
|
-
return { modelKey, status: info.status, task: info.task, model: info.model };
|
|
206
|
-
}
|
|
207
|
-
if (info.status === "progress_total") {
|
|
208
|
-
return {
|
|
209
|
-
modelKey,
|
|
210
|
-
status: info.status,
|
|
211
|
-
name: info.name,
|
|
212
|
-
progress: info.progress,
|
|
213
|
-
loaded: info.loaded,
|
|
214
|
-
total: info.total,
|
|
215
|
-
files: info.files,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
if (info.status === "progress") {
|
|
219
|
-
return {
|
|
220
|
-
modelKey,
|
|
221
|
-
status: info.status,
|
|
222
|
-
name: info.name,
|
|
223
|
-
file: info.file,
|
|
224
|
-
progress: info.progress,
|
|
225
|
-
loaded: info.loaded,
|
|
226
|
-
total: info.total,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
return { modelKey, status: info.status, name: info.name, file: info.file };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sendProgress(transport: TtsTransport, id: string, modelKey: TtsLocalModelKey, info: ProgressInfo): void {
|
|
233
|
-
transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
|
|
234
171
|
}
|
|
235
172
|
|
|
236
173
|
async function loadModelOnDevice(
|
|
@@ -295,19 +232,8 @@ async function loadModel(
|
|
|
295
232
|
): Promise<KokoroTtsInstance> {
|
|
296
233
|
const spec = getTtsLocalModelSpec(modelKey);
|
|
297
234
|
if (!spec) throw new Error(`Unknown local TTS model: ${modelKey}`);
|
|
298
|
-
const cached = models.
|
|
299
|
-
if (cached)
|
|
300
|
-
void cached
|
|
301
|
-
.then(() => {
|
|
302
|
-
transport.send({
|
|
303
|
-
type: "progress",
|
|
304
|
-
id: requestId,
|
|
305
|
-
event: { modelKey, status: "ready", task: TTS_TASK, model: spec.repo },
|
|
306
|
-
});
|
|
307
|
-
})
|
|
308
|
-
.catch(() => undefined);
|
|
309
|
-
return cached;
|
|
310
|
-
}
|
|
235
|
+
const cached = replayCachedReady(models, modelKey, transport, requestId, TTS_TASK, spec.repo);
|
|
236
|
+
if (cached) return cached;
|
|
311
237
|
|
|
312
238
|
const runtime = await loadKokoroRuntime(transport, requestId, modelKey);
|
|
313
239
|
const startedAt = performance.now();
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -32,6 +32,13 @@ export interface CodeCellOptions {
|
|
|
32
32
|
*/
|
|
33
33
|
codeTail?: boolean;
|
|
34
34
|
expanded?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Prefix the header with the cell's language icon (resolved through the
|
|
37
|
+
* active symbol preset: nerd-font devicon, unicode emoji, or ascii
|
|
38
|
+
* shorthand). Opt-in so only the eval kernel renderer labels each cell;
|
|
39
|
+
* read/write/browser code cells stay icon-free.
|
|
40
|
+
*/
|
|
41
|
+
showLanguage?: boolean;
|
|
35
42
|
width: number;
|
|
36
43
|
codeStartLine?: number;
|
|
37
44
|
codeLineNumbers?: Array<number | null>;
|
|
@@ -47,8 +54,12 @@ function getState(status?: CodeCellOptions["status"]): State | undefined {
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function formatHeader(options: CodeCellOptions, theme: Theme): { title: string; meta?: string } {
|
|
50
|
-
const { index, total, title, status, spinnerFrame, duration } = options;
|
|
57
|
+
const { index, total, title, status, spinnerFrame, duration, language, showLanguage } = options;
|
|
51
58
|
const parts: string[] = [];
|
|
59
|
+
if (showLanguage && language) {
|
|
60
|
+
const langIcon = theme.getLangIconStyled(language);
|
|
61
|
+
if (langIcon) parts.push(langIcon);
|
|
62
|
+
}
|
|
52
63
|
if (status) {
|
|
53
64
|
const icon = formatStatusIcon(
|
|
54
65
|
status === "complete"
|
|
@@ -14,7 +14,7 @@ export interface FileDisplayModeSession {
|
|
|
14
14
|
/** Whether the edit tool is available. Hashlines are suppressed without it. */
|
|
15
15
|
hasEditTool?: boolean;
|
|
16
16
|
settings: {
|
|
17
|
-
get(key: "readLineNumbers" | "
|
|
17
|
+
get(key: "readLineNumbers" | "edit.mode"): unknown;
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -36,8 +36,7 @@ export function resolveFileDisplayMode(
|
|
|
36
36
|
const usesHashLineAnchors = editMode === "hashline";
|
|
37
37
|
const raw = options?.raw === true;
|
|
38
38
|
const immutable = options?.immutable === true;
|
|
39
|
-
const hashLines =
|
|
40
|
-
!raw && !immutable && hasEditTool && usesHashLineAnchors && settings.get("readHashLines") !== false;
|
|
39
|
+
const hashLines = !raw && !immutable && hasEditTool && usesHashLineAnchors;
|
|
41
40
|
return {
|
|
42
41
|
hashLines,
|
|
43
42
|
lineNumbers: !raw && (hashLines || settings.get("readLineNumbers") === true),
|
|
@@ -8,11 +8,72 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
12
12
|
|
|
13
13
|
const cachedSnapshotPaths = new Map<string, string>();
|
|
14
14
|
const SNAPSHOT_TIMEOUT_MS = 2_000;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Characters that force brush's primitive alias expander down a path it does
|
|
18
|
+
* not implement. brush-core resolves aliases via `value.split_ascii_whitespace()`
|
|
19
|
+
* (`crates/brush-core-vendored/src/interp.rs:1500`, tracking
|
|
20
|
+
* https://github.com/reubeno/brush/issues/57): the resulting pieces are dropped
|
|
21
|
+
* into argv verbatim instead of going through the shell parser. Any alias body
|
|
22
|
+
* containing subshells `(...)`, pipes `|`, redirections `<` `>`, separators
|
|
23
|
+
* `;` `&`, or command substitutions `` ` `` turns the first whitespace-split
|
|
24
|
+
* piece into the command name and produces `command not found: (alias;` style
|
|
25
|
+
* failures (issue #3234, Fedora's default `which` alias is the canonical case).
|
|
26
|
+
*
|
|
27
|
+
* Until brush implements proper alias parsing we drop these from the snapshot;
|
|
28
|
+
* brush then falls through to whatever lives on `PATH`, which is what the user
|
|
29
|
+
* actually expected when they invoked `which` / `ls` / etc.
|
|
30
|
+
*/
|
|
31
|
+
const BRUSH_INCOMPATIBLE_ALIAS_BODY = /[()|&;<>`]/;
|
|
32
|
+
|
|
33
|
+
/** Matches `alias -- NAME='VALUE'` lines emitted by `generateSnapshotScript`. */
|
|
34
|
+
const SNAPSHOT_ALIAS_LINE = /^alias -- ([^\s=]+)='(.*)'\s*$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Strip alias definitions brush's whitespace-only expander cannot execute.
|
|
38
|
+
*
|
|
39
|
+
* Returns the rewritten snapshot plus the list of dropped alias names so the
|
|
40
|
+
* caller can surface them in the debug log.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeSnapshotForBrush(content: string): { content: string; dropped: string[] } {
|
|
43
|
+
const dropped: string[] = [];
|
|
44
|
+
const lines = content.split("\n");
|
|
45
|
+
const out: string[] = [];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const m = line.match(SNAPSHOT_ALIAS_LINE);
|
|
48
|
+
if (m) {
|
|
49
|
+
// Decode the bash-quoting escape `'\''` → `'` so we test the real value.
|
|
50
|
+
const value = m[2].replace(/'\\''/g, "'");
|
|
51
|
+
if (BRUSH_INCOMPATIBLE_ALIAS_BODY.test(value)) {
|
|
52
|
+
dropped.push(m[1]);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
out.push(line);
|
|
57
|
+
}
|
|
58
|
+
return { content: out.join("\n"), dropped };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Apply {@link sanitizeSnapshotForBrush} to the freshly generated snapshot
|
|
63
|
+
* file. Best-effort: I/O failures here must not poison `getOrCreateSnapshot`.
|
|
64
|
+
*/
|
|
65
|
+
function scrubSnapshotInPlace(snapshotPath: string): void {
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(snapshotPath, "utf8");
|
|
68
|
+
const { content, dropped } = sanitizeSnapshotForBrush(raw);
|
|
69
|
+
if (dropped.length === 0) return;
|
|
70
|
+
fs.writeFileSync(snapshotPath, content);
|
|
71
|
+
logger.debug("shell-snapshot: dropped brush-incompatible aliases", { dropped });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.debug("shell-snapshot: scrub failed", { err: String(err) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
16
77
|
function sanitizeSnapshotEnv(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
17
78
|
const sanitized = { ...env };
|
|
18
79
|
delete sanitized.BASH_ENV;
|
|
@@ -169,6 +230,7 @@ export async function getOrCreateSnapshot(
|
|
|
169
230
|
|
|
170
231
|
await child.exited;
|
|
171
232
|
if (child.exitCode === 0 && fs.existsSync(snapshotPath)) {
|
|
233
|
+
scrubSnapshotInPlace(snapshotPath);
|
|
172
234
|
cachedSnapshotPaths.set(cacheKey, snapshotPath);
|
|
173
235
|
return snapshotPath;
|
|
174
236
|
}
|