@openclaw/diffs 2026.7.2-beta.7 → 2026.7.34

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.
Files changed (122) hide show
  1. package/README.md +9 -2
  2. package/dist/assets/viewer-runtime.js +31 -31
  3. package/dist/index.js +307 -290
  4. package/node_modules/@pierre/diffs/node_modules/@shikijs/core/package.json +3 -3
  5. package/node_modules/@pierre/diffs/node_modules/@shikijs/engine-javascript/package.json +2 -2
  6. package/node_modules/@pierre/diffs/node_modules/@shikijs/engine-oniguruma/package.json +2 -2
  7. package/node_modules/@pierre/diffs/node_modules/@shikijs/primitive/dist/index.mjs +5 -6
  8. package/node_modules/@pierre/diffs/node_modules/@shikijs/primitive/package.json +2 -2
  9. package/node_modules/@pierre/diffs/node_modules/@shikijs/themes/package.json +2 -2
  10. package/node_modules/@pierre/diffs/node_modules/@shikijs/transformers/package.json +3 -3
  11. package/node_modules/@pierre/diffs/node_modules/@shikijs/types/dist/index.d.mts +1 -9
  12. package/node_modules/@pierre/diffs/node_modules/@shikijs/types/package.json +1 -1
  13. package/node_modules/@pierre/diffs/node_modules/@types/hast/README.md +2 -2
  14. package/node_modules/@pierre/diffs/node_modules/@types/hast/index.d.ts +0 -642
  15. package/node_modules/@pierre/diffs/node_modules/@types/hast/package.json +3 -9
  16. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/cjs/deserialize.js +1 -6
  17. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/cjs/serialize.js +2 -2
  18. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/esm/deserialize.js +1 -6
  19. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/esm/serialize.js +2 -2
  20. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/package.json +1 -1
  21. package/node_modules/@pierre/diffs/node_modules/@ungap/structured-clone/structured-json.js +1 -1
  22. package/node_modules/@pierre/diffs/node_modules/shiki/package.json +7 -7
  23. package/node_modules/@shikijs/langs/node_modules/@shikijs/types/dist/index.d.mts +1 -9
  24. package/node_modules/@shikijs/langs/node_modules/@shikijs/types/package.json +1 -1
  25. package/node_modules/@shikijs/langs/node_modules/@types/hast/README.md +2 -2
  26. package/node_modules/@shikijs/langs/node_modules/@types/hast/index.d.ts +0 -642
  27. package/node_modules/@shikijs/langs/node_modules/@types/hast/package.json +3 -9
  28. package/node_modules/@shikijs/langs/package.json +2 -2
  29. package/node_modules/playwright-core/browsers.json +18 -12
  30. package/node_modules/playwright-core/lib/bootstrap.js +1 -1
  31. package/node_modules/playwright-core/lib/coreBundle.js +12247 -13691
  32. package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +2739 -0
  33. package/node_modules/playwright-core/lib/server/electron/loader.js +2 -5
  34. package/node_modules/playwright-core/lib/serverRegistry.js +1 -5
  35. package/node_modules/playwright-core/lib/tools/cli-client/help.json +6 -21
  36. package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
  37. package/node_modules/playwright-core/lib/tools/cli-client/session.js +0 -4
  38. package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/SKILL.md +5 -21
  39. package/node_modules/playwright-core/lib/tools/{skills/playwright-cli/references/test-generation.md → cli-client/skill/references/spec-driven-testing.md} +7 -135
  40. package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +134 -0
  41. package/node_modules/playwright-core/lib/tools/utils/extension.js +8 -31
  42. package/node_modules/playwright-core/lib/utilsBundle.js +48599 -52627
  43. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +268 -391
  44. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +1 -0
  45. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +52 -0
  46. package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
  47. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +1 -2
  48. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +55 -15
  49. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +1 -0
  50. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +32 -0
  51. package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +1 -0
  52. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +193 -0
  53. package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
  54. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +32 -0
  55. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +264 -0
  56. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +1 -0
  57. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +9 -0
  58. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +1 -0
  59. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +1 -0
  60. package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +1 -0
  61. package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +2 -0
  62. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
  63. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
  64. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +2 -0
  65. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -3
  66. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +1 -0
  67. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +6 -0
  68. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
  69. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +32 -0
  70. package/node_modules/playwright-core/lib/xdg-open +137 -338
  71. package/node_modules/playwright-core/package.json +2 -2
  72. package/node_modules/playwright-core/types/protocol.d.ts +36 -249
  73. package/node_modules/playwright-core/types/structs.d.ts +1 -3
  74. package/node_modules/playwright-core/types/types.d.ts +268 -2011
  75. package/node_modules/typebox/build/compile/validator.d.mts +4 -1
  76. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  77. package/node_modules/typebox/build/guard/guard.mjs +2 -5
  78. package/node_modules/typebox/build/guard/string.mjs +4 -16
  79. package/node_modules/typebox/build/schema/engine/_functions.mjs +15 -34
  80. package/node_modules/typebox/build/type/action/readonly_object.d.mts +0 -5
  81. package/node_modules/typebox/build/type/action/readonly_object.mjs +0 -8
  82. package/node_modules/typebox/build/typebox.d.mts +1 -1
  83. package/node_modules/typebox/build/typebox.mjs +1 -1
  84. package/node_modules/typebox/package.json +1 -1
  85. package/node_modules/typebox/readme.md +32 -29
  86. package/openclaw.plugin.json +0 -1
  87. package/package.json +7 -7
  88. package/skills/diffs/SKILL.md +2 -2
  89. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +0 -143
  90. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +0 -144
  91. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +0 -85
  92. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +0 -67
  93. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +0 -75
  94. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +0 -11
  95. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +0 -1
  96. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +0 -1
  97. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +0 -32
  98. package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +0 -129
  99. package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +0 -1
  100. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +0 -32
  101. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +0 -181
  102. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +0 -1
  103. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +0 -7
  104. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +0 -1
  105. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +0 -1
  106. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +0 -1
  107. package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +0 -1
  108. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +0 -1
  109. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +0 -1
  110. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +0 -5
  111. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +0 -1
  112. package/node_modules/playwright-core/lib/webp_codec.LICENSE +0 -173
  113. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  114. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/element-attributes.md +0 -0
  115. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/playwright-tests.md +0 -0
  116. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/request-mocking.md +0 -0
  117. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/running-code.md +0 -0
  118. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/session-management.md +0 -0
  119. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/storage-state.md +0 -0
  120. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/tracing.md +0 -0
  121. /package/node_modules/playwright-core/lib/tools/{skills/playwright-cli → cli-client/skill}/references/video-recording.md +0 -0
  122. /package/node_modules/playwright-core/lib/tools/{skills/playwright-trace → trace}/SKILL.md +0 -0
