@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli.js +5615 -2833
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +64 -14
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/export/share.d.ts +8 -1
  24. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  26. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  27. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  28. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  29. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  30. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  31. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  32. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  33. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  34. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +0 -2
  36. package/dist/types/modes/theme/theme.d.ts +8 -1
  37. package/dist/types/modes/types.d.ts +0 -2
  38. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  39. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  40. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/secrets/obfuscator.d.ts +3 -3
  43. package/dist/types/session/agent-session.d.ts +2 -2
  44. package/dist/types/stt/asr-client.d.ts +3 -29
  45. package/dist/types/subprocess/worker-client.d.ts +149 -0
  46. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  47. package/dist/types/tiny/title-client.d.ts +14 -34
  48. package/dist/types/tools/eval-backends.d.ts +6 -3
  49. package/dist/types/tools/eval-render.d.ts +6 -5
  50. package/dist/types/tools/eval.d.ts +13 -15
  51. package/dist/types/tools/index.d.ts +3 -3
  52. package/dist/types/tts/tts-client.d.ts +3 -28
  53. package/dist/types/tui/code-cell.d.ts +7 -0
  54. package/dist/types/utils/file-display-mode.d.ts +1 -1
  55. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  56. package/dist/types/web/parallel.d.ts +6 -0
  57. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  58. package/package.json +12 -12
  59. package/src/advisor/__tests__/advisor.test.ts +114 -0
  60. package/src/advisor/runtime.ts +129 -1
  61. package/src/config/model-registry.ts +12 -4
  62. package/src/config/settings-schema.ts +74 -18
  63. package/src/config/settings.ts +5 -0
  64. package/src/dap/client.ts +13 -107
  65. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  66. package/src/eval/backend-helpers.ts +48 -0
  67. package/src/eval/executor-base.ts +425 -0
  68. package/src/eval/index.ts +2 -0
  69. package/src/eval/jl/executor.ts +540 -0
  70. package/src/eval/jl/index.ts +54 -0
  71. package/src/eval/jl/kernel.ts +235 -0
  72. package/src/eval/jl/prelude.jl +930 -0
  73. package/src/eval/jl/prelude.ts +3 -0
  74. package/src/eval/jl/runner.jl +634 -0
  75. package/src/eval/jl/runtime.ts +118 -0
  76. package/src/eval/js/index.ts +3 -14
  77. package/src/eval/kernel-base.ts +569 -0
  78. package/src/eval/py/executor.ts +43 -252
  79. package/src/eval/py/index.ts +9 -20
  80. package/src/eval/py/kernel.ts +29 -544
  81. package/src/eval/rb/executor.ts +504 -0
  82. package/src/eval/rb/index.ts +54 -0
  83. package/src/eval/rb/kernel.ts +230 -0
  84. package/src/eval/rb/prelude.rb +721 -0
  85. package/src/eval/rb/prelude.ts +3 -0
  86. package/src/eval/rb/runner.rb +474 -0
  87. package/src/eval/rb/runtime.ts +132 -0
  88. package/src/eval/runtime-env.ts +104 -0
  89. package/src/eval/types.ts +3 -3
  90. package/src/exec/bash-executor.ts +44 -0
  91. package/src/export/share.ts +51 -28
  92. package/src/extensibility/extensions/runner.ts +4 -11
  93. package/src/extensibility/hooks/loader.ts +3 -21
  94. package/src/extensibility/session-handler-types.ts +21 -0
  95. package/src/internal-urls/docs-index.generated.txt +1 -1
  96. package/src/jsonrpc/message-framing.ts +142 -0
  97. package/src/lsp/client.ts +13 -109
  98. package/src/mcp/transports/stdio.ts +20 -4
  99. package/src/mnemopi/embed-client.ts +43 -198
  100. package/src/modes/components/agent-dashboard.ts +17 -40
  101. package/src/modes/components/chat-transcript-builder.ts +18 -102
  102. package/src/modes/components/custom-editor.test.ts +22 -0
  103. package/src/modes/components/custom-editor.ts +29 -1
  104. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  105. package/src/modes/components/extensions/extension-list.ts +16 -44
  106. package/src/modes/components/footer.ts +4 -3
  107. package/src/modes/components/history-search.ts +4 -16
  108. package/src/modes/components/selector-helpers.ts +129 -0
  109. package/src/modes/components/settings-selector.ts +7 -5
  110. package/src/modes/components/status-line/component.ts +5 -1
  111. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  112. package/src/modes/components/status-line/segments.ts +1 -1
  113. package/src/modes/components/status-line/types.ts +1 -0
  114. package/src/modes/components/tree-selector.ts +13 -18
  115. package/src/modes/controllers/command-controller.ts +1 -0
  116. package/src/modes/controllers/event-controller.ts +3 -9
  117. package/src/modes/controllers/input-controller.ts +32 -54
  118. package/src/modes/interactive-mode.ts +5 -7
  119. package/src/modes/theme/theme.ts +35 -0
  120. package/src/modes/types.ts +0 -2
  121. package/src/modes/utils/copy-targets.ts +3 -2
  122. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  123. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  124. package/src/modes/utils/ui-helpers.ts +21 -126
  125. package/src/prompts/system/system-prompt.md +5 -5
  126. package/src/prompts/tools/bash.md +2 -3
  127. package/src/prompts/tools/eval.md +6 -4
  128. package/src/prompts/tools/find.md +0 -4
  129. package/src/prompts/tools/read.md +1 -2
  130. package/src/prompts/tools/replace.md +1 -1
  131. package/src/sdk.ts +13 -7
  132. package/src/secrets/obfuscator.ts +3 -9
  133. package/src/session/agent-session.ts +42 -9
  134. package/src/slash-commands/builtin-registry.ts +2 -1
  135. package/src/stt/asr-client.ts +35 -215
  136. package/src/stt/asr-worker.ts +29 -181
  137. package/src/subprocess/worker-client.ts +297 -0
  138. package/src/subprocess/worker-runtime.ts +277 -0
  139. package/src/task/executor.ts +4 -4
  140. package/src/tiny/title-client.ts +53 -219
  141. package/src/tiny/worker.ts +29 -180
  142. package/src/tools/eval-backends.ts +10 -3
  143. package/src/tools/eval-render.ts +17 -8
  144. package/src/tools/eval.ts +187 -22
  145. package/src/tools/index.ts +51 -28
  146. package/src/tts/tts-client.ts +38 -206
  147. package/src/tts/tts-worker.ts +23 -97
  148. package/src/tui/code-cell.ts +12 -1
  149. package/src/utils/file-display-mode.ts +2 -3
  150. package/src/utils/shell-snapshot.ts +63 -1
  151. package/src/web/parallel.ts +43 -42
  152. package/src/web/search/providers/parallel.ts +10 -99
  153. package/src/web/search/providers/perplexity.ts +18 -6
@@ -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),
@@ -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
  }