@oh-my-pi/pi-coding-agent 16.1.13 → 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.
Files changed (129) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5547 -2759
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  5. package/dist/types/eval/backend-helpers.d.ts +23 -0
  6. package/dist/types/eval/executor-base.d.ts +118 -0
  7. package/dist/types/eval/index.d.ts +2 -0
  8. package/dist/types/eval/jl/executor.d.ts +44 -0
  9. package/dist/types/eval/jl/index.d.ts +11 -0
  10. package/dist/types/eval/jl/kernel.d.ts +28 -0
  11. package/dist/types/eval/jl/prelude.d.ts +1 -0
  12. package/dist/types/eval/jl/runtime.d.ts +22 -0
  13. package/dist/types/eval/kernel-base.d.ts +105 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -61
  15. package/dist/types/eval/rb/executor.d.ts +77 -0
  16. package/dist/types/eval/rb/index.d.ts +11 -0
  17. package/dist/types/eval/rb/kernel.d.ts +31 -0
  18. package/dist/types/eval/rb/prelude.d.ts +1 -0
  19. package/dist/types/eval/rb/runtime.d.ts +23 -0
  20. package/dist/types/eval/runtime-env.d.ts +24 -0
  21. package/dist/types/eval/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  23. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  24. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  25. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  26. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  29. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  30. package/dist/types/modes/interactive-mode.d.ts +0 -2
  31. package/dist/types/modes/theme/theme.d.ts +8 -1
  32. package/dist/types/modes/types.d.ts +0 -2
  33. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  34. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  35. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +2 -2
  38. package/dist/types/stt/asr-client.d.ts +3 -29
  39. package/dist/types/subprocess/worker-client.d.ts +149 -0
  40. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  41. package/dist/types/tiny/title-client.d.ts +14 -34
  42. package/dist/types/tools/eval-backends.d.ts +6 -3
  43. package/dist/types/tools/eval-render.d.ts +6 -5
  44. package/dist/types/tools/eval.d.ts +13 -15
  45. package/dist/types/tools/index.d.ts +3 -3
  46. package/dist/types/tts/tts-client.d.ts +3 -28
  47. package/dist/types/tui/code-cell.d.ts +7 -0
  48. package/dist/types/utils/file-display-mode.d.ts +1 -1
  49. package/dist/types/web/parallel.d.ts +6 -0
  50. package/package.json +12 -12
  51. package/src/config/settings-schema.ts +50 -18
  52. package/src/config/settings.ts +5 -0
  53. package/src/dap/client.ts +13 -107
  54. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  55. package/src/eval/backend-helpers.ts +48 -0
  56. package/src/eval/executor-base.ts +425 -0
  57. package/src/eval/index.ts +2 -0
  58. package/src/eval/jl/executor.ts +540 -0
  59. package/src/eval/jl/index.ts +54 -0
  60. package/src/eval/jl/kernel.ts +235 -0
  61. package/src/eval/jl/prelude.jl +930 -0
  62. package/src/eval/jl/prelude.ts +3 -0
  63. package/src/eval/jl/runner.jl +634 -0
  64. package/src/eval/jl/runtime.ts +118 -0
  65. package/src/eval/js/index.ts +3 -14
  66. package/src/eval/kernel-base.ts +569 -0
  67. package/src/eval/py/executor.ts +43 -252
  68. package/src/eval/py/index.ts +9 -20
  69. package/src/eval/py/kernel.ts +29 -544
  70. package/src/eval/rb/executor.ts +504 -0
  71. package/src/eval/rb/index.ts +54 -0
  72. package/src/eval/rb/kernel.ts +230 -0
  73. package/src/eval/rb/prelude.rb +721 -0
  74. package/src/eval/rb/prelude.ts +3 -0
  75. package/src/eval/rb/runner.rb +474 -0
  76. package/src/eval/rb/runtime.ts +132 -0
  77. package/src/eval/runtime-env.ts +104 -0
  78. package/src/eval/types.ts +3 -3
  79. package/src/extensibility/extensions/runner.ts +4 -11
  80. package/src/extensibility/hooks/loader.ts +3 -21
  81. package/src/extensibility/session-handler-types.ts +21 -0
  82. package/src/internal-urls/docs-index.generated.txt +1 -1
  83. package/src/jsonrpc/message-framing.ts +142 -0
  84. package/src/lsp/client.ts +13 -109
  85. package/src/mnemopi/embed-client.ts +43 -198
  86. package/src/modes/components/agent-dashboard.ts +17 -40
  87. package/src/modes/components/chat-transcript-builder.ts +18 -102
  88. package/src/modes/components/custom-editor.ts +19 -0
  89. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  90. package/src/modes/components/extensions/extension-list.ts +16 -44
  91. package/src/modes/components/history-search.ts +4 -16
  92. package/src/modes/components/selector-helpers.ts +129 -0
  93. package/src/modes/components/settings-selector.ts +7 -5
  94. package/src/modes/components/tree-selector.ts +13 -18
  95. package/src/modes/controllers/event-controller.ts +3 -9
  96. package/src/modes/controllers/input-controller.ts +32 -54
  97. package/src/modes/interactive-mode.ts +5 -7
  98. package/src/modes/theme/theme.ts +35 -0
  99. package/src/modes/types.ts +0 -2
  100. package/src/modes/utils/copy-targets.ts +3 -2
  101. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  102. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  103. package/src/modes/utils/ui-helpers.ts +21 -126
  104. package/src/prompts/system/system-prompt.md +10 -10
  105. package/src/prompts/tools/bash.md +0 -1
  106. package/src/prompts/tools/eval.md +6 -4
  107. package/src/prompts/tools/find.md +0 -4
  108. package/src/prompts/tools/read.md +1 -2
  109. package/src/prompts/tools/replace.md +1 -1
  110. package/src/prompts/tools/search.md +0 -1
  111. package/src/sdk.ts +13 -7
  112. package/src/session/agent-session.ts +9 -7
  113. package/src/stt/asr-client.ts +35 -215
  114. package/src/stt/asr-worker.ts +29 -181
  115. package/src/subprocess/worker-client.ts +297 -0
  116. package/src/subprocess/worker-runtime.ts +277 -0
  117. package/src/task/executor.ts +4 -4
  118. package/src/tiny/title-client.ts +53 -219
  119. package/src/tiny/worker.ts +29 -180
  120. package/src/tools/eval-backends.ts +10 -3
  121. package/src/tools/eval-render.ts +17 -8
  122. package/src/tools/eval.ts +187 -22
  123. package/src/tools/index.ts +51 -28
  124. package/src/tts/tts-client.ts +38 -206
  125. package/src/tts/tts-worker.ts +23 -97
  126. package/src/tui/code-cell.ts +12 -1
  127. package/src/utils/file-display-mode.ts +2 -3
  128. package/src/web/parallel.ts +43 -42
  129. package/src/web/search/providers/parallel.ts +10 -99
