@oh-my-pi/pi-coding-agent 16.1.12 → 16.1.14
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 +27 -0
- package/dist/cli.js +5650 -2860
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/cursor.d.ts +9 -9
- 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/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/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/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/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/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -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/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/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.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- 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/tree-selector.ts +13 -18
- 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 +10 -10
- package/src/prompts/tools/bash.md +0 -1
- 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/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- 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/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
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),
|
package/src/web/parallel.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { AgentStorage } from "../session/agent-storage";
|
|
|
3
3
|
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
4
4
|
|
|
5
5
|
const PARALLEL_API_URL = "https://api.parallel.ai";
|
|
6
|
-
const PARALLEL_SEARCH_URL = `${PARALLEL_API_URL}/v1beta/search`;
|
|
6
|
+
export const PARALLEL_SEARCH_URL = `${PARALLEL_API_URL}/v1beta/search`;
|
|
7
7
|
const PARALLEL_EXTRACT_URL = `${PARALLEL_API_URL}/v1beta/extract`;
|
|
8
|
-
const PARALLEL_BETA_HEADER = "search-extract-2025-10-10";
|
|
8
|
+
export const PARALLEL_BETA_HEADER = "search-extract-2025-10-10";
|
|
9
9
|
|
|
10
10
|
export interface ParallelUsageItem {
|
|
11
11
|
name?: string;
|
|
@@ -76,22 +76,6 @@ export class ParallelApiError extends Error {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function findParallelApiKey(storage: AgentStorage | null | undefined): string | null {
|
|
80
|
-
return findCredential(storage, getEnvApiKey("parallel"), "parallel");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function getParallelExtractContent(document: ParallelExtractDocument): string {
|
|
84
|
-
const excerptContent = document.excerpts
|
|
85
|
-
.filter(excerpt => excerpt.trim().length > 0)
|
|
86
|
-
.join("\n\n")
|
|
87
|
-
.trim();
|
|
88
|
-
if (excerptContent.length > 0) {
|
|
89
|
-
return excerptContent;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return document.fullContent?.trim() ?? "";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
79
|
function isObject(value: unknown): value is object {
|
|
96
80
|
return typeof value === "object" && value !== null;
|
|
97
81
|
}
|
|
@@ -148,7 +132,7 @@ function createParallelApiError(statusCode: number, detail?: string): ParallelAp
|
|
|
148
132
|
);
|
|
149
133
|
}
|
|
150
134
|
|
|
151
|
-
function parseParallelErrorResponse(statusCode: number, responseText: string): ParallelApiError {
|
|
135
|
+
export function parseParallelErrorResponse(statusCode: number, responseText: string): ParallelApiError {
|
|
152
136
|
const trimmedResponseText = responseText.trim();
|
|
153
137
|
if (trimmedResponseText.length === 0) {
|
|
154
138
|
return createParallelApiError(statusCode);
|
|
@@ -176,24 +160,6 @@ function getAuthHeaders(apiKey: string): {
|
|
|
176
160
|
};
|
|
177
161
|
}
|
|
178
162
|
|
|
179
|
-
function normalizeSearchMode(mode: ParallelSearchOptions["mode"]): "fast" | "one-shot" {
|
|
180
|
-
return mode === "research" ? "one-shot" : "fast";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function parseUsageItems(payload: unknown): ParallelUsageItem[] {
|
|
184
|
-
if (!Array.isArray(payload)) return [];
|
|
185
|
-
|
|
186
|
-
const usageItems: ParallelUsageItem[] = [];
|
|
187
|
-
for (const item of payload) {
|
|
188
|
-
if (!isObject(item)) continue;
|
|
189
|
-
usageItems.push({
|
|
190
|
-
name: getString(item, "name"),
|
|
191
|
-
count: getNumber(item, "count"),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return usageItems;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
163
|
function parseWarnings(payload: unknown): string[] {
|
|
198
164
|
if (!Array.isArray(payload)) return [];
|
|
199
165
|
|
|
@@ -212,7 +178,24 @@ function parseWarnings(payload: unknown): string[] {
|
|
|
212
178
|
return warnings;
|
|
213
179
|
}
|
|
214
180
|
|
|
215
|
-
function
|
|
181
|
+
function parseUsageItems(payload: unknown): ParallelUsageItem[] {
|
|
182
|
+
if (!Array.isArray(payload)) return [];
|
|
183
|
+
|
|
184
|
+
const usageItems: ParallelUsageItem[] = [];
|
|
185
|
+
for (const item of payload) {
|
|
186
|
+
if (!isObject(item)) continue;
|
|
187
|
+
usageItems.push({
|
|
188
|
+
name: getString(item, "name"),
|
|
189
|
+
count: getNumber(item, "count"),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return usageItems;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseParallelSearchPayload(
|
|
196
|
+
payload: unknown,
|
|
197
|
+
options?: { parseMetadata?: boolean },
|
|
198
|
+
): ParallelSearchResult {
|
|
216
199
|
if (!isObject(payload)) {
|
|
217
200
|
throw new ParallelApiError("Parallel search returned an invalid response payload.");
|
|
218
201
|
}
|
|
@@ -236,14 +219,32 @@ function parseSearchPayload(payload: unknown): ParallelSearchResult {
|
|
|
236
219
|
});
|
|
237
220
|
}
|
|
238
221
|
|
|
222
|
+
const parseMetadata = options?.parseMetadata ?? true;
|
|
223
|
+
|
|
239
224
|
return {
|
|
240
225
|
requestId,
|
|
241
226
|
sources,
|
|
242
|
-
warnings: parseWarnings(getOwnValue(payload, "warnings")),
|
|
243
|
-
usage: parseUsageItems(getOwnValue(payload, "usage")),
|
|
227
|
+
warnings: parseMetadata ? parseWarnings(getOwnValue(payload, "warnings")) : [],
|
|
228
|
+
usage: parseMetadata ? parseUsageItems(getOwnValue(payload, "usage")) : [],
|
|
244
229
|
};
|
|
245
230
|
}
|
|
246
231
|
|
|
232
|
+
export function findParallelApiKey(storage: AgentStorage | null | undefined): string | null {
|
|
233
|
+
return findCredential(storage, getEnvApiKey("parallel"), "parallel");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getParallelExtractContent(document: ParallelExtractDocument): string {
|
|
237
|
+
const excerptContent = document.excerpts
|
|
238
|
+
.filter(excerpt => excerpt.trim().length > 0)
|
|
239
|
+
.join("\n\n")
|
|
240
|
+
.trim();
|
|
241
|
+
if (excerptContent.length > 0) {
|
|
242
|
+
return excerptContent;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return document.fullContent?.trim() ?? "";
|
|
246
|
+
}
|
|
247
|
+
|
|
247
248
|
function parseExtractPayload(payload: unknown): ParallelExtractResult {
|
|
248
249
|
if (!isObject(payload)) {
|
|
249
250
|
throw new ParallelApiError("Parallel extract returned an invalid response payload.");
|
|
@@ -304,7 +305,7 @@ export async function searchWithParallel(
|
|
|
304
305
|
body: JSON.stringify({
|
|
305
306
|
objective,
|
|
306
307
|
search_queries: queries,
|
|
307
|
-
mode:
|
|
308
|
+
mode: options.mode === "research" ? "one-shot" : "fast",
|
|
308
309
|
excerpts: {
|
|
309
310
|
max_chars_per_result: options.maxCharsPerResult ?? 10_000,
|
|
310
311
|
},
|
|
@@ -316,7 +317,7 @@ export async function searchWithParallel(
|
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
const payload: unknown = await response.json();
|
|
319
|
-
return
|
|
320
|
+
return parseParallelSearchPayload(payload);
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
export async function extractWithParallel(
|