@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.1

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 (140) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/completion-gen.d.ts +80 -0
  5. package/dist/types/commands/complete.d.ts +6 -0
  6. package/dist/types/commands/completions.d.ts +13 -0
  7. package/dist/types/commands/setup.d.ts +10 -1
  8. package/dist/types/config/settings-schema.d.ts +170 -10
  9. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  10. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  13. package/dist/types/edit/hashline/index.d.ts +1 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -0
  15. package/dist/types/eval/py/runtime.d.ts +11 -1
  16. package/dist/types/export/html/template.generated.d.ts +1 -1
  17. package/dist/types/main.d.ts +1 -0
  18. package/dist/types/modes/components/index.d.ts +1 -0
  19. package/dist/types/modes/components/segment-track.d.ts +22 -0
  20. package/dist/types/modes/components/welcome.d.ts +21 -0
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  23. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  24. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  25. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  26. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  27. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  28. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  29. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  30. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  31. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  32. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  33. package/dist/types/modes/theme/theme.d.ts +11 -0
  34. package/dist/types/modes/types.d.ts +5 -1
  35. package/dist/types/tiny/device.d.ts +78 -0
  36. package/dist/types/tiny/dtype.d.ts +85 -0
  37. package/dist/types/tiny/models.d.ts +6 -6
  38. package/dist/types/tiny/text.d.ts +15 -0
  39. package/dist/types/tiny/title-client.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +0 -1
  41. package/dist/types/tools/eval.d.ts +1 -1
  42. package/dist/types/tools/index.d.ts +0 -1
  43. package/dist/types/tui/code-cell.d.ts +2 -0
  44. package/dist/types/tui/output-block.d.ts +17 -0
  45. package/package.json +9 -9
  46. package/src/capability/rule-buckets.ts +64 -0
  47. package/src/capability/rule.ts +8 -0
  48. package/src/cli/completion-gen.ts +550 -0
  49. package/src/cli/setup-cli.ts +5 -3
  50. package/src/cli-commands.ts +2 -0
  51. package/src/cli.ts +1 -7
  52. package/src/commands/complete.ts +66 -0
  53. package/src/commands/completions.ts +60 -0
  54. package/src/commands/setup.ts +29 -4
  55. package/src/config/settings-schema.ts +70 -11
  56. package/src/discovery/builtin-defaults.ts +39 -0
  57. package/src/discovery/builtin-rules/index.ts +48 -0
  58. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  59. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  60. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  61. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  62. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  63. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  64. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  65. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  66. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  67. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  68. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  69. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  70. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  71. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  72. package/src/discovery/index.ts +1 -0
  73. package/src/edit/hashline/block-resolver.ts +14 -0
  74. package/src/edit/hashline/diff.ts +4 -1
  75. package/src/edit/hashline/execute.ts +2 -1
  76. package/src/edit/hashline/index.ts +1 -0
  77. package/src/eval/py/kernel.ts +37 -15
  78. package/src/eval/py/runtime.ts +57 -28
  79. package/src/export/html/template.generated.ts +1 -1
  80. package/src/export/html/template.js +0 -12
  81. package/src/export/ttsr.ts +2 -0
  82. package/src/internal-urls/docs-index.generated.ts +7 -8
  83. package/src/main.ts +18 -1
  84. package/src/modes/components/hook-selector.ts +15 -17
  85. package/src/modes/components/index.ts +1 -0
  86. package/src/modes/components/segment-track.ts +52 -0
  87. package/src/modes/components/tips.txt +2 -1
  88. package/src/modes/components/tool-execution.ts +5 -1
  89. package/src/modes/components/welcome.ts +47 -42
  90. package/src/modes/controllers/input-controller.ts +12 -21
  91. package/src/modes/interactive-mode.ts +17 -5
  92. package/src/modes/setup-wizard/index.ts +88 -0
  93. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  94. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  95. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  96. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  97. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  98. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  99. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  100. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  101. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  102. package/src/modes/theme/shimmer.ts +5 -0
  103. package/src/modes/theme/theme.ts +44 -20
  104. package/src/modes/types.ts +6 -1
  105. package/src/prompts/system/orchestrate-notice.md +1 -1
  106. package/src/prompts/tools/read.md +4 -0
  107. package/src/sdk.ts +5 -15
  108. package/src/slash-commands/builtin-registry.ts +8 -0
  109. package/src/tiny/device.ts +117 -0
  110. package/src/tiny/dtype.ts +101 -0
  111. package/src/tiny/models.ts +7 -6
  112. package/src/tiny/text.ts +36 -1
  113. package/src/tiny/title-client.ts +58 -3
  114. package/src/tiny/worker.ts +93 -29
  115. package/src/tools/bash.ts +16 -13
  116. package/src/tools/eval.ts +9 -4
  117. package/src/tools/index.ts +0 -11
  118. package/src/tools/read.ts +1 -0
  119. package/src/tools/renderers.ts +0 -2
  120. package/src/tui/code-cell.ts +6 -1
  121. package/src/tui/output-block.ts +199 -38
  122. package/dist/types/tools/recipe/index.d.ts +0 -46
  123. package/dist/types/tools/recipe/render.d.ts +0 -36
  124. package/dist/types/tools/recipe/runner.d.ts +0 -60
  125. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  126. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  127. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  128. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  129. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  130. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  131. package/src/prompts/tools/recipe.md +0 -16
  132. package/src/tools/recipe/index.ts +0 -81
  133. package/src/tools/recipe/render.ts +0 -19
  134. package/src/tools/recipe/runner.ts +0 -219
  135. package/src/tools/recipe/runners/cargo.ts +0 -131
  136. package/src/tools/recipe/runners/index.ts +0 -8
  137. package/src/tools/recipe/runners/just.ts +0 -73
  138. package/src/tools/recipe/runners/make.ts +0 -101
  139. package/src/tools/recipe/runners/pkg.ts +0 -167
  140. package/src/tools/recipe/runners/task.ts +0 -72
