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

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 (36) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/config/settings-schema.d.ts +24 -4
  4. package/dist/types/edit/hashline/diff.d.ts +6 -0
  5. package/dist/types/modes/components/model-selector.d.ts +3 -2
  6. package/dist/types/modes/theme/theme.d.ts +2 -1
  7. package/dist/types/sdk.d.ts +2 -1
  8. package/dist/types/session/agent-session.d.ts +22 -9
  9. package/dist/types/thinking.d.ts +39 -1
  10. package/dist/types/tiny/device.d.ts +3 -3
  11. package/dist/types/tiny/models.d.ts +19 -0
  12. package/package.json +9 -9
  13. package/src/auto-thinking/classifier.ts +180 -0
  14. package/src/config/settings-schema.ts +24 -4
  15. package/src/edit/hashline/diff.ts +10 -2
  16. package/src/edit/streaming.ts +17 -6
  17. package/src/eval/__tests__/shared-executors.test.ts +32 -0
  18. package/src/eval/js/shared/local-module-loader.ts +75 -10
  19. package/src/internal-urls/docs-index.generated.ts +2 -2
  20. package/src/main.ts +6 -1
  21. package/src/modes/acp/acp-agent.ts +13 -3
  22. package/src/modes/components/footer.ts +10 -3
  23. package/src/modes/components/model-selector.ts +20 -11
  24. package/src/modes/components/settings-defs.ts +7 -0
  25. package/src/modes/components/settings-selector.ts +4 -1
  26. package/src/modes/components/status-line/segments.ts +13 -5
  27. package/src/modes/controllers/event-controller.ts +5 -1
  28. package/src/modes/controllers/selector-controller.ts +20 -6
  29. package/src/modes/theme/theme.ts +6 -0
  30. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  31. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  32. package/src/sdk.ts +25 -7
  33. package/src/session/agent-session.ts +193 -32
  34. package/src/thinking.ts +73 -1
  35. package/src/tiny/device.ts +4 -10
  36. package/src/tiny/models.ts +24 -0
