fix(coding-agent): kept local plan writes off acp bridge
Route '/data/workspaces/can1357__oh-my-pi__1863/.omp-session/2026-06-04T13-36-11-717Z_019e92d9-3fc5-7000-a66f-15cad94b7e75/local' plan artifacts through OMP's session-local storage instead of the editor writeTextFile bridge, preserving ACP bridge routing for regular editor-visible files. Fixes #1863
This commit is contained in:
@@ -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 <package>` and `omp plugin install <name>@<marketplace>`) ([#1842](https://github.com/can1357/oh-my-pi/issues/1842)).
|
||||
|
||||
@@ -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<typeof writeSchema, WriteToolDetails
|
||||
await assertEditableFile(absolutePath, path);
|
||||
}
|
||||
|
||||
// Try ACP bridge first — no disk write when client handles it
|
||||
const bridgePromise = this.#routeWriteThroughBridge(absolutePath, cleanContent);
|
||||
// Try ACP bridge first for editor-visible filesystem paths. Internal
|
||||
// artifacts such as local:// plans are owned by OMP, not the editor.
|
||||
const bridgePromise = shouldRouteWriteThroughBridge(this.session, path, absolutePath)
|
||||
? this.#routeWriteThroughBridge(absolutePath, cleanContent)
|
||||
: undefined;
|
||||
if (bridgePromise !== undefined) {
|
||||
try {
|
||||
await bridgePromise;
|
||||
|
||||
@@ -3,22 +3,35 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { resolveLocalUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
|
||||
import type { PlanModeState } from "@oh-my-pi/pi-coding-agent/plan-mode/state";
|
||||
import type { ClientBridge } from "@oh-my-pi/pi-coding-agent/session/client-bridge";
|
||||
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
import { WriteTool } from "@oh-my-pi/pi-coding-agent/tools/write";
|
||||
|
||||
const FILE_CONTENT = "bridge write content\n";
|
||||
|
||||
function createSession(cwd: string, bridge?: ClientBridge): ToolSession {
|
||||
interface SessionOptions {
|
||||
bridge?: ClientBridge;
|
||||
planMode?: PlanModeState;
|
||||
}
|
||||
|
||||
function createSession(cwd: string, options: SessionOptions = {}): ToolSession {
|
||||
const getArtifactsDir = () => 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user