Diffs: Migrate tool usage guidance from before_prompt_build to a plugin skill (#32630)

Merged via squash.

Prepared head SHA: 585697a4e1
Co-authored-by: sircrumpet <4436535+sircrumpet@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Eugene 2026-03-03 16:50:59 +10:00 committed by GitHub
parent 997197c6c9
commit 5341b5c71c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 91 additions and 19 deletions

View File

@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.
- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.
### Fixes

View File

@ -10,7 +10,7 @@ read_when:
# Diffs
`diffs` is an optional plugin tool that turns change content into a read-only diff artifact for agents.
`diffs` is an optional plugin tool and companion skill that turns change content into a read-only diff artifact for agents.
It accepts either:

View File

@ -16,6 +16,8 @@ The tool can return:
- `details.filePath`: a local rendered artifact path when file rendering is requested
- `details.fileFormat`: the rendered file format (`png` or `pdf`)
When the plugin is enabled, it also ships a companion skill from `skills/` that guides when to use `diffs`. This guidance is delivered through normal skill loading, not unconditional prompt-hook injection on every turn.
This means an agent can:
- call `diffs` with `mode=view`, then pass `details.viewerUrl` to `canvas present`

View File

@ -4,7 +4,7 @@ import { createMockServerResponse } from "../../src/test-utils/mock-http-respons
import plugin from "./index.js";
describe("diffs plugin registration", () => {
it("registers the tool, http route, and prompt guidance hook", () => {
it("registers the tool and http route", () => {
const registerTool = vi.fn();
const registerHttpRoute = vi.fn();
const on = vi.fn();
@ -43,8 +43,7 @@ describe("diffs plugin registration", () => {
auth: "plugin",
match: "prefix",
});
expect(on).toHaveBeenCalledTimes(1);
expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build");
expect(on).not.toHaveBeenCalled();
});
it("applies plugin-config defaults through registered tool and viewer handler", async () => {

View File

@ -7,7 +7,6 @@ import {
resolveDiffsPluginSecurity,
} from "./src/config.js";
import { createDiffsHttpHandler } from "./src/http.js";
import { DIFFS_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
import { DiffArtifactStore } from "./src/store.js";
import { createDiffsTool } from "./src/tool.js";
@ -35,9 +34,6 @@ const plugin = {
allowRemoteViewer: security.allowRemoteViewer,
}),
});
api.on("before_prompt_build", async () => ({
prependContext: DIFFS_AGENT_GUIDANCE,
}));
},
};

View File

@ -2,6 +2,7 @@
"id": "diffs",
"name": "Diffs",
"description": "Read-only diff viewer and file renderer for agents.",
"skills": ["./skills"],
"uiHints": {
"defaults.fontFamily": {
"label": "Default Font",

View File

@ -0,0 +1,22 @@
---
name: diffs
description: Use the diffs tool to produce real, shareable diffs (viewer URL, file artifact, or both) instead of manual edit summaries.
---
When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.
The `diffs` tool accepts either `before` + `after` text, or a unified `patch` string.
Use `mode=view` when you want an interactive gateway-hosted viewer. After the tool returns, use `details.viewerUrl` with the canvas tool via `canvas present` or `canvas navigate`.
Use `mode=file` when you need a rendered file artifact. Set `fileFormat=png` (default) or `fileFormat=pdf`. The tool result includes `details.filePath`.
For large or high-fidelity files, use `fileQuality` (`standard`|`hq`|`print`) and optionally override `fileScale`/`fileMaxWidth`.
When you need to deliver the rendered file to a user or channel, do not rely on the raw tool-result renderer. Instead, call the `message` tool and pass `details.filePath` through `path` or `filePath`.
Use `mode=both` when you want both the gateway viewer URL and the rendered artifact.
If the user has configured diffs plugin defaults, prefer omitting `mode`, `theme`, `layout`, and related presentation options unless you need to override them for this specific diff.
Include `path` for before/after text when you know the file name.

View File

@ -1,11 +0,0 @@
export const DIFFS_AGENT_GUIDANCE = [
"When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
"The `diffs` tool accepts either `before` + `after` text, or a unified `patch` string.",
"Use `mode=view` when you want an interactive gateway-hosted viewer. After the tool returns, use `details.viewerUrl` with the canvas tool via `canvas present` or `canvas navigate`.",
"Use `mode=file` when you need a rendered file artifact. Set `fileFormat=png` (default) or `fileFormat=pdf`. The tool result includes `details.filePath`.",
"For large or high-fidelity files, use `fileQuality` (`standard`|`hq`|`print`) and optionally override `fileScale`/`fileMaxWidth`.",
"When you need to deliver the rendered file to a user or channel, do not rely on the raw tool-result renderer. Instead, call the `message` tool and pass `details.filePath` through `path` or `filePath`.",
"Use `mode=both` when you want both the gateway viewer URL and the rendered artifact.",
"If the user has configured diffs plugin defaults, prefer omitting `mode`, `theme`, `layout`, and related presentation options unless you need to override them for this specific diff.",
"Include `path` for before/after text when you know the file name.",
].join("\n");

View File

@ -48,6 +48,36 @@ async function setupWorkspaceWithProsePlugin() {
return { workspaceDir, managedDir, bundledDir };
}
async function setupWorkspaceWithDiffsPlugin() {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const bundledDir = path.join(workspaceDir, ".bundled");
const pluginRoot = path.join(workspaceDir, ".openclaw", "extensions", "diffs");
await fs.mkdir(path.join(pluginRoot, "skills", "diffs"), { recursive: true });
await fs.writeFile(
path.join(pluginRoot, "openclaw.plugin.json"),
JSON.stringify(
{
id: "diffs",
skills: ["./skills"],
configSchema: { type: "object", additionalProperties: false, properties: {} },
},
null,
2,
),
"utf-8",
);
await fs.writeFile(path.join(pluginRoot, "index.ts"), "export {};\n", "utf-8");
await fs.writeFile(
path.join(pluginRoot, "skills", "diffs", "SKILL.md"),
`---\nname: diffs\ndescription: test\n---\n`,
"utf-8",
);
return { workspaceDir, managedDir, bundledDir };
}
describe("loadWorkspaceSkillEntries", () => {
it("handles an empty managed skills dir without throwing", async () => {
const workspaceDir = await createTempWorkspaceDir();
@ -93,4 +123,36 @@ describe("loadWorkspaceSkillEntries", () => {
expect(entries.map((entry) => entry.skill.name)).not.toContain("prose");
});
it("includes diffs plugin skill when the plugin is enabled", async () => {
const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin();
const entries = loadWorkspaceSkillEntries(workspaceDir, {
config: {
plugins: {
entries: { diffs: { enabled: true } },
},
},
managedSkillsDir: managedDir,
bundledSkillsDir: bundledDir,
});
expect(entries.map((entry) => entry.skill.name)).toContain("diffs");
});
it("excludes diffs plugin skill when the plugin is disabled", async () => {
const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin();
const entries = loadWorkspaceSkillEntries(workspaceDir, {
config: {
plugins: {
entries: { diffs: { enabled: false } },
},
},
managedSkillsDir: managedDir,
bundledSkillsDir: bundledDir,
});
expect(entries.map((entry) => entry.skill.name)).not.toContain("diffs");
});
});