fix(coding-agent/browser): bundle tab-worker-entry for prebuilt binaries (#1011)

The Worker spawn in spawnTabWorker used `new Worker(new URL(...).href, ...)`
through a local variable, which Bun's --compile static analysis does not
recognize. As a result, tab-worker-entry.ts was never embedded in single-file
binaries (v14.5.13+), and the worker thread failed to load its entry at
/$bunfs/root/tab-worker-entry.ts, surfacing as the user-visible
"Timed out initializing browser tab worker".

Switch to a `with { type: "file" }` import of the entry module so the
bundler discovers and embeds it in both dev and compiled targets.

Regression test asserts `Bun.build` emits tab-worker-entry as an asset
alongside the tab-supervisor entry point — the same static-analysis pass
that powers `bun build --compile`.
This commit is contained in:
can1357
2026-05-12 06:15:14 +02:00
parent 9b687a6dc7
commit ebbe66a3d6
3 changed files with 75 additions and 2 deletions
+1
View File
@@ -33,6 +33,7 @@
- Fixed multi-entry edits silently rendering a fake success when every entry failed (e.g. all hit the auto-generated guard), by surfacing `isError: true` from the single-path edit orchestrator so the renderer takes the error branch instead of falling through to the streaming-preview fallback that displays the *proposed* diff
- Fixed the auto-generated streaming guard being gated behind `edit.streamingAbort` (default false), so it now pre-empts streaming edit tool calls targeting auto-generated files regardless of that setting
- Fixed subagents launched in the same parallel batch not seeing each other in their initial `# IRC Peers` system-prompt block by pre-registering the agent in the global `AgentRegistry` before `rebuildSystemPrompt` runs and attaching the live session afterwards
- Fixed `Timed out initializing browser tab worker` on prebuilt binaries by rewriting `spawnTabWorker` to import the worker entry with `with { type: "file" }` so Bun's `--compile` bundler statically discovers and embeds `tab-worker-entry.ts` in the single-file binary ([#1011](https://github.com/can1357/oh-my-pi/issues/1011))
## [14.9.3] - 2026-05-10
### Breaking Changes
@@ -16,6 +16,13 @@ import type {
WorkerInitPayload,
WorkerOutbound,
} from "./tab-protocol";
// Imported with `type: "file"` so Bun's bundler statically discovers the
// worker entry and embeds it inside `bun build --compile` single-file
// binaries. Without this attribute the bundler cannot reach the entry through
// a `new URL(..., import.meta.url)` literal stored in a local variable, and
// the prebuilt binary surfaces `Timed out initializing browser tab worker`
// (issue #1011) because `/$bunfs/root/tab-worker-entry.ts` is missing.
import tabWorkerEntryUrl from "./tab-worker-entry.ts" with { type: "file" };
interface WorkerHandle {
send(msg: WorkerInbound, transferList?: Transferable[]): void;
@@ -364,8 +371,7 @@ async function raceWithTimeout<T>(
async function spawnTabWorker(): Promise<WorkerHandle> {
try {
const url = new URL("./tab-worker-entry.ts", import.meta.url);
const worker = new Worker(url.href, { type: "module" });
const worker = new Worker(tabWorkerEntryUrl, { type: "module" });
return wrapBunWorker(worker);
} catch (err) {
logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
@@ -0,0 +1,66 @@
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
/**
* Regression for https://github.com/can1357/oh-my-pi/issues/1011
*
* In v14.5.13 `spawnTabWorker` (in `src/tools/browser/tab-supervisor.ts`) was
* introduced to host browser tabs in a `Worker`. The worker URL was assembled
* as:
*
* ```ts
* const url = new URL("./tab-worker-entry.ts", import.meta.url);
* const worker = new Worker(url.href, { type: "module" });
* ```
*
* Bun's `--compile` bundler does NOT statically discover that pattern (the
* worker entry is hidden behind a local variable and `.href`), so the entry
* file is never embedded in the single-file binary. At runtime the worker
* thread tries to load `/$bunfs/root/tab-worker-entry.ts`, the module is
* missing, and the supervisor surfaces the symptom from the issue:
* `Timed out initializing browser tab worker`.
*
* `Bun.build` exposes the same static-analysis pass that drives `--compile`.
* If `tab-worker-entry.ts` is reachable to the bundler, it appears in the
* outputs as a separate `asset` chunk. If the spawn pattern hides it from
* the bundler, only the entry point is emitted.
*
* The bundler is driven through a `bun -e` subprocess that writes its report
* to a tmp file: invoking `Bun.build` directly from inside `bun test` does
* not auto-resolve TypeScript imports the way the real build pipeline does.
*/
describe("issue #1011 — tab worker entry must survive `bun build --compile`", () => {
it("bundles tab-worker-entry.ts as a discoverable asset of tab-supervisor.ts", async () => {
const supervisor = path.resolve(import.meta.dir, "../src/tools/browser/tab-supervisor.ts");
const packageDir = path.resolve(import.meta.dir, "..");
const reportPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "issue-1011-")), "report.json");
const script = `const r = await Bun.build({ entrypoints: [${JSON.stringify(supervisor)}], target: "bun" }); await Bun.write(${JSON.stringify(reportPath)}, JSON.stringify({ success: r.success, outputs: r.outputs.map(o => ({ path: o.path, kind: o.kind })), logs: r.logs.map(l => l.message) }));`;
const proc = Bun.spawnSync(["bun", "-e", script], {
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
});
const stderr = proc.stderr.toString();
expect(proc.exitCode, `bun -e exited with ${proc.exitCode}; stderr=${stderr}`).toBe(0);
const report = (await Bun.file(reportPath).json()) as {
success: boolean;
outputs: { path: string; kind: string }[];
logs: string[];
};
expect(report.success, `bundler logs: ${report.logs.join("; ")}`).toBe(true);
const workerAssets = report.outputs.filter(out => out.kind === "asset" && out.path.includes("tab-worker-entry"));
if (workerAssets.length === 0) {
const summary = report.outputs.map(o => `${o.kind}:${o.path}`).join(", ");
throw new Error(
`tab-worker-entry.ts was not bundled as an asset of tab-supervisor.ts. ` +
`Bun's --compile bundler cannot embed the worker because the Worker ` +
`constructor argument is not a statically-analyzable URL literal. ` +
`Bundler outputs were: [${summary}]`,
);
}
});
});