mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
adding config layer
This commit is contained in:
@@ -54,6 +54,35 @@ Useful options:
|
||||
- `ttlSeconds`: artifact lifetime
|
||||
- `baseUrl`: override the gateway base URL used in the returned viewer link
|
||||
|
||||
## Plugin Defaults
|
||||
|
||||
Set plugin-wide defaults in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
diffs: {
|
||||
enabled: true,
|
||||
config: {
|
||||
defaults: {
|
||||
fontFamily: "Fira Code",
|
||||
fontSize: 15,
|
||||
layout: "unified",
|
||||
wordWrap: true,
|
||||
background: true,
|
||||
theme: "dark",
|
||||
mode: "both",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Explicit tool parameters still win over these defaults.
|
||||
|
||||
## Example Agent Prompts
|
||||
|
||||
Open in canvas:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createMockServerResponse } from "../../src/test-utils/mock-http-response.js";
|
||||
import plugin from "./index.js";
|
||||
|
||||
describe("diffs plugin registration", () => {
|
||||
@@ -40,4 +42,82 @@ describe("diffs plugin registration", () => {
|
||||
expect(on).toHaveBeenCalledTimes(1);
|
||||
expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build");
|
||||
});
|
||||
|
||||
it("applies plugin-config defaults through registered tool and viewer handler", async () => {
|
||||
let registeredTool:
|
||||
| { execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown> }
|
||||
| undefined;
|
||||
let registeredHttpHandler:
|
||||
| ((
|
||||
req: IncomingMessage,
|
||||
res: ReturnType<typeof createMockServerResponse>,
|
||||
) => Promise<boolean>)
|
||||
| undefined;
|
||||
|
||||
plugin.register?.({
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Diffs",
|
||||
source: "test",
|
||||
config: {
|
||||
gateway: {
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
},
|
||||
},
|
||||
pluginConfig: {
|
||||
defaults: {
|
||||
theme: "light",
|
||||
background: false,
|
||||
layout: "split",
|
||||
},
|
||||
},
|
||||
runtime: {} as never,
|
||||
logger: {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
},
|
||||
registerTool(tool) {
|
||||
registeredTool = typeof tool === "function" ? undefined : tool;
|
||||
},
|
||||
registerHook() {},
|
||||
registerHttpHandler(handler) {
|
||||
registeredHttpHandler = handler as typeof registeredHttpHandler;
|
||||
},
|
||||
registerHttpRoute() {},
|
||||
registerChannel() {},
|
||||
registerGatewayMethod() {},
|
||||
registerCli() {},
|
||||
registerService() {},
|
||||
registerProvider() {},
|
||||
registerCommand() {},
|
||||
resolvePath(input: string) {
|
||||
return input;
|
||||
},
|
||||
on() {},
|
||||
});
|
||||
|
||||
const result = await registeredTool?.execute?.("tool-1", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
});
|
||||
const viewerPath = String(
|
||||
(result as { details?: Record<string, unknown> } | undefined)?.details?.viewerPath,
|
||||
);
|
||||
const res = createMockServerResponse();
|
||||
const handled = await registeredHttpHandler?.(
|
||||
{
|
||||
method: "GET",
|
||||
url: viewerPath,
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(String(res.body)).toContain('body data-theme="light"');
|
||||
expect(String(res.body)).toContain('"backgroundEnabled":false');
|
||||
expect(String(res.body)).toContain('"diffStyle":"split"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { emptyPluginConfigSchema, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
|
||||
import { diffsPluginConfigSchema, resolveDiffsPluginDefaults } 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";
|
||||
@@ -10,14 +11,15 @@ const plugin = {
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Read-only diff viewer and PNG renderer for agents.",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
configSchema: diffsPluginConfigSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
const defaults = resolveDiffsPluginDefaults(api.pluginConfig);
|
||||
const store = new DiffArtifactStore({
|
||||
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
|
||||
logger: api.logger,
|
||||
});
|
||||
|
||||
api.registerTool(createDiffsTool({ api, store }));
|
||||
api.registerTool(createDiffsTool({ api, store, defaults }));
|
||||
api.registerHttpHandler(createDiffsHttpHandler({ store, logger: api.logger }));
|
||||
api.on("before_prompt_build", async () => ({
|
||||
prependContext: DIFFS_AGENT_GUIDANCE,
|
||||
|
||||
@@ -2,9 +2,79 @@
|
||||
"id": "diffs",
|
||||
"name": "Diffs",
|
||||
"description": "Read-only diff viewer and image renderer for agents.",
|
||||
"uiHints": {
|
||||
"defaults.fontFamily": {
|
||||
"label": "Default Font",
|
||||
"help": "Preferred font family name for diff content and headers."
|
||||
},
|
||||
"defaults.fontSize": {
|
||||
"label": "Default Font Size",
|
||||
"help": "Base diff font size in pixels."
|
||||
},
|
||||
"defaults.layout": {
|
||||
"label": "Default Layout",
|
||||
"help": "Initial diff layout shown in the viewer."
|
||||
},
|
||||
"defaults.wordWrap": {
|
||||
"label": "Default Word Wrap",
|
||||
"help": "Wrap long lines by default."
|
||||
},
|
||||
"defaults.background": {
|
||||
"label": "Default Background Highlights",
|
||||
"help": "Show added/removed background highlights by default."
|
||||
},
|
||||
"defaults.theme": {
|
||||
"label": "Default Theme",
|
||||
"help": "Initial viewer theme."
|
||||
},
|
||||
"defaults.mode": {
|
||||
"label": "Default Output Mode",
|
||||
"help": "Tool default when mode is omitted. Use view for canvas/gateway viewer, image for PNG, or both."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"defaults": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fontFamily": {
|
||||
"type": "string",
|
||||
"default": "Fira Code"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"minimum": 10,
|
||||
"maximum": 24,
|
||||
"default": 15
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"enum": ["unified", "split"],
|
||||
"default": "unified"
|
||||
},
|
||||
"wordWrap": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"background": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark"],
|
||||
"default": "dark"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["view", "image", "both"],
|
||||
"default": "both"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
extensions/diffs/src/config.test.ts
Normal file
32
extensions/diffs/src/config.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_DIFFS_TOOL_DEFAULTS, resolveDiffsPluginDefaults } from "./config.js";
|
||||
|
||||
describe("resolveDiffsPluginDefaults", () => {
|
||||
it("returns built-in defaults when config is missing", () => {
|
||||
expect(resolveDiffsPluginDefaults(undefined)).toEqual(DEFAULT_DIFFS_TOOL_DEFAULTS);
|
||||
});
|
||||
|
||||
it("applies configured defaults from plugin config", () => {
|
||||
expect(
|
||||
resolveDiffsPluginDefaults({
|
||||
defaults: {
|
||||
fontFamily: "JetBrains Mono",
|
||||
fontSize: 17,
|
||||
layout: "split",
|
||||
wordWrap: false,
|
||||
background: false,
|
||||
theme: "light",
|
||||
mode: "view",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
fontFamily: "JetBrains Mono",
|
||||
fontSize: 17,
|
||||
layout: "split",
|
||||
wordWrap: false,
|
||||
background: false,
|
||||
theme: "light",
|
||||
mode: "view",
|
||||
});
|
||||
});
|
||||
});
|
||||
147
extensions/diffs/src/config.ts
Normal file
147
extensions/diffs/src/config.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
DIFF_LAYOUTS,
|
||||
DIFF_MODES,
|
||||
DIFF_THEMES,
|
||||
type DiffLayout,
|
||||
type DiffMode,
|
||||
type DiffPresentationDefaults,
|
||||
type DiffTheme,
|
||||
type DiffToolDefaults,
|
||||
} from "./types.js";
|
||||
|
||||
type DiffsPluginConfig = {
|
||||
defaults?: {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
layout?: DiffLayout;
|
||||
wordWrap?: boolean;
|
||||
background?: boolean;
|
||||
theme?: DiffTheme;
|
||||
mode?: DiffMode;
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_DIFFS_TOOL_DEFAULTS: DiffToolDefaults = {
|
||||
fontFamily: "Fira Code",
|
||||
fontSize: 15,
|
||||
layout: "unified",
|
||||
wordWrap: true,
|
||||
background: true,
|
||||
theme: "dark",
|
||||
mode: "both",
|
||||
};
|
||||
|
||||
const DIFFS_PLUGIN_CONFIG_JSON_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
defaults: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
fontFamily: { type: "string", default: DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily },
|
||||
fontSize: {
|
||||
type: "number",
|
||||
minimum: 10,
|
||||
maximum: 24,
|
||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize,
|
||||
},
|
||||
layout: {
|
||||
type: "string",
|
||||
enum: [...DIFF_LAYOUTS],
|
||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.layout,
|
||||
},
|
||||
wordWrap: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap },
|
||||
background: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.background },
|
||||
theme: {
|
||||
type: "string",
|
||||
enum: [...DIFF_THEMES],
|
||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.theme,
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
enum: [...DIFF_MODES],
|
||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const diffsPluginConfigSchema: OpenClawPluginConfigSchema = {
|
||||
safeParse(value: unknown) {
|
||||
if (value === undefined) {
|
||||
return { success: true, data: undefined };
|
||||
}
|
||||
try {
|
||||
return { success: true, data: resolveDiffsPluginDefaults(value) };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
issues: [{ path: [], message: error instanceof Error ? error.message : String(error) }],
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
jsonSchema: DIFFS_PLUGIN_CONFIG_JSON_SCHEMA,
|
||||
};
|
||||
|
||||
export function resolveDiffsPluginDefaults(config: unknown): DiffToolDefaults {
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||
return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
|
||||
}
|
||||
|
||||
const defaults = (config as DiffsPluginConfig).defaults;
|
||||
if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
|
||||
return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
|
||||
}
|
||||
|
||||
return {
|
||||
fontFamily: normalizeFontFamily(defaults.fontFamily),
|
||||
fontSize: normalizeFontSize(defaults.fontSize),
|
||||
layout: normalizeLayout(defaults.layout),
|
||||
wordWrap: defaults.wordWrap !== false,
|
||||
background: defaults.background !== false,
|
||||
theme: normalizeTheme(defaults.theme),
|
||||
mode: normalizeMode(defaults.mode),
|
||||
};
|
||||
}
|
||||
|
||||
export function toPresentationDefaults(defaults: DiffToolDefaults): DiffPresentationDefaults {
|
||||
const { fontFamily, fontSize, layout, wordWrap, background, theme } = defaults;
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
layout,
|
||||
wordWrap,
|
||||
background,
|
||||
theme,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFontFamily(fontFamily?: string): string {
|
||||
const normalized = fontFamily?.trim();
|
||||
return normalized || DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily;
|
||||
}
|
||||
|
||||
function normalizeFontSize(fontSize?: number): number {
|
||||
if (fontSize === undefined || !Number.isFinite(fontSize)) {
|
||||
return DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize;
|
||||
}
|
||||
const rounded = Math.floor(fontSize);
|
||||
return Math.min(Math.max(rounded, 10), 24);
|
||||
}
|
||||
|
||||
function normalizeLayout(layout?: DiffLayout): DiffLayout {
|
||||
return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
|
||||
}
|
||||
|
||||
function normalizeTheme(theme?: DiffTheme): DiffTheme {
|
||||
return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
|
||||
}
|
||||
|
||||
function normalizeMode(mode?: DiffMode): DiffMode {
|
||||
return mode && DIFF_MODES.includes(mode) ? mode : DEFAULT_DIFFS_TOOL_DEFAULTS.mode;
|
||||
}
|
||||
@@ -5,5 +5,6 @@ export const DIFFS_AGENT_GUIDANCE = [
|
||||
"Use `mode=image` when you need a rendered PNG. The tool result includes `details.imagePath` for the generated file.",
|
||||
"When you need to deliver the PNG to a user or channel, do not rely on the raw tool-result image renderer. Instead, call the `message` tool and pass `details.imagePath` through `path` or `filePath`.",
|
||||
"Use `mode=both` when you want both the gateway viewer URL and the PNG artifact.",
|
||||
"Good defaults: `theme=dark` for canvas rendering, `layout=unified` for most diffs, and include `path` for before/after text when you know the file name.",
|
||||
"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");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
|
||||
describe("renderDiffDocument", () => {
|
||||
@@ -11,9 +12,8 @@ describe("renderDiffDocument", () => {
|
||||
path: "src/example.ts",
|
||||
},
|
||||
{
|
||||
layout: "unified",
|
||||
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
expandUnchanged: false,
|
||||
theme: "light",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -48,9 +48,12 @@ describe("renderDiffDocument", () => {
|
||||
title: "Workspace patch",
|
||||
},
|
||||
{
|
||||
layout: "split",
|
||||
presentation: {
|
||||
...DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
layout: "split",
|
||||
theme: "dark",
|
||||
},
|
||||
expandUnchanged: true,
|
||||
theme: "dark",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import { VIEWER_LOADER_PATH } from "./viewer-assets.js";
|
||||
|
||||
const DEFAULT_FILE_NAME = "diff.txt";
|
||||
|
||||
function escapeCssString(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
@@ -46,21 +50,25 @@ function resolveBeforeAfterFileName(input: Extract<DiffInput, { kind: "before_af
|
||||
}
|
||||
|
||||
function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions {
|
||||
const fontFamily = escapeCssString(options.presentation.fontFamily);
|
||||
const fontSize = Math.max(10, Math.floor(options.presentation.fontSize));
|
||||
const lineHeight = Math.max(20, Math.round(fontSize * 1.6));
|
||||
return {
|
||||
theme: {
|
||||
light: "pierre-light",
|
||||
dark: "pierre-dark",
|
||||
},
|
||||
diffStyle: options.layout,
|
||||
diffStyle: options.presentation.layout,
|
||||
expandUnchanged: options.expandUnchanged,
|
||||
themeType: options.theme,
|
||||
overflow: "wrap" as const,
|
||||
themeType: options.presentation.theme,
|
||||
backgroundEnabled: options.presentation.background,
|
||||
overflow: options.presentation.wordWrap ? "wrap" : "scroll",
|
||||
unsafeCSS: `
|
||||
:host {
|
||||
--diffs-font-family: "Fira Code", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-header-font-family: "Fira Code", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-font-size: 15px;
|
||||
--diffs-line-height: 24px;
|
||||
--diffs-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-header-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-font-size: ${fontSize}px;
|
||||
--diffs-line-height: ${lineHeight}px;
|
||||
}
|
||||
|
||||
[data-diffs-header] {
|
||||
@@ -166,7 +174,7 @@ function renderDiffCard(payload: DiffViewerPayload): string {
|
||||
function buildHtmlDocument(params: {
|
||||
title: string;
|
||||
bodyHtml: string;
|
||||
theme: DiffRenderOptions["theme"];
|
||||
theme: DiffRenderOptions["presentation"]["theme"];
|
||||
}): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -341,7 +349,7 @@ export async function renderDiffDocument(
|
||||
html: buildHtmlDocument({
|
||||
title,
|
||||
bodyHtml: rendered.bodyHtml,
|
||||
theme: options.theme,
|
||||
theme: options.presentation.theme,
|
||||
}),
|
||||
title,
|
||||
fileCount: rendered.fileCount,
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
import { createDiffsTool } from "./tool.js";
|
||||
|
||||
@@ -23,6 +24,7 @@ describe("diffs tool", () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-1", {
|
||||
@@ -49,6 +51,7 @@ describe("diffs tool", () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
screenshotter,
|
||||
});
|
||||
|
||||
@@ -69,6 +72,7 @@ describe("diffs tool", () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
screenshotter: {
|
||||
screenshotHtml: vi.fn(async () => {
|
||||
throw new Error("browser missing");
|
||||
@@ -91,6 +95,7 @@ describe("diffs tool", () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -102,6 +107,66 @@ describe("diffs tool", () => {
|
||||
}),
|
||||
).rejects.toThrow("Invalid baseUrl");
|
||||
});
|
||||
|
||||
it("uses configured defaults when tool params omit them", async () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: {
|
||||
...DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
mode: "view",
|
||||
theme: "light",
|
||||
layout: "split",
|
||||
wordWrap: false,
|
||||
background: false,
|
||||
fontFamily: "JetBrains Mono",
|
||||
fontSize: 17,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-5", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
path: "README.md",
|
||||
});
|
||||
|
||||
expect(readTextContent(result, 0)).toContain("Diff viewer ready.");
|
||||
expect((result?.details as Record<string, unknown>).mode).toBe("view");
|
||||
|
||||
const viewerPath = String((result?.details as Record<string, unknown>).viewerPath);
|
||||
const [id] = viewerPath.split("/").filter(Boolean).slice(-2);
|
||||
const html = await store.readHtml(id);
|
||||
expect(html).toContain('body data-theme="light"');
|
||||
expect(html).toContain("--diffs-font-size: 17px;");
|
||||
expect(html).toContain('--diffs-font-family: "JetBrains Mono"');
|
||||
});
|
||||
|
||||
it("prefers explicit tool params over configured defaults", async () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
defaults: {
|
||||
...DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
mode: "view",
|
||||
theme: "light",
|
||||
layout: "split",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-6", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
mode: "both",
|
||||
theme: "dark",
|
||||
layout: "unified",
|
||||
});
|
||||
|
||||
expect((result?.details as Record<string, unknown>).mode).toBe("both");
|
||||
const viewerPath = String((result?.details as Record<string, unknown>).viewerPath);
|
||||
const [id] = viewerPath.split("/").filter(Boolean).slice(-2);
|
||||
const html = await store.readHtml(id);
|
||||
expect(html).toContain('body data-theme="dark"');
|
||||
});
|
||||
});
|
||||
|
||||
function createApi(): OpenClawPluginApi {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
import type { DiffArtifactStore } from "./store.js";
|
||||
import type { DiffToolDefaults } from "./types.js";
|
||||
import {
|
||||
DIFF_LAYOUTS,
|
||||
DIFF_MODES,
|
||||
@@ -63,6 +64,7 @@ type DiffsToolParams = Static<typeof DiffsToolSchema>;
|
||||
export function createDiffsTool(params: {
|
||||
api: OpenClawPluginApi;
|
||||
store: DiffArtifactStore;
|
||||
defaults: DiffToolDefaults;
|
||||
screenshotter?: DiffScreenshotter;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
@@ -74,16 +76,19 @@ export function createDiffsTool(params: {
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const toolParams = rawParams as DiffsToolParams;
|
||||
const input = normalizeDiffInput(toolParams);
|
||||
const mode = normalizeMode(toolParams.mode);
|
||||
const theme = normalizeTheme(toolParams.theme);
|
||||
const layout = normalizeLayout(toolParams.layout);
|
||||
const mode = normalizeMode(toolParams.mode, params.defaults.mode);
|
||||
const theme = normalizeTheme(toolParams.theme, params.defaults.theme);
|
||||
const layout = normalizeLayout(toolParams.layout, params.defaults.layout);
|
||||
const expandUnchanged = toolParams.expandUnchanged === true;
|
||||
const ttlMs = normalizeTtlMs(toolParams.ttlSeconds);
|
||||
|
||||
const rendered = await renderDiffDocument(input, {
|
||||
layout,
|
||||
presentation: {
|
||||
...params.defaults,
|
||||
layout,
|
||||
theme,
|
||||
},
|
||||
expandUnchanged,
|
||||
theme,
|
||||
});
|
||||
|
||||
const artifact = await params.store.createArtifact({
|
||||
@@ -218,16 +223,16 @@ function normalizeBaseUrl(baseUrl?: string): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMode(mode?: DiffMode): DiffMode {
|
||||
return mode && DIFF_MODES.includes(mode) ? mode : "both";
|
||||
function normalizeMode(mode: DiffMode | undefined, fallback: DiffMode): DiffMode {
|
||||
return mode && DIFF_MODES.includes(mode) ? mode : fallback;
|
||||
}
|
||||
|
||||
function normalizeTheme(theme?: DiffTheme): DiffTheme {
|
||||
return theme && DIFF_THEMES.includes(theme) ? theme : "dark";
|
||||
function normalizeTheme(theme: DiffTheme | undefined, fallback: DiffTheme): DiffTheme {
|
||||
return theme && DIFF_THEMES.includes(theme) ? theme : fallback;
|
||||
}
|
||||
|
||||
function normalizeLayout(layout?: DiffLayout): DiffLayout {
|
||||
return layout && DIFF_LAYOUTS.includes(layout) ? layout : "unified";
|
||||
function normalizeLayout(layout: DiffLayout | undefined, fallback: DiffLayout): DiffLayout {
|
||||
return layout && DIFF_LAYOUTS.includes(layout) ? layout : fallback;
|
||||
}
|
||||
|
||||
function normalizeTtlMs(ttlSeconds?: number): number | undefined {
|
||||
|
||||
@@ -8,6 +8,19 @@ export type DiffLayout = (typeof DIFF_LAYOUTS)[number];
|
||||
export type DiffMode = (typeof DIFF_MODES)[number];
|
||||
export type DiffTheme = (typeof DIFF_THEMES)[number];
|
||||
|
||||
export type DiffPresentationDefaults = {
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
layout: DiffLayout;
|
||||
wordWrap: boolean;
|
||||
background: boolean;
|
||||
theme: DiffTheme;
|
||||
};
|
||||
|
||||
export type DiffToolDefaults = DiffPresentationDefaults & {
|
||||
mode: DiffMode;
|
||||
};
|
||||
|
||||
export type BeforeAfterDiffInput = {
|
||||
kind: "before_after";
|
||||
before: string;
|
||||
@@ -26,9 +39,8 @@ export type PatchDiffInput = {
|
||||
export type DiffInput = BeforeAfterDiffInput | PatchDiffInput;
|
||||
|
||||
export type DiffRenderOptions = {
|
||||
layout: DiffLayout;
|
||||
presentation: DiffPresentationDefaults;
|
||||
expandUnchanged: boolean;
|
||||
theme: DiffTheme;
|
||||
};
|
||||
|
||||
export type DiffViewerOptions = {
|
||||
@@ -39,6 +51,7 @@ export type DiffViewerOptions = {
|
||||
diffStyle: DiffLayout;
|
||||
expandUnchanged: boolean;
|
||||
themeType: DiffTheme;
|
||||
backgroundEnabled: boolean;
|
||||
overflow: "scroll" | "wrap";
|
||||
unsafeCSS: string;
|
||||
};
|
||||
|
||||
@@ -281,6 +281,7 @@ async function hydrateViewer(): Promise<void> {
|
||||
if (firstPayload) {
|
||||
viewerState.theme = firstPayload.options.themeType;
|
||||
viewerState.layout = firstPayload.options.diffStyle;
|
||||
viewerState.backgroundEnabled = firstPayload.options.backgroundEnabled;
|
||||
viewerState.wrapEnabled = firstPayload.options.overflow === "wrap";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user