@@ -1,8 +1,17 @@
1
- import * as path from "node:path";
2
- import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
3
- import type { Subprocess } from "bun";
4
- import { settings } from "../config/settings";
5
- import { tinyWorkerEnvOverlay } from "../tiny/title-client";
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
- const inbound = new Set<(message: TtsWorkerOutbound) => void>();
216
- const errors = new Set<(error: Error) => void>();
217
- const intentionalExit = { value: false };
218
- const spawnCommand = ttsWorkerSpawnCmd();
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({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
148
+ function wrapSubprocess(
149
+ spawned: SpawnedSubprocess<TtsWorkerOutbound>,
150
+ ): RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound> {
151
+ const { proc } = spawned;
247
152
  return {
248
- send(message) {
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): WorkerHandle {
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
- send(message) {
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(): WorkerHandle {
319
- try {
320
- return wrapSubprocess(createTtsSubprocess());
321
- } catch (error) {
322
- logger.warn("TTS worker spawn failed; local TTS disabled", {
323
- error: error instanceof Error ? error.message : String(error),
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: WorkerHandle | null = null;
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: () => WorkerHandle;
195
+ #spawnWorker: () => RefCountedWorkerHandle<TtsWorkerInbound, TtsWorkerOutbound>;
344
196
 
345
- constructor(spawnWorker: () => WorkerHandle = spawnTtsWorker) {
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: WorkerHandle;
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(): WorkerHandle {
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
- const handle = wrapSubprocess(createTtsSubprocess());
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
  }
@@ -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
- ensureRuntimeInstalled,
6
- getTinyModelsCacheDir,
7
- installRuntimeModuleResolver,
8
- resolveRuntimeModule,
9
- } from "@oh-my-pi/pi-utils";
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 { TtsProgressEvent, TtsTransport, TtsWorkerInbound } from "./tts-protocol";
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
- let kokoroRuntime: Promise<KokoroRuntime> | null = null;
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
- async function loadKokoroRuntime(
141
+ function loadKokoroRuntime(
169
142
  transport: TtsTransport,
170
143
  requestId: string,
171
144
  modelKey: TtsLocalModelKey,
172
145
  ): Promise<KokoroRuntime> {
173
- if (kokoroRuntime) return kokoroRuntime;
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 => sendRuntimeInstallProgress(transport, requestId, modelKey, 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 = path.join(runtimeDir, "node_modules");
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.get(modelKey);
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();
@@ -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" | "readHashLines" | "edit.mode"): unknown;
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),
@@ -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 parseSearchPayload(payload: unknown): ParallelSearchResult {
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: normalizeSearchMode(options.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 parseSearchPayload(payload);
320
+ return parseParallelSearchPayload(payload);
320
321
  }
321
322
 
322
323
  export async function extractWithParallel(