diff --git a/docs/tools/eval.md b/docs/tools/eval.md index 399d4fbb7..0fccef25b 100644 --- a/docs/tools/eval.md +++ b/docs/tools/eval.md @@ -125,7 +125,7 @@ Implemented in `packages/coding-agent/src/eval/js/worker-core.ts`, `packages/cod - Persistent worker-backed VM sessions keyed by `js:${sessionId}` - `reset: true` calls `resetVmContext(sessionKey)` before the cell executes; reset is destructive for all live runs on that JS session - Top-level `await` and bare `return` are supported by wrapping code in an async IIFE when `wrapCode()` sees `await` or `return` -- Top-level static `import ... from ...` and dynamic `import(...)` calls are routed through `rewriteImports()`, which sends them via `__omp_import__` so the specifier resolves against the session cwd +- Top-level static `import ... from ...` and dynamic `import(...)` calls are routed through `rewriteImports()`, which sends them via `__omp_import__` so the specifier resolves against the session cwd. Dynamic-import call sites are swapped for a guarded shim (`typeof __omp_import__ === "function" ? __omp_import__ : (s, o) => import(s, o)`) rather than the bare helper identifier: functions handed to puppeteer (`tab.evaluate`, `page.evaluate`, ...) are serialized with `Function.prototype.toString()` and re-evaluated inside the browser page, where the worker-injected helper does not exist, so the shim falls back to native dynamic import there - Module cache is busted for **local** imports between cells so edits to source files are picked up without restarting the runtime. `__omp_import__` deletes `require.cache[absPath]` before re-importing whenever the original specifier is a filesystem path: relative (`./x`, `../x`, `.`, `..`), POSIX-absolute (`/...`), home-prefixed (`~/...`), or Windows drive-letter (`C:\...` / `C:/...`). Bare specifiers (`react`, `lodash/x`) and URL/scheme specifiers (`node:fs`, `file://...`, `https://...`) are left in cache so package identity stays stable across cells. The cache-bust only fires when the resolved target is an absolute path — unresolved bare-package fallbacks (`resolveImportSpecifier()` returning the original specifier) skip it. - The prelude installs globals: - `display`, `print` diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 901f8a178..4e73ef357 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixed - Fixed the model selector dropping an immediate Enter when cached models were available but the selector's offline refresh was still pending. +- Fixed dynamic `import(...)` inside functions passed to the browser tool's `tab.evaluate`/`page.evaluate` failing with `__omp_import__ is not defined`. The eval/browser JS runtime rewrites dynamic-import callees to the worker-injected `__omp_import__` helper, but puppeteer serializes evaluate callbacks with `Function.prototype.toString()` and re-runs them inside the page, where the helper does not exist. The rewriter now substitutes a guarded shim that falls back to native dynamic import when the helper is absent, so serialized code works in the page realm while in-worker imports keep resolving against the session cwd. - Transcript block freezing is now unconditional instead of gated on ED3-risk terminal detection: every finalized block replays its frozen snapshot once it crosses out of the live region, on all terminals including Windows, because the rewritten renderer's committed scrollback is immutable everywhere. Still-mutating blocks (pending tools, streaming messages, async thinking renderers) anchor the live region and keep repainting until they finalize, which structurally fixes stale/duplicated output from late async expansions ([#1823](https://github.com/can1357/oh-my-pi/issues/1823)). - Fixed the edit tool's post-edit diff preview occasionally echoing a context line twice with out-of-order numbering. Block-boundary context injection classified space-prefixed diff rows as old-file-only, so an unchanged line sitting in a net-offset region (old N / new N+k) was missing from the new file's visibility window; `findBlockContextLines` then re-surfaced it under its post-edit number and the row was spliced in after the adjacent change run. New-file boundary lines are now translated back to pre-edit numbers (the compact-preview renumbering contract) and merged into a single old-numbered insertion pass — also fixing closers below a net-offset edit being dropped or renumbered incorrectly. - Fixed the Anthropic web-search provider claiming the Claude Code identity on API-key requests: the CC billing header + system instruction were injected whenever the model wasn't Haiku 3.5, regardless of auth mode. Injection is now OAuth-gated like the streaming path, and OAuth search requests patch the billing header's `cch` attestation (via `wrapFetchForCch`) instead of shipping the `cch=00000` placeholder. diff --git a/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts b/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts index a5c1673b6..997d7b764 100644 --- a/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts +++ b/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts @@ -82,6 +82,14 @@ function parseProgram(code: string): { program: { body: ReadonlyArray { expect(out).toContain("const data ="); }); + // Dynamic `import(...)` callees are swapped for a shim that prefers the worker-injected + // `__omp_import__` helper but falls back to native dynamic import. The fallback matters: + // puppeteer serializes functions with `Function.prototype.toString()` and re-evaluates + // them inside the browser page, where the helper global does not exist. + const SHIM = '(typeof __omp_import__ === "function" ? __omp_import__ : (s, o) => import(s, o))'; + it("rewrites bare dynamic import() so its specifier resolves against the session cwd", () => { const out = rewriteImports(`const m = await ${dyn('("./foo.ts")')};`); - expect(out).toContain('await __omp_import__("./foo.ts")'); + expect(out).toContain(`await ${SHIM}("./foo.ts")`); expect(out).not.toContain(dyn('("./foo.ts")')); }); it("rewrites dynamic import() with an options bag (passes options through unchanged)", () => { const out = rewriteImports(`const m = await ${dyn('("./d.json", { with: { type: "json" } })')};`); - expect(out).toContain('__omp_import__("./d.json", { with: { type: "json" } })'); + expect(out).toContain(`${SHIM}("./d.json", { with: { type: "json" } })`); }); it("rewrites nested and chained dynamic import() calls", () => { const out = rewriteImports( `Promise.all([${dyn('("./a.ts")')}, ${dyn('("./b.ts")')}]).then(([a, b]) => a.run(b));`, ); - expect(out).toContain('__omp_import__("./a.ts")'); - expect(out).toContain('__omp_import__("./b.ts")'); + expect(out).toContain(`${SHIM}("./a.ts")`); + expect(out).toContain(`${SHIM}("./b.ts")`); expect(out).not.toContain(dyn('("./a.ts")')); }); it("rewrites dynamic import() with a non-literal specifier", () => { const out = rewriteImports(`const m = await ${dyn("(spec)")};`); - expect(out).toContain("__omp_import__(spec)"); + expect(out).toContain(`${SHIM}(spec)`); + }); + + it("routes dynamic import through the helper when present and native import when serialized into a foreign realm", async () => { + const out = rewriteImports(`const load = async () => await ${dyn('("node:path")')}; load;`); + const globals = globalThis as Record; + expect("__omp_import__" in globals).toBe(false); + + // Worker realm: helper global exists, call must route through it. + const seen: string[] = []; + globals.__omp_import__ = async (source: string) => { + seen.push(source); + return { stubbed: true }; + }; + try { + const load = indirectEval(out) as () => Promise<{ stubbed?: boolean }>; + expect((await load()).stubbed).toBe(true); + expect(seen).toEqual(["node:path"]); + + // Page realm: puppeteer ships `load.toString()` to a realm without the helper — + // the shim must fall back to native dynamic import instead of throwing. + const serialized = indirectEval(`(${load.toString()})`) as () => Promise; + delete globals.__omp_import__; + const mod = await serialized(); + expect(typeof mod.join).toBe("function"); + } finally { + delete globals.__omp_import__; + } }); it("does not rewrite import statements embedded in template literals (the bug)", () => {