@@ -1,4 +1,4 @@
1
- /** Default session-title model: the online pi/smol path (no local download / CPU inference). */
1
+ /** Default session-title model: the online pi/smol path (no local download / on-device inference). */
2
2
  export const ONLINE_TINY_TITLE_MODEL_KEY = "online";
3
3
  /** Local model the `tiny-models` CLI downloads when none is named. Not the session-title default — that is {@link ONLINE_TINY_TITLE_MODEL_KEY}. */
4
4
  export const DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY = "lfm2-700m";
@@ -19,7 +19,7 @@ export const TINY_TITLE_LOCAL_MODELS = [
19
19
  dtype: "q4",
20
20
  label: "LFM2 350M",
21
21
  description: "Recommended local model; best speed/quality balance, about 212 MB cached.",
22
- contextNote: "Best local default from the CPU title-generation spike.",
22
+ contextNote: "Best local default from the title-generation spike.",
23
23
  },
24
24
  {
25
25
  key: "qwen3-0.6b",
@@ -83,7 +83,7 @@ export const TINY_TITLE_MODEL_OPTIONS = [
83
83
  {
84
84
  value: ONLINE_TINY_TITLE_MODEL_KEY,
85
85
  label: "Online (pi/smol)",
86
- description: "Current online title generation path; no local model download or CPU inference.",
86
+ description: "Current online title generation path; no local model download or on-device inference.",
87
87
  },
88
88
  ...TINY_TITLE_LOCAL_MODELS.map(model => ({
89
89
  value: model.key,
@@ -110,7 +110,7 @@ export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "qwen3-1.7b";
110
110
  /**
111
111
  * Local models for Mnemosyne memory tasks (fact extraction + consolidation).
112
112
  * These are larger (1B-1.7B) than the title models: structured extraction and
113
- * faithful summarization need more capacity than 3-6 word titles. All q4, CPU.
113
+ * faithful summarization need more capacity than 3-6 word titles. All q4.
114
114
  * Ranking/recipe rationale lives in docs/local-models.md.
115
115
  */
116
116
  export const TINY_MEMORY_LOCAL_MODELS = [
@@ -121,7 +121,7 @@ export const TINY_MEMORY_LOCAL_MODELS = [
121
121
  label: "Qwen3 1.7B",
122
122
  description:
123
123
  "Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.",
124
- contextNote: "Best single-model pick for memory from the CPU experiment.",
124
+ contextNote: "Best single-model pick for memory from the local experiment.",
125
125
  },
126
126
  {
127
127
  key: "gemma-3-1b",
@@ -176,7 +176,8 @@ export const TINY_MEMORY_MODEL_OPTIONS = [
176
176
  {
177
177
  value: ONLINE_MEMORY_MODEL_KEY,
178
178
  label: "Online (smol/remote)",
179
- description: "Use the configured Mnemosyne LLM mode (smol or remote); no local model download or CPU inference.",
179
+ description:
180
+ "Use the configured Mnemosyne LLM mode (smol or remote); no local model download or on-device inference.",
180
181
  },
181
182
  ...TINY_MEMORY_LOCAL_MODELS.map(model => ({
182
183
  value: model.key,
package/src/tiny/text.ts CHANGED
@@ -1,11 +1,46 @@
1
1
  export const MAX_TITLE_INPUT_CHARS = 2000;
2
2
 
3
+ /**
4
+ * Minimum length of code-stripped input below which we fall back to the
5
+ * original message. Guards against messages that are (almost) entirely a code
6
+ * block — stripping would otherwise leave the model nothing to title from.
7
+ */
8
+ const MIN_STRIPPED_TITLE_CHARS = 12;
9
+ /** Matches a fenced code block (3+ backticks), including an unterminated trailing fence. */
10
+ const FENCED_CODE_BLOCK = /```+[\s\S]*?(?:```+|$)/g;
11
+
3
12
  export function truncateTitleInput(message: string): string {
4
13
  return message.length > MAX_TITLE_INPUT_CHARS ? `${message.slice(0, MAX_TITLE_INPUT_CHARS)}…` : message;
5
14
  }
6
15
 
16
+ /**
17
+ * Strip fenced code blocks from a message before titling.
18
+ *
19
+ * Small title models latch onto literal text inside code blocks — e.g. a pasted
20
+ * UI mockup containing "Welcome to Claude Code v2.1.158" yields that string as
21
+ * the title instead of the surrounding intent. Removing fenced blocks leaves the
22
+ * prose that actually describes the task. Inline code (single backticks) is kept
23
+ * — it is short, high-signal context like `/login`.
24
+ *
25
+ * Falls back to the original message when stripping leaves too little to title
26
+ * (a message that is essentially just a code block).
27
+ */
28
+ export function stripCodeBlocks(message: string): string {
29
+ const cleaned = message
30
+ .replace(FENCED_CODE_BLOCK, " ")
31
+ .replace(/[ \t]+/g, " ")
32
+ .replace(/\n{3,}/g, "\n\n")
33
+ .trim();
34
+ return cleaned.length >= MIN_STRIPPED_TITLE_CHARS ? cleaned : message;
35
+ }
36
+
37
+ /** Prepare a raw user message for titling: drop code blocks, then bound length. */
38
+ export function prepareTitleInput(message: string): string {
39
+ return truncateTitleInput(stripCodeBlocks(message));
40
+ }
41
+
7
42
  export function formatTitleUserMessage(message: string): string {
8
- return `<user-message>\n${truncateTitleInput(message)}\n</user-message>`;
43
+ return `<user-message>\n${prepareTitleInput(message)}\n</user-message>`;
9
44
  }
10
45
 
11
46
  export function normalizeGeneratedTitle(value: string | null | undefined): string | null {
@@ -1,4 +1,7 @@
1
- import { isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
1
+ import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
2
+ import { settings } from "../config/settings";
3
+ import { tinyModelDeviceSettingToEnv } from "./device";
4
+ import { tinyModelDtypeSettingToEnv } from "./dtype";
2
5
  import {
3
6
  isTinyLocalModelKey,
4
7
  isTinyMemoryLocalModelKey,
@@ -28,10 +31,62 @@ export interface TinyTitleDownloadOptions {
28
31
 
29
32
  const SMOKE_TEST_TIMEOUT_MS = 5_000;
30
33
 
34
+ function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
35
+ try {
36
+ const value = settings.get(path);
37
+ return typeof value === "string" ? value : undefined;
38
+ } catch {
39
+ // Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Decide which `PI_TINY_DEVICE` / `PI_TINY_DTYPE` vars to overlay onto the worker
46
+ * env. A present env var wins (left untouched); otherwise the mapped persisted
47
+ * setting is used. Returns only the keys to add — never the default sentinel.
48
+ * Pure for testability; see {@link tinyWorkerEnv} for the spawn-time glue.
49
+ * @internal
50
+ */
51
+ export function tinyWorkerEnvOverlay(
52
+ env: Record<string, string | undefined>,
53
+ deviceSetting: string | undefined,
54
+ dtypeSetting: string | undefined,
55
+ ): Record<string, string> {
56
+ const overlay: Record<string, string> = {};
57
+ if (!env.PI_TINY_DEVICE) {
58
+ const device = tinyModelDeviceSettingToEnv(deviceSetting);
59
+ if (device) overlay.PI_TINY_DEVICE = device;
60
+ }
61
+ if (!env.PI_TINY_DTYPE) {
62
+ const dtype = tinyModelDtypeSettingToEnv(dtypeSetting);
63
+ if (dtype) overlay.PI_TINY_DTYPE = dtype;
64
+ }
65
+ return overlay;
66
+ }
67
+
68
+ /**
69
+ * Env handed to the tiny-model worker. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env
70
+ * vars win; otherwise the persisted `providers.tinyModelDevice` /
71
+ * `providers.tinyModelDtype` settings are mapped onto those vars so the worker's
72
+ * env-based resolution picks them up. Resolved once at spawn (pipelines are cached).
73
+ */
74
+ function tinyWorkerEnv(): Record<string, string> | undefined {
75
+ const overlay = tinyWorkerEnvOverlay(
76
+ $env,
77
+ readTinyModelSetting("providers.tinyModelDevice"),
78
+ readTinyModelSetting("providers.tinyModelDtype"),
79
+ );
80
+ if (Object.keys(overlay).length === 0) return undefined;
81
+ return { ...($env as Record<string, string>), ...overlay };
82
+ }
83
+
31
84
  export function createTinyTitleWorker(): Worker {
85
+ const env = tinyWorkerEnv();
86
+ const options: WorkerOptions = env ? { type: "module", env } : { type: "module" };
32
87
  return isCompiledBinary()
33
- ? new Worker("./packages/coding-agent/src/tiny/worker.ts", { type: "module" })
34
- : new Worker(new URL("./worker.ts", import.meta.url).href, { type: "module" });
88
+ ? new Worker("./packages/coding-agent/src/tiny/worker.ts", options)
89
+ : new Worker(new URL("./worker.ts", import.meta.url).href, options);
35
90
  }
36
91
 
37
92
  function wrapBunWorker(worker: Worker): WorkerHandle {
@@ -11,7 +11,14 @@ import type {
11
11
  import { getTinyModelsCacheDir, isCompiledBinary, prompt } from "@oh-my-pi/pi-utils";
12
12
  import packageJson from "../../package.json" with { type: "json" };
13
13
  import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
14
- import { getTinyLocalModelSpec, type TinyLocalModelKey, type TinyTitleLocalModelKey } from "./models";
14
+ import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
15
+ import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
16
+ import {
17
+ getTinyLocalModelSpec,
18
+ type TinyLocalModelKey,
19
+ type TinyTitleLocalModelKey,
20
+ type TinyTitleLocalModelSpec,
21
+ } from "./models";
15
22
  import { formatTitleUserMessage, normalizeGeneratedTitle } from "./text";
16
23
  import type {
17
24
  TinyTitleProgressEvent,
@@ -31,6 +38,9 @@ const sourceRequire = createRequire(import.meta.url);
31
38
  const INSTALL_LOCK_ATTEMPTS = 240;
32
39
  const INSTALL_LOCK_SLEEP_MS = 250;
33
40
 
41
+ const tinyModelDevicePreference = resolveTinyModelDevicePreference();
42
+ const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
43
+
34
44
  interface TransformersRuntime {
35
45
  env: {
36
46
  cacheDir?: string;
@@ -45,8 +55,8 @@ interface TransformersRuntime {
45
55
  task: "text-generation",
46
56
  model: string,
47
57
  options: {
48
- device: "cpu";
49
- dtype: "q4";
58
+ device: TinyModelDevice;
59
+ dtype: TinyModelDtype;
50
60
  progress_callback: (info: ProgressInfo) => void;
51
61
  },
52
62
  ) => Promise<TextGenerationPipeline>;
@@ -304,6 +314,63 @@ function sendProgress(
304
314
  transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
305
315
  }
306
316
 
317
+ function errorMessage(error: unknown): string {
318
+ return error instanceof Error ? error.message : String(error);
319
+ }
320
+
321
+ async function loadPipelineOnDevice(
322
+ transformers: TransformersRuntime,
323
+ spec: TinyTitleLocalModelSpec,
324
+ modelKey: TinyLocalModelKey,
325
+ transport: TinyTitleTransport,
326
+ requestId: string,
327
+ device: TinyModelDevice,
328
+ ): Promise<TextGenerationPipeline> {
329
+ return transformers.pipeline("text-generation", spec.repo, {
330
+ device,
331
+ dtype: tinyModelDtypeOverride ?? spec.dtype,
332
+ progress_callback: info => sendProgress(transport, requestId, modelKey, info),
333
+ });
334
+ }
335
+
336
+ async function loadPipelineWithDeviceFallback(
337
+ transformers: TransformersRuntime,
338
+ spec: TinyTitleLocalModelSpec,
339
+ modelKey: TinyLocalModelKey,
340
+ transport: TinyTitleTransport,
341
+ requestId: string,
342
+ ): Promise<{ generator: TextGenerationPipeline; device: TinyModelDevice }> {
343
+ const devices = tinyModelDeviceLoadOrder(tinyModelDevicePreference);
344
+ if (devices[0] !== tinyModelDevicePreference.device) {
345
+ sendLog(transport, "warn", "tiny-model: requested device is unsafe in the worker; using CPU", {
346
+ modelKey,
347
+ repo: spec.repo,
348
+ requestedDevice: tinyModelDevicePreference.device,
349
+ device: devices[0],
350
+ });
351
+ }
352
+ for (let i = 0; i < devices.length; i += 1) {
353
+ const device = devices[i]!;
354
+ try {
355
+ return {
356
+ generator: await loadPipelineOnDevice(transformers, spec, modelKey, transport, requestId, device),
357
+ device,
358
+ };
359
+ } catch (error) {
360
+ if (i === devices.length - 1) throw error;
361
+ const fallbackDevice = devices[i + 1]!;
362
+ sendLog(transport, "warn", "tiny-model: accelerated device failed; falling back", {
363
+ modelKey,
364
+ repo: spec.repo,
365
+ device,
366
+ fallbackDevice,
367
+ error: errorMessage(error),
368
+ });
369
+ }
370
+ }
371
+ throw new Error("No tiny model devices configured");
372
+ }
373
+
307
374
  async function loadPipeline(
308
375
  modelKey: TinyLocalModelKey,
309
376
  transport: TinyTitleTransport,
@@ -327,31 +394,28 @@ async function loadPipeline(
327
394
 
328
395
  const transformers = await loadTransformers(transport, requestId, modelKey);
329
396
  const startedAt = performance.now();
330
- const loaded = transformers
331
- .pipeline("text-generation", spec.repo, {
332
- device: "cpu",
333
- dtype: spec.dtype,
334
- progress_callback: info => sendProgress(transport, requestId, modelKey, info),
335
- })
336
- .then(
337
- generator => {
338
- sendLog(transport, "debug", "tiny-model: local model loaded", {
339
- modelKey,
340
- repo: spec.repo,
341
- elapsedMs: Math.round(performance.now() - startedAt),
342
- });
343
- transport.send({
344
- type: "progress",
345
- id: requestId,
346
- event: { modelKey, status: "ready", task: "text-generation", model: spec.repo },
347
- });
348
- return generator;
349
- },
350
- error => {
351
- pipelines.delete(modelKey);
352
- throw error;
353
- },
354
- );
397
+ const loaded = loadPipelineWithDeviceFallback(transformers, spec, modelKey, transport, requestId).then(
398
+ ({ generator, device }) => {
399
+ sendLog(transport, "debug", "tiny-model: local model loaded", {
400
+ modelKey,
401
+ repo: spec.repo,
402
+ device,
403
+ requestedDevice: tinyModelDevicePreference.device,
404
+ dtype: tinyModelDtypeOverride ?? spec.dtype,
405
+ elapsedMs: Math.round(performance.now() - startedAt),
406
+ });
407
+ transport.send({
408
+ type: "progress",
409
+ id: requestId,
410
+ event: { modelKey, status: "ready", task: "text-generation", model: spec.repo },
411
+ });
412
+ return generator;
413
+ },
414
+ error => {
415
+ pipelines.delete(modelKey);
416
+ throw error;
417
+ },
418
+ );
355
419
  pipelines.set(modelKey, loaded);
356
420
  return loaded;
357
421
  }
@@ -411,7 +475,7 @@ function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: st
411
475
  * Generic single-turn completion used by Mnemosyne memory tasks (fact extraction
412
476
  * and consolidation). The caller (Mnemosyne) supplies the full task prompt; we
413
477
  * wrap it as the user turn, decode greedily, and return the raw text for the
414
- * caller's own parser. Output is capped to keep CPU latency bounded.
478
+ * caller's own parser. Output is capped to keep local inference latency bounded.
415
479
  */
416
480
  async function generateCompletion(
417
481
  transport: TinyTitleTransport,
package/src/tools/bash.ts CHANGED
@@ -7,7 +7,7 @@ import type {
7
7
  ToolApprovalDecision,
8
8
  } from "@oh-my-pi/pi-agent-core";
9
9
  import type { Component } from "@oh-my-pi/pi-tui";
10
- import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
10
+ import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
11
11
  import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
12
12
  import * as z from "zod/v4";
13
13
  import { AsyncJobManager } from "../async";
@@ -15,6 +15,7 @@ import { type BashResult, executeBash } from "../exec/bash-executor";
15
15
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
16
16
  import { InternalUrlRouter } from "../internal-urls";
17
17
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
18
+ import { shimmerEnabled } from "../modes/theme/shimmer";
18
19
  import { highlightCode, type Theme } from "../modes/theme/theme";
19
20
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
20
21
  import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
@@ -1045,15 +1046,6 @@ export function getBashEnvForDisplay(args: BashRenderArgs): Record<string, strin
1045
1046
  return args.env ?? partialEnv;
1046
1047
  }
1047
1048
 
1048
- export function formatBashCommand(args: BashRenderArgs): string {
1049
- const command = replaceTabs(args.command || "…");
1050
- const prompt = "$";
1051
- const cwd = getProjectDir();
1052
- const displayWorkdir = formatToolWorkingDirectory(args.cwd, cwd);
1053
- const renderedCommand = [formatBashEnvAssignments(getBashEnvForDisplay(args)), command].filter(Boolean).join(" ");
1054
- return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
1055
- }
1056
-
1057
1049
  /**
1058
1050
  * Returns the bash command formatted for the result body: the dim `$ cd … &&`
1059
1051
  * prefix joined with syntax-highlighted command lines. The prefix is applied
@@ -1088,10 +1080,20 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1088
1080
  return {
1089
1081
  renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
1090
1082
  const renderArgs = toBashRenderArgs(args, config);
1091
- const cmdText = formatBashCommand(renderArgs);
1092
1083
  const title = config.resolveTitle(args, options);
1093
- const text = renderStatusLine({ icon: "pending", title, description: cmdText }, uiTheme);
1094
- return new Text(text, 0, 0);
1084
+ const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
1085
+ const header = renderStatusLine({ icon: "pending", title }, uiTheme);
1086
+ const outputBlock = new CachedOutputBlock();
1087
+ return {
1088
+ render: (width: number): string[] =>
1089
+ outputBlock.render(
1090
+ { header, state: "pending", sections: [{ lines: cmdLines }], width, animate: true },
1091
+ uiTheme,
1092
+ ),
1093
+ invalidate: () => {
1094
+ outputBlock.invalidate();
1095
+ },
1096
+ };
1095
1097
  },
1096
1098
 
1097
1099
  renderResult(
@@ -1204,6 +1206,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1204
1206
  { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
1205
1207
  ],
1206
1208
  width,
1209
+ animate: options.isPartial && shimmerEnabled(),
1207
1210
  },
1208
1211
  uiTheme,
1209
1212
  );
package/src/tools/eval.ts CHANGED
@@ -10,10 +10,11 @@ import { defaultEvalSessionId } from "../eval/session-id";
10
10
  import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
13
+ import { shimmerEnabled } from "../modes/theme/shimmer";
13
14
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
14
15
  import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
15
16
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
16
- import { renderCodeCell } from "../tui";
17
+ import { borderShimmerTick, renderCodeCell } from "../tui";
17
18
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
18
19
  import { resolveEvalBackends, type ToolSession } from ".";
19
20
  import { truncateForPrompt } from "./approval";
@@ -857,7 +858,7 @@ function formatCellOutputLines(
857
858
  }
858
859
 
859
860
  export const evalToolRenderer = {
860
- renderCall(args: EvalRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
861
+ renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
861
862
  const cells = getRenderCells(args);
862
863
 
863
864
  if (cells.length === 0) {
@@ -870,7 +871,8 @@ export const evalToolRenderer = {
870
871
 
871
872
  return {
872
873
  render: (width: number): string[] => {
873
- const key = cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|");
874
+ const animate = options.isPartial && shimmerEnabled();
875
+ const key = `${animate ? borderShimmerTick() : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
874
876
  if (cached && cached.key === key && cached.width === width) {
875
877
  return cached.result;
876
878
  }
@@ -889,6 +891,7 @@ export const evalToolRenderer = {
889
891
  width,
890
892
  codeMaxLines: EVAL_DEFAULT_PREVIEW_LINES,
891
893
  expanded: true,
894
+ animate,
892
895
  },
893
896
  uiTheme,
894
897
  );
@@ -951,7 +954,8 @@ export const evalToolRenderer = {
951
954
  render: (width: number): string[] => {
952
955
  const expanded = options.renderContext?.expanded ?? options.expanded;
953
956
  const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
954
- const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
957
+ const animate = options.isPartial && shimmerEnabled();
958
+ const key = `${expanded}|${previewLines}|${options.spinnerFrame}|${animate ? borderShimmerTick() : 0}`;
955
959
  if (cached && cached.key === key && cached.width === width) {
956
960
  return cached.result;
957
961
  }
@@ -988,6 +992,7 @@ export const evalToolRenderer = {
988
992
  codeMaxLines: expanded ? Number.POSITIVE_INFINITY : EVAL_DEFAULT_PREVIEW_LINES,
989
993
  expanded,
990
994
  width,
995
+ animate,
991
996
  },
992
997
  uiTheme,
993
998
  );
@@ -43,7 +43,6 @@ import { MemoryReflectTool } from "./memory-reflect";
43
43
  import { MemoryRetainTool } from "./memory-retain";
44
44
  import { wrapToolWithMetaNotice } from "./output-meta";
45
45
  import { ReadTool } from "./read";
46
- import { RecipeTool } from "./recipe";
47
46
  import { RenderMermaidTool } from "./render-mermaid";
48
47
  import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
49
48
  import { ResolveTool } from "./resolve";
@@ -84,7 +83,6 @@ export * from "./memory-recall";
84
83
  export * from "./memory-reflect";
85
84
  export * from "./memory-retain";
86
85
  export * from "./read";
87
- export * from "./recipe";
88
86
  export * from "./render-mermaid";
89
87
  export * from "./report-tool-issue";
90
88
  export * from "./resolve";
@@ -300,7 +298,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
300
298
  rewind: RewindTool.createIf,
301
299
  task: s => TaskTool.create(s),
302
300
  job: JobTool.createIf,
303
- recipe: RecipeTool.createIf,
304
301
  irc: IrcTool.createIf,
305
302
  todo_write: s => new TodoWriteTool(s),
306
303
  web_search: s => new WebSearchTool(s),
@@ -416,13 +413,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
416
413
  ) {
417
414
  requestedTools.push("ast_edit");
418
415
  }
419
- if (
420
- requestedTools.includes("bash") &&
421
- !requestedTools.includes("recipe") &&
422
- session.settings.get("recipe.enabled")
423
- ) {
424
- requestedTools.push("recipe");
425
- }
426
416
  if (["hindsight", "mnemosyne"].includes(session.settings.get("memory.backend") ?? "")) {
427
417
  for (const name of ["recall", "retain", "reflect"]) {
428
418
  if (!requestedTools.includes(name)) requestedTools.push(name);
@@ -467,7 +457,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
467
457
  if (!session.settings.get("async.enabled") && session.getAgentId?.() === MAIN_AGENT_ID) return false;
468
458
  return true;
469
459
  }
470
- if (name === "recipe") return session.settings.get("recipe.enabled");
471
460
  if (name === "retain" || name === "recall" || name === "reflect") {
472
461
  return ["hindsight", "mnemosyne"].includes(session.settings.get("memory.backend") ?? "");
473
462
  }
package/src/tools/read.ts CHANGED
@@ -689,6 +689,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
689
689
  DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
690
690
  IS_HL_MODE: displayMode.hashLines,
691
691
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
692
+ INSPECT_IMAGE_ENABLED: this.#inspectImageEnabled,
692
693
  });
693
694
  }
694
695
 
@@ -24,7 +24,6 @@ import { inspectImageToolRenderer } from "./inspect-image-renderer";
24
24
  import { jobToolRenderer } from "./job";
25
25
  import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
26
26
  import { readToolRenderer } from "./read";
27
- import { recipeToolRenderer } from "./recipe/render";
28
27
  import { resolveToolRenderer } from "./resolve";
29
28
  import { searchToolRenderer } from "./search";
30
29
  import { searchToolBm25Renderer } from "./search-tool-bm25";
@@ -51,7 +50,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
51
50
  ast_edit: astEditToolRenderer as ToolRenderer,
52
51
  bash: bashToolRenderer as ToolRenderer,
53
52
  browser: browserToolRenderer as ToolRenderer,
54
- recipe: recipeToolRenderer as ToolRenderer,
55
53
  debug: debugToolRenderer as ToolRenderer,
56
54
  eval: evalToolRenderer as ToolRenderer,
57
55
  edit: editToolRenderer as ToolRenderer,
@@ -26,6 +26,8 @@ export interface CodeCellOptions {
26
26
  outputMaxLines?: number;
27
27
  codeMaxLines?: number;
28
28
  expanded?: boolean;
29
+ /** Animate the cell border with a sweeping segment while pending/running. */
30
+ animate?: boolean;
29
31
  width: number;
30
32
  }
31
33
 
@@ -130,7 +132,10 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
130
132
  sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
131
133
  }
132
134
 
133
- return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
135
+ return renderOutputBlock(
136
+ { header: title, headerMeta: meta, state, sections, width, animate: options.animate },
137
+ theme,
138
+ );
134
139
  }
135
140
 
136
141
  export interface MarkdownCellOptions {