@openclaw/diffs 2026.9.8 → 2026.10.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { a as getServedViewerAsset, i as getServedLanguagePackViewerAsset, n as VIEWER_ASSET_PREFIX, t as LANGUAGE_PACK_VIEWER_ASSET_PREFIX } from "./viewer-assets-Bg9efLih.mjs";
1
+ import { a as getServedViewerAsset, i as getServedLanguagePackViewerAsset, n as VIEWER_ASSET_PREFIX, t as LANGUAGE_PACK_VIEWER_ASSET_PREFIX } from "./viewer-assets-C646cpoV.mjs";
2
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2
3
  import { constants } from "node:fs";
3
4
  import path from "node:path";
4
- import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
5
  import fs$1 from "node:fs/promises";
6
6
  import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
7
7
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
@@ -54,14 +54,8 @@ var PlaywrightDiffScreenshotter = class {
54
54
  await route.continue();
55
55
  return;
56
56
  }
57
- let parsed;
58
- try {
59
- parsed = new URL(requestUrl);
60
- } catch {
61
- await route.abort();
62
- return;
63
- }
64
- if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
57
+ const parsed = URL.parse(requestUrl);
58
+ if (!parsed || parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
65
59
  await route.abort();
66
60
  return;
67
61
  }
@@ -89,7 +83,7 @@ var PlaywrightDiffScreenshotter = class {
89
83
  return [...document.querySelectorAll("[data-openclaw-diff-host]")].every((element) => {
90
84
  return element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]");
91
85
  });
92
- }, { timeout: 1e4 });
86
+ }, void 0, { timeout: 1e4 });
93
87
  await page.evaluate(async () => {
94
88
  await document.fonts.ready;
95
89
  });
