diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f81b8308c..a6a12e842 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed +- Fixed ACP plan-mode writes to `local://PLAN.md` so session-local plan artifacts are written to OMP's local artifact root instead of being routed through the editor `writeTextFile` bridge, avoiding Zen's `Internal error` and making the plan readable after creation ([#1863](https://github.com/can1357/oh-my-pi/issues/1863)). + - Fixed `provider.appendOnlyContext: "auto"` staying inactive for Xiaomi Token Plan/SGLang endpoints, preserving prefix-cache hits without forcing append-only mode globally ([#1851](https://github.com/can1357/oh-my-pi/issues/1851)). - Fixed `models.yml` compatibility parsing to preserve `compat.cacheControlFormat: "anthropic"` for custom OpenAI-compatible Claude proxies. ([#1845](https://github.com/can1357/oh-my-pi/issues/1845)) - Fixed the TUI's `Settings → Plugins` panel reporting "No plugins installed" when only marketplace plugins were installed. The panel now merges `PluginManager.list()` with `MarketplaceManager.listInstalledPlugins()` — the same data source the `/plugins list` slash command and `omp plugin list` CLI already used — and tags each row with an `[npm]` / `[marketplace]` kind badge, a scope tag, and a shadow indicator for project-shadowed user installs. Selecting a marketplace row opens a new `MarketplacePluginDetailComponent` whose single `Enabled` toggle calls `MarketplaceManager.setPluginEnabled(pluginId, enabled, scope)`, with read-only metadata (version, install path, installed-at, last-updated, git commit SHA) listed below the toggle. The empty-state now lists both install commands (`omp plugin install ` and `omp plugin install @`) ([#1842](https://github.com/can1357/oh-my-pi/issues/1842)). diff --git a/packages/coding-agent/src/tools/write.ts b/packages/coding-agent/src/tools/write.ts index 9d6e0086f..409030bad 100644 --- a/packages/coding-agent/src/tools/write.ts +++ b/packages/coding-agent/src/tools/write.ts @@ -135,6 +135,15 @@ function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, co return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag); } +function shouldRouteWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string): boolean { + if (isInternalUrlPath(requestedPath)) return false; + + const state = session.getPlanModeState?.(); + if (!state?.enabled || !isInternalUrlPath(state.planFilePath)) return true; + + return absolutePath !== resolvePlanPath(session, state.planFilePath); +} + /** * Append a trailing note line to the first text block of a tool result. * Mutates `result` in place (the result object is owned by this call). @@ -845,8 +854,11 @@ export class WriteTool implements AgentTool path.join(cwd, "artifacts"); + const getSessionId = () => "session-a"; return { cwd, hasUI: false, + enableLsp: false, getSessionFile: () => path.join(cwd, "session.jsonl"), getSessionSpawns: () => "*", - getArtifactsDir: () => path.join(cwd, "artifacts"), + getArtifactsDir, + getSessionId, + localProtocolOptions: { getArtifactsDir, getSessionId }, allocateOutputArtifact: async () => ({ id: "artifact-1", path: path.join(cwd, "artifact-1.log") }), settings: Settings.isolated(), - getClientBridge: bridge ? () => bridge : undefined, + getClientBridge: options.bridge ? () => options.bridge : undefined, + getPlanModeState: options.planMode ? () => options.planMode : undefined, }; } @@ -45,7 +58,7 @@ describe("write tool ACP fs routing", () => { const bunWriteSpy = spyOn(Bun, "write"); try { - const session = createSession(tmpDir, bridge); + const session = createSession(tmpDir, { bridge }); const tool = new WriteTool(session); await tool.execute("call-1", { path: filePath, content: FILE_CONTENT }); @@ -59,4 +72,32 @@ describe("write tool ACP fs routing", () => { bunWriteSpy.mockRestore(); } }); + + it("writes local plan artifacts to disk instead of the ACP bridge", async () => { + const planPath = "local://PLAN.md"; + const planContent = "# Plan\n\nhello world\n"; + const bridge: ClientBridge = { + capabilities: { writeTextFile: true }, + writeTextFile: async () => { + throw new Error("Internal error"); + }, + }; + const bridgeSpy = spyOn(bridge, "writeTextFile"); + const session = createSession(tmpDir, { + bridge, + planMode: { enabled: true, planFilePath: planPath, workflow: "parallel", reentry: false }, + }); + + await new WriteTool(session).execute("call-plan", { path: planPath, content: planContent }); + + expect(bridgeSpy).not.toHaveBeenCalled(); + expect( + await Bun.file( + resolveLocalUrlToPath(planPath, { + getArtifactsDir: session.getArtifactsDir, + getSessionId: session.getSessionId, + }), + ).text(), + ).toBe(planContent); + }); });