package/dist/index.js CHANGED
@@ -6,22 +6,19 @@ import { z } from "zod";
6
6
  import fs, { constants } from "node:fs";
7
7
  import path from "node:path";
8
8
  import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
9
- import { isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
10
9
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
11
10
  import crypto from "node:crypto";
12
11
  import fs$1 from "node:fs/promises";
13
12
  import { fileURLToPath } from "node:url";
14
- import { gunzip, gzip } from "node:zlib";
15
13
  import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
16
- import { safeEqualSecret, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
14
+ import { root, safeEqualSecret, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
17
15
  import { optionalFiniteNumberSchema, stringEnum } from "openclaw/plugin-sdk/channel-actions";
18
16
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
19
17
  import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
20
18
  import { Type } from "typebox";
21
19
  import { chromium } from "playwright-core";
22
20
  import { parsePatchFiles, resolveLanguage } from "@pierre/diffs";
23
- import { preloadDiffHTML } from "@pierre/diffs/ssr";
24
- import { escapeHtml } from "openclaw/plugin-sdk/text-utility-runtime";
21
+ import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
25
22
  //#region extensions/diffs/src/types.ts
26
23
  const DIFF_LAYOUTS = ["unified", "split"];
27
24
  const DIFF_MODES = [
@@ -287,15 +284,15 @@ function normalizeTtlSeconds(ttlSeconds) {
287
284
  return Math.min(Math.max(Math.floor(ttlSeconds), 1), 21600);
288
285
  }
289
286
  function resolveDiffImageRenderOptions(params) {
290
- const format = normalizeFileFormat(params.fileFormat ?? params.defaults.fileFormat);
291
- const qualityOverrideProvided = params.fileQuality !== void 0;
292
- const qualityPreset = normalizeFileQuality$1(params.fileQuality ?? params.defaults.fileQuality);
287
+ const format = normalizeFileFormat(params.fileFormat ?? params.imageFormat ?? params.format ?? params.defaults.fileFormat);
288
+ const qualityOverrideProvided = params.fileQuality !== void 0 || params.imageQuality !== void 0;
289
+ const qualityPreset = normalizeFileQuality$1(params.fileQuality ?? params.imageQuality ?? params.defaults.fileQuality);
293
290
  const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset];
294
291
  return {
295
292
  format,
296
293
  qualityPreset,
297
- scale: normalizeFileScale(params.fileScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale),
298
- maxWidth: normalizeFileMaxWidth(params.fileMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth),
294
+ scale: normalizeFileScale(params.fileScale ?? params.imageScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale),
295
+ maxWidth: normalizeFileMaxWidth(params.fileMaxWidth ?? params.imageMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth),
299
296
  maxPixels: profile.maxPixels
300
297
  };
301
298
  }
@@ -474,20 +471,19 @@ function createDiffsHttpHandler(params) {
474
471
  respondText(res, 404, "Diff not found");
475
472
  return true;
476
473
  }
474
+ if (!await params.store.getArtifact(id, token)) {
475
+ recordRemoteFailure(viewerFailureLimiter, access);
476
+ respondText(res, 404, "Diff not found or expired");
477
+ return true;
478
+ }
477
479
  try {
478
- const viewer = await params.store.readAuthorizedViewer(id, token);
479
- if (!viewer) {
480
- recordRemoteFailure(viewerFailureLimiter, access);
481
- respondText(res, 404, "Diff not found or expired");
482
- return true;
483
- }
480
+ const html = await params.store.readHtml(id);
484
481
  resetRemoteFailures(viewerFailureLimiter, access);
485
482
  res.statusCode = 200;
486
483
  setSharedHeaders(res, "text/html; charset=utf-8");
487
484
  res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
488
- res.setHeader("content-length", String(viewer.html.byteLength));
489
485
  if (req.method === "HEAD") res.end();
490
- else res.end(Buffer.from(viewer.html));
486
+ else res.end(html);
491
487
  return true;
492
488
  } catch (error) {
493
489
  recordRemoteFailure(viewerFailureLimiter, access);
@@ -544,7 +540,7 @@ function normalizeRemoteClientKey(remoteAddress) {
544
540
  return normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
545
541
  }
546
542
  function isLoopbackClientIp(clientIp) {
547
- return isLoopbackHost(clientIp);
543
+ return clientIp === "127.0.0.1" || clientIp === "::1";
548
544
  }
549
545
  function hasProxyForwardingHints(req) {
550
546
  const headers = req.headers ?? {};
@@ -627,177 +623,144 @@ const DIFFS_AGENT_GUIDANCE = [
627
623
  "When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
628
624
  "It accepts either `before` + `after` text or a unified `patch`.",
629
625
  "Check `details.changed`: identical before/after input returns `false` without creating an artifact; rendered results return `true`.",
630
- "`mode=view` returns `details.viewerUrl` for interactive viewing; `mode=file` returns `details.filePath`; `mode=both` returns both.",
631
- "To send the rendered file, use an available file-sending tool to send `details.filePath` as an attachment.",
626
+ "`mode=view` returns `details.viewerUrl` for canvas use; `mode=file` returns `details.filePath`; `mode=both` returns both.",
627
+ "If you need to send the rendered file, use the `message` tool with `path` or `filePath`.",
632
628
  "Include `path` when you know the filename, and omit presentation overrides unless needed."
633
629
  ].join("\n");
634
630
  //#endregion
635
631
  //#region extensions/diffs/src/store.ts
636
- const DEFAULT_TTL_MS = 1800 * 1e3;
637
- const MAX_TTL_MS = 360 * 60 * 1e3;
638
- const SWEEP_FALLBACK_AGE_MS = 1440 * 60 * 1e3;
639
- const DEFAULT_CLEANUP_INTERVAL_MS = 300 * 1e3;
640
- const MAX_DECODED_HTML_BYTES = 64 * 1024 * 1024;
641
- const ARTIFACT_ID_ATTEMPTS = 8;
632
+ const DEFAULT_TTL_MS = 18e5;
633
+ const MAX_TTL_MS = 216e5;
634
+ const SWEEP_FALLBACK_AGE_MS = 864e5;
635
+ const DEFAULT_CLEANUP_INTERVAL_MS = 3e5;
642
636
  const VIEWER_PREFIX = "/plugins/diffs/view";
643
- const EMPTY_BLOB = /* @__PURE__ */ new Uint8Array();
644
- function isBlobLimitError(error) {
645
- return typeof error === "object" && error !== null && "code" in error && error.code === "PLUGIN_BLOB_LIMIT_EXCEEDED";
646
- }
647
637
  var DiffArtifactStore = class {
648
638
  constructor(params) {
649
- this.renderingFileIds = /* @__PURE__ */ new Set();
650
639
  this.cleanupInFlight = null;
651
640
  this.nextCleanupAt = 0;
652
641
  this.rootDir = path.resolve(params.rootDir);
653
- this.blobStore = params.blobStore;
654
642
  this.logger = params.logger;
655
643
  this.cleanupIntervalMs = params.cleanupIntervalMs === void 0 ? DEFAULT_CLEANUP_INTERVAL_MS : Math.max(0, Math.floor(params.cleanupIntervalMs));
656
644
  }
657
645
  async createArtifact(params) {
658
- const html = Buffer.from(params.html, "utf8");
659
- if (html.byteLength > MAX_DECODED_HTML_BYTES) throw new Error(`Diff viewer HTML exceeds ${MAX_DECODED_HTML_BYTES} bytes.`);
660
- const compressedHtml = await gzipAsync(html);
646
+ await this.ensureRoot();
647
+ const id = crypto.randomBytes(10).toString("hex");
661
648
  const token = crypto.randomBytes(24).toString("hex");
649
+ const artifactDir = this.artifactDir(id);
650
+ const htmlPath = path.join(artifactDir, "viewer.html");
662
651
  const ttlMs = normalizeTtlMs$1(params.ttlMs);
663
- const metadata = {
664
- version: 1,
665
- kind: "viewer",
666
- encoding: "gzip",
667
- tokenHash: hashToken(token),
652
+ const createdAt = /* @__PURE__ */ new Date();
653
+ const createdAtIso = createdAt.toISOString();
654
+ const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
655
+ const meta = {
656
+ id,
657
+ token,
668
658
  title: params.title,
669
659
  inputKind: params.inputKind,
670
660
  fileCount: params.fileCount,
671
- decodedBytes: html.byteLength,
661
+ createdAt: createdAtIso,
662
+ expiresAt,
663
+ viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
664
+ htmlPath,
672
665
  ...params.context ? { context: params.context } : {}
673
666
  };
674
- const entry = await this.registerUnique(compressedHtml, metadata, ttlMs);
667
+ const root = await this.artifactRoot();
668
+ await root.mkdir(id);
669
+ await root.write(path.posix.join(id, "viewer.html"), params.html);
670
+ await this.writeMeta(meta);
675
671
  this.scheduleCleanup();
676
- return viewerEntryToMeta(entry, token);
672
+ return meta;
677
673
  }
678
- async readAuthorizedViewer(id, token) {
679
- if (!DIFF_ARTIFACT_ID_PATTERN.test(id) || !DIFF_ARTIFACT_TOKEN_PATTERN.test(token)) return null;
680
- const entry = await this.blobStore.lookup(id);
681
- if (!entry) {
682
- const expired = await this.blobStore.deleteExpiredKey(id);
683
- if (expired) await this.deleteExpiredFile(expired);
674
+ async getArtifact(id, token) {
675
+ const meta = await this.readMeta(id);
676
+ if (!meta) return null;
677
+ if (!safeEqualSecret(token, meta.token)) return null;
678
+ if (isExpired(meta)) {
679
+ await this.deleteArtifact(id);
684
680
  return null;
685
681
  }
686
- if (!isViewerMetadata(entry.metadata)) return null;
687
- if (!safeEqualSecret(hashToken(token), entry.metadata.tokenHash)) return null;
688
- const html = await gunzipAsync(entry.bytes, MAX_DECODED_HTML_BYTES);
689
- if (html.byteLength !== entry.metadata.decodedBytes) throw new Error(`Diff artifact ${id} decoded size does not match its metadata.`);
690
- return {
691
- artifact: viewerEntryToMeta(entry, token),
692
- html
682
+ return meta;
683
+ }
684
+ async readHtml(id) {
685
+ const meta = await this.readMeta(id);
686
+ if (!meta) throw new Error(`Diff artifact not found: ${id}`);
687
+ const htmlPath = this.normalizeStoredPath(meta.htmlPath, "htmlPath");
688
+ return await (await this.artifactRoot()).readText(this.relativeStoredPath(htmlPath));
689
+ }
690
+ async updateFilePath(id, filePath) {
691
+ const meta = await this.readMeta(id);
692
+ if (!meta) throw new Error(`Diff artifact not found: ${id}`);
693
+ const normalizedFilePath = this.normalizeStoredPath(filePath, "filePath");
694
+ const next = {
695
+ ...meta,
696
+ filePath: normalizedFilePath,
697
+ imagePath: normalizedFilePath
693
698
  };
699
+ await this.writeMeta(next);
700
+ return next;
701
+ }
702
+ async updateImagePath(id, imagePath) {
703
+ return this.updateFilePath(id, imagePath);
704
+ }
705
+ allocateFilePath(id, format = "png") {
706
+ return path.join(this.artifactDir(id), `preview.${format}`);
694
707
  }
695
708
  async createStandaloneFileArtifact(params = {}) {
709
+ await this.ensureRoot();
710
+ const id = crypto.randomBytes(10).toString("hex");
711
+ const artifactDir = this.artifactDir(id);
696
712
  const format = params.format ?? "png";
713
+ const filePath = path.join(artifactDir, `preview.${format}`);
697
714
  const ttlMs = normalizeTtlMs$1(params.ttlMs);
698
- const metadata = {
699
- version: 1,
700
- kind: "rendered_file",
701
- format,
715
+ const createdAt = /* @__PURE__ */ new Date();
716
+ const meta = {
717
+ kind: "standalone_file",
718
+ id,
719
+ createdAt: createdAt.toISOString(),
720
+ expiresAt: resolveExpiresAtIso(createdAt.getTime(), ttlMs),
721
+ filePath: this.normalizeStoredPath(filePath, "filePath"),
702
722
  ...params.context ? { context: params.context } : {}
703
723
  };
704
- for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
705
- const id = crypto.randomBytes(10).toString("hex");
706
- if (!await this.registerIfAbsentWithCleanup(id, EMPTY_BLOB, metadata, ttlMs)) continue;
707
- const artifactDir = this.artifactDir(id);
708
- try {
709
- await fs$1.mkdir(this.rootDir, { recursive: true });
710
- await fs$1.mkdir(artifactDir);
711
- const entry = await this.blobStore.lookup(id);
712
- if (!entry || !isRenderedFileMetadata(entry.metadata)) throw new Error(`Diff file artifact expired before materialization: ${id}`);
713
- this.renderingFileIds.add(id);
714
- this.scheduleCleanup();
715
- return {
716
- id,
717
- filePath: path.join(artifactDir, `preview.${format}`),
718
- expiresAt: resolveEntryExpiresAt(entry),
719
- ...params.context ? { context: params.context } : {}
720
- };
721
- } catch (error) {
722
- await this.blobStore.delete(id).catch(() => false);
723
- await fs$1.rm(artifactDir, {
724
- recursive: true,
725
- force: true
726
- }).catch(() => {});
727
- if (isFileExists(error)) continue;
728
- throw error;
729
- }
730
- }
731
- throw new Error("Failed to allocate a unique diff file artifact id.");
732
- }
733
- async completeFileArtifact(id) {
734
- try {
735
- const entry = await this.blobStore.lookup(id);
736
- if (!entry || !isRenderedFileMetadata(entry.metadata)) {
737
- await fs$1.rm(this.artifactDir(id), {
738
- recursive: true,
739
- force: true
740
- }).catch(() => {});
741
- throw new Error(`Diff file artifact expired during rendering: ${id}`);
742
- }
743
- } finally {
744
- this.renderingFileIds.delete(id);
745
- }
724
+ await (await this.artifactRoot()).mkdir(id);
725
+ await this.writeStandaloneMeta(meta);
726
+ this.scheduleCleanup();
727
+ return {
728
+ id,
729
+ filePath: meta.filePath,
730
+ expiresAt: meta.expiresAt,
731
+ ...meta.context ? { context: meta.context } : {}
732
+ };
746
733
  }
747
- async deleteFileArtifact(id) {
748
- this.renderingFileIds.delete(id);
749
- await this.blobStore.delete(id).catch(() => false);
750
- await fs$1.rm(this.artifactDir(id), {
751
- recursive: true,
752
- force: true
753
- }).catch(() => {});
734
+ allocateImagePath(id, format = "png") {
735
+ return this.allocateFilePath(id, format);
754
736
  }
755
737
  scheduleCleanup() {
756
738
  this.maybeCleanupExpired();
757
739
  }
758
740
  async cleanupExpired() {
759
- const expired = await this.blobStore.deleteExpired();
760
- await Promise.all(expired.map(async (entry) => await this.deleteExpiredFile(entry)));
761
- const entries = await fs$1.readdir(this.rootDir, { withFileTypes: true }).catch((error) => {
762
- if (isFileNotFound(error)) return [];
763
- throw error;
764
- });
741
+ const entries = await (await this.artifactRoot()).list("", { withFileTypes: true }).catch(() => []);
765
742
  const now = Date.now();
766
- await Promise.all(entries.filter((entry) => entry.isDirectory() && DIFF_ARTIFACT_ID_PATTERN.test(entry.name)).map(async (entry) => {
767
- if (this.renderingFileIds.has(entry.name) || await this.blobStore.lookup(entry.name)) return;
768
- const artifactDir = this.artifactDir(entry.name);
769
- const stats = await fs$1.stat(artifactDir).catch(() => null);
770
- if (stats && now - stats.mtimeMs > SWEEP_FALLBACK_AGE_MS) await fs$1.rm(artifactDir, {
771
- recursive: true,
772
- force: true
773
- }).catch(() => {});
743
+ await Promise.all(entries.filter((entry) => entry.isDirectory).map(async (entry) => {
744
+ const id = entry.name;
745
+ const meta = await this.readMeta(id);
746
+ if (meta) {
747
+ if (isExpired(meta)) await this.deleteArtifact(id);
748
+ return;
749
+ }
750
+ const standaloneMeta = await this.readStandaloneMeta(id);
751
+ if (standaloneMeta) {
752
+ if (isExpired(standaloneMeta)) await this.deleteArtifact(id);
753
+ return;
754
+ }
755
+ if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) await this.deleteArtifact(id);
774
756
  }));
775
757
  }
776
- async registerUnique(bytes, metadata, ttlMs) {
777
- for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
778
- const id = crypto.randomBytes(10).toString("hex");
779
- if (!await this.registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs)) continue;
780
- const entry = await this.blobStore.lookup(id);
781
- if (entry) return entry;
782
- }
783
- throw new Error("Failed to allocate a unique diff artifact id.");
784
- }
785
- async registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs) {
786
- try {
787
- return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
788
- } catch (error) {
789
- if (!isBlobLimitError(error)) throw error;
790
- await this.cleanupExpired();
791
- return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
792
- }
758
+ async ensureRoot() {
759
+ await fs$1.mkdir(this.rootDir, { recursive: true });
793
760
  }
794
- async deleteExpiredFile(entry) {
795
- if (!isRenderedFileMetadata(entry.metadata) || this.renderingFileIds.has(entry.key)) return;
796
- if (await this.blobStore.lookup(entry.key)) return;
797
- await fs$1.rm(this.artifactDir(entry.key), {
798
- recursive: true,
799
- force: true
800
- }).catch(() => {});
761
+ async artifactRoot() {
762
+ await this.ensureRoot();
763
+ return await root(this.rootDir);
801
764
  }
802
765
  maybeCleanupExpired() {
803
766
  const now = Date.now();
@@ -812,85 +775,104 @@ var DiffArtifactStore = class {
812
775
  this.cleanupInFlight = cleanupPromise;
813
776
  }
814
777
  artifactDir(id) {
815
- if (!DIFF_ARTIFACT_ID_PATTERN.test(id)) throw new Error(`Invalid diff artifact id: ${id}`);
816
- return path.join(this.rootDir, id);
778
+ return this.resolveWithinRoot(id);
779
+ }
780
+ async writeMeta(meta) {
781
+ await this.writeJsonMeta(meta.id, "meta.json", meta);
782
+ }
783
+ async readMeta(id) {
784
+ const parsed = await this.readJsonMeta(id, "meta.json", "diff artifact");
785
+ if (!parsed) return null;
786
+ return parsed;
787
+ }
788
+ async writeStandaloneMeta(meta) {
789
+ await this.writeJsonMeta(meta.id, "file-meta.json", meta);
790
+ }
791
+ async readStandaloneMeta(id) {
792
+ const parsed = await this.readJsonMeta(id, "file-meta.json", "standalone diff");
793
+ if (!parsed) return null;
794
+ try {
795
+ const value = parsed;
796
+ if (value.kind !== "standalone_file" || typeof value.id !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string" || typeof value.filePath !== "string") return null;
797
+ return {
798
+ kind: value.kind,
799
+ id: value.id,
800
+ createdAt: value.createdAt,
801
+ expiresAt: value.expiresAt,
802
+ filePath: this.normalizeStoredPath(value.filePath, "filePath"),
803
+ ...value.context ? { context: normalizeArtifactContext(value.context) } : {}
804
+ };
805
+ } catch (error) {
806
+ this.logger?.warn(`Failed to normalize standalone diff metadata for ${id}: ${String(error)}`);
807
+ return null;
808
+ }
809
+ }
810
+ async writeJsonMeta(id, fileName, data) {
811
+ await (await this.artifactRoot()).writeJson(path.posix.join(id, fileName), data, { space: 2 });
812
+ }
813
+ async readJsonMeta(id, fileName, context) {
814
+ try {
815
+ const raw = await (await this.artifactRoot()).readText(path.posix.join(id, fileName));
816
+ return JSON.parse(raw);
817
+ } catch (error) {
818
+ if (isFileNotFound(error)) return null;
819
+ this.logger?.warn(`Failed to read ${context} metadata for ${id}: ${String(error)}`);
820
+ return null;
821
+ }
822
+ }
823
+ async deleteArtifact(id) {
824
+ await fs$1.rm(this.artifactDir(id), {
825
+ recursive: true,
826
+ force: true
827
+ }).catch(() => {});
828
+ }
829
+ resolveWithinRoot(...parts) {
830
+ const candidate = path.resolve(this.rootDir, ...parts);
831
+ this.assertWithinRoot(candidate);
832
+ return candidate;
833
+ }
834
+ normalizeStoredPath(rawPath, label) {
835
+ const candidate = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.rootDir, rawPath);
836
+ this.assertWithinRoot(candidate, label);
837
+ return candidate;
838
+ }
839
+ relativeStoredPath(storedPath) {
840
+ return path.relative(this.rootDir, this.normalizeStoredPath(storedPath, "path")).split(path.sep).join(path.posix.sep);
841
+ }
842
+ assertWithinRoot(candidate, label = "path") {
843
+ const relative = path.relative(this.rootDir, candidate);
844
+ if (relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) return;
845
+ throw new Error(`Diff artifact ${label} escapes store root: ${candidate}`);
817
846
  }
818
847
  };
819
- function viewerEntryToMeta(entry, token) {
820
- if (!isViewerMetadata(entry.metadata)) throw new Error(`Diff artifact ${entry.key} is not a viewer.`);
821
- return {
822
- id: entry.key,
823
- token,
824
- createdAt: timestampMsToIsoString(entry.createdAt) ?? "1970-01-01T00:00:00.000Z",
825
- expiresAt: resolveEntryExpiresAt(entry),
826
- title: entry.metadata.title,
827
- inputKind: entry.metadata.inputKind,
828
- fileCount: entry.metadata.fileCount,
829
- viewerPath: `${VIEWER_PREFIX}/${entry.key}/${token}`,
830
- ...entry.metadata.context ? { context: entry.metadata.context } : {}
831
- };
832
- }
833
- function resolveEntryExpiresAt(entry) {
834
- return timestampMsToIsoString(entry.expiresAt ?? MAX_DATE_TIMESTAMP_MS) ?? timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ?? "1970-01-01T00:00:00.000Z";
835
- }
836
- function hashToken(token) {
837
- return crypto.createHash("sha256").update(token).digest("hex");
838
- }
839
848
  function normalizeTtlMs$1(value) {
840
- const rounded = value === void 0 || !Number.isFinite(value) ? 0 : Math.floor(value);
841
- const requestedTtlMs = rounded > 0 ? rounded : DEFAULT_TTL_MS;
842
- const remainingDateRangeMs = Math.floor(MAX_DATE_TIMESTAMP_MS - Date.now());
843
- return Math.min(requestedTtlMs, MAX_TTL_MS, Math.max(1, remainingDateRangeMs));
844
- }
845
- function isViewerMetadata(value) {
846
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
847
- const metadata = value;
848
- return metadata.version === 1 && metadata.kind === "viewer" && metadata.encoding === "gzip" && typeof metadata.tokenHash === "string" && /^[0-9a-f]{64}$/u.test(metadata.tokenHash) && typeof metadata.title === "string" && (metadata.inputKind === "before_after" || metadata.inputKind === "patch") && Number.isSafeInteger(metadata.fileCount) && typeof metadata.fileCount === "number" && metadata.fileCount >= 0 && Number.isSafeInteger(metadata.decodedBytes) && typeof metadata.decodedBytes === "number" && metadata.decodedBytes >= 0 && metadata.decodedBytes <= MAX_DECODED_HTML_BYTES && isArtifactContext(metadata.context);
849
- }
850
- function isRenderedFileMetadata(value) {
851
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
852
- const metadata = value;
853
- return metadata.version === 1 && metadata.kind === "rendered_file" && (metadata.format === "png" || metadata.format === "pdf") && isArtifactContext(metadata.context);
854
- }
855
- function isArtifactContext(value) {
856
- if (value === void 0) return true;
857
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
858
- const context = value;
859
- const allowed = /* @__PURE__ */ new Set([
860
- "agentId",
861
- "sessionId",
862
- "messageChannel",
863
- "agentAccountId"
864
- ]);
865
- return Object.entries(context).every(([key, entry]) => allowed.has(key) && (entry === void 0 || typeof entry === "string"));
866
- }
867
- async function gzipAsync(input) {
868
- return await new Promise((resolve, reject) => {
869
- gzip(input, (error, result) => {
870
- if (error) {
871
- reject(error);
872
- return;
873
- }
874
- resolve(result);
875
- });
876
- });
849
+ if (!Number.isFinite(value) || value === void 0) return DEFAULT_TTL_MS;
850
+ const rounded = Math.floor(value);
851
+ if (rounded <= 0) return DEFAULT_TTL_MS;
852
+ return Math.min(rounded, MAX_TTL_MS);
877
853
  }
878
- async function gunzipAsync(input, maxOutputLength) {
879
- return await new Promise((resolve, reject) => {
880
- gunzip(input, { maxOutputLength }, (error, result) => {
881
- if (error) {
882
- reject(error);
883
- return;
884
- }
885
- resolve(result);
886
- });
887
- });
854
+ function resolveExpiresAtIso(createdAtMs, ttlMs) {
855
+ return timestampMsToIsoString(createdAtMs + ttlMs) ?? timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ?? "1970-01-01T00:00:00.000Z";
888
856
  }
889
- function isFileExists(error) {
890
- return error instanceof Error && "code" in error && error.code === "EEXIST";
857
+ function isExpired(meta) {
858
+ const expiresAt = Date.parse(meta.expiresAt);
859
+ if (!Number.isFinite(expiresAt)) return true;
860
+ return Date.now() >= expiresAt;
891
861
  }
892
862
  function isFileNotFound(error) {
893
- return error instanceof Error && "code" in error && error.code === "ENOENT";
863
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
864
+ return code === "ENOENT" || code === "not-found";
865
+ }
866
+ function normalizeArtifactContext(value) {
867
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
868
+ const raw = value;
869
+ const context = {
870
+ agentId: normalizeOptionalString(raw.agentId),
871
+ sessionId: normalizeOptionalString(raw.sessionId),
872
+ messageChannel: normalizeOptionalString(raw.messageChannel),
873
+ agentAccountId: normalizeOptionalString(raw.agentAccountId)
874
+ };
875
+ return Object.values(context).some((entry) => entry !== void 0) ? context : void 0;
894
876
  }
895
877
  //#endregion
896
878
  //#region extensions/diffs/src/browser.ts
@@ -1500,6 +1482,11 @@ const bundledLanguagesBase = Object.fromEntries(bundledLanguagesInfo.map((langua
1500
1482
  function getBundledLanguageAliases(language) {
1501
1483
  return "aliases" in language ? language.aliases : [];
1502
1484
  }
1485
+ const bundledLanguagesAlias = Object.fromEntries(bundledLanguagesInfo.flatMap((language) => getBundledLanguageAliases(language).map((alias) => [alias, language.import])));
1486
+ ({
1487
+ ...bundledLanguagesBase,
1488
+ ...bundledLanguagesAlias
1489
+ });
1503
1490
  //#endregion
1504
1491
  //#region extensions/diffs/src/language-hints.ts
1505
1492
  const BASE_DIFF_VIEWER_LANGUAGE_HINTS = [
@@ -1603,6 +1590,9 @@ var DiffRenderInputError = class extends Error {
1603
1590
  function escapeCssString(value) {
1604
1591
  return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
1605
1592
  }
1593
+ function escapeHtml(value) {
1594
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
1595
+ }
1606
1596
  function escapeJsonScript(value) {
1607
1597
  return JSON.stringify(value).replaceAll("<", "\\u003c");
1608
1598
  }
@@ -2018,18 +2008,19 @@ async function renderBeforeAfterDiff(input, options, target) {
2018
2008
  });
2019
2009
  const preloadOptions = viewerOptions ?? imageOptions;
2020
2010
  if (!preloadOptions) throw new Error(`Unsupported diff render target: ${target}`);
2021
- const normalizedPayload = await normalizeDiffViewerPayloadLanguages({
2022
- prerenderedHTML: await preloadDiffHTMLWithFallback({
2023
- oldFile,
2024
- newFile,
2025
- options: preloadOptions
2026
- }),
2011
+ const preloadResult = await preloadMultiFileDiffWithFallback({
2027
2012
  oldFile,
2028
2013
  newFile,
2014
+ options: preloadOptions
2015
+ });
2016
+ const normalizedPayload = await normalizeDiffViewerPayloadLanguages({
2017
+ prerenderedHTML: preloadResult.prerenderedHTML,
2018
+ oldFile: preloadResult.oldFile,
2019
+ newFile: preloadResult.newFile,
2029
2020
  options: preloadOptions,
2030
2021
  langs: collectDiffPayloadLanguageHints({
2031
- oldFile,
2032
- newFile
2022
+ oldFile: preloadResult.oldFile,
2023
+ newFile: preloadResult.newFile
2033
2024
  })
2034
2025
  }, { languagePackAvailable });
2035
2026
  const viewerPayload = viewerOptions ? {
@@ -2072,14 +2063,15 @@ async function renderPatchDiff(input, options, target) {
2072
2063
  stats: computeFileDiffStats(fileDiff)
2073
2064
  }));
2074
2065
  const sections = await Promise.all(files.map(async (fileDiff, index) => {
2075
- const normalizedPayload = await normalizeDiffViewerPayloadLanguages({
2076
- prerenderedHTML: await preloadDiffHTMLWithFallback({
2077
- fileDiff,
2078
- options: preloadOptions
2079
- }),
2066
+ const preloadResult = await preloadFileDiffWithFallback({
2080
2067
  fileDiff,
2068
+ options: preloadOptions
2069
+ });
2070
+ const normalizedPayload = await normalizeDiffViewerPayloadLanguages({
2071
+ prerenderedHTML: preloadResult.prerenderedHTML,
2072
+ fileDiff: preloadResult.fileDiff,
2081
2073
  options: preloadOptions,
2082
- langs: collectDiffPayloadLanguageHints({ fileDiff })
2074
+ langs: collectDiffPayloadLanguageHints({ fileDiff: preloadResult.fileDiff })
2083
2075
  }, { languagePackAvailable });
2084
2076
  const viewerPayload = viewerOptions ? {
2085
2077
  ...normalizedPayload,
@@ -2142,18 +2134,33 @@ async function renderDiffDocument(input, options, target = "both") {
2142
2134
  function shouldFallbackToClientHydration(error) {
2143
2135
  return error instanceof TypeError && error.message.includes("needs an import attribute of \"type: json\"");
2144
2136
  }
2145
- async function preloadDiffHTMLWithFallback(params) {
2137
+ async function preloadFileDiffWithFallback(params) {
2146
2138
  try {
2147
- return await preloadDiffHTML(params);
2139
+ return await preloadFileDiff(params);
2148
2140
  } catch (error) {
2149
2141
  if (!shouldFallbackToClientHydration(error)) throw error;
2150
- return "";
2142
+ return {
2143
+ fileDiff: params.fileDiff,
2144
+ prerenderedHTML: ""
2145
+ };
2146
+ }
2147
+ }
2148
+ async function preloadMultiFileDiffWithFallback(params) {
2149
+ try {
2150
+ return await preloadMultiFileDiff(params);
2151
+ } catch (error) {
2152
+ if (!shouldFallbackToClientHydration(error)) throw error;
2153
+ return {
2154
+ oldFile: params.oldFile,
2155
+ newFile: params.newFile,
2156
+ prerenderedHTML: ""
2157
+ };
2151
2158
  }
2152
2159
  }
2153
2160
  //#endregion
2154
2161
  //#region extensions/diffs/src/tool.ts
2155
- const MAX_BEFORE_AFTER_BYTES = 512 * 1024;
2156
- const MAX_PATCH_BYTES = 2 * 1024 * 1024;
2162
+ const MAX_BEFORE_AFTER_BYTES = 524288;
2163
+ const MAX_PATCH_BYTES = 2097152;
2157
2164
  const MAX_TITLE_BYTES = 1024;
2158
2165
  const MAX_PATH_BYTES = 2048;
2159
2166
  const MAX_LANG_BYTES = 128;
@@ -2192,6 +2199,30 @@ const DiffsToolSchema = Type.Object({
2192
2199
  minimum: 640,
2193
2200
  maximum: 2400
2194
2201
  }),
2202
+ /** @deprecated Use fileQuality. */
2203
+ imageQuality: Type.Optional(stringEnum(DIFF_IMAGE_QUALITY_PRESETS, {
2204
+ description: "Deprecated alias for fileQuality.",
2205
+ deprecated: true
2206
+ })),
2207
+ /** @deprecated Use fileFormat. */
2208
+ imageFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, {
2209
+ description: "Deprecated alias for fileFormat.",
2210
+ deprecated: true
2211
+ })),
2212
+ /** @deprecated Use fileScale. */
2213
+ imageScale: optionalFiniteNumberSchema({
2214
+ description: "Deprecated alias for fileScale.",
2215
+ deprecated: true,
2216
+ minimum: 1,
2217
+ maximum: 4
2218
+ }),
2219
+ /** @deprecated Use fileMaxWidth. */
2220
+ imageMaxWidth: optionalFiniteNumberSchema({
2221
+ description: "Deprecated alias for fileMaxWidth.",
2222
+ deprecated: true,
2223
+ minimum: 640,
2224
+ maximum: 2400
2225
+ }),
2195
2226
  expandUnchanged: Type.Optional(Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." })),
2196
2227
  ttlSeconds: optionalFiniteNumberSchema({
2197
2228
  description: "Artifact lifetime in seconds. Default: 1800. Maximum: 21600.",
@@ -2204,7 +2235,7 @@ function createDiffsTool(params) {
2204
2235
  return {
2205
2236
  name: "diffs",
2206
2237
  label: "Diffs",
2207
- description: "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for interactive viewing and can also render the same diff to a PNG or PDF.",
2238
+ description: "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for canvas use and can also render the same diff to a PNG or PDF.",
2208
2239
  parameters: DiffsToolSchema,
2209
2240
  execute: async (_toolCallId, rawParams) => {
2210
2241
  const toolParams = rawParams;
@@ -2226,13 +2257,13 @@ function createDiffsTool(params) {
2226
2257
  const layout = normalizeLayout(toolParams.layout, params.defaults.layout);
2227
2258
  const expandUnchanged = toolParams.expandUnchanged === true;
2228
2259
  const ttlSeconds = readFiniteNumberParam(rawRecord, "ttlSeconds") ?? params.defaults.ttlSeconds;
2229
- const fileScale = readFiniteNumberParam(rawRecord, "fileScale");
2230
- const fileMaxWidth = readFiniteNumberParam(rawRecord, "fileMaxWidth");
2260
+ const fileScale = readFiniteNumberParam(rawRecord, "fileScale") ?? readFiniteNumberParam(rawRecord, "imageScale");
2261
+ const fileMaxWidth = readFiniteNumberParam(rawRecord, "fileMaxWidth") ?? readFiniteNumberParam(rawRecord, "imageMaxWidth");
2231
2262
  const ttlMs = normalizeTtlMs(ttlSeconds);
2232
2263
  const image = resolveDiffImageRenderOptions({
2233
2264
  defaults: params.defaults,
2234
- fileFormat: normalizeOutputFormat(toolParams.fileFormat),
2235
- fileQuality: normalizeFileQuality(toolParams.fileQuality),
2265
+ fileFormat: normalizeOutputFormat(toolParams.fileFormat ?? toolParams.imageFormat ?? toolParams.format),
2266
+ fileQuality: normalizeFileQuality(toolParams.fileQuality ?? toolParams.imageQuality),
2236
2267
  fileScale,
2237
2268
  fileMaxWidth
2238
2269
  });
@@ -2321,12 +2352,12 @@ function createDiffsTool(params) {
2321
2352
  const artifactFile = await renderDiffArtifactFile({
2322
2353
  screenshotter,
2323
2354
  store: params.store,
2355
+ artifactId: artifact.id,
2324
2356
  html: requireRenderedHtml(rendered.imageHtml, "image"),
2325
2357
  theme,
2326
- image,
2327
- ttlMs,
2328
- context: artifactContext
2358
+ image
2329
2359
  });
2360
+ await params.store.updateFilePath(artifact.id, artifactFile.path);
2330
2361
  return {
2331
2362
  content: [{
2332
2363
  type: "text",
@@ -2352,7 +2383,8 @@ function createDiffsTool(params) {
2352
2383
  }],
2353
2384
  details: {
2354
2385
  ...baseDetails,
2355
- fileError: errorMessage
2386
+ fileError: errorMessage,
2387
+ imageError: errorMessage
2356
2388
  }
2357
2389
  };
2358
2390
  }
@@ -2383,59 +2415,55 @@ function buildArtifactDetails(params) {
2383
2415
  return {
2384
2416
  ...params.baseDetails,
2385
2417
  filePath: params.artifactFile.path,
2418
+ imagePath: params.artifactFile.path,
2386
2419
  path: params.artifactFile.path,
2387
2420
  fileBytes: params.artifactFile.bytes,
2421
+ imageBytes: params.artifactFile.bytes,
2422
+ format: params.image.format,
2388
2423
  fileFormat: params.image.format,
2389
2424
  fileQuality: params.image.qualityPreset,
2425
+ imageQuality: params.image.qualityPreset,
2390
2426
  fileScale: params.image.scale,
2391
- fileMaxWidth: params.image.maxWidth
2427
+ imageScale: params.image.scale,
2428
+ fileMaxWidth: params.image.maxWidth,
2429
+ imageMaxWidth: params.image.maxWidth
2392
2430
  };
2393
2431
  }
2394
2432
  function buildFileArtifactMessage(params) {
2395
2433
  const lines = params.viewerUrl ? [`Diff viewer: ${params.viewerUrl}`] : [];
2396
2434
  lines.push(`Diff ${params.format.toUpperCase()} generated at: ${params.filePath}`);
2397
- lines.push("To send this file, use an available file-sending tool to send it as an attachment.");
2435
+ lines.push("Use the `message` tool with `path` or `filePath` to send this file.");
2398
2436
  return lines.join("\n");
2399
2437
  }
2400
2438
  async function renderDiffArtifactFile(params) {
2401
- const fileArtifact = await params.store.createStandaloneFileArtifact({
2439
+ const standaloneArtifact = params.artifactId ? void 0 : await params.store.createStandaloneFileArtifact({
2402
2440
  format: params.image.format,
2403
2441
  ttlMs: params.ttlMs,
2404
2442
  context: params.context
2405
2443
  });
2406
- try {
2407
- await params.screenshotter.screenshotHtml({
2408
- html: params.html,
2409
- outputPath: fileArtifact.filePath,
2410
- theme: params.theme,
2411
- image: params.image
2412
- });
2413
- const stats = await fs$1.stat(fileArtifact.filePath);
2414
- await params.store.completeFileArtifact(fileArtifact.id);
2415
- return {
2416
- path: fileArtifact.filePath,
2417
- bytes: stats.size,
2418
- artifactId: fileArtifact.id,
2419
- expiresAt: fileArtifact.expiresAt
2420
- };
2421
- } catch (error) {
2422
- await params.store.deleteFileArtifact(fileArtifact.id);
2423
- throw error;
2424
- }
2444
+ const outputPath = params.artifactId ? params.store.allocateFilePath(params.artifactId, params.image.format) : standaloneArtifact.filePath;
2445
+ await params.screenshotter.screenshotHtml({
2446
+ html: params.html,
2447
+ outputPath,
2448
+ theme: params.theme,
2449
+ image: params.image
2450
+ });
2451
+ return {
2452
+ path: outputPath,
2453
+ bytes: (await fs$1.stat(outputPath)).size,
2454
+ ...standaloneArtifact?.id ? { artifactId: standaloneArtifact.id } : {},
2455
+ ...standaloneArtifact?.expiresAt ? { expiresAt: standaloneArtifact.expiresAt } : {}
2456
+ };
2425
2457
  }
2426
2458
  function buildArtifactContext(context) {
2427
2459
  if (!context) return;
2428
- const agentId = normalizeOptionalString(context.agentId);
2429
- const sessionId = normalizeOptionalString(context.sessionId);
2430
- const messageChannel = normalizeOptionalString(context.messageChannel);
2431
- const agentAccountId = normalizeOptionalString(context.agentAccountId);
2432
2460
  const artifactContext = {
2433
- ...agentId ? { agentId } : {},
2434
- ...sessionId ? { sessionId } : {},
2435
- ...messageChannel ? { messageChannel } : {},
2436
- ...agentAccountId ? { agentAccountId } : {}
2461
+ agentId: normalizeOptionalString(context.agentId),
2462
+ sessionId: normalizeOptionalString(context.sessionId),
2463
+ messageChannel: normalizeOptionalString(context.messageChannel),
2464
+ agentAccountId: normalizeOptionalString(context.agentAccountId)
2437
2465
  };
2438
- return Object.keys(artifactContext).length > 0 ? artifactContext : void 0;
2466
+ return Object.values(artifactContext).some((value) => value !== void 0) ? artifactContext : void 0;
2439
2467
  }
2440
2468
  function normalizeDiffInput(params) {
2441
2469
  const patch = params.patch?.trim();
@@ -2505,20 +2533,9 @@ var PluginToolInputError = class extends Error {
2505
2533
  //#endregion
2506
2534
  //#region extensions/diffs/src/plugin.ts
2507
2535
  const DIFFS_LANGUAGE_PACK_PLUGIN_ID = "diffs-language-pack";
2508
- const DIFF_ARTIFACT_NAMESPACE = "diff-artifacts";
2509
- const DIFF_ARTIFACT_MAX_ENTRIES = 2048;
2510
- const DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY = 32 * 1024 * 1024;
2511
- const DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE = 256 * 1024 * 1024;
2512
2536
  function registerDiffsPlugin(api) {
2513
2537
  const store = new DiffArtifactStore({
2514
2538
  rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
2515
- blobStore: api.runtime.state.openBlobStore({
2516
- namespace: DIFF_ARTIFACT_NAMESPACE,
2517
- maxEntries: DIFF_ARTIFACT_MAX_ENTRIES,
2518
- maxBytesPerEntry: DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY,
2519
- maxBytesPerNamespace: DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE,
2520
- overflowPolicy: "reject-new"
2521
- }),
2522
2539
  logger: api.logger
2523
2540
  });
2524
2541
  const resolveCurrentPluginConfig = () => resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "diffs", api.pluginConfig) ?? {};