@@ -25,16 +25,25 @@ function createViewerAssetHandler(prefix, relativePaths, optional = false) {
25
25
  const loaderPath = `${prefix}viewer.js`;
26
26
  const runtimePath = `${prefix}viewer-runtime.js`;
27
27
  let cache = null;
28
+ const loadRuntimeAssets = async (runtimeUrl) => {
29
+ const cached = cache;
30
+ const runtimeFilePath = fileURLToPath(runtimeUrl);
31
+ const runtimeStat = await fs.stat(runtimeFilePath);
32
+ if (cached && cached.mtimeMs === runtimeStat.mtimeMs) return cached;
33
+ const runtimeBody = await fs.readFile(runtimeFilePath);
34
+ const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
35
+ cache = {
36
+ mtimeMs: runtimeStat.mtimeMs,
37
+ runtimeBody,
38
+ loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`
39
+ };
40
+ return cache;
41
+ };
28
42
  return async (pathname) => {
29
43
  if (pathname !== loaderPath && pathname !== runtimePath) return null;
30
44
  try {
31
- const assets = await loadRuntimeAssets({
32
- runtimeUrl: await resolveRuntimeFileUrl(relativePaths),
33
- cache,
34
- updateCache: (updated) => {
35
- cache = updated;
36
- }
37
- });
45
+ const runtimeUrl = await resolveRuntimeFileUrl(relativePaths);
46
+ const assets = await loadRuntimeAssets(runtimeUrl);
38
47
  return {
39
48
  body: pathname === loaderPath ? assets.loaderBody : assets.runtimeBody,
40
49
  contentType: "text/javascript; charset=utf-8"
@@ -45,20 +54,6 @@ function createViewerAssetHandler(prefix, relativePaths, optional = false) {
45
54
  }
46
55
  };
47
56
  }
48
- async function loadRuntimeAssets(params) {
49
- const runtimePath = fileURLToPath(params.runtimeUrl);
50
- const runtimeStat = await fs.stat(runtimePath);
51
- if (params.cache && params.cache.mtimeMs === runtimeStat.mtimeMs) return params.cache;
52
- const runtimeBody = await fs.readFile(runtimePath);
53
- const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
54
- const cache = {
55
- mtimeMs: runtimeStat.mtimeMs,
56
- runtimeBody,
57
- loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`
58
- };
59
- params.updateCache(cache);
60
- return cache;
61
- }
62
57
  async function resolveRuntimeFileUrl(relativePaths) {
63
58
  let missingFileError = null;
64
59
  for (const relativePath of relativePaths) {
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import { a as getServedViewerAsset, r as VIEWER_RUNTIME_PATH } from "./.setup/viewer-assets-Bg9efLih.mjs";
1
+ import { a as getServedViewerAsset, r as VIEWER_RUNTIME_PATH } from "./.setup/viewer-assets-C646cpoV.mjs";
2
2
  import { buildPluginConfigSchema, definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
3
  import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
4
+ import { asFiniteNumber, asNonArrayRecord, asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ import { clampInt, clampNumber, escapeHtml } from "openclaw/plugin-sdk/text-utility-runtime";
4
6
  import { z } from "zod";
5
7
  import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
6
8
  import fs from "node:fs";
@@ -8,7 +10,6 @@ import path from "node:path";
8
10
  import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
9
11
  import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
10
12
  import { isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
11
- import { asNonArrayRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
12
13
  import { createAuthRateLimiter, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
13
14
  import crypto from "node:crypto";
14
15
  import fs$1 from "node:fs/promises";
@@ -21,10 +22,10 @@ import { optionalFiniteNumberSchema, stringEnum } from "openclaw/plugin-sdk/chan
21
22
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
22
23
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
23
24
  import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
25
+ import { textResult } from "openclaw/plugin-sdk/tool-results";
24
26
  import { Type } from "typebox";
25
27
  import { parsePatchFiles, resolveLanguage } from "@pierre/diffs";
26
28
  import { preloadDiffHTML } from "@pierre/diffs/ssr";
27
- import { escapeHtml } from "openclaw/plugin-sdk/text-utility-runtime";
28
29
  //#region \0rolldown/runtime.js
29
30
  var __defProp = Object.defineProperty;
30
31
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -224,12 +225,8 @@ function buildViewerUrl(params) {
224
225
  return parsedBase.toString();
225
226
  }
226
227
  function normalizeViewerBaseUrl(raw, fieldName = "baseUrl") {
227
- let parsed;
228
- try {
229
- parsed = new URL(raw);
230
- } catch {
231
- throw new Error(`Invalid ${fieldName}: ${raw}`);
232
- }
228
+ const parsed = URL.parse(raw);
229
+ if (!parsed) throw new Error(`Invalid ${fieldName}: ${raw}`);
233
230
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${fieldName} must use http or https: ${raw}`);
234
231
  if (parsed.search || parsed.hash) throw new Error(`${fieldName} must not include query/hash: ${raw}`);
235
232
  parsed.search = "";
@@ -359,36 +356,32 @@ function resolveDiffsPluginDefaults(config) {
359
356
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
360
357
  const defaults = config.defaults;
361
358
  if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
362
- const fileQuality = normalizeFileQuality$1(defaults.fileQuality ?? defaults.imageQuality);
359
+ const fileQuality = normalizeFileQuality(defaults.fileQuality ?? defaults.imageQuality);
363
360
  const profile = DEFAULT_IMAGE_QUALITY_PROFILES[fileQuality];
364
361
  const fileFormat = defaults.fileFormat ?? (defaults.imageFormat !== void 0 ? defaults.imageFormat : defaults.format);
365
362
  return {
366
363
  fontFamily: normalizeFontFamily(defaults.fontFamily),
367
364
  fontSize: normalizeDiffFontSize(defaults.fontSize),
368
365
  lineSpacing: normalizeDiffLineSpacing(defaults.lineSpacing),
369
- layout: normalizeLayout$1(defaults.layout),
366
+ layout: DIFF_LAYOUTS.find((value) => value === defaults.layout) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.layout,
370
367
  showLineNumbers: defaults.showLineNumbers !== false,
371
- diffIndicators: normalizeDiffIndicators(defaults.diffIndicators),
368
+ diffIndicators: DIFF_INDICATORS.find((value) => value === defaults.diffIndicators) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators,
372
369
  wordWrap: defaults.wordWrap !== false,
373
370
  background: defaults.background !== false,
374
- theme: normalizeTheme$1(defaults.theme),
371
+ theme: DIFF_THEMES.find((value) => value === defaults.theme) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.theme,
375
372
  fileFormat: normalizeFileFormat(fileFormat),
376
373
  fileQuality,
377
374
  fileScale: normalizeFileScale(defaults.fileScale ?? defaults.imageScale, profile.scale),
378
375
  fileMaxWidth: normalizeFileMaxWidth(defaults.fileMaxWidth ?? defaults.imageMaxWidth, profile.maxWidth),
379
- mode: normalizeMode$1(defaults.mode),
376
+ mode: DIFF_MODES.find((value) => value === defaults.mode) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.mode,
380
377
  ttlSeconds: normalizeTtlSeconds(defaults.ttlSeconds)
381
378
  };
382
379
  }
383
380
  function resolveDiffsPluginSecurity(config) {
384
- if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
385
- const security = config.security;
386
- if (!security || typeof security !== "object" || Array.isArray(security)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
387
- return { allowRemoteViewer: security.allowRemoteViewer === true };
381
+ return { allowRemoteViewer: asOptionalRecord(asOptionalRecord(config)?.security)?.allowRemoteViewer === true };
388
382
  }
389
383
  function resolveDiffsPluginViewerBaseUrl(config) {
390
- if (!config || typeof config !== "object" || Array.isArray(config)) return;
391
- const viewerBaseUrl = config.viewerBaseUrl;
384
+ const viewerBaseUrl = asOptionalRecord(config)?.viewerBaseUrl;
392
385
  if (typeof viewerBaseUrl !== "string") return;
393
386
  const normalized = viewerBaseUrl.trim();
394
387
  return normalized ? normalizeViewerBaseUrl(normalized) : void 0;
@@ -397,48 +390,32 @@ function normalizeFontFamily(fontFamily) {
397
390
  return fontFamily?.trim() || DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily;
398
391
  }
399
392
  function normalizeDiffFontSize(fontSize) {
400
- if (fontSize === void 0 || !Number.isFinite(fontSize)) return DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize;
401
- return Math.min(Math.max(Math.floor(fontSize), 10), 24);
393
+ return clampInt(asFiniteNumber(fontSize) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize, 10, 24);
402
394
  }
403
395
  function normalizeDiffLineSpacing(lineSpacing) {
404
- if (lineSpacing === void 0 || !Number.isFinite(lineSpacing)) return DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing;
405
- return Math.min(Math.max(lineSpacing, 1), 3);
406
- }
407
- function normalizeLayout$1(layout) {
408
- return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
409
- }
410
- function normalizeDiffIndicators(diffIndicators) {
411
- return diffIndicators && DIFF_INDICATORS.includes(diffIndicators) ? diffIndicators : DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators;
412
- }
413
- function normalizeTheme$1(theme) {
414
- return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
396
+ return clampNumber(asFiniteNumber(lineSpacing) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing, 1, 3);
415
397
  }
416
398
  function normalizeFileFormat(fileFormat) {
417
399
  return fileFormat && DIFF_OUTPUT_FORMATS.includes(fileFormat) ? fileFormat : DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat;
418
400
  }
419
- function normalizeFileQuality$1(fileQuality) {
401
+ function normalizeFileQuality(fileQuality) {
420
402
  return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality;
421
403
  }
422
404
  function normalizeFileScale(fileScale, fallback) {
423
- if (fileScale === void 0 || !Number.isFinite(fileScale)) return fallback;
424
- const rounded = Math.round(fileScale * 100) / 100;
425
- return Math.min(Math.max(rounded, 1), 4);
405
+ const value = asFiniteNumber(fileScale);
406
+ return value === void 0 ? fallback : clampNumber(Math.round(value * 100) / 100, 1, 4);
426
407
  }
427
408
  function normalizeFileMaxWidth(fileMaxWidth, fallback) {
428
- if (fileMaxWidth === void 0 || !Number.isFinite(fileMaxWidth)) return fallback;
429
- return Math.min(Math.max(Math.round(fileMaxWidth), 640), 2400);
430
- }
431
- function normalizeMode$1(mode) {
432
- return mode && DIFF_MODES.includes(mode) ? mode : DEFAULT_DIFFS_TOOL_DEFAULTS.mode;
409
+ const value = asFiniteNumber(fileMaxWidth);
410
+ return value === void 0 ? fallback : clampNumber(Math.round(value), 640, 2400);
433
411
  }
434
412
  function normalizeTtlSeconds(ttlSeconds) {
435
- if (ttlSeconds === void 0 || !Number.isFinite(ttlSeconds)) return DEFAULT_DIFFS_TOOL_DEFAULTS.ttlSeconds;
436
- return Math.min(Math.max(Math.floor(ttlSeconds), 1), 21600);
413
+ return clampInt(asFiniteNumber(ttlSeconds) ?? DEFAULT_DIFFS_TOOL_DEFAULTS.ttlSeconds, 1, 21600);
437
414
  }
438
415
  function resolveDiffImageRenderOptions(params) {
439
416
  const format = normalizeFileFormat(params.fileFormat ?? params.defaults.fileFormat);
440
417
  const qualityOverrideProvided = params.fileQuality !== void 0;
441
- const qualityPreset = normalizeFileQuality$1(params.fileQuality ?? params.defaults.fileQuality);
418
+ const qualityPreset = normalizeFileQuality(params.fileQuality ?? params.defaults.fileQuality);
442
419
  const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset];
443
420
  return {
444
421
  format,
@@ -477,7 +454,7 @@ function createDiffsHttpHandler(params) {
477
454
  maxEntries: VIEWER_LIMITER_MAX_KEYS
478
455
  });
479
456
  return async (req, res) => {
480
- const parsed = parseRequestUrl(req.url);
457
+ const parsed = req.url ? URL.parse(req.url, "http://127.0.0.1") : null;
481
458
  if (!parsed) return false;
482
459
  if (parsed.pathname.startsWith("/plugins/diffs/assets/")) return await serveAsset(req, res, parsed.pathname, params.logger);
483
460
  if (!parsed.pathname.startsWith(VIEW_PREFIX)) return false;
@@ -537,14 +514,6 @@ function createDiffsHttpHandler(params) {
537
514
  }
538
515
  };
539
516
  }
540
- function parseRequestUrl(rawUrl) {
541
- if (!rawUrl) return null;
542
- try {
543
- return new URL(rawUrl, "http://127.0.0.1");
544
- } catch {
545
- return null;
546
- }
547
- }
548
517
  async function serveAsset(req, res, pathname, logger) {
549
518
  if (req.method !== "GET" && req.method !== "HEAD") {
550
519
  respondText(res, 405, "Method not allowed");
@@ -1188,12 +1157,6 @@ function buildDiffTitle(input) {
1188
1157
  if (input.kind === "before_after") return input.path?.trim() || "Text diff";
1189
1158
  return "Patch diff";
1190
1159
  }
1191
- function resolveBeforeAfterFileName(params) {
1192
- const { input, lang } = params;
1193
- if (input.path?.trim()) return input.path.trim();
1194
- if (lang && lang !== "text") return `diff.${lang.replace(/^\.+/, "")}`;
1195
- return DEFAULT_FILE_NAME;
1196
- }
1197
1160
  function resolveDiffTypography(presentation) {
1198
1161
  const fontSize = normalizeDiffFontSize(presentation.fontSize);
1199
1162
  const lineSpacing = normalizeDiffLineSpacing(presentation.lineSpacing);
@@ -1574,43 +1537,29 @@ async function renderSection(input, { viewerOptions, imageOptions, preloadOption
1574
1537
  usesLanguagePack: payload.langs.some((lang) => !isBaseDiffViewerLanguage(lang))
1575
1538
  };
1576
1539
  }
1577
- function buildRenderedBodies(sections, leadingHtml) {
1578
- const lead = leadingHtml ? [leadingHtml] : [];
1579
- const viewerSections = sections.flatMap((section) => section.viewer ? [section.viewer] : []);
1580
- const imageSections = sections.flatMap((section) => section.image ? [section.image] : []);
1581
- return {
1582
- ...viewerSections.length > 0 ? { viewerBodyHtml: [...lead, ...viewerSections].join("\n") } : {},
1583
- ...imageSections.length > 0 ? { imageBodyHtml: [...lead, ...imageSections].join("\n") } : {}
1584
- };
1585
- }
1586
1540
  async function renderBeforeAfterDiff(input, options, target) {
1587
1541
  const languagePackAvailable = options.languagePackAvailable === true;
1588
1542
  const lang = await normalizeSupportedLanguageHint(input.lang, { languagePackAvailable });
1589
- const fileName = resolveBeforeAfterFileName({
1590
- input,
1591
- lang
1592
- });
1593
- const section = await renderSection({
1594
- oldFile: {
1595
- name: fileName,
1596
- contents: input.before,
1597
- ...lang ? { lang } : {}
1598
- },
1599
- newFile: {
1600
- name: fileName,
1601
- contents: input.after,
1602
- ...lang ? { lang } : {}
1603
- }
1604
- }, buildRenderVariants(options, target), languagePackAvailable);
1543
+ const fileName = input.path?.trim() || (lang && lang !== "text" ? `diff.${lang.replace(/^\.+/, "")}` : DEFAULT_FILE_NAME);
1605
1544
  return {
1606
- ...buildRenderedBodies([section]),
1607
- fileCount: 1,
1608
- usesLanguagePack: section.usesLanguagePack === true
1545
+ sections: [await renderSection({
1546
+ oldFile: {
1547
+ name: fileName,
1548
+ contents: input.before,
1549
+ ...lang ? { lang } : {}
1550
+ },
1551
+ newFile: {
1552
+ name: fileName,
1553
+ contents: input.after,
1554
+ ...lang ? { lang } : {}
1555
+ }
1556
+ }, buildRenderVariants(options, target), languagePackAvailable)],
1557
+ leadingHtml: void 0
1609
1558
  };
1610
1559
  }
1611
1560
  async function renderPatchDiff(input, options, target) {
1612
1561
  const languagePackAvailable = options.languagePackAvailable === true;
1613
- const files = await Promise.all(parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []).map((fileDiff) => normalizePatchFileLanguage(fileDiff, { languagePackAvailable })));
1562
+ const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []);
1614
1563
  if (files.length === 0) throw new DiffRenderInputError("Patch input did not contain any file diffs.");
1615
1564
  if (files.length > MAX_PATCH_FILE_COUNT) throw new DiffRenderInputError(`Patch input contains too many files (max ${MAX_PATCH_FILE_COUNT}).`);
1616
1565
  if (files.reduce((sum, fileDiff) => {
@@ -1624,39 +1573,34 @@ async function renderPatchDiff(input, options, target) {
1624
1573
  fileDiff,
1625
1574
  stats: computeFileDiffStats(fileDiff)
1626
1575
  }));
1627
- const sections = await Promise.all(files.map((fileDiff, index) => renderSection({ fileDiff }, variants, languagePackAvailable, navEntries[index]?.anchorId)));
1628
1576
  return {
1629
- ...buildRenderedBodies(sections, files.length > 1 ? renderFileSummaryNav(navEntries) : void 0),
1630
- fileCount: files.length,
1631
- usesLanguagePack: sections.some((section) => section.usesLanguagePack === true)
1632
- };
1633
- }
1634
- async function normalizePatchFileLanguage(fileDiff, options) {
1635
- const lang = await normalizeSupportedLanguageHint(fileDiff.lang, options);
1636
- if (lang === fileDiff.lang) return fileDiff;
1637
- return {
1638
- ...fileDiff,
1639
- ...lang ? { lang } : { lang: "text" }
1577
+ sections: await Promise.all(files.map((fileDiff, index) => renderSection({ fileDiff }, variants, languagePackAvailable, navEntries[index]?.anchorId))),
1578
+ leadingHtml: files.length > 1 ? renderFileSummaryNav(navEntries) : void 0
1640
1579
  };
1641
1580
  }
1642
1581
  async function renderDiffDocument(input, options, target = "both") {
1643
1582
  const title = buildDiffTitle(input);
1644
- const rendered = input.kind === "before_after" ? await renderBeforeAfterDiff(input, options, target) : await renderPatchDiff(input, options, target);
1645
- const viewerRuntime = rendered.usesLanguagePack ? "language-pack" : "base";
1583
+ const { sections, leadingHtml } = input.kind === "before_after" ? await renderBeforeAfterDiff(input, options, target) : await renderPatchDiff(input, options, target);
1584
+ const lead = leadingHtml ? [leadingHtml] : [];
1585
+ const viewerSections = sections.flatMap((section) => section.viewer ? [section.viewer] : []);
1586
+ const imageSections = sections.flatMap((section) => section.image ? [section.image] : []);
1587
+ const viewerBodyHtml = viewerSections.length ? [...lead, ...viewerSections].join("\n") : void 0;
1588
+ const imageBodyHtml = imageSections.length ? [...lead, ...imageSections].join("\n") : void 0;
1589
+ const viewerRuntime = sections.some((section) => section.usesLanguagePack) ? "language-pack" : "base";
1646
1590
  const imageTypography = resolveDiffTypography(buildImageRenderOptions(options).presentation);
1647
1591
  return {
1648
- ...rendered.viewerBodyHtml ? { html: buildHtmlDocument({
1592
+ ...viewerBodyHtml ? { html: buildHtmlDocument({
1649
1593
  title,
1650
- bodyHtml: rendered.viewerBodyHtml,
1594
+ bodyHtml: viewerBodyHtml,
1651
1595
  theme: options.presentation.theme,
1652
1596
  imageMaxWidth: options.image.maxWidth,
1653
1597
  imageTypography,
1654
1598
  runtimeMode: "viewer",
1655
1599
  viewerRuntime
1656
1600
  }) } : {},
1657
- ...rendered.imageBodyHtml ? { imageHtml: buildHtmlDocument({
1601
+ ...imageBodyHtml ? { imageHtml: buildHtmlDocument({
1658
1602
  title,
1659
- bodyHtml: rendered.imageBodyHtml,
1603
+ bodyHtml: imageBodyHtml,
1660
1604
  theme: options.presentation.theme,
1661
1605
  imageMaxWidth: options.image.maxWidth,
1662
1606
  imageTypography,
@@ -1664,20 +1608,17 @@ async function renderDiffDocument(input, options, target = "both") {
1664
1608
  viewerRuntime
1665
1609
  }) } : {},
1666
1610
  title,
1667
- fileCount: rendered.fileCount,
1611
+ fileCount: sections.length,
1668
1612
  inputKind: input.kind,
1669
1613
  viewerRuntime
1670
1614
  };
1671
1615
  }
1672
- function shouldFallbackToClientHydration(error) {
1673
- return error instanceof TypeError && error.message.includes("needs an import attribute of \"type: json\"");
1674
- }
1675
1616
  async function preloadDiffHTMLWithFallback(params) {
1676
1617
  try {
1677
1618
  return await preloadDiffHTML(params);
1678
1619
  } catch (error) {
1679
- if (!shouldFallbackToClientHydration(error)) throw error;
1680
- return "";
1620
+ if (error instanceof TypeError && error.message.includes("needs an import attribute of \"type: json\"")) return "";
1621
+ throw error;
1681
1622
  }
1682
1623
  }
1683
1624
  //#endregion
@@ -1688,7 +1629,7 @@ const MAX_TITLE_BYTES = 1024;
1688
1629
  const MAX_PATH_BYTES = 2048;
1689
1630
  const MAX_LANG_BYTES = 128;
1690
1631
  const MAX_DIFF_ARTIFACT_TTL_SECONDS = 21600;
1691
- const loadDiffsBrowserRuntime = createLazyRuntimeModule(() => import("./.setup/browser.runtime-v15gbeC0.mjs"));
1632
+ const loadDiffsBrowserRuntime = createLazyRuntimeModule(() => import("./.setup/browser.runtime-B8JeyUXz.mjs"));
1692
1633
  const DiffsToolSchema = Type.Object({
1693
1634
  before: Type.Optional(Type.String({ description: "Original text content." })),
1694
1635
  after: Type.Optional(Type.String({ description: "Updated text content." })),
@@ -1732,7 +1673,6 @@ const DiffsToolSchema = Type.Object({
1732
1673
  baseUrl: Type.Optional(Type.String({ description: "Optional gateway base URL override used when building the viewer URL. Overrides configured viewerBaseUrl, for example https://gateway.example.com." }))
1733
1674
  }, { additionalProperties: false });
1734
1675
  function createDiffsTool(params) {
1735
- const loadScreenshotter = async (config) => params.screenshotter ?? new (await (loadDiffsBrowserRuntime())).PlaywrightDiffScreenshotter({ config });
1736
1676
  return {
1737
1677
  name: "diffs",
1738
1678
  label: "Diffs",
@@ -1744,19 +1684,13 @@ function createDiffsTool(params) {
1744
1684
  const rawRecord = toolParams;
1745
1685
  const artifactContext = buildArtifactContext(params.context);
1746
1686
  const input = normalizeDiffInput(toolParams);
1747
- if (input.kind === "before_after" && input.before === input.after) return {
1748
- content: [{
1749
- type: "text",
1750
- text: "Before and after are identical — no changes to render."
1751
- }],
1752
- details: {
1753
- changed: false,
1754
- ...artifactContext ? { context: artifactContext } : {}
1755
- }
1756
- };
1757
- const mode = normalizeMode(toolParams.mode, params.defaults.mode);
1758
- const theme = normalizeTheme(toolParams.theme, params.defaults.theme);
1759
- const layout = normalizeLayout(toolParams.layout, params.defaults.layout);
1687
+ if (input.kind === "before_after" && input.before === input.after) return textResult("Before and after are identical — no changes to render.", {
1688
+ changed: false,
1689
+ ...artifactContext ? { context: artifactContext } : {}
1690
+ });
1691
+ const mode = DIFF_MODES.find((value) => value === toolParams.mode) ?? params.defaults.mode;
1692
+ const theme = DIFF_THEMES.find((value) => value === toolParams.theme) ?? params.defaults.theme;
1693
+ const layout = DIFF_LAYOUTS.find((value) => value === toolParams.layout) ?? params.defaults.layout;
1760
1694
  const expandUnchanged = toolParams.expandUnchanged === true;
1761
1695
  const ttlSeconds = readFiniteNumberParam(rawRecord, "ttlSeconds") ?? params.defaults.ttlSeconds;
1762
1696
  const fileScale = readFiniteNumberParam(rawRecord, "fileScale");
@@ -1764,8 +1698,8 @@ function createDiffsTool(params) {
1764
1698
  const ttlMs = normalizeTtlMs(ttlSeconds);
1765
1699
  const image = resolveDiffImageRenderOptions({
1766
1700
  defaults: params.defaults,
1767
- fileFormat: normalizeOutputFormat(toolParams.fileFormat),
1768
- fileQuality: normalizeFileQuality(toolParams.fileQuality),
1701
+ fileFormat: DIFF_OUTPUT_FORMATS.find((value) => value === toolParams.fileFormat),
1702
+ fileQuality: DIFF_IMAGE_QUALITY_PRESETS.find((value) => value === toolParams.fileQuality),
1769
1703
  fileScale,
1770
1704
  fileMaxWidth
1771
1705
  });
@@ -1809,72 +1743,65 @@ function createDiffsTool(params) {
1809
1743
  mode,
1810
1744
  ...artifactContext ? { context: artifactContext } : {}
1811
1745
  } : void 0;
1812
- if (mode === "view") return {
1813
- content: [{
1814
- type: "text",
1815
- text: `Diff viewer ready.\n${viewerUrl}`
1816
- }],
1817
- details: viewerDetails
1818
- };
1746
+ if (mode === "view") return textResult(`Diff viewer ready.\n${viewerUrl}`, viewerDetails);
1819
1747
  try {
1820
- const artifactFile = await renderDiffArtifactFile({
1821
- screenshotter: await loadScreenshotter(config),
1822
- store: params.store,
1823
- html: requireRenderedHtml(rendered.imageHtml, "image"),
1824
- theme,
1825
- image,
1748
+ const screenshotter = params.screenshotter ?? new (await (loadDiffsBrowserRuntime())).PlaywrightDiffScreenshotter({ config });
1749
+ const html = requireRenderedHtml(rendered.imageHtml, "image");
1750
+ const artifactFile = await params.store.createStandaloneFileArtifact({
1751
+ format: image.format,
1826
1752
  ttlMs,
1827
1753
  context: artifactContext
1828
1754
  });
1829
- return {
1830
- content: [{
1831
- type: "text",
1832
- text: buildFileArtifactMessage({
1833
- format: image.format,
1834
- filePath: artifactFile.path,
1835
- viewerUrl
1836
- })
1837
- }],
1838
- details: buildArtifactDetails({
1839
- baseDetails: viewerDetails ?? {
1840
- changed: true,
1841
- ...artifactFile.artifactId ? { artifactId: artifactFile.artifactId } : {},
1842
- ...artifactFile.expiresAt ? { expiresAt: artifactFile.expiresAt } : {},
1843
- title: rendered.title,
1844
- inputKind: rendered.inputKind,
1845
- fileCount: rendered.fileCount,
1846
- mode,
1847
- ...artifactContext ? { context: artifactContext } : {}
1848
- },
1849
- artifactFile,
1755
+ let fileBytes;
1756
+ try {
1757
+ await screenshotter.screenshotHtml({
1758
+ html,
1759
+ outputPath: artifactFile.filePath,
1760
+ theme,
1850
1761
  image
1851
- })
1852
- };
1762
+ });
1763
+ fileBytes = (await fs$1.stat(artifactFile.filePath)).size;
1764
+ await params.store.completeFileArtifact(artifactFile.id);
1765
+ } catch (error) {
1766
+ await params.store.deleteFileArtifact(artifactFile.id);
1767
+ throw error;
1768
+ }
1769
+ return textResult([
1770
+ ...viewerUrl ? [`Diff viewer: ${viewerUrl}`] : [],
1771
+ `Diff ${image.format.toUpperCase()} generated at: ${artifactFile.filePath}`,
1772
+ "To send this file, use an available file-sending tool to send it as an attachment."
1773
+ ].join("\n"), {
1774
+ ...viewerDetails ?? {
1775
+ changed: true,
1776
+ artifactId: artifactFile.id,
1777
+ expiresAt: artifactFile.expiresAt,
1778
+ title: rendered.title,
1779
+ inputKind: rendered.inputKind,
1780
+ fileCount: rendered.fileCount,
1781
+ mode,
1782
+ ...artifactContext ? { context: artifactContext } : {}
1783
+ },
1784
+ filePath: artifactFile.filePath,
1785
+ path: artifactFile.filePath,
1786
+ fileBytes,
1787
+ fileFormat: image.format,
1788
+ fileQuality: image.qualityPreset,
1789
+ fileScale: image.scale,
1790
+ fileMaxWidth: image.maxWidth
1791
+ });
1853
1792
  } catch (error) {
1854
1793
  if (mode === "both") {
1855
1794
  const errorMessage = formatErrorMessage(error);
1856
- return {
1857
- content: [{
1858
- type: "text",
1859
- text: `Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`
1860
- }],
1861
- details: {
1862
- ...viewerDetails,
1863
- fileError: errorMessage
1864
- }
1865
- };
1795
+ return textResult(`Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`, {
1796
+ ...viewerDetails,
1797
+ fileError: errorMessage
1798
+ });
1866
1799
  }
1867
1800
  throw error;
1868
1801
  }
1869
1802
  }
1870
1803
  };
1871
1804
  }
1872
- function normalizeFileQuality(fileQuality) {
1873
- return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : void 0;
1874
- }
1875
- function normalizeOutputFormat(format) {
1876
- return format && DIFF_OUTPUT_FORMATS.includes(format) ? format : void 0;
1877
- }
1878
1805
  function isArtifactOnlyMode(mode) {
1879
1806
  return mode === "image" || mode === "file";
1880
1807
  }
@@ -1887,62 +1814,18 @@ function requireRenderedHtml(html, target) {
1887
1814
  if (html !== void 0) return html;
1888
1815
  throw new Error(`Missing ${target} render output.`);
1889
1816
  }
1890
- function buildArtifactDetails(params) {
1891
- return {
1892
- ...params.baseDetails,
1893
- filePath: params.artifactFile.path,
1894
- path: params.artifactFile.path,
1895
- fileBytes: params.artifactFile.bytes,
1896
- fileFormat: params.image.format,
1897
- fileQuality: params.image.qualityPreset,
1898
- fileScale: params.image.scale,
1899
- fileMaxWidth: params.image.maxWidth
1900
- };
1901
- }
1902
- function buildFileArtifactMessage(params) {
1903
- const lines = params.viewerUrl ? [`Diff viewer: ${params.viewerUrl}`] : [];
1904
- lines.push(`Diff ${params.format.toUpperCase()} generated at: ${params.filePath}`);
1905
- lines.push("To send this file, use an available file-sending tool to send it as an attachment.");
1906
- return lines.join("\n");
1907
- }
1908
- async function renderDiffArtifactFile(params) {
1909
- const fileArtifact = await params.store.createStandaloneFileArtifact({
1910
- format: params.image.format,
1911
- ttlMs: params.ttlMs,
1912
- context: params.context
1913
- });
1914
- try {
1915
- await params.screenshotter.screenshotHtml({
1916
- html: params.html,
1917
- outputPath: fileArtifact.filePath,
1918
- theme: params.theme,
1919
- image: params.image
1920
- });
1921
- const stats = await fs$1.stat(fileArtifact.filePath);
1922
- await params.store.completeFileArtifact(fileArtifact.id);
1923
- return {
1924
- path: fileArtifact.filePath,
1925
- bytes: stats.size,
1926
- artifactId: fileArtifact.id,
1927
- expiresAt: fileArtifact.expiresAt
1928
- };
1929
- } catch (error) {
1930
- await params.store.deleteFileArtifact(fileArtifact.id);
1931
- throw error;
1932
- }
1933
- }
1934
1817
  function buildArtifactContext(context) {
1935
1818
  if (!context) return;
1936
- const agentId = normalizeOptionalString(context.agentId);
1937
- const sessionId = normalizeOptionalString(context.sessionId);
1938
- const messageChannel = normalizeOptionalString(context.messageChannel);
1939
- const agentAccountId = normalizeOptionalString(context.agentAccountId);
1940
- const artifactContext = {
1941
- ...agentId ? { agentId } : {},
1942
- ...sessionId ? { sessionId } : {},
1943
- ...messageChannel ? { messageChannel } : {},
1944
- ...agentAccountId ? { agentAccountId } : {}
1945
- };
1819
+ const artifactContext = {};
1820
+ for (const key of [
1821
+ "agentId",
1822
+ "sessionId",
1823
+ "messageChannel",
1824
+ "agentAccountId"
1825
+ ]) {
1826
+ const value = normalizeOptionalString(context[key]);
1827
+ if (value) artifactContext[key] = value;
1828
+ }
1946
1829
  return Object.keys(artifactContext).length > 0 ? artifactContext : void 0;
1947
1830
  }
1948
1831
  function normalizeDiffInput(params) {
@@ -1991,15 +1874,6 @@ function normalizeBaseUrl(baseUrl) {
1991
1874
  throw new PluginToolInputError(`Invalid baseUrl: ${normalized}`);
1992
1875
  }
1993
1876
  }
1994
- function normalizeMode(mode, fallback) {
1995
- return mode && DIFF_MODES.includes(mode) ? mode : fallback;
1996
- }
1997
- function normalizeTheme(theme, fallback) {
1998
- return theme && DIFF_THEMES.includes(theme) ? theme : fallback;
1999
- }
2000
- function normalizeLayout(layout, fallback) {
2001
- return layout && DIFF_LAYOUTS.includes(layout) ? layout : fallback;
2002
- }
2003
1877
  function normalizeTtlMs(ttlSeconds) {
2004
1878
  if (!Number.isFinite(ttlSeconds) || ttlSeconds === void 0) return;
2005
1879
  return Math.floor(Math.min(Math.max(ttlSeconds, 1), MAX_DIFF_ARTIFACT_TTL_SECONDS) * 1e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/diffs",
3
- "version": "2026.9.8",
3
+ "version": "2026.10.1-beta.2",
4
4
  "description": "OpenClaw read-only diff viewer plugin and file renderer for agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,14 +25,14 @@
25
25
  "minHostVersion": ">=2026.4.30"
26
26
  },
27
27
  "compat": {
28
- "pluginApi": ">=2026.9.8"
28
+ "pluginApi": ">=2026.10.1-beta.2"
29
29
  },
30
30
  "assetScripts": {
31
31
  "build": "node --import tsx ../../scripts/build-diffs-viewer-runtime.mts curated"
32
32
  },
33
33
  "build": {
34
34
  "bundledDist": false,
35
- "openclawVersion": "2026.9.8",
35
+ "openclawVersion": "2026.10.1-beta.2",
36
36
  "staticAssets": [
37
37
  {
38
38
  "source": "./assets/viewer-runtime.js",
@@ -57,7 +57,7 @@
57
57
  "skills/**"
58
58
  ],
59
59
  "peerDependencies": {
60
- "openclaw": ">=2026.9.8"
60
+ "openclaw": ">=2026.10.1-beta.2"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "openclaw": {