@@ -1,6 +1,6 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
- import { getThinkingLevelMetadata } from "../thinking";
3
+ import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
4
4
  import {
5
5
  TINY_MODEL_DEVICE_DEFAULT,
6
6
  TINY_MODEL_DEVICE_SETTING_OPTIONS,
@@ -12,6 +12,9 @@ import {
12
12
  TINY_MODEL_DTYPE_SETTING_VALUES,
13
13
  } from "../tiny/dtype";
14
14
  import {
15
+ AUTO_THINKING_MODEL_OPTIONS,
16
+ AUTO_THINKING_MODEL_VALUES,
17
+ ONLINE_AUTO_THINKING_MODEL_KEY,
15
18
  ONLINE_MEMORY_MODEL_KEY,
16
19
  ONLINE_TINY_TITLE_MODEL_KEY,
17
20
  TINY_MEMORY_MODEL_OPTIONS,
@@ -671,13 +674,16 @@ export const SETTINGS_SCHEMA = {
671
674
  // Reasoning and prompts
672
675
  defaultThinkingLevel: {
673
676
  type: "enum",
674
- values: THINKING_EFFORTS,
677
+ values: [...THINKING_EFFORTS, AUTO_THINKING],
675
678
  default: "high",
676
679
  ui: {
677
680
  tab: "model",
678
681
  label: "Thinking Level",
679
682
  description: "Reasoning depth for thinking-capable models",
680
- options: [...THINKING_EFFORTS.map(getThinkingLevelMetadata)],
683
+ options: [
684
+ getConfiguredThinkingLevelMetadata(AUTO_THINKING),
685
+ ...THINKING_EFFORTS.map(getThinkingLevelMetadata),
686
+ ],
681
687
  },
682
688
  },
683
689
 
@@ -2954,7 +2960,7 @@ export const SETTINGS_SCHEMA = {
2954
2960
  tab: "providers",
2955
2961
  label: "Tiny Model Device",
2956
2962
  description:
2957
- "ONNX execution provider for local tiny models (titles + memory). Default picks DirectML on Windows, CUDA on Linux x64, CPU elsewhere. The PI_TINY_DEVICE env var overrides this.",
2963
+ "ONNX execution provider for local tiny models (titles + memory). Default uses CPU-only inference. The PI_TINY_DEVICE env var overrides this.",
2958
2964
  options: TINY_MODEL_DEVICE_SETTING_OPTIONS,
2959
2965
  },
2960
2966
  },
@@ -2984,6 +2990,20 @@ export const SETTINGS_SCHEMA = {
2984
2990
  },
2985
2991
  },
2986
2992
 
2993
+ "providers.autoThinkingModel": {
2994
+ type: "enum",
2995
+ values: AUTO_THINKING_MODEL_VALUES,
2996
+ default: ONLINE_AUTO_THINKING_MODEL_KEY,
2997
+ ui: {
2998
+ tab: "model",
2999
+ label: "Auto Thinking Model",
3000
+ description:
3001
+ "Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model",
3002
+ condition: "autoThinkingActive",
3003
+ options: AUTO_THINKING_MODEL_OPTIONS,
3004
+ },
3005
+ },
3006
+
2987
3007
  "providers.kimiApiFormat": {
2988
3008
  type: "enum",
2989
3009
  values: ["openai", "anthropic"] as const,
@@ -31,6 +31,12 @@ export interface HashlineDiffOptions {
31
31
  * preview path only.
32
32
  */
33
33
  streaming?: boolean;
34
+ /**
35
+ * Skip snapshot-tag validation. Streaming previews use this so transient
36
+ * stale/missing tags do not flash re-read errors while the model is still
37
+ * authoring input; the final apply path still validates through Patcher.
38
+ */
39
+ skipHashValidation?: boolean;
34
40
  }
35
41
 
36
42
  async function readSectionText(absolutePath: string, sectionPath: string): Promise<string> {
@@ -73,8 +79,10 @@ export async function computeHashlineSectionDiff(
73
79
  const rawContent = await readSectionText(absolutePath, section.path);
74
80
  const { text: content } = stripBom(rawContent);
75
81
  const normalized = normalizeToLF(content);
76
- const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
77
- if (hashError) return { error: hashError };
82
+ if (!options.skipHashValidation) {
83
+ const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
84
+ if (hashError) return { error: hashError };
85
+ }
78
86
  const result = options.streaming
79
87
  ? section.applyPartialTo(normalized, nativeBlockResolver)
80
88
  : section.applyTo(normalized, nativeBlockResolver);
@@ -314,8 +314,14 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
314
314
  },
315
315
  async computeDiffPreview(args, ctx) {
316
316
  if (typeof args.input !== "string" || args.input.length === 0) return null;
317
- const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
318
- if (input.length === 0) return null;
317
+ // Unlike apply_patch, hashline previews flow through `applyPartialTo`,
318
+ // whose streaming-tolerant parser (`parsePatchStreaming` `endStreaming`)
319
+ // drops a payload-less trailing op and projects a partially-typed payload
320
+ // line onto the file as it grows. Trimming the trailing partial line here
321
+ // would instead strip the sole payload of a single-op `replace`/`insert`
322
+ // for almost the entire stream, collapsing the preview to "No changes" and
323
+ // rendering a blank box. Feed the raw in-flight text straight through.
324
+ const input = args.input;
319
325
  ctx.signal.throwIfAborted();
320
326
 
321
327
  let sections: readonly HashlineInputSection[];
@@ -347,12 +353,17 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
347
353
  const section = sectionsToProcess[i];
348
354
  const result = await computeHashlineSectionDiff(section, ctx.cwd, ctx.snapshots, {
349
355
  streaming: ctx.isStreaming,
356
+ skipHashValidation: ctx.isStreaming === true,
350
357
  });
351
358
  ctx.signal.throwIfAborted();
352
- // In a multi-section preview, ignore parse/apply errors from the
353
- // last section: it's still streaming and the partial op may not
354
- // parse yet. Earlier sections are stable and stay rendered.
355
- if (sectionsToProcess.length > 1 && i === trailingProcessedIndex && "error" in result) {
359
+ // Ignore parse/apply errors from the trailing (actively-typed)
360
+ // section while streaming: a mid-typed op may transiently resolve to
361
+ // "No changes" or an out-of-bounds anchor, and surfacing that would
362
+ // wipe the already-stable previews (or, for a lone section, the prior
363
+ // good frame). Returning no entry preserves the last preview. Earlier
364
+ // sections, and every section once args are complete, stay rendered so
365
+ // real errors still reach the model.
366
+ if ((ctx.isStreaming || sectionsToProcess.length > 1) && i === trailingProcessedIndex && "error" in result) {
356
367
  continue;
357
368
  }
358
369
  previews.push(toPerFilePreview(section.path, result));
@@ -492,6 +492,38 @@ display({"label": "A"})`,
492
492
  expect(reloaded.output.trim()).toBe("2");
493
493
  });
494
494
 
495
+ it("links a cyclic local module graph without crashing", async () => {
496
+ // Regression: the loader used to link()+evaluate() each local module individually
497
+ // inside the recursive linker callback. On any import cycle that re-entered Bun's
498
+ // node:vm linker mid-instantiation and segfaulted the process (SIGTRAP,
499
+ // getImportedModule on a null record) — e.g. `await import("…/edit/streaming.ts")`,
500
+ // whose relative-import subtree is cyclic. The graph must now link in a single pass.
501
+ using tempDir = TempDir.createSync("@omp-eval-js-cycle-");
502
+ const sessionFile = path.join(tempDir.path(), "session.jsonl");
503
+ const sessionId = `js-cycle:${crypto.randomUUID()}`;
504
+ const session = createToolSession(tempDir.path(), sessionFile);
505
+ const alphaPath = path.join(tempDir.path(), "alpha.ts");
506
+ const betaPath = path.join(tempDir.path(), "beta.ts");
507
+ const alphaSpec = JSON.stringify(alphaPath);
508
+ const betaSpec = JSON.stringify(betaPath);
509
+ await Bun.write(
510
+ alphaPath,
511
+ 'import { betaName } from "./beta.ts";\nexport const alphaName = "alpha";\nexport function combined() { return alphaName + ":" + betaName; }\n',
512
+ );
513
+ await Bun.write(
514
+ betaPath,
515
+ 'import { alphaName } from "./alpha.ts";\nexport const betaName = "beta";\nexport function viaAlpha() { return alphaName; }\n',
516
+ );
517
+
518
+ const result = await executeJs(
519
+ `const a = await import(${alphaSpec});\nconst b = await import(${betaSpec});\nreturn [a.combined(), b.viaAlpha()].join("|");`,
520
+ { sessionId, session, sessionFile },
521
+ );
522
+
523
+ expect(result.exitCode).toBe(0);
524
+ expect(result.output.trim()).toBe("alpha:beta|alpha");
525
+ });
526
+
495
527
  it("loads TypeScript type-only imports in cells and local modules", async () => {
496
528
  using tempDir = TempDir.createSync("@omp-eval-js-type-imports-");
497
529
  const sessionFile = path.join(tempDir.path(), "session.jsonl");
@@ -9,6 +9,8 @@ interface LocalModuleEntry {
9
9
  version: number;
10
10
  identifier: string;
11
11
  module: vm.SourceTextModule;
12
+ /** Memoized link+evaluate of this module as a graph root; set lazily by `#loadLocalModule`. */
13
+ loaded?: Promise<void>;
12
14
  }
13
15
 
14
16
  export type LocalImportResolution = { mode: "local"; value: unknown } | { mode: "external"; target: string };
@@ -26,6 +28,7 @@ export class LocalModuleLoader {
26
28
  #moduleBuilds = new Map<string, Promise<LocalModuleEntry>>();
27
29
  #externalModules = new Map<string, Promise<vm.Module>>();
28
30
  #requireCache = new Map<string, NodeJS.Require>();
31
+ #modulePaths = new WeakMap<vm.Module, string>();
29
32
 
30
33
  constructor(sessionId: string) {
31
34
  this.#context = vm.createContext(globalThis);
@@ -68,8 +71,8 @@ export class LocalModuleLoader {
68
71
  async #resolveFromBase(baseDir: string, source: string): Promise<LocalImportResolution> {
69
72
  const resolved = resolveImportSpecifier(baseDir, source);
70
73
  if (isLocalPathSpecifier(source) && isManagedLocalModulePath(resolved)) {
71
- const entry = await this.#ensureLocalModule(resolved);
72
- return { mode: "local", value: entry.module.namespace };
74
+ const module = await this.#loadLocalModule(resolved);
75
+ return { mode: "local", value: module.namespace };
73
76
  }
74
77
  return { mode: "external", target: normalizeImportTarget(resolved) };
75
78
  }
@@ -86,6 +89,11 @@ export class LocalModuleLoader {
86
89
  return await buildPromise;
87
90
  }
88
91
 
92
+ // Construct (parse + register) a local module WITHOUT linking or evaluating it.
93
+ // Linking and evaluation are driven once from the graph root in `#linkAndEvaluate`;
94
+ // doing them per-module inside the recursive linker re-enters Bun's node:vm linker
95
+ // mid-instantiation, which segfaults JSC (getImportedModule on a null record) whenever
96
+ // the local graph contains an import cycle.
89
97
  async #buildLocalModule(modulePath: string): Promise<LocalModuleEntry> {
90
98
  const rawSource = fs.readFileSync(modulePath, "utf8");
91
99
  const stripped = stripTypeScriptSyntax(rawSource, {
@@ -116,28 +124,85 @@ export class LocalModuleLoader {
116
124
  (meta as { url?: string; path?: string; dir?: string }).dir = moduleDir;
117
125
  },
118
126
  importModuleDynamically: async specifier => {
119
- return await this.#resolveLinkedModule(modulePath, String(specifier));
127
+ return await this.#resolveDynamicImport(modulePath, String(specifier));
120
128
  },
121
129
  });
130
+ this.#modulePaths.set(module, modulePath);
122
131
  const entry: LocalModuleEntry = { version, identifier, module };
123
132
  this.#moduleEntries.set(modulePath, entry);
133
+ return entry;
134
+ }
135
+
136
+ // Construct (if needed) then link+evaluate a local module as a graph root, returning
137
+ // the evaluated module. Link and evaluate run exactly once over the whole reachable
138
+ // graph; the static linker only constructs dependencies, letting node:vm instantiate
139
+ // cyclic graphs in a single pass.
140
+ async #loadLocalModule(modulePath: string): Promise<vm.SourceTextModule> {
141
+ const entry = await this.#ensureLocalModule(modulePath);
142
+ entry.loaded ??= this.#linkAndEvaluate(entry, modulePath);
143
+ await entry.loaded;
144
+ return entry.module;
145
+ }
146
+
147
+ async #linkAndEvaluate(entry: LocalModuleEntry, modulePath: string): Promise<void> {
148
+ const { module } = entry;
124
149
  try {
125
- await module.link(async specifier => await this.#resolveLinkedModule(modulePath, specifier));
126
- await module.evaluate();
127
- return entry;
150
+ if (module.status === "unlinked") await module.link(this.#linkResolve);
151
+ if (module.status === "linked") await module.evaluate();
128
152
  } catch (error) {
129
- this.#moduleEntries.delete(modulePath);
153
+ this.#invalidateFailedLoad(modulePath);
130
154
  throw error;
131
155
  }
156
+ if (module.status === "errored") {
157
+ this.#invalidateFailedLoad(modulePath);
158
+ throw module.error;
159
+ }
132
160
  }
133
161
 
134
- async #resolveLinkedModule(referrerPath: string, specifier: string): Promise<vm.Module> {
135
- const baseDir = path.dirname(referrerPath);
136
- const resolved = resolveImportSpecifier(baseDir, specifier);
162
+ // Shared static-link resolver for `module.link()`. node:vm passes the referencing
163
+ // module and reuses this one resolver for the entire graph, so the referrer path is
164
+ // recovered from `#modulePaths`. Local dependencies are constructed but NOT linked or
165
+ // evaluated here (the root drives that); externals are loaded eagerly — they carry no
166
+ // imports and cannot participate in a cycle.
167
+ #linkResolve = async (specifier: string, referencingModule: vm.Module): Promise<vm.Module> => {
168
+ const referrerPath = this.#modulePaths.get(referencingModule);
169
+ if (referrerPath === undefined) {
170
+ throw new Error(`local module loader: unknown referrer while linking "${specifier}"`);
171
+ }
172
+ const resolved = resolveImportSpecifier(path.dirname(referrerPath), specifier);
137
173
  if (isLocalPathSpecifier(specifier) && isManagedLocalModulePath(resolved)) {
138
174
  return (await this.#ensureLocalModule(resolved)).module;
139
175
  }
140
176
  return await this.#ensureExternalModule(normalizeImportTarget(resolved));
177
+ };
178
+
179
+ // Resolver for runtime `import()` inside evaluated module code: the result must be a
180
+ // fully linked+evaluated module, so local targets are loaded as graph roots.
181
+ async #resolveDynamicImport(referrerPath: string, specifier: string): Promise<vm.Module> {
182
+ const resolved = resolveImportSpecifier(path.dirname(referrerPath), specifier);
183
+ if (isLocalPathSpecifier(specifier) && isManagedLocalModulePath(resolved)) {
184
+ return await this.#loadLocalModule(resolved);
185
+ }
186
+ return await this.#ensureExternalModule(normalizeImportTarget(resolved));
187
+ }
188
+
189
+ // A failed link/evaluate can leave a partial graph cached. Drop every reachable module
190
+ // that is not fully evaluated so the next attempt reconstructs it; fully evaluated
191
+ // modules keep valid namespaces and stay cached.
192
+ #invalidateFailedLoad(rootPath: string): void {
193
+ const stack = [rootPath];
194
+ const seen = new Set<string>();
195
+ while (stack.length > 0) {
196
+ const current = stack.pop();
197
+ if (current === undefined || seen.has(current)) continue;
198
+ seen.add(current);
199
+ const entry = this.#moduleEntries.get(current);
200
+ if (entry && entry.module.status === "evaluated") continue;
201
+ this.#moduleEntries.delete(current);
202
+ this.#moduleBuilds.delete(current);
203
+ const deps = this.#moduleDeps.get(current);
204
+ if (deps) for (const dep of deps) stack.push(dep);
205
+ }
141
206
  }
142
207
 
143
208
  async #ensureExternalModule(target: string): Promise<vm.Module> {