Skip to content

Commit 612ed5b

Browse files
committed
diffs plugin
1 parent fca0467 commit 612ed5b

23 files changed

Lines changed: 4067 additions & 4 deletions

extensions/diffs/README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# @openclaw/diffs
2+
3+
Read-only diff viewer plugin for **OpenClaw** agents.
4+
5+
It gives agents one tool, `diffs`, that can:
6+
7+
- render a gateway-hosted diff viewer for canvas use
8+
- render the same diff to a PNG image
9+
- accept either arbitrary `before`/`after` text or a unified patch
10+
11+
## What Agents Get
12+
13+
The tool can return:
14+
15+
- `details.viewerUrl`: a gateway URL that can be opened in the canvas
16+
- `details.imagePath`: a local PNG artifact when image rendering is requested
17+
18+
This means an agent can:
19+
20+
- call `diffs` with `mode=view`, then pass `details.viewerUrl` to `canvas present`
21+
- call `diffs` with `mode=image`, then send the PNG through the normal `message` tool using `path` or `filePath`
22+
- call `diffs` with `mode=both` when it wants both outputs
23+
24+
## Tool Inputs
25+
26+
Before/after:
27+
28+
```json
29+
{
30+
"before": "# Hello\n\nOne",
31+
"after": "# Hello\n\nTwo",
32+
"path": "docs/example.md",
33+
"mode": "view"
34+
}
35+
```
36+
37+
Patch:
38+
39+
```json
40+
{
41+
"patch": "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-const x = 1;\n+const x = 2;\n",
42+
"mode": "both"
43+
}
44+
```
45+
46+
Useful options:
47+
48+
- `mode`: `view`, `image`, or `both`
49+
- `layout`: `unified` or `split`
50+
- `theme`: `light` or `dark` (default: `dark`)
51+
- `expandUnchanged`: expand unchanged sections
52+
- `path`: display name for before/after input
53+
- `title`: explicit viewer title
54+
- `ttlSeconds`: artifact lifetime
55+
- `baseUrl`: override the gateway base URL used in the returned viewer link
56+
57+
## Example Agent Prompts
58+
59+
Open in canvas:
60+
61+
```text
62+
Use the `diffs` tool in `view` mode for this before/after content, then open the returned viewer URL in the canvas.
63+
64+
Path: docs/example.md
65+
66+
Before:
67+
# Hello
68+
69+
This is version one.
70+
71+
After:
72+
# Hello
73+
74+
This is version two.
75+
```
76+
77+
Render a PNG:
78+
79+
```text
80+
Use the `diffs` tool in `image` mode for this before/after input. After it returns `details.imagePath`, use the `message` tool with `path` or `filePath` to send me the rendered diff image.
81+
82+
Path: README.md
83+
84+
Before:
85+
OpenClaw supports plugins.
86+
87+
After:
88+
OpenClaw supports plugins and hosted diff views.
89+
```
90+
91+
Do both:
92+
93+
```text
94+
Use the `diffs` tool in `both` mode for this diff. Open the viewer in the canvas and then send the rendered PNG by passing `details.imagePath` to the `message` tool.
95+
96+
Path: src/demo.ts
97+
98+
Before:
99+
const status = "old";
100+
101+
After:
102+
const status = "new";
103+
```
104+
105+
Patch input:
106+
107+
```text
108+
Use the `diffs` tool with this unified patch in `view` mode. After it returns the viewer URL, present it in the canvas.
109+
110+
diff --git a/src/example.ts b/src/example.ts
111+
--- a/src/example.ts
112+
+++ b/src/example.ts
113+
@@ -1,3 +1,3 @@
114+
export function add(a: number, b: number) {
115+
- return a + b;
116+
+ return a + b + 1;
117+
}
118+
```
119+
120+
## Notes
121+
122+
- The viewer is hosted locally through the gateway under `/plugins/diffs/...`.
123+
- Artifacts are ephemeral and stored in the local temp directory.
124+
- PNG rendering requires a Chromium-compatible browser. Set `browser.executablePath` if auto-detection is not enough.

extensions/diffs/assets/viewer-runtime.js

Lines changed: 1309 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/diffs/index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import plugin from "./index.js";
3+
4+
describe("diffs plugin registration", () => {
5+
it("registers the tool, http handler, and prompt guidance hook", () => {
6+
const registerTool = vi.fn();
7+
const registerHttpHandler = vi.fn();
8+
const on = vi.fn();
9+
10+
plugin.register?.({
11+
id: "diffs",
12+
name: "Diffs",
13+
description: "Diffs",
14+
source: "test",
15+
config: {},
16+
runtime: {} as never,
17+
logger: {
18+
info() {},
19+
warn() {},
20+
error() {},
21+
},
22+
registerTool,
23+
registerHook() {},
24+
registerHttpHandler,
25+
registerHttpRoute() {},
26+
registerChannel() {},
27+
registerGatewayMethod() {},
28+
registerCli() {},
29+
registerService() {},
30+
registerProvider() {},
31+
registerCommand() {},
32+
resolvePath(input: string) {
33+
return input;
34+
},
35+
on,
36+
});
37+
38+
expect(registerTool).toHaveBeenCalledTimes(1);
39+
expect(registerHttpHandler).toHaveBeenCalledTimes(1);
40+
expect(on).toHaveBeenCalledTimes(1);
41+
expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build");
42+
});
43+
});

extensions/diffs/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import path from "node:path";
2+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3+
import { emptyPluginConfigSchema, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
4+
import { createDiffsHttpHandler } from "./src/http.js";
5+
import { DIFFS_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
6+
import { DiffArtifactStore } from "./src/store.js";
7+
import { createDiffsTool } from "./src/tool.js";
8+
9+
const plugin = {
10+
id: "diffs",
11+
name: "Diffs",
12+
description: "Read-only diff viewer and PNG renderer for agents.",
13+
configSchema: emptyPluginConfigSchema(),
14+
register(api: OpenClawPluginApi) {
15+
const store = new DiffArtifactStore({
16+
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
17+
logger: api.logger,
18+
});
19+
20+
api.registerTool(createDiffsTool({ api, store }));
21+
api.registerHttpHandler(createDiffsHttpHandler({ store, logger: api.logger }));
22+
api.on("before_prompt_build", async () => ({
23+
prependContext: DIFFS_AGENT_GUIDANCE,
24+
}));
25+
},
26+
};
27+
28+
export default plugin;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"id": "diffs",
3+
"name": "Diffs",
4+
"description": "Read-only diff viewer and image renderer for agents.",
5+
"configSchema": {
6+
"type": "object",
7+
"additionalProperties": false,
8+
"properties": {}
9+
}
10+
}

extensions/diffs/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@openclaw/diffs",
3+
"version": "2026.2.27",
4+
"private": true,
5+
"description": "OpenClaw diff viewer plugin",
6+
"type": "module",
7+
"scripts": {
8+
"build:viewer": "bun build src/viewer-client.ts --target browser --format esm --minify --outfile assets/viewer-runtime.js"
9+
},
10+
"dependencies": {
11+
"@pierre/diffs": "1.0.11",
12+
"@sinclair/typebox": "0.34.48",
13+
"playwright-core": "1.58.2"
14+
},
15+
"openclaw": {
16+
"extensions": [
17+
"./index.ts"
18+
]
19+
}
20+
}

0 commit comments

Comments
 (0)