@gakr-gakr/diffs 0.1.0 → 0.1.1
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.
- package/dist/api.js +3 -0
- package/dist/index.js +2104 -0
- package/dist/runtime-api.js +2 -0
- package/package.json +18 -1
- package/api.ts +0 -10
- package/index.ts +0 -11
- package/runtime-api.ts +0 -1
- package/src/browser.ts +0 -564
- package/src/config.ts +0 -443
- package/src/http.ts +0 -324
- package/src/language-hints.ts +0 -117
- package/src/pierre-themes.ts +0 -59
- package/src/plugin.ts +0 -73
- package/src/prompt-guidance.ts +0 -7
- package/src/render.ts +0 -557
- package/src/store.ts +0 -387
- package/src/tool.ts +0 -547
- package/src/types.ts +0 -127
- package/src/url.ts +0 -60
- package/src/viewer-assets.ts +0 -103
- package/src/viewer-client.ts +0 -353
- package/src/viewer-payload.ts +0 -94
- package/tsconfig.json +0 -16
- /package/{assets → dist/assets}/viewer-runtime.js +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2104 @@
|
|
|
1
|
+
import { definePluginEntry, resolvePreferredAutoBotTmpDir } from "./api.js";
|
|
2
|
+
import { resolveRequestClientIp } from "./runtime-api.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { buildPluginConfigSchema } from "autobot/plugin-sdk/plugin-entry";
|
|
5
|
+
import { mapPluginConfigIssues } from "autobot/plugin-sdk/extension-shared";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { resolveLivePluginConfigObject } from "autobot/plugin-sdk/plugin-config-runtime";
|
|
9
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { root, writeExternalFileWithinRoot } from "autobot/plugin-sdk/security-runtime";
|
|
14
|
+
import { stringEnum } from "autobot/plugin-sdk/channel-actions";
|
|
15
|
+
import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
import { constants } from "node:fs";
|
|
18
|
+
import { chromium } from "playwright-core";
|
|
19
|
+
import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes, parsePatchFiles, resolveLanguage } from "@pierre/diffs";
|
|
20
|
+
import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
|
|
21
|
+
import { readJsonFileWithFallback } from "autobot/plugin-sdk/json-store";
|
|
22
|
+
//#region extensions/diffs/src/types.ts
|
|
23
|
+
const DIFF_LAYOUTS = ["unified", "split"];
|
|
24
|
+
const DIFF_MODES = [
|
|
25
|
+
"view",
|
|
26
|
+
"image",
|
|
27
|
+
"file",
|
|
28
|
+
"both"
|
|
29
|
+
];
|
|
30
|
+
const DIFF_THEMES = ["light", "dark"];
|
|
31
|
+
const DIFF_INDICATORS = [
|
|
32
|
+
"bars",
|
|
33
|
+
"classic",
|
|
34
|
+
"none"
|
|
35
|
+
];
|
|
36
|
+
const DIFF_IMAGE_QUALITY_PRESETS = [
|
|
37
|
+
"standard",
|
|
38
|
+
"hq",
|
|
39
|
+
"print"
|
|
40
|
+
];
|
|
41
|
+
const DIFF_OUTPUT_FORMATS = ["png", "pdf"];
|
|
42
|
+
const DIFF_ARTIFACT_ID_PATTERN = /^[0-9a-f]{20}$/;
|
|
43
|
+
const DIFF_ARTIFACT_TOKEN_PATTERN = /^[0-9a-f]{48}$/;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region extensions/diffs/src/url.ts
|
|
46
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
47
|
+
function buildViewerUrl(params) {
|
|
48
|
+
const normalizedBase = normalizeViewerBaseUrl(params.baseUrl?.trim() || resolveGatewayBaseUrl(params.config));
|
|
49
|
+
const viewerPath = params.viewerPath.startsWith("/") ? params.viewerPath : `/${params.viewerPath}`;
|
|
50
|
+
const parsedBase = new URL(normalizedBase);
|
|
51
|
+
parsedBase.pathname = `${parsedBase.pathname === "/" ? "" : parsedBase.pathname.replace(/\/+$/, "")}${viewerPath}`;
|
|
52
|
+
parsedBase.search = "";
|
|
53
|
+
parsedBase.hash = "";
|
|
54
|
+
return parsedBase.toString();
|
|
55
|
+
}
|
|
56
|
+
function normalizeViewerBaseUrl(raw, fieldName = "baseUrl") {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = new URL(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`Invalid ${fieldName}: ${raw}`);
|
|
62
|
+
}
|
|
63
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${fieldName} must use http or https: ${raw}`);
|
|
64
|
+
if (parsed.search || parsed.hash) throw new Error(`${fieldName} must not include query/hash: ${raw}`);
|
|
65
|
+
parsed.search = "";
|
|
66
|
+
parsed.hash = "";
|
|
67
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
68
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
69
|
+
}
|
|
70
|
+
function resolveGatewayBaseUrl(config) {
|
|
71
|
+
const scheme = config.gateway?.tls?.enabled ? "https" : "http";
|
|
72
|
+
const port = typeof config.gateway?.port === "number" ? config.gateway.port : DEFAULT_GATEWAY_PORT;
|
|
73
|
+
const customHost = config.gateway?.customBindHost?.trim();
|
|
74
|
+
if (config.gateway?.bind === "custom" && customHost) return `${scheme}://${customHost}:${port}`;
|
|
75
|
+
return `${scheme}://127.0.0.1:${port}`;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region extensions/diffs/src/config.ts
|
|
79
|
+
const DEFAULT_IMAGE_QUALITY_PROFILES = {
|
|
80
|
+
standard: {
|
|
81
|
+
scale: 2,
|
|
82
|
+
maxWidth: 960,
|
|
83
|
+
maxPixels: 8e6
|
|
84
|
+
},
|
|
85
|
+
hq: {
|
|
86
|
+
scale: 2.5,
|
|
87
|
+
maxWidth: 1200,
|
|
88
|
+
maxPixels: 14e6
|
|
89
|
+
},
|
|
90
|
+
print: {
|
|
91
|
+
scale: 3,
|
|
92
|
+
maxWidth: 1400,
|
|
93
|
+
maxPixels: 24e6
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const DEFAULT_DIFFS_TOOL_DEFAULTS = {
|
|
97
|
+
fontFamily: "Fira Code",
|
|
98
|
+
fontSize: 15,
|
|
99
|
+
lineSpacing: 1.6,
|
|
100
|
+
layout: "unified",
|
|
101
|
+
showLineNumbers: true,
|
|
102
|
+
diffIndicators: "bars",
|
|
103
|
+
wordWrap: true,
|
|
104
|
+
background: true,
|
|
105
|
+
theme: "dark",
|
|
106
|
+
fileFormat: "png",
|
|
107
|
+
fileQuality: "standard",
|
|
108
|
+
fileScale: DEFAULT_IMAGE_QUALITY_PROFILES.standard.scale,
|
|
109
|
+
fileMaxWidth: DEFAULT_IMAGE_QUALITY_PROFILES.standard.maxWidth,
|
|
110
|
+
mode: "both",
|
|
111
|
+
ttlSeconds: 1800
|
|
112
|
+
};
|
|
113
|
+
const DEFAULT_DIFFS_PLUGIN_SECURITY = { allowRemoteViewer: false };
|
|
114
|
+
const VIEWER_BASE_URL_JSON_SCHEMA = {
|
|
115
|
+
type: "string",
|
|
116
|
+
format: "uri",
|
|
117
|
+
pattern: "^[Hh][Tt][Tt][Pp][Ss]?://",
|
|
118
|
+
not: { pattern: "[?#]" }
|
|
119
|
+
};
|
|
120
|
+
const DiffsPluginJsonSchemaSource = z.strictObject({
|
|
121
|
+
viewerBaseUrl: z.string().superRefine((value, ctx) => {
|
|
122
|
+
try {
|
|
123
|
+
normalizeViewerBaseUrl(value, "viewerBaseUrl");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: "custom",
|
|
127
|
+
message: error instanceof Error ? error.message : "Invalid viewerBaseUrl"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}).optional(),
|
|
131
|
+
defaults: z.strictObject({
|
|
132
|
+
fontFamily: z.string().default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily).optional(),
|
|
133
|
+
fontSize: z.number().min(10).max(24).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize).optional(),
|
|
134
|
+
lineSpacing: z.number().min(1).max(3).default(DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing).optional(),
|
|
135
|
+
layout: z.enum(DIFF_LAYOUTS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.layout).optional(),
|
|
136
|
+
showLineNumbers: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.showLineNumbers).optional(),
|
|
137
|
+
diffIndicators: z.enum(DIFF_INDICATORS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators).optional(),
|
|
138
|
+
wordWrap: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap).optional(),
|
|
139
|
+
background: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.background).optional(),
|
|
140
|
+
theme: z.enum(DIFF_THEMES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.theme).optional(),
|
|
141
|
+
fileFormat: z.enum(DIFF_OUTPUT_FORMATS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat).optional(),
|
|
142
|
+
format: z.enum(DIFF_OUTPUT_FORMATS).optional().describe("Deprecated alias for fileFormat."),
|
|
143
|
+
fileQuality: z.enum(DIFF_IMAGE_QUALITY_PRESETS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality).optional(),
|
|
144
|
+
fileScale: z.number().min(1).max(4).optional(),
|
|
145
|
+
fileMaxWidth: z.number().min(640).max(2400).optional(),
|
|
146
|
+
imageFormat: z.enum(DIFF_OUTPUT_FORMATS).optional().describe("Deprecated alias for fileFormat."),
|
|
147
|
+
imageQuality: z.enum(DIFF_IMAGE_QUALITY_PRESETS).optional().describe("Deprecated alias for fileQuality."),
|
|
148
|
+
imageScale: z.number().min(1).max(4).optional().describe("Deprecated alias for fileScale."),
|
|
149
|
+
imageMaxWidth: z.number().min(640).max(2400).optional().describe("Deprecated alias for fileMaxWidth."),
|
|
150
|
+
mode: z.enum(DIFF_MODES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.mode).optional(),
|
|
151
|
+
ttlSeconds: z.number().min(1).max(21600).default(DEFAULT_DIFFS_TOOL_DEFAULTS.ttlSeconds).optional()
|
|
152
|
+
}).optional(),
|
|
153
|
+
security: z.strictObject({ allowRemoteViewer: z.boolean().default(DEFAULT_DIFFS_PLUGIN_SECURITY.allowRemoteViewer).optional() }).optional()
|
|
154
|
+
});
|
|
155
|
+
const diffsPluginConfigSchemaBase = buildPluginConfigSchema(DiffsPluginJsonSchemaSource, { safeParse(value) {
|
|
156
|
+
if (value === void 0) return {
|
|
157
|
+
success: true,
|
|
158
|
+
data: void 0
|
|
159
|
+
};
|
|
160
|
+
const result = DiffsPluginJsonSchemaSource.safeParse(value);
|
|
161
|
+
if (result.success) return {
|
|
162
|
+
success: true,
|
|
163
|
+
data: buildDiffsPluginConfigShape(result.data)
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: { issues: mapPluginConfigIssues(result.error.issues) }
|
|
168
|
+
};
|
|
169
|
+
} });
|
|
170
|
+
const diffsPluginConfigSchema = {
|
|
171
|
+
...diffsPluginConfigSchemaBase,
|
|
172
|
+
jsonSchema: {
|
|
173
|
+
...diffsPluginConfigSchemaBase.jsonSchema,
|
|
174
|
+
properties: {
|
|
175
|
+
...diffsPluginConfigSchemaBase.jsonSchema.properties,
|
|
176
|
+
viewerBaseUrl: VIEWER_BASE_URL_JSON_SCHEMA
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function resolveConfiguredValue(options) {
|
|
181
|
+
const alias = options.aliases.find((value) => value !== void 0);
|
|
182
|
+
if (alias !== void 0 && options.primary === options.schemaDefault) return alias;
|
|
183
|
+
return options.primary ?? alias;
|
|
184
|
+
}
|
|
185
|
+
function buildDiffsPluginConfigShape(config) {
|
|
186
|
+
const viewerBaseUrl = resolveDiffsPluginViewerBaseUrl(config);
|
|
187
|
+
return {
|
|
188
|
+
...viewerBaseUrl !== void 0 ? { viewerBaseUrl } : {},
|
|
189
|
+
...config.defaults !== void 0 ? { defaults: resolveDiffsPluginDefaults(config) } : {},
|
|
190
|
+
...config.security !== void 0 ? { security: resolveDiffsPluginSecurity(config) } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resolveDiffsPluginDefaults(config) {
|
|
194
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
|
|
195
|
+
const defaults = config.defaults;
|
|
196
|
+
if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
|
|
197
|
+
const fileQuality = normalizeFileQuality$1(resolveConfiguredValue({
|
|
198
|
+
primary: defaults.fileQuality,
|
|
199
|
+
aliases: [defaults.imageQuality],
|
|
200
|
+
schemaDefault: DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality
|
|
201
|
+
}));
|
|
202
|
+
const profile = DEFAULT_IMAGE_QUALITY_PROFILES[fileQuality];
|
|
203
|
+
const fileFormat = resolveConfiguredValue({
|
|
204
|
+
primary: defaults.fileFormat,
|
|
205
|
+
aliases: [defaults.imageFormat, defaults.format],
|
|
206
|
+
schemaDefault: DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat
|
|
207
|
+
});
|
|
208
|
+
const fileScale = resolveConfiguredValue({
|
|
209
|
+
primary: defaults.fileScale,
|
|
210
|
+
aliases: [defaults.imageScale]
|
|
211
|
+
});
|
|
212
|
+
const fileMaxWidth = resolveConfiguredValue({
|
|
213
|
+
primary: defaults.fileMaxWidth,
|
|
214
|
+
aliases: [defaults.imageMaxWidth]
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
fontFamily: normalizeFontFamily(defaults.fontFamily),
|
|
218
|
+
fontSize: normalizeFontSize(defaults.fontSize),
|
|
219
|
+
lineSpacing: normalizeLineSpacing(defaults.lineSpacing),
|
|
220
|
+
layout: normalizeLayout$1(defaults.layout),
|
|
221
|
+
showLineNumbers: defaults.showLineNumbers !== false,
|
|
222
|
+
diffIndicators: normalizeDiffIndicators(defaults.diffIndicators),
|
|
223
|
+
wordWrap: defaults.wordWrap !== false,
|
|
224
|
+
background: defaults.background !== false,
|
|
225
|
+
theme: normalizeTheme$1(defaults.theme),
|
|
226
|
+
fileFormat: normalizeFileFormat(fileFormat),
|
|
227
|
+
fileQuality,
|
|
228
|
+
fileScale: normalizeFileScale(fileScale, profile.scale),
|
|
229
|
+
fileMaxWidth: normalizeFileMaxWidth(fileMaxWidth, profile.maxWidth),
|
|
230
|
+
mode: normalizeMode$1(defaults.mode),
|
|
231
|
+
ttlSeconds: normalizeTtlSeconds(defaults.ttlSeconds)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function resolveDiffsPluginSecurity(config) {
|
|
235
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
|
|
236
|
+
const security = config.security;
|
|
237
|
+
if (!security || typeof security !== "object" || Array.isArray(security)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
|
|
238
|
+
return { allowRemoteViewer: security.allowRemoteViewer === true };
|
|
239
|
+
}
|
|
240
|
+
function resolveDiffsPluginViewerBaseUrl(config) {
|
|
241
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return;
|
|
242
|
+
const viewerBaseUrl = config.viewerBaseUrl;
|
|
243
|
+
if (typeof viewerBaseUrl !== "string") return;
|
|
244
|
+
const normalized = viewerBaseUrl.trim();
|
|
245
|
+
return normalized ? normalizeViewerBaseUrl(normalized) : void 0;
|
|
246
|
+
}
|
|
247
|
+
function normalizeFontFamily(fontFamily) {
|
|
248
|
+
return fontFamily?.trim() || DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily;
|
|
249
|
+
}
|
|
250
|
+
function normalizeFontSize(fontSize) {
|
|
251
|
+
if (fontSize === void 0 || !Number.isFinite(fontSize)) return DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize;
|
|
252
|
+
return Math.min(Math.max(Math.floor(fontSize), 10), 24);
|
|
253
|
+
}
|
|
254
|
+
function normalizeLineSpacing(lineSpacing) {
|
|
255
|
+
if (lineSpacing === void 0 || !Number.isFinite(lineSpacing)) return DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing;
|
|
256
|
+
return Math.min(Math.max(lineSpacing, 1), 3);
|
|
257
|
+
}
|
|
258
|
+
function normalizeLayout$1(layout) {
|
|
259
|
+
return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
|
|
260
|
+
}
|
|
261
|
+
function normalizeDiffIndicators(diffIndicators) {
|
|
262
|
+
return diffIndicators && DIFF_INDICATORS.includes(diffIndicators) ? diffIndicators : DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators;
|
|
263
|
+
}
|
|
264
|
+
function normalizeTheme$1(theme) {
|
|
265
|
+
return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
|
|
266
|
+
}
|
|
267
|
+
function normalizeFileFormat(fileFormat) {
|
|
268
|
+
return fileFormat && DIFF_OUTPUT_FORMATS.includes(fileFormat) ? fileFormat : DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat;
|
|
269
|
+
}
|
|
270
|
+
function normalizeFileQuality$1(fileQuality) {
|
|
271
|
+
return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality;
|
|
272
|
+
}
|
|
273
|
+
function normalizeFileScale(fileScale, fallback) {
|
|
274
|
+
if (fileScale === void 0 || !Number.isFinite(fileScale)) return fallback;
|
|
275
|
+
const rounded = Math.round(fileScale * 100) / 100;
|
|
276
|
+
return Math.min(Math.max(rounded, 1), 4);
|
|
277
|
+
}
|
|
278
|
+
function normalizeFileMaxWidth(fileMaxWidth, fallback) {
|
|
279
|
+
if (fileMaxWidth === void 0 || !Number.isFinite(fileMaxWidth)) return fallback;
|
|
280
|
+
return Math.min(Math.max(Math.round(fileMaxWidth), 640), 2400);
|
|
281
|
+
}
|
|
282
|
+
function normalizeMode$1(mode) {
|
|
283
|
+
return mode && DIFF_MODES.includes(mode) ? mode : DEFAULT_DIFFS_TOOL_DEFAULTS.mode;
|
|
284
|
+
}
|
|
285
|
+
function normalizeTtlSeconds(ttlSeconds) {
|
|
286
|
+
if (ttlSeconds === void 0 || !Number.isFinite(ttlSeconds)) return DEFAULT_DIFFS_TOOL_DEFAULTS.ttlSeconds;
|
|
287
|
+
return Math.min(Math.max(Math.floor(ttlSeconds), 1), 21600);
|
|
288
|
+
}
|
|
289
|
+
function resolveDiffImageRenderOptions(params) {
|
|
290
|
+
const format = normalizeFileFormat(params.fileFormat ?? params.imageFormat ?? params.format ?? params.defaults.fileFormat);
|
|
291
|
+
const qualityOverrideProvided = params.fileQuality !== void 0 || params.imageQuality !== void 0;
|
|
292
|
+
const qualityPreset = normalizeFileQuality$1(params.fileQuality ?? params.imageQuality ?? params.defaults.fileQuality);
|
|
293
|
+
const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset];
|
|
294
|
+
return {
|
|
295
|
+
format,
|
|
296
|
+
qualityPreset,
|
|
297
|
+
scale: normalizeFileScale(params.fileScale ?? params.imageScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale),
|
|
298
|
+
maxWidth: normalizeFileMaxWidth(params.fileMaxWidth ?? params.imageMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth),
|
|
299
|
+
maxPixels: profile.maxPixels
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region extensions/diffs/src/viewer-assets.ts
|
|
304
|
+
const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
|
|
305
|
+
const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
|
306
|
+
const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
|
307
|
+
const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
|
|
308
|
+
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = ["./assets/viewer-runtime.js", "../assets/viewer-runtime.js"];
|
|
309
|
+
let runtimeAssetCache = null;
|
|
310
|
+
function isMissingFileError(error) {
|
|
311
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
312
|
+
}
|
|
313
|
+
async function resolveViewerRuntimeFileUrl(params = {}) {
|
|
314
|
+
const baseUrl = params.baseUrl ?? import.meta.url;
|
|
315
|
+
const stat = params.stat ?? ((path) => fs.stat(path));
|
|
316
|
+
let missingFileError = null;
|
|
317
|
+
for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
|
|
318
|
+
const candidateUrl = new URL(relativePath, baseUrl);
|
|
319
|
+
try {
|
|
320
|
+
await stat(fileURLToPath(candidateUrl));
|
|
321
|
+
return candidateUrl;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (isMissingFileError(error)) {
|
|
324
|
+
missingFileError = error;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (missingFileError) throw missingFileError;
|
|
331
|
+
throw new Error("viewer runtime asset candidates were not checked");
|
|
332
|
+
}
|
|
333
|
+
async function getServedViewerAsset(pathname) {
|
|
334
|
+
if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) return null;
|
|
335
|
+
const assets = await loadViewerAssets();
|
|
336
|
+
if (pathname === VIEWER_LOADER_PATH) return {
|
|
337
|
+
body: assets.loaderBody,
|
|
338
|
+
contentType: "text/javascript; charset=utf-8"
|
|
339
|
+
};
|
|
340
|
+
if (pathname === VIEWER_RUNTIME_PATH) return {
|
|
341
|
+
body: assets.runtimeBody,
|
|
342
|
+
contentType: "text/javascript; charset=utf-8"
|
|
343
|
+
};
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
async function loadViewerAssets() {
|
|
347
|
+
const runtimePath = fileURLToPath(await resolveViewerRuntimeFileUrl());
|
|
348
|
+
const runtimeStat = await fs.stat(runtimePath);
|
|
349
|
+
if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) return runtimeAssetCache;
|
|
350
|
+
const runtimeBody = await fs.readFile(runtimePath);
|
|
351
|
+
const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
|
|
352
|
+
runtimeAssetCache = {
|
|
353
|
+
mtimeMs: runtimeStat.mtimeMs,
|
|
354
|
+
runtimeBody,
|
|
355
|
+
loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`
|
|
356
|
+
};
|
|
357
|
+
return runtimeAssetCache;
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region extensions/diffs/src/http.ts
|
|
361
|
+
const VIEW_PREFIX = "/plugins/diffs/view/";
|
|
362
|
+
const VIEWER_MAX_FAILURES_PER_WINDOW = 40;
|
|
363
|
+
const VIEWER_FAILURE_WINDOW_MS = 6e4;
|
|
364
|
+
const VIEWER_LOCKOUT_MS = 6e4;
|
|
365
|
+
const VIEWER_LIMITER_MAX_KEYS = 2048;
|
|
366
|
+
const VIEWER_CONTENT_SECURITY_POLICY = [
|
|
367
|
+
"default-src 'none'",
|
|
368
|
+
"script-src 'self'",
|
|
369
|
+
"style-src 'unsafe-inline'",
|
|
370
|
+
"img-src 'self' data:",
|
|
371
|
+
"font-src 'self' data:",
|
|
372
|
+
"connect-src 'none'",
|
|
373
|
+
"base-uri 'none'",
|
|
374
|
+
"frame-ancestors 'self'",
|
|
375
|
+
"object-src 'none'"
|
|
376
|
+
].join("; ");
|
|
377
|
+
function createDiffsHttpHandler(params) {
|
|
378
|
+
const viewerFailureLimiter = new ViewerFailureLimiter();
|
|
379
|
+
return async (req, res) => {
|
|
380
|
+
const parsed = parseRequestUrl(req.url);
|
|
381
|
+
if (!parsed) return false;
|
|
382
|
+
if (parsed.pathname.startsWith("/plugins/diffs/assets/")) return await serveAsset(req, res, parsed.pathname, params.logger);
|
|
383
|
+
if (!parsed.pathname.startsWith(VIEW_PREFIX)) return false;
|
|
384
|
+
const accessConfig = params.resolveAccessConfig?.() ?? {
|
|
385
|
+
allowRemoteViewer: params.allowRemoteViewer,
|
|
386
|
+
trustedProxies: params.trustedProxies,
|
|
387
|
+
allowRealIpFallback: params.allowRealIpFallback
|
|
388
|
+
};
|
|
389
|
+
const access = resolveViewerAccess(req, {
|
|
390
|
+
trustedProxies: accessConfig.trustedProxies,
|
|
391
|
+
allowRealIpFallback: accessConfig.allowRealIpFallback
|
|
392
|
+
});
|
|
393
|
+
if (!access.localRequest && accessConfig.allowRemoteViewer !== true) {
|
|
394
|
+
respondText(res, 404, "Diff not found");
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
398
|
+
respondText(res, 405, "Method not allowed");
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (!access.localRequest) {
|
|
402
|
+
const throttled = viewerFailureLimiter.check(access.remoteKey);
|
|
403
|
+
if (!throttled.allowed) {
|
|
404
|
+
res.statusCode = 429;
|
|
405
|
+
setSharedHeaders(res, "text/plain; charset=utf-8");
|
|
406
|
+
res.setHeader("Retry-After", String(Math.max(1, Math.ceil(throttled.retryAfterMs / 1e3))));
|
|
407
|
+
res.end("Too Many Requests");
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const pathParts = parsed.pathname.split("/").filter(Boolean);
|
|
412
|
+
const id = pathParts[3];
|
|
413
|
+
const token = pathParts[4];
|
|
414
|
+
if (!id || !token || !DIFF_ARTIFACT_ID_PATTERN.test(id) || !DIFF_ARTIFACT_TOKEN_PATTERN.test(token)) {
|
|
415
|
+
recordRemoteFailure(viewerFailureLimiter, access);
|
|
416
|
+
respondText(res, 404, "Diff not found");
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
if (!await params.store.getArtifact(id, token)) {
|
|
420
|
+
recordRemoteFailure(viewerFailureLimiter, access);
|
|
421
|
+
respondText(res, 404, "Diff not found or expired");
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const html = await params.store.readHtml(id);
|
|
426
|
+
resetRemoteFailures(viewerFailureLimiter, access);
|
|
427
|
+
res.statusCode = 200;
|
|
428
|
+
setSharedHeaders(res, "text/html; charset=utf-8");
|
|
429
|
+
res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
|
|
430
|
+
if (req.method === "HEAD") res.end();
|
|
431
|
+
else res.end(html);
|
|
432
|
+
return true;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
recordRemoteFailure(viewerFailureLimiter, access);
|
|
435
|
+
params.logger?.warn(`Failed to serve diff artifact ${id}: ${String(error)}`);
|
|
436
|
+
respondText(res, 500, "Failed to load diff");
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function parseRequestUrl(rawUrl) {
|
|
442
|
+
if (!rawUrl) return null;
|
|
443
|
+
try {
|
|
444
|
+
return new URL(rawUrl, "http://127.0.0.1");
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function serveAsset(req, res, pathname, logger) {
|
|
450
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
451
|
+
respondText(res, 405, "Method not allowed");
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const asset = await getServedViewerAsset(pathname);
|
|
456
|
+
if (!asset) {
|
|
457
|
+
respondText(res, 404, "Asset not found");
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
res.statusCode = 200;
|
|
461
|
+
setSharedHeaders(res, asset.contentType);
|
|
462
|
+
if (req.method === "HEAD") res.end();
|
|
463
|
+
else res.end(asset.body);
|
|
464
|
+
return true;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger?.warn(`Failed to serve diffs asset ${pathname}: ${String(error)}`);
|
|
467
|
+
respondText(res, 500, "Failed to load asset");
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function respondText(res, statusCode, body) {
|
|
472
|
+
res.statusCode = statusCode;
|
|
473
|
+
setSharedHeaders(res, "text/plain; charset=utf-8");
|
|
474
|
+
res.end(body);
|
|
475
|
+
}
|
|
476
|
+
function setSharedHeaders(res, contentType) {
|
|
477
|
+
res.setHeader("cache-control", "no-store, max-age=0");
|
|
478
|
+
res.setHeader("content-type", contentType);
|
|
479
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
480
|
+
res.setHeader("referrer-policy", "no-referrer");
|
|
481
|
+
}
|
|
482
|
+
function normalizeRemoteClientKey(remoteAddress) {
|
|
483
|
+
const normalized = normalizeLowercaseStringOrEmpty(remoteAddress);
|
|
484
|
+
if (!normalized) return "unknown";
|
|
485
|
+
return normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
|
|
486
|
+
}
|
|
487
|
+
function isLoopbackClientIp(clientIp) {
|
|
488
|
+
return clientIp === "127.0.0.1" || clientIp === "::1";
|
|
489
|
+
}
|
|
490
|
+
function hasProxyForwardingHints(req) {
|
|
491
|
+
const headers = req.headers ?? {};
|
|
492
|
+
return Boolean(headers["x-forwarded-for"] || headers["x-real-ip"] || headers.forwarded || headers["x-forwarded-host"] || headers["x-forwarded-proto"]);
|
|
493
|
+
}
|
|
494
|
+
function resolveViewerAccess(req, params) {
|
|
495
|
+
const proxyHintsPresent = hasProxyForwardingHints(req);
|
|
496
|
+
const clientIp = proxyHintsPresent || (params.trustedProxies?.length ?? 0) > 0 ? resolveRequestClientIp(req, params.trustedProxies ? [...params.trustedProxies] : void 0, params.allowRealIpFallback === true) : req.socket?.remoteAddress;
|
|
497
|
+
const remoteKey = normalizeRemoteClientKey(clientIp ?? req.socket?.remoteAddress);
|
|
498
|
+
return {
|
|
499
|
+
remoteKey,
|
|
500
|
+
localRequest: !proxyHintsPresent && typeof clientIp === "string" && isLoopbackClientIp(remoteKey)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function recordRemoteFailure(limiter, access) {
|
|
504
|
+
if (!access.localRequest) limiter.recordFailure(access.remoteKey);
|
|
505
|
+
}
|
|
506
|
+
function resetRemoteFailures(limiter, access) {
|
|
507
|
+
if (!access.localRequest) limiter.reset(access.remoteKey);
|
|
508
|
+
}
|
|
509
|
+
var ViewerFailureLimiter = class {
|
|
510
|
+
constructor() {
|
|
511
|
+
this.failures = /* @__PURE__ */ new Map();
|
|
512
|
+
}
|
|
513
|
+
check(key) {
|
|
514
|
+
this.prune();
|
|
515
|
+
const state = this.failures.get(key);
|
|
516
|
+
if (!state) return {
|
|
517
|
+
allowed: true,
|
|
518
|
+
retryAfterMs: 0
|
|
519
|
+
};
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
if (state.lockUntilMs > now) return {
|
|
522
|
+
allowed: false,
|
|
523
|
+
retryAfterMs: state.lockUntilMs - now
|
|
524
|
+
};
|
|
525
|
+
if (now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) {
|
|
526
|
+
this.failures.delete(key);
|
|
527
|
+
return {
|
|
528
|
+
allowed: true,
|
|
529
|
+
retryAfterMs: 0
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
allowed: true,
|
|
534
|
+
retryAfterMs: 0
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
recordFailure(key) {
|
|
538
|
+
this.prune();
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const current = this.failures.get(key);
|
|
541
|
+
const next = !current || now - current.windowStartMs >= VIEWER_FAILURE_WINDOW_MS ? {
|
|
542
|
+
windowStartMs: now,
|
|
543
|
+
failures: 1,
|
|
544
|
+
lockUntilMs: 0
|
|
545
|
+
} : {
|
|
546
|
+
...current,
|
|
547
|
+
failures: current.failures + 1
|
|
548
|
+
};
|
|
549
|
+
if (next.failures >= VIEWER_MAX_FAILURES_PER_WINDOW) next.lockUntilMs = now + VIEWER_LOCKOUT_MS;
|
|
550
|
+
this.failures.set(key, next);
|
|
551
|
+
}
|
|
552
|
+
reset(key) {
|
|
553
|
+
this.failures.delete(key);
|
|
554
|
+
}
|
|
555
|
+
prune() {
|
|
556
|
+
if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
for (const [key, state] of this.failures) {
|
|
559
|
+
if (state.lockUntilMs <= now && now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) this.failures.delete(key);
|
|
560
|
+
if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
|
|
561
|
+
}
|
|
562
|
+
if (this.failures.size >= VIEWER_LIMITER_MAX_KEYS) this.failures.clear();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region extensions/diffs/src/prompt-guidance.ts
|
|
567
|
+
const DIFFS_AGENT_GUIDANCE = [
|
|
568
|
+
"When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
|
|
569
|
+
"It accepts either `before` + `after` text or a unified `patch`.",
|
|
570
|
+
"`mode=view` returns `details.viewerUrl` for canvas use; `mode=file` returns `details.filePath`; `mode=both` returns both.",
|
|
571
|
+
"If you need to send the rendered file, use the `message` tool with `path` or `filePath`.",
|
|
572
|
+
"Include `path` when you know the filename, and omit presentation overrides unless needed."
|
|
573
|
+
].join("\n");
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region extensions/diffs/src/store.ts
|
|
576
|
+
const DEFAULT_TTL_MS = 1800 * 1e3;
|
|
577
|
+
const MAX_TTL_MS = 360 * 60 * 1e3;
|
|
578
|
+
const SWEEP_FALLBACK_AGE_MS = 1440 * 60 * 1e3;
|
|
579
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 300 * 1e3;
|
|
580
|
+
const VIEWER_PREFIX = "/plugins/diffs/view";
|
|
581
|
+
var DiffArtifactStore = class {
|
|
582
|
+
constructor(params) {
|
|
583
|
+
this.cleanupInFlight = null;
|
|
584
|
+
this.nextCleanupAt = 0;
|
|
585
|
+
this.rootDir = path.resolve(params.rootDir);
|
|
586
|
+
this.logger = params.logger;
|
|
587
|
+
this.cleanupIntervalMs = params.cleanupIntervalMs === void 0 ? DEFAULT_CLEANUP_INTERVAL_MS : Math.max(0, Math.floor(params.cleanupIntervalMs));
|
|
588
|
+
}
|
|
589
|
+
async createArtifact(params) {
|
|
590
|
+
await this.ensureRoot();
|
|
591
|
+
const id = crypto.randomBytes(10).toString("hex");
|
|
592
|
+
const token = crypto.randomBytes(24).toString("hex");
|
|
593
|
+
const artifactDir = this.artifactDir(id);
|
|
594
|
+
const htmlPath = path.join(artifactDir, "viewer.html");
|
|
595
|
+
const ttlMs = normalizeTtlMs$1(params.ttlMs);
|
|
596
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
597
|
+
const expiresAt = new Date(createdAt.getTime() + ttlMs);
|
|
598
|
+
const meta = {
|
|
599
|
+
id,
|
|
600
|
+
token,
|
|
601
|
+
title: params.title,
|
|
602
|
+
inputKind: params.inputKind,
|
|
603
|
+
fileCount: params.fileCount,
|
|
604
|
+
createdAt: createdAt.toISOString(),
|
|
605
|
+
expiresAt: expiresAt.toISOString(),
|
|
606
|
+
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
|
|
607
|
+
htmlPath,
|
|
608
|
+
...params.context ? { context: params.context } : {}
|
|
609
|
+
};
|
|
610
|
+
const root = await this.artifactRoot();
|
|
611
|
+
await root.mkdir(id);
|
|
612
|
+
await root.write(path.posix.join(id, "viewer.html"), params.html);
|
|
613
|
+
await this.writeMeta(meta);
|
|
614
|
+
this.scheduleCleanup();
|
|
615
|
+
return meta;
|
|
616
|
+
}
|
|
617
|
+
async getArtifact(id, token) {
|
|
618
|
+
const meta = await this.readMeta(id);
|
|
619
|
+
if (!meta) return null;
|
|
620
|
+
if (meta.token !== token) return null;
|
|
621
|
+
if (isExpired(meta)) {
|
|
622
|
+
await this.deleteArtifact(id);
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
return meta;
|
|
626
|
+
}
|
|
627
|
+
async readHtml(id) {
|
|
628
|
+
const meta = await this.readMeta(id);
|
|
629
|
+
if (!meta) throw new Error(`Diff artifact not found: ${id}`);
|
|
630
|
+
const htmlPath = this.normalizeStoredPath(meta.htmlPath, "htmlPath");
|
|
631
|
+
return await (await this.artifactRoot()).readText(this.relativeStoredPath(htmlPath));
|
|
632
|
+
}
|
|
633
|
+
async updateFilePath(id, filePath) {
|
|
634
|
+
const meta = await this.readMeta(id);
|
|
635
|
+
if (!meta) throw new Error(`Diff artifact not found: ${id}`);
|
|
636
|
+
const normalizedFilePath = this.normalizeStoredPath(filePath, "filePath");
|
|
637
|
+
const next = {
|
|
638
|
+
...meta,
|
|
639
|
+
filePath: normalizedFilePath,
|
|
640
|
+
imagePath: normalizedFilePath
|
|
641
|
+
};
|
|
642
|
+
await this.writeMeta(next);
|
|
643
|
+
return next;
|
|
644
|
+
}
|
|
645
|
+
async updateImagePath(id, imagePath) {
|
|
646
|
+
return this.updateFilePath(id, imagePath);
|
|
647
|
+
}
|
|
648
|
+
allocateFilePath(id, format = "png") {
|
|
649
|
+
return path.join(this.artifactDir(id), `preview.${format}`);
|
|
650
|
+
}
|
|
651
|
+
async createStandaloneFileArtifact(params = {}) {
|
|
652
|
+
await this.ensureRoot();
|
|
653
|
+
const id = crypto.randomBytes(10).toString("hex");
|
|
654
|
+
const artifactDir = this.artifactDir(id);
|
|
655
|
+
const format = params.format ?? "png";
|
|
656
|
+
const filePath = path.join(artifactDir, `preview.${format}`);
|
|
657
|
+
const ttlMs = normalizeTtlMs$1(params.ttlMs);
|
|
658
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
659
|
+
const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
|
|
660
|
+
const meta = {
|
|
661
|
+
kind: "standalone_file",
|
|
662
|
+
id,
|
|
663
|
+
createdAt: createdAt.toISOString(),
|
|
664
|
+
expiresAt,
|
|
665
|
+
filePath: this.normalizeStoredPath(filePath, "filePath"),
|
|
666
|
+
...params.context ? { context: params.context } : {}
|
|
667
|
+
};
|
|
668
|
+
await (await this.artifactRoot()).mkdir(id);
|
|
669
|
+
await this.writeStandaloneMeta(meta);
|
|
670
|
+
this.scheduleCleanup();
|
|
671
|
+
return {
|
|
672
|
+
id,
|
|
673
|
+
filePath: meta.filePath,
|
|
674
|
+
expiresAt: meta.expiresAt,
|
|
675
|
+
...meta.context ? { context: meta.context } : {}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
allocateImagePath(id, format = "png") {
|
|
679
|
+
return this.allocateFilePath(id, format);
|
|
680
|
+
}
|
|
681
|
+
scheduleCleanup() {
|
|
682
|
+
this.maybeCleanupExpired();
|
|
683
|
+
}
|
|
684
|
+
async cleanupExpired() {
|
|
685
|
+
const entries = await (await this.artifactRoot()).list("", { withFileTypes: true }).catch(() => []);
|
|
686
|
+
const now = Date.now();
|
|
687
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory).map(async (entry) => {
|
|
688
|
+
const id = entry.name;
|
|
689
|
+
const meta = await this.readMeta(id);
|
|
690
|
+
if (meta) {
|
|
691
|
+
if (isExpired(meta)) await this.deleteArtifact(id);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const standaloneMeta = await this.readStandaloneMeta(id);
|
|
695
|
+
if (standaloneMeta) {
|
|
696
|
+
if (isExpired(standaloneMeta)) await this.deleteArtifact(id);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) await this.deleteArtifact(id);
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
async ensureRoot() {
|
|
703
|
+
await fs.mkdir(this.rootDir, { recursive: true });
|
|
704
|
+
}
|
|
705
|
+
async artifactRoot() {
|
|
706
|
+
await this.ensureRoot();
|
|
707
|
+
return await root(this.rootDir);
|
|
708
|
+
}
|
|
709
|
+
maybeCleanupExpired() {
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
if (this.cleanupInFlight || now < this.nextCleanupAt) return;
|
|
712
|
+
this.nextCleanupAt = now + this.cleanupIntervalMs;
|
|
713
|
+
const cleanupPromise = this.cleanupExpired().catch((error) => {
|
|
714
|
+
this.nextCleanupAt = 0;
|
|
715
|
+
this.logger?.warn(`Failed to clean expired diff artifacts: ${String(error)}`);
|
|
716
|
+
}).finally(() => {
|
|
717
|
+
if (this.cleanupInFlight === cleanupPromise) this.cleanupInFlight = null;
|
|
718
|
+
});
|
|
719
|
+
this.cleanupInFlight = cleanupPromise;
|
|
720
|
+
}
|
|
721
|
+
artifactDir(id) {
|
|
722
|
+
return this.resolveWithinRoot(id);
|
|
723
|
+
}
|
|
724
|
+
async writeMeta(meta) {
|
|
725
|
+
await this.writeJsonMeta(meta.id, "meta.json", meta);
|
|
726
|
+
}
|
|
727
|
+
async readMeta(id) {
|
|
728
|
+
const parsed = await this.readJsonMeta(id, "meta.json", "diff artifact");
|
|
729
|
+
if (!parsed) return null;
|
|
730
|
+
return parsed;
|
|
731
|
+
}
|
|
732
|
+
async writeStandaloneMeta(meta) {
|
|
733
|
+
await this.writeJsonMeta(meta.id, "file-meta.json", meta);
|
|
734
|
+
}
|
|
735
|
+
async readStandaloneMeta(id) {
|
|
736
|
+
const parsed = await this.readJsonMeta(id, "file-meta.json", "standalone diff");
|
|
737
|
+
if (!parsed) return null;
|
|
738
|
+
try {
|
|
739
|
+
const value = parsed;
|
|
740
|
+
if (value.kind !== "standalone_file" || typeof value.id !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string" || typeof value.filePath !== "string") return null;
|
|
741
|
+
return {
|
|
742
|
+
kind: value.kind,
|
|
743
|
+
id: value.id,
|
|
744
|
+
createdAt: value.createdAt,
|
|
745
|
+
expiresAt: value.expiresAt,
|
|
746
|
+
filePath: this.normalizeStoredPath(value.filePath, "filePath"),
|
|
747
|
+
...value.context ? { context: normalizeArtifactContext(value.context) } : {}
|
|
748
|
+
};
|
|
749
|
+
} catch (error) {
|
|
750
|
+
this.logger?.warn(`Failed to normalize standalone diff metadata for ${id}: ${String(error)}`);
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async writeJsonMeta(id, fileName, data) {
|
|
755
|
+
await (await this.artifactRoot()).writeJson(path.posix.join(id, fileName), data, { space: 2 });
|
|
756
|
+
}
|
|
757
|
+
async readJsonMeta(id, fileName, context) {
|
|
758
|
+
try {
|
|
759
|
+
const raw = await (await this.artifactRoot()).readText(path.posix.join(id, fileName));
|
|
760
|
+
return JSON.parse(raw);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (isFileNotFound(error)) return null;
|
|
763
|
+
this.logger?.warn(`Failed to read ${context} metadata for ${id}: ${String(error)}`);
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async deleteArtifact(id) {
|
|
768
|
+
await fs.rm(this.artifactDir(id), {
|
|
769
|
+
recursive: true,
|
|
770
|
+
force: true
|
|
771
|
+
}).catch(() => {});
|
|
772
|
+
}
|
|
773
|
+
resolveWithinRoot(...parts) {
|
|
774
|
+
const candidate = path.resolve(this.rootDir, ...parts);
|
|
775
|
+
this.assertWithinRoot(candidate);
|
|
776
|
+
return candidate;
|
|
777
|
+
}
|
|
778
|
+
normalizeStoredPath(rawPath, label) {
|
|
779
|
+
const candidate = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.rootDir, rawPath);
|
|
780
|
+
this.assertWithinRoot(candidate, label);
|
|
781
|
+
return candidate;
|
|
782
|
+
}
|
|
783
|
+
relativeStoredPath(storedPath) {
|
|
784
|
+
return path.relative(this.rootDir, this.normalizeStoredPath(storedPath, "path")).split(path.sep).join(path.posix.sep);
|
|
785
|
+
}
|
|
786
|
+
assertWithinRoot(candidate, label = "path") {
|
|
787
|
+
const relative = path.relative(this.rootDir, candidate);
|
|
788
|
+
if (relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) return;
|
|
789
|
+
throw new Error(`Diff artifact ${label} escapes store root: ${candidate}`);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
function normalizeTtlMs$1(value) {
|
|
793
|
+
if (!Number.isFinite(value) || value === void 0) return DEFAULT_TTL_MS;
|
|
794
|
+
const rounded = Math.floor(value);
|
|
795
|
+
if (rounded <= 0) return DEFAULT_TTL_MS;
|
|
796
|
+
return Math.min(rounded, MAX_TTL_MS);
|
|
797
|
+
}
|
|
798
|
+
function isExpired(meta) {
|
|
799
|
+
const expiresAt = Date.parse(meta.expiresAt);
|
|
800
|
+
if (!Number.isFinite(expiresAt)) return true;
|
|
801
|
+
return Date.now() >= expiresAt;
|
|
802
|
+
}
|
|
803
|
+
function isFileNotFound(error) {
|
|
804
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
805
|
+
return code === "ENOENT" || code === "not-found";
|
|
806
|
+
}
|
|
807
|
+
function normalizeArtifactContext(value) {
|
|
808
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
809
|
+
const raw = value;
|
|
810
|
+
const context = {
|
|
811
|
+
agentId: normalizeOptionalString(raw.agentId),
|
|
812
|
+
sessionId: normalizeOptionalString(raw.sessionId),
|
|
813
|
+
messageChannel: normalizeOptionalString(raw.messageChannel),
|
|
814
|
+
agentAccountId: normalizeOptionalString(raw.agentAccountId)
|
|
815
|
+
};
|
|
816
|
+
return Object.values(context).some((entry) => entry !== void 0) ? context : void 0;
|
|
817
|
+
}
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region extensions/diffs/src/browser.ts
|
|
820
|
+
const DEFAULT_BROWSER_IDLE_MS = 3e4;
|
|
821
|
+
const SHARED_BROWSER_KEY = "__default__";
|
|
822
|
+
const IMAGE_SIZE_LIMIT_ERROR = "Diff frame did not render within image size limits.";
|
|
823
|
+
const PDF_REFERENCE_PAGE_HEIGHT_PX = 1056;
|
|
824
|
+
const MAX_PDF_PAGES = 50;
|
|
825
|
+
const LOCAL_VIEWER_BASE_HREF = "http://127.0.0.1/plugins/diffs/view/local/local";
|
|
826
|
+
let sharedBrowserState = null;
|
|
827
|
+
let executablePathCache = null;
|
|
828
|
+
var PlaywrightDiffScreenshotter = class {
|
|
829
|
+
constructor(params) {
|
|
830
|
+
this.config = params.config;
|
|
831
|
+
this.browserIdleMs = params.browserIdleMs ?? DEFAULT_BROWSER_IDLE_MS;
|
|
832
|
+
}
|
|
833
|
+
async screenshotHtml(params) {
|
|
834
|
+
const lease = await acquireSharedBrowser({
|
|
835
|
+
config: this.config,
|
|
836
|
+
idleMs: this.browserIdleMs
|
|
837
|
+
});
|
|
838
|
+
let page;
|
|
839
|
+
let currentScale = params.image.scale;
|
|
840
|
+
const maxRetries = 2;
|
|
841
|
+
try {
|
|
842
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
843
|
+
page = await lease.browser.newPage({
|
|
844
|
+
viewport: {
|
|
845
|
+
width: Math.max(Math.ceil(params.image.maxWidth + 240), 1200),
|
|
846
|
+
height: 900
|
|
847
|
+
},
|
|
848
|
+
deviceScaleFactor: currentScale,
|
|
849
|
+
colorScheme: params.theme
|
|
850
|
+
});
|
|
851
|
+
await page.route("**/*", async (route) => {
|
|
852
|
+
const requestUrl = route.request().url();
|
|
853
|
+
if (requestUrl === "about:blank" || requestUrl.startsWith("data:")) {
|
|
854
|
+
await route.continue();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
let parsed;
|
|
858
|
+
try {
|
|
859
|
+
parsed = new URL(requestUrl);
|
|
860
|
+
} catch {
|
|
861
|
+
await route.abort();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
|
|
865
|
+
await route.abort();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (!parsed.pathname.startsWith("/plugins/diffs/assets/")) {
|
|
869
|
+
await route.abort();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const pathname = parsed.pathname;
|
|
873
|
+
const asset = await getServedViewerAsset(pathname);
|
|
874
|
+
if (!asset) {
|
|
875
|
+
await route.abort();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
await route.fulfill({
|
|
879
|
+
status: 200,
|
|
880
|
+
contentType: asset.contentType,
|
|
881
|
+
body: asset.body
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
await page.setContent(injectBaseHref(params.html), { waitUntil: "load" });
|
|
885
|
+
await page.waitForFunction(() => {
|
|
886
|
+
if (document.documentElement.dataset.autobotDiffsReady === "true") return true;
|
|
887
|
+
return [...document.querySelectorAll("[data-autobot-diff-host]")].every((element) => {
|
|
888
|
+
return element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]");
|
|
889
|
+
});
|
|
890
|
+
}, { timeout: 1e4 });
|
|
891
|
+
await page.evaluate(async () => {
|
|
892
|
+
await document.fonts.ready;
|
|
893
|
+
});
|
|
894
|
+
await page.evaluate(() => {
|
|
895
|
+
const frame = document.querySelector(".oc-frame");
|
|
896
|
+
if (frame instanceof HTMLElement) frame.dataset.renderMode = "image";
|
|
897
|
+
});
|
|
898
|
+
const frame = page.locator(".oc-frame");
|
|
899
|
+
await frame.waitFor();
|
|
900
|
+
const initialBox = await frame.boundingBox();
|
|
901
|
+
if (!initialBox) throw new Error("Diff frame did not render.");
|
|
902
|
+
const isPdf = params.image.format === "pdf";
|
|
903
|
+
const padding = isPdf ? 0 : 20;
|
|
904
|
+
const clipWidth = Math.ceil(initialBox.width + padding * 2);
|
|
905
|
+
const clipHeight = Math.ceil(Math.max(initialBox.height + padding * 2, 320));
|
|
906
|
+
await page.setViewportSize({
|
|
907
|
+
width: Math.max(clipWidth + padding, 900),
|
|
908
|
+
height: Math.max(clipHeight + padding, 700)
|
|
909
|
+
});
|
|
910
|
+
const box = await frame.boundingBox();
|
|
911
|
+
if (!box) throw new Error("Diff frame was lost after resizing.");
|
|
912
|
+
if (isPdf) {
|
|
913
|
+
await page.emulateMedia({ media: "screen" });
|
|
914
|
+
await page.evaluate(() => {
|
|
915
|
+
const html = document.documentElement;
|
|
916
|
+
const body = document.body;
|
|
917
|
+
const frame = document.querySelector(".oc-frame");
|
|
918
|
+
html.style.background = "transparent";
|
|
919
|
+
body.style.margin = "0";
|
|
920
|
+
body.style.padding = "0";
|
|
921
|
+
body.style.background = "transparent";
|
|
922
|
+
body.style.setProperty("-webkit-print-color-adjust", "exact");
|
|
923
|
+
if (frame instanceof HTMLElement) frame.style.margin = "0";
|
|
924
|
+
});
|
|
925
|
+
const pdfBox = await frame.boundingBox();
|
|
926
|
+
if (!pdfBox) throw new Error("Diff frame was lost before PDF render.");
|
|
927
|
+
const pdfWidth = Math.max(Math.ceil(pdfBox.width), 1);
|
|
928
|
+
const pdfHeight = Math.max(Math.ceil(pdfBox.height), 1);
|
|
929
|
+
const estimatedPixels = pdfWidth * pdfHeight;
|
|
930
|
+
const estimatedPages = Math.ceil(pdfHeight / PDF_REFERENCE_PAGE_HEIGHT_PX);
|
|
931
|
+
if (estimatedPixels > params.image.maxPixels || estimatedPages > MAX_PDF_PAGES) throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
932
|
+
const pageForPdf = page;
|
|
933
|
+
await writeExternalArtifactFile({
|
|
934
|
+
outputPath: params.outputPath,
|
|
935
|
+
write: async (tempPath) => {
|
|
936
|
+
await pageForPdf.pdf({
|
|
937
|
+
path: tempPath,
|
|
938
|
+
width: `${pdfWidth}px`,
|
|
939
|
+
height: `${pdfHeight}px`,
|
|
940
|
+
printBackground: true,
|
|
941
|
+
margin: {
|
|
942
|
+
top: "0",
|
|
943
|
+
right: "0",
|
|
944
|
+
bottom: "0",
|
|
945
|
+
left: "0"
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
return params.outputPath;
|
|
951
|
+
}
|
|
952
|
+
const dpr = await page.evaluate(() => window.devicePixelRatio || 1);
|
|
953
|
+
const rawX = Math.max(box.x - padding, 0);
|
|
954
|
+
const rawY = Math.max(box.y - padding, 0);
|
|
955
|
+
const rawRight = rawX + clipWidth;
|
|
956
|
+
const rawBottom = rawY + clipHeight;
|
|
957
|
+
const x = Math.floor(rawX * dpr) / dpr;
|
|
958
|
+
const y = Math.floor(rawY * dpr) / dpr;
|
|
959
|
+
const right = Math.ceil(rawRight * dpr) / dpr;
|
|
960
|
+
const bottom = Math.ceil(rawBottom * dpr) / dpr;
|
|
961
|
+
const cssWidth = Math.max(right - x, 1);
|
|
962
|
+
const cssHeight = Math.max(bottom - y, 1);
|
|
963
|
+
if (cssWidth * cssHeight * dpr * dpr > params.image.maxPixels) {
|
|
964
|
+
if (currentScale > 1) {
|
|
965
|
+
const maxScaleForPixels = Math.sqrt(params.image.maxPixels / (cssWidth * cssHeight));
|
|
966
|
+
const reducedScale = Math.max(1, Math.round(Math.min(currentScale, maxScaleForPixels) * 100) / 100);
|
|
967
|
+
if (reducedScale < currentScale - .01 && attempt < maxRetries) {
|
|
968
|
+
await page.close().catch(() => {});
|
|
969
|
+
page = void 0;
|
|
970
|
+
currentScale = reducedScale;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
975
|
+
}
|
|
976
|
+
const pageForScreenshot = page;
|
|
977
|
+
await writeExternalArtifactFile({
|
|
978
|
+
outputPath: params.outputPath,
|
|
979
|
+
write: async (tempPath) => {
|
|
980
|
+
await pageForScreenshot.screenshot({
|
|
981
|
+
path: tempPath,
|
|
982
|
+
type: "png",
|
|
983
|
+
scale: "device",
|
|
984
|
+
clip: {
|
|
985
|
+
x,
|
|
986
|
+
y,
|
|
987
|
+
width: cssWidth,
|
|
988
|
+
height: cssHeight
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return params.outputPath;
|
|
994
|
+
}
|
|
995
|
+
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (error instanceof Error && error.message === IMAGE_SIZE_LIMIT_ERROR) throw error;
|
|
998
|
+
const reason = formatErrorMessage(error);
|
|
999
|
+
throw new Error(`Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`, { cause: error });
|
|
1000
|
+
} finally {
|
|
1001
|
+
await page?.close().catch(() => {});
|
|
1002
|
+
await lease.release();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
async function writeExternalArtifactFile(params) {
|
|
1007
|
+
const rootDir = path.dirname(params.outputPath);
|
|
1008
|
+
await fs.mkdir(rootDir, { recursive: true });
|
|
1009
|
+
await writeExternalFileWithinRoot({
|
|
1010
|
+
rootDir,
|
|
1011
|
+
path: path.basename(params.outputPath),
|
|
1012
|
+
write: params.write
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function injectBaseHref(html) {
|
|
1016
|
+
if (html.includes("<base ")) return html;
|
|
1017
|
+
return html.replace("<head>", `<head><base href="${LOCAL_VIEWER_BASE_HREF}" />`);
|
|
1018
|
+
}
|
|
1019
|
+
async function resolveBrowserExecutablePath(config) {
|
|
1020
|
+
const cacheKey = JSON.stringify({
|
|
1021
|
+
configPath: config.browser?.executablePath?.trim() || "",
|
|
1022
|
+
env: [
|
|
1023
|
+
process.env.AUTOBOT_BROWSER_EXECUTABLE_PATH ?? "",
|
|
1024
|
+
process.env.BROWSER_EXECUTABLE_PATH ?? "",
|
|
1025
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? ""
|
|
1026
|
+
],
|
|
1027
|
+
path: process.env.PATH ?? ""
|
|
1028
|
+
});
|
|
1029
|
+
if (executablePathCache?.key === cacheKey) return await executablePathCache.valuePromise;
|
|
1030
|
+
const valuePromise = resolveBrowserExecutablePathUncached(config).catch((error) => {
|
|
1031
|
+
if (executablePathCache?.valuePromise === valuePromise) executablePathCache = null;
|
|
1032
|
+
throw error;
|
|
1033
|
+
});
|
|
1034
|
+
executablePathCache = {
|
|
1035
|
+
key: cacheKey,
|
|
1036
|
+
valuePromise
|
|
1037
|
+
};
|
|
1038
|
+
return await valuePromise;
|
|
1039
|
+
}
|
|
1040
|
+
async function resolveBrowserExecutablePathUncached(config) {
|
|
1041
|
+
const configPath = config.browser?.executablePath?.trim();
|
|
1042
|
+
if (configPath) {
|
|
1043
|
+
await assertExecutable(configPath, "browser.executablePath");
|
|
1044
|
+
return configPath;
|
|
1045
|
+
}
|
|
1046
|
+
const envCandidates = [
|
|
1047
|
+
process.env.AUTOBOT_BROWSER_EXECUTABLE_PATH,
|
|
1048
|
+
process.env.BROWSER_EXECUTABLE_PATH,
|
|
1049
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
1050
|
+
].map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
1051
|
+
for (const candidate of envCandidates) if (await isExecutable(candidate)) return candidate;
|
|
1052
|
+
for (const candidate of await collectExecutableCandidates()) if (await isExecutable(candidate)) return candidate;
|
|
1053
|
+
}
|
|
1054
|
+
async function acquireSharedBrowser(params) {
|
|
1055
|
+
const executablePath = await resolveBrowserExecutablePath(params.config);
|
|
1056
|
+
const desiredKey = executablePath || SHARED_BROWSER_KEY;
|
|
1057
|
+
if (sharedBrowserState && sharedBrowserState.key !== desiredKey) await closeSharedBrowser();
|
|
1058
|
+
if (!sharedBrowserState) {
|
|
1059
|
+
const browserPromise = chromium.launch({
|
|
1060
|
+
headless: true,
|
|
1061
|
+
...executablePath ? { executablePath } : {},
|
|
1062
|
+
args: ["--disable-dev-shm-usage"]
|
|
1063
|
+
}).then((browser) => {
|
|
1064
|
+
if (sharedBrowserState?.browserPromise === browserPromise) {
|
|
1065
|
+
sharedBrowserState.browser = browser;
|
|
1066
|
+
browser.on("disconnected", () => {
|
|
1067
|
+
if (sharedBrowserState?.browser === browser) {
|
|
1068
|
+
clearIdleTimer(sharedBrowserState);
|
|
1069
|
+
sharedBrowserState = null;
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
return browser;
|
|
1074
|
+
}).catch((error) => {
|
|
1075
|
+
if (sharedBrowserState?.browserPromise === browserPromise) sharedBrowserState = null;
|
|
1076
|
+
throw error;
|
|
1077
|
+
});
|
|
1078
|
+
sharedBrowserState = {
|
|
1079
|
+
browserPromise,
|
|
1080
|
+
idleTimer: null,
|
|
1081
|
+
key: desiredKey,
|
|
1082
|
+
users: 0
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
clearIdleTimer(sharedBrowserState);
|
|
1086
|
+
const state = sharedBrowserState;
|
|
1087
|
+
const browser = await state.browserPromise;
|
|
1088
|
+
state.users += 1;
|
|
1089
|
+
let released = false;
|
|
1090
|
+
return {
|
|
1091
|
+
browser,
|
|
1092
|
+
release: async () => {
|
|
1093
|
+
if (released) return;
|
|
1094
|
+
released = true;
|
|
1095
|
+
state.users = Math.max(0, state.users - 1);
|
|
1096
|
+
if (state.users === 0) scheduleIdleBrowserClose(state, params.idleMs);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function scheduleIdleBrowserClose(state, idleMs) {
|
|
1101
|
+
clearIdleTimer(state);
|
|
1102
|
+
state.idleTimer = setTimeout(() => {
|
|
1103
|
+
if (sharedBrowserState === state && state.users === 0) closeSharedBrowser();
|
|
1104
|
+
}, idleMs);
|
|
1105
|
+
}
|
|
1106
|
+
function clearIdleTimer(state) {
|
|
1107
|
+
if (!state.idleTimer) return;
|
|
1108
|
+
clearTimeout(state.idleTimer);
|
|
1109
|
+
state.idleTimer = null;
|
|
1110
|
+
}
|
|
1111
|
+
async function closeSharedBrowser() {
|
|
1112
|
+
const state = sharedBrowserState;
|
|
1113
|
+
if (!state) return;
|
|
1114
|
+
sharedBrowserState = null;
|
|
1115
|
+
clearIdleTimer(state);
|
|
1116
|
+
await (state.browser ?? await state.browserPromise.catch(() => null))?.close().catch(() => {});
|
|
1117
|
+
}
|
|
1118
|
+
async function collectExecutableCandidates() {
|
|
1119
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const command of pathCommandsForPlatform()) {
|
|
1121
|
+
const resolved = await findExecutableInPath(command);
|
|
1122
|
+
if (resolved) candidates.add(resolved);
|
|
1123
|
+
}
|
|
1124
|
+
for (const candidate of commonExecutablePathsForPlatform()) candidates.add(candidate);
|
|
1125
|
+
return [...candidates];
|
|
1126
|
+
}
|
|
1127
|
+
function pathCommandsForPlatform() {
|
|
1128
|
+
if (process.platform === "win32") return [
|
|
1129
|
+
"chrome.exe",
|
|
1130
|
+
"msedge.exe",
|
|
1131
|
+
"brave.exe"
|
|
1132
|
+
];
|
|
1133
|
+
if (process.platform === "darwin") return [
|
|
1134
|
+
"google-chrome",
|
|
1135
|
+
"chromium",
|
|
1136
|
+
"msedge",
|
|
1137
|
+
"brave-browser",
|
|
1138
|
+
"brave"
|
|
1139
|
+
];
|
|
1140
|
+
return [
|
|
1141
|
+
"chromium",
|
|
1142
|
+
"chromium-browser",
|
|
1143
|
+
"google-chrome",
|
|
1144
|
+
"google-chrome-stable",
|
|
1145
|
+
"msedge",
|
|
1146
|
+
"brave-browser",
|
|
1147
|
+
"brave"
|
|
1148
|
+
];
|
|
1149
|
+
}
|
|
1150
|
+
function commonExecutablePathsForPlatform() {
|
|
1151
|
+
if (process.platform === "darwin") return [
|
|
1152
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1153
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1154
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1155
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1156
|
+
];
|
|
1157
|
+
if (process.platform === "win32") {
|
|
1158
|
+
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
1159
|
+
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
1160
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
1161
|
+
return [
|
|
1162
|
+
path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1163
|
+
path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1164
|
+
path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1165
|
+
path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
1166
|
+
path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
1167
|
+
path.join(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
1168
|
+
path.join(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1169
|
+
];
|
|
1170
|
+
}
|
|
1171
|
+
return [
|
|
1172
|
+
"/usr/bin/chromium",
|
|
1173
|
+
"/usr/bin/chromium-browser",
|
|
1174
|
+
"/usr/bin/google-chrome",
|
|
1175
|
+
"/usr/bin/google-chrome-stable",
|
|
1176
|
+
"/usr/bin/msedge",
|
|
1177
|
+
"/usr/bin/brave-browser",
|
|
1178
|
+
"/snap/bin/chromium"
|
|
1179
|
+
];
|
|
1180
|
+
}
|
|
1181
|
+
async function findExecutableInPath(command) {
|
|
1182
|
+
const pathValue = process.env.PATH;
|
|
1183
|
+
if (!pathValue) return;
|
|
1184
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
1185
|
+
if (!directory) continue;
|
|
1186
|
+
const candidate = path.join(directory, command);
|
|
1187
|
+
if (await isExecutable(candidate)) return candidate;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function assertExecutable(candidate, label) {
|
|
1191
|
+
if (!await isExecutable(candidate)) throw new Error(`${label} not found or not executable: ${candidate}`);
|
|
1192
|
+
}
|
|
1193
|
+
async function isExecutable(candidate) {
|
|
1194
|
+
try {
|
|
1195
|
+
await fs.access(candidate, constants.X_OK);
|
|
1196
|
+
return true;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region extensions/diffs/src/language-hints.ts
|
|
1203
|
+
const PASSTHROUGH_LANGUAGE_HINTS = new Set(["ansi", "text"]);
|
|
1204
|
+
async function normalizeSupportedLanguageHint(value) {
|
|
1205
|
+
const normalized = normalizeOptionalString(value);
|
|
1206
|
+
if (!normalized) return;
|
|
1207
|
+
if (PASSTHROUGH_LANGUAGE_HINTS.has(normalized)) return normalized;
|
|
1208
|
+
try {
|
|
1209
|
+
await resolveLanguage(normalized);
|
|
1210
|
+
return normalized;
|
|
1211
|
+
} catch {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function normalizeSupportedLanguageHints(values, options) {
|
|
1216
|
+
const supported = /* @__PURE__ */ new Set();
|
|
1217
|
+
for (const value of values) {
|
|
1218
|
+
const normalized = await normalizeSupportedLanguageHint(value);
|
|
1219
|
+
if (!normalized) continue;
|
|
1220
|
+
supported.add(normalized);
|
|
1221
|
+
}
|
|
1222
|
+
if (options.fallbackToText && supported.size === 0) supported.add("text");
|
|
1223
|
+
return [...supported];
|
|
1224
|
+
}
|
|
1225
|
+
function collectDiffPayloadLanguageHints(payload) {
|
|
1226
|
+
const langs = /* @__PURE__ */ new Set();
|
|
1227
|
+
if (payload.fileDiff?.lang) langs.add(payload.fileDiff.lang);
|
|
1228
|
+
if (payload.oldFile?.lang) langs.add(payload.oldFile.lang);
|
|
1229
|
+
if (payload.newFile?.lang) langs.add(payload.newFile.lang);
|
|
1230
|
+
return [...langs];
|
|
1231
|
+
}
|
|
1232
|
+
async function normalizeDiffPayloadFileLanguage(file) {
|
|
1233
|
+
if (!file) return;
|
|
1234
|
+
if (typeof file.lang !== "string") return file;
|
|
1235
|
+
const normalized = await normalizeSupportedLanguageHint(file.lang);
|
|
1236
|
+
if (file.lang === normalized) return file;
|
|
1237
|
+
if (!normalized) return {
|
|
1238
|
+
...file,
|
|
1239
|
+
lang: "text"
|
|
1240
|
+
};
|
|
1241
|
+
return {
|
|
1242
|
+
...file,
|
|
1243
|
+
lang: normalized
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async function normalizeDiffViewerPayloadLanguages(payload) {
|
|
1247
|
+
const [fileDiff, oldFile, newFile, payloadLangs] = await Promise.all([
|
|
1248
|
+
normalizeDiffPayloadFileLanguage(payload.fileDiff),
|
|
1249
|
+
normalizeDiffPayloadFileLanguage(payload.oldFile),
|
|
1250
|
+
normalizeDiffPayloadFileLanguage(payload.newFile),
|
|
1251
|
+
normalizeSupportedLanguageHints(payload.langs, { fallbackToText: false })
|
|
1252
|
+
]);
|
|
1253
|
+
const langs = new Set(payloadLangs);
|
|
1254
|
+
for (const lang of collectDiffPayloadLanguageHints({
|
|
1255
|
+
fileDiff,
|
|
1256
|
+
oldFile,
|
|
1257
|
+
newFile
|
|
1258
|
+
})) langs.add(lang);
|
|
1259
|
+
if (langs.size === 0) langs.add("text");
|
|
1260
|
+
return {
|
|
1261
|
+
...payload,
|
|
1262
|
+
fileDiff,
|
|
1263
|
+
oldFile,
|
|
1264
|
+
newFile,
|
|
1265
|
+
langs: [...langs]
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
//#endregion
|
|
1269
|
+
//#region extensions/diffs/src/pierre-themes.ts
|
|
1270
|
+
const themeRequire = createRequire(import.meta.url);
|
|
1271
|
+
const PIERRE_THEME_SPECS = [["pierre-dark", "@pierre/theme/themes/pierre-dark.json"], ["pierre-light", "@pierre/theme/themes/pierre-light.json"]];
|
|
1272
|
+
function createThemeLoader(themeName, themeSpecifier) {
|
|
1273
|
+
let cachedTheme;
|
|
1274
|
+
return async () => {
|
|
1275
|
+
if (cachedTheme) return cachedTheme;
|
|
1276
|
+
const { value: theme } = await readJsonFileWithFallback(themeRequire.resolve(themeSpecifier), {});
|
|
1277
|
+
cachedTheme = {
|
|
1278
|
+
...theme,
|
|
1279
|
+
name: themeName
|
|
1280
|
+
};
|
|
1281
|
+
return cachedTheme;
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const PIERRE_THEME_LOADERS = new Map(PIERRE_THEME_SPECS.map(([themeName, themeSpecifier]) => [themeName, createThemeLoader(themeName, themeSpecifier)]));
|
|
1285
|
+
function ensurePierreThemesRegistered() {
|
|
1286
|
+
let replacedThemeLoader = false;
|
|
1287
|
+
for (const [themeName, loader] of PIERRE_THEME_LOADERS) if (RegisteredCustomThemes.get(themeName) !== loader) {
|
|
1288
|
+
RegisteredCustomThemes.set(themeName, loader);
|
|
1289
|
+
replacedThemeLoader = true;
|
|
1290
|
+
}
|
|
1291
|
+
if (!replacedThemeLoader) return;
|
|
1292
|
+
for (const [themeName] of PIERRE_THEME_LOADERS) {
|
|
1293
|
+
ResolvedThemes.delete(themeName);
|
|
1294
|
+
ResolvingThemes.delete(themeName);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region extensions/diffs/src/render.ts
|
|
1299
|
+
const DEFAULT_FILE_NAME = "diff.txt";
|
|
1300
|
+
const MAX_PATCH_FILE_COUNT = 128;
|
|
1301
|
+
const MAX_PATCH_TOTAL_LINES = 12e4;
|
|
1302
|
+
const VIEWER_LOADER_DOCUMENT_PATH = "../../assets/viewer.js";
|
|
1303
|
+
function escapeCssString(value) {
|
|
1304
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
1305
|
+
}
|
|
1306
|
+
function escapeHtml(value) {
|
|
1307
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1308
|
+
}
|
|
1309
|
+
function escapeJsonScript(value) {
|
|
1310
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
1311
|
+
}
|
|
1312
|
+
function buildDiffTitle(input) {
|
|
1313
|
+
if (input.title?.trim()) return input.title.trim();
|
|
1314
|
+
if (input.kind === "before_after") return input.path?.trim() || "Text diff";
|
|
1315
|
+
return "Patch diff";
|
|
1316
|
+
}
|
|
1317
|
+
function resolveBeforeAfterFileName(params) {
|
|
1318
|
+
const { input, lang } = params;
|
|
1319
|
+
if (input.path?.trim()) return input.path.trim();
|
|
1320
|
+
if (lang && lang !== "text") return `diff.${lang.replace(/^\.+/, "")}`;
|
|
1321
|
+
return DEFAULT_FILE_NAME;
|
|
1322
|
+
}
|
|
1323
|
+
function buildDiffOptions(options) {
|
|
1324
|
+
const fontFamily = escapeCssString(options.presentation.fontFamily);
|
|
1325
|
+
const fontSize = Math.max(10, Math.floor(options.presentation.fontSize));
|
|
1326
|
+
const lineHeight = Math.max(20, Math.round(fontSize * options.presentation.lineSpacing));
|
|
1327
|
+
return {
|
|
1328
|
+
theme: {
|
|
1329
|
+
light: "pierre-light",
|
|
1330
|
+
dark: "pierre-dark"
|
|
1331
|
+
},
|
|
1332
|
+
diffStyle: options.presentation.layout,
|
|
1333
|
+
diffIndicators: options.presentation.diffIndicators,
|
|
1334
|
+
disableLineNumbers: !options.presentation.showLineNumbers,
|
|
1335
|
+
expandUnchanged: options.expandUnchanged,
|
|
1336
|
+
themeType: options.presentation.theme,
|
|
1337
|
+
backgroundEnabled: options.presentation.background,
|
|
1338
|
+
overflow: options.presentation.wordWrap ? "wrap" : "scroll",
|
|
1339
|
+
unsafeCSS: `
|
|
1340
|
+
:host {
|
|
1341
|
+
--diffs-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1342
|
+
--diffs-header-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1343
|
+
--diffs-font-size: ${fontSize}px;
|
|
1344
|
+
--diffs-line-height: ${lineHeight}px;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
[data-diffs-header] {
|
|
1348
|
+
min-height: 64px;
|
|
1349
|
+
padding-inline: 18px 14px;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
[data-header-content] {
|
|
1353
|
+
gap: 10px;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
[data-metadata] {
|
|
1357
|
+
gap: 10px;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.oc-diff-toolbar {
|
|
1361
|
+
display: inline-flex;
|
|
1362
|
+
align-items: center;
|
|
1363
|
+
gap: 6px;
|
|
1364
|
+
margin-inline-start: 6px;
|
|
1365
|
+
flex: 0 0 auto;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.oc-diff-toolbar-button {
|
|
1369
|
+
display: inline-flex;
|
|
1370
|
+
align-items: center;
|
|
1371
|
+
justify-content: center;
|
|
1372
|
+
width: 24px;
|
|
1373
|
+
height: 24px;
|
|
1374
|
+
padding: 0;
|
|
1375
|
+
margin: 0;
|
|
1376
|
+
border: 0;
|
|
1377
|
+
border-radius: 0;
|
|
1378
|
+
background: transparent;
|
|
1379
|
+
color: inherit;
|
|
1380
|
+
cursor: pointer;
|
|
1381
|
+
opacity: 0.6;
|
|
1382
|
+
line-height: 0;
|
|
1383
|
+
overflow: visible;
|
|
1384
|
+
transition: opacity 120ms ease;
|
|
1385
|
+
flex: 0 0 auto;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.oc-diff-toolbar-button:hover {
|
|
1389
|
+
opacity: 1;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.oc-diff-toolbar-button[data-active="true"] {
|
|
1393
|
+
opacity: 0.92;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.oc-diff-toolbar-button svg {
|
|
1397
|
+
display: block;
|
|
1398
|
+
width: 16px;
|
|
1399
|
+
height: 16px;
|
|
1400
|
+
min-width: 16px;
|
|
1401
|
+
min-height: 16px;
|
|
1402
|
+
overflow: visible;
|
|
1403
|
+
flex: 0 0 auto;
|
|
1404
|
+
color: inherit;
|
|
1405
|
+
fill: currentColor;
|
|
1406
|
+
pointer-events: none;
|
|
1407
|
+
}
|
|
1408
|
+
`
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function buildImageRenderOptions(options) {
|
|
1412
|
+
return {
|
|
1413
|
+
...options,
|
|
1414
|
+
presentation: {
|
|
1415
|
+
...options.presentation,
|
|
1416
|
+
fontSize: Math.max(16, options.presentation.fontSize)
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function shouldRenderViewer(target) {
|
|
1421
|
+
return target === "viewer" || target === "both";
|
|
1422
|
+
}
|
|
1423
|
+
function shouldRenderImage(target) {
|
|
1424
|
+
return target === "image" || target === "both";
|
|
1425
|
+
}
|
|
1426
|
+
function buildRenderVariants(params) {
|
|
1427
|
+
return {
|
|
1428
|
+
...shouldRenderViewer(params.target) ? { viewerOptions: buildDiffOptions(params.options) } : {},
|
|
1429
|
+
...shouldRenderImage(params.target) ? { imageOptions: buildDiffOptions(buildImageRenderOptions(params.options)) } : {}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function renderDiffCard(payload) {
|
|
1433
|
+
return `<section class="oc-diff-card">
|
|
1434
|
+
<diffs-container class="oc-diff-host" data-autobot-diff-host>
|
|
1435
|
+
<template shadowrootmode="open">${payload.prerenderedHTML}</template>
|
|
1436
|
+
</diffs-container>
|
|
1437
|
+
<script type="application/json" data-autobot-diff-payload>${escapeJsonScript(payload)}<\/script>
|
|
1438
|
+
</section>`;
|
|
1439
|
+
}
|
|
1440
|
+
function buildHtmlDocument(params) {
|
|
1441
|
+
return `<!doctype html>
|
|
1442
|
+
<html lang="en">
|
|
1443
|
+
<head>
|
|
1444
|
+
<meta charset="utf-8" />
|
|
1445
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1446
|
+
<meta name="color-scheme" content="dark light" />
|
|
1447
|
+
<title>${escapeHtml(params.title)}</title>
|
|
1448
|
+
<style>
|
|
1449
|
+
* {
|
|
1450
|
+
box-sizing: border-box;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
html,
|
|
1454
|
+
body {
|
|
1455
|
+
min-height: 100%;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
html {
|
|
1459
|
+
background: #05070b;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
body {
|
|
1463
|
+
margin: 0;
|
|
1464
|
+
min-height: 100vh;
|
|
1465
|
+
padding: 22px;
|
|
1466
|
+
font-family:
|
|
1467
|
+
"Fira Code",
|
|
1468
|
+
"SF Mono",
|
|
1469
|
+
Monaco,
|
|
1470
|
+
Consolas,
|
|
1471
|
+
monospace;
|
|
1472
|
+
background: #05070b;
|
|
1473
|
+
color: #f8fafc;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
body[data-theme="light"] {
|
|
1477
|
+
background: #f3f5f8;
|
|
1478
|
+
color: #0f172a;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.oc-frame {
|
|
1482
|
+
max-width: 1560px;
|
|
1483
|
+
margin: 0 auto;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.oc-frame[data-render-mode="image"] {
|
|
1487
|
+
max-width: ${Math.max(640, Math.round(params.imageMaxWidth))}px;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
[data-autobot-diff-root] {
|
|
1491
|
+
display: grid;
|
|
1492
|
+
gap: 18px;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.oc-diff-card {
|
|
1496
|
+
overflow: hidden;
|
|
1497
|
+
border-radius: 18px;
|
|
1498
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
1499
|
+
background: rgba(15, 23, 42, 0.14);
|
|
1500
|
+
box-shadow: 0 18px 48px rgba(2, 6, 23, 0.22);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
body[data-theme="light"] .oc-diff-card {
|
|
1504
|
+
border-color: rgba(148, 163, 184, 0.22);
|
|
1505
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1506
|
+
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.oc-diff-host {
|
|
1510
|
+
display: block;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.oc-frame[data-render-mode="image"] .oc-diff-card {
|
|
1514
|
+
min-height: 240px;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
@media (max-width: 720px) {
|
|
1518
|
+
body {
|
|
1519
|
+
padding: 12px;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
[data-autobot-diff-root] {
|
|
1523
|
+
gap: 12px;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
</style>
|
|
1527
|
+
</head>
|
|
1528
|
+
<body data-theme="${params.theme}">
|
|
1529
|
+
<main class="oc-frame" data-render-mode="${params.runtimeMode}">
|
|
1530
|
+
<div data-autobot-diff-root>
|
|
1531
|
+
${params.bodyHtml}
|
|
1532
|
+
</div>
|
|
1533
|
+
</main>
|
|
1534
|
+
<script type="module" src="${VIEWER_LOADER_DOCUMENT_PATH}"><\/script>
|
|
1535
|
+
</body>
|
|
1536
|
+
</html>`;
|
|
1537
|
+
}
|
|
1538
|
+
function buildRenderedSection(params) {
|
|
1539
|
+
return {
|
|
1540
|
+
...params.viewerPayload ? { viewer: renderDiffCard(params.viewerPayload) } : {},
|
|
1541
|
+
...params.imagePayload ? { image: renderDiffCard(params.imagePayload) } : {}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function buildRenderedBodies(sections) {
|
|
1545
|
+
const viewerSections = sections.flatMap((section) => section.viewer ? [section.viewer] : []);
|
|
1546
|
+
const imageSections = sections.flatMap((section) => section.image ? [section.image] : []);
|
|
1547
|
+
return {
|
|
1548
|
+
...viewerSections.length > 0 ? { viewerBodyHtml: viewerSections.join("\n") } : {},
|
|
1549
|
+
...imageSections.length > 0 ? { imageBodyHtml: imageSections.join("\n") } : {}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
async function renderBeforeAfterDiff(input, options, target) {
|
|
1553
|
+
ensurePierreThemesRegistered();
|
|
1554
|
+
const lang = await normalizeSupportedLanguageHint(input.lang);
|
|
1555
|
+
const fileName = resolveBeforeAfterFileName({
|
|
1556
|
+
input,
|
|
1557
|
+
lang
|
|
1558
|
+
});
|
|
1559
|
+
const oldFile = {
|
|
1560
|
+
name: fileName,
|
|
1561
|
+
contents: input.before,
|
|
1562
|
+
...lang ? { lang } : {}
|
|
1563
|
+
};
|
|
1564
|
+
const newFile = {
|
|
1565
|
+
name: fileName,
|
|
1566
|
+
contents: input.after,
|
|
1567
|
+
...lang ? { lang } : {}
|
|
1568
|
+
};
|
|
1569
|
+
const { viewerOptions, imageOptions } = buildRenderVariants({
|
|
1570
|
+
options,
|
|
1571
|
+
target
|
|
1572
|
+
});
|
|
1573
|
+
const [viewerResult, imageResult] = await Promise.all([viewerOptions ? preloadMultiFileDiffWithFallback({
|
|
1574
|
+
oldFile,
|
|
1575
|
+
newFile,
|
|
1576
|
+
options: viewerOptions
|
|
1577
|
+
}) : Promise.resolve(void 0), imageOptions ? preloadMultiFileDiffWithFallback({
|
|
1578
|
+
oldFile,
|
|
1579
|
+
newFile,
|
|
1580
|
+
options: imageOptions
|
|
1581
|
+
}) : Promise.resolve(void 0)]);
|
|
1582
|
+
const [viewerPayload, imagePayload] = await Promise.all([viewerResult && viewerOptions ? normalizeDiffViewerPayloadLanguages({
|
|
1583
|
+
prerenderedHTML: viewerResult.prerenderedHTML,
|
|
1584
|
+
oldFile: viewerResult.oldFile,
|
|
1585
|
+
newFile: viewerResult.newFile,
|
|
1586
|
+
options: viewerOptions,
|
|
1587
|
+
langs: collectDiffPayloadLanguageHints({
|
|
1588
|
+
oldFile: viewerResult.oldFile,
|
|
1589
|
+
newFile: viewerResult.newFile
|
|
1590
|
+
})
|
|
1591
|
+
}) : Promise.resolve(void 0), imageResult && imageOptions ? normalizeDiffViewerPayloadLanguages({
|
|
1592
|
+
prerenderedHTML: imageResult.prerenderedHTML,
|
|
1593
|
+
oldFile: imageResult.oldFile,
|
|
1594
|
+
newFile: imageResult.newFile,
|
|
1595
|
+
options: imageOptions,
|
|
1596
|
+
langs: collectDiffPayloadLanguageHints({
|
|
1597
|
+
oldFile: imageResult.oldFile,
|
|
1598
|
+
newFile: imageResult.newFile
|
|
1599
|
+
})
|
|
1600
|
+
}) : Promise.resolve(void 0)]);
|
|
1601
|
+
return {
|
|
1602
|
+
...buildRenderedBodies([buildRenderedSection({
|
|
1603
|
+
...viewerPayload ? { viewerPayload } : {},
|
|
1604
|
+
...imagePayload ? { imagePayload } : {}
|
|
1605
|
+
})]),
|
|
1606
|
+
fileCount: 1
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
async function renderPatchDiff(input, options, target) {
|
|
1610
|
+
ensurePierreThemesRegistered();
|
|
1611
|
+
const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []);
|
|
1612
|
+
if (files.length === 0) throw new Error("Patch input did not contain any file diffs.");
|
|
1613
|
+
if (files.length > MAX_PATCH_FILE_COUNT) throw new Error(`Patch input contains too many files (max ${MAX_PATCH_FILE_COUNT}).`);
|
|
1614
|
+
if (files.reduce((sum, fileDiff) => {
|
|
1615
|
+
const splitLines = Number.isFinite(fileDiff.splitLineCount) ? fileDiff.splitLineCount : 0;
|
|
1616
|
+
const unifiedLines = Number.isFinite(fileDiff.unifiedLineCount) ? fileDiff.unifiedLineCount : 0;
|
|
1617
|
+
return sum + Math.max(splitLines, unifiedLines, 0);
|
|
1618
|
+
}, 0) > MAX_PATCH_TOTAL_LINES) throw new Error(`Patch input is too large to render (max ${MAX_PATCH_TOTAL_LINES} lines).`);
|
|
1619
|
+
const { viewerOptions, imageOptions } = buildRenderVariants({
|
|
1620
|
+
options,
|
|
1621
|
+
target
|
|
1622
|
+
});
|
|
1623
|
+
return {
|
|
1624
|
+
...buildRenderedBodies(await Promise.all(files.map(async (fileDiff) => {
|
|
1625
|
+
const [viewerResult, imageResult] = await Promise.all([viewerOptions ? preloadFileDiffWithFallback({
|
|
1626
|
+
fileDiff,
|
|
1627
|
+
options: viewerOptions
|
|
1628
|
+
}) : Promise.resolve(void 0), imageOptions ? preloadFileDiffWithFallback({
|
|
1629
|
+
fileDiff,
|
|
1630
|
+
options: imageOptions
|
|
1631
|
+
}) : Promise.resolve(void 0)]);
|
|
1632
|
+
const [viewerPayload, imagePayload] = await Promise.all([viewerResult && viewerOptions ? normalizeDiffViewerPayloadLanguages({
|
|
1633
|
+
prerenderedHTML: viewerResult.prerenderedHTML,
|
|
1634
|
+
fileDiff: viewerResult.fileDiff,
|
|
1635
|
+
options: viewerOptions,
|
|
1636
|
+
langs: collectDiffPayloadLanguageHints({ fileDiff: viewerResult.fileDiff })
|
|
1637
|
+
}) : Promise.resolve(void 0), imageResult && imageOptions ? normalizeDiffViewerPayloadLanguages({
|
|
1638
|
+
prerenderedHTML: imageResult.prerenderedHTML,
|
|
1639
|
+
fileDiff: imageResult.fileDiff,
|
|
1640
|
+
options: imageOptions,
|
|
1641
|
+
langs: collectDiffPayloadLanguageHints({ fileDiff: imageResult.fileDiff })
|
|
1642
|
+
}) : Promise.resolve(void 0)]);
|
|
1643
|
+
return buildRenderedSection({
|
|
1644
|
+
...viewerPayload ? { viewerPayload } : {},
|
|
1645
|
+
...imagePayload ? { imagePayload } : {}
|
|
1646
|
+
});
|
|
1647
|
+
}))),
|
|
1648
|
+
fileCount: files.length
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async function renderDiffDocument(input, options, target = "both") {
|
|
1652
|
+
const title = buildDiffTitle(input);
|
|
1653
|
+
const rendered = input.kind === "before_after" ? await renderBeforeAfterDiff(input, options, target) : await renderPatchDiff(input, options, target);
|
|
1654
|
+
return {
|
|
1655
|
+
...rendered.viewerBodyHtml ? { html: buildHtmlDocument({
|
|
1656
|
+
title,
|
|
1657
|
+
bodyHtml: rendered.viewerBodyHtml,
|
|
1658
|
+
theme: options.presentation.theme,
|
|
1659
|
+
imageMaxWidth: options.image.maxWidth,
|
|
1660
|
+
runtimeMode: "viewer"
|
|
1661
|
+
}) } : {},
|
|
1662
|
+
...rendered.imageBodyHtml ? { imageHtml: buildHtmlDocument({
|
|
1663
|
+
title,
|
|
1664
|
+
bodyHtml: rendered.imageBodyHtml,
|
|
1665
|
+
theme: options.presentation.theme,
|
|
1666
|
+
imageMaxWidth: options.image.maxWidth,
|
|
1667
|
+
runtimeMode: "image"
|
|
1668
|
+
}) } : {},
|
|
1669
|
+
title,
|
|
1670
|
+
fileCount: rendered.fileCount,
|
|
1671
|
+
inputKind: input.kind
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function shouldFallbackToClientHydration(error) {
|
|
1675
|
+
return error instanceof TypeError && error.message.includes("needs an import attribute of \"type: json\"");
|
|
1676
|
+
}
|
|
1677
|
+
async function preloadFileDiffWithFallback(params) {
|
|
1678
|
+
try {
|
|
1679
|
+
return await preloadFileDiff(params);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (!shouldFallbackToClientHydration(error)) throw error;
|
|
1682
|
+
return {
|
|
1683
|
+
fileDiff: params.fileDiff,
|
|
1684
|
+
prerenderedHTML: ""
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
async function preloadMultiFileDiffWithFallback(params) {
|
|
1689
|
+
try {
|
|
1690
|
+
return await preloadMultiFileDiff(params);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
if (!shouldFallbackToClientHydration(error)) throw error;
|
|
1693
|
+
return {
|
|
1694
|
+
oldFile: params.oldFile,
|
|
1695
|
+
newFile: params.newFile,
|
|
1696
|
+
prerenderedHTML: ""
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1701
|
+
//#region extensions/diffs/src/tool.ts
|
|
1702
|
+
const MAX_BEFORE_AFTER_BYTES = 512 * 1024;
|
|
1703
|
+
const MAX_PATCH_BYTES = 2 * 1024 * 1024;
|
|
1704
|
+
const MAX_TITLE_BYTES = 1024;
|
|
1705
|
+
const MAX_PATH_BYTES = 2048;
|
|
1706
|
+
const MAX_LANG_BYTES = 128;
|
|
1707
|
+
const DiffsToolSchema = Type.Object({
|
|
1708
|
+
before: Type.Optional(Type.String({ description: "Original text content." })),
|
|
1709
|
+
after: Type.Optional(Type.String({ description: "Updated text content." })),
|
|
1710
|
+
patch: Type.Optional(Type.String({
|
|
1711
|
+
description: "Unified diff or patch text.",
|
|
1712
|
+
maxLength: MAX_PATCH_BYTES
|
|
1713
|
+
})),
|
|
1714
|
+
path: Type.Optional(Type.String({
|
|
1715
|
+
description: "Display path for before/after input.",
|
|
1716
|
+
maxLength: MAX_PATH_BYTES
|
|
1717
|
+
})),
|
|
1718
|
+
lang: Type.Optional(Type.String({
|
|
1719
|
+
description: "Optional language override for before/after input.",
|
|
1720
|
+
maxLength: MAX_LANG_BYTES
|
|
1721
|
+
})),
|
|
1722
|
+
title: Type.Optional(Type.String({
|
|
1723
|
+
description: "Optional title for the rendered diff.",
|
|
1724
|
+
maxLength: MAX_TITLE_BYTES
|
|
1725
|
+
})),
|
|
1726
|
+
mode: Type.Optional(stringEnum(DIFF_MODES, { description: "Output mode: view, file, image (deprecated alias for file), or both. Default: both." })),
|
|
1727
|
+
theme: Type.Optional(stringEnum(DIFF_THEMES, { description: "Viewer theme. Default: dark." })),
|
|
1728
|
+
layout: Type.Optional(stringEnum(DIFF_LAYOUTS, { description: "Diff layout. Default: unified." })),
|
|
1729
|
+
fileQuality: Type.Optional(stringEnum(DIFF_IMAGE_QUALITY_PRESETS, { description: "File quality preset: standard, hq, or print." })),
|
|
1730
|
+
fileFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, { description: "Rendered file format: png or pdf." })),
|
|
1731
|
+
fileScale: Type.Optional(Type.Number({
|
|
1732
|
+
description: "Optional rendered-file device scale factor override (1-4).",
|
|
1733
|
+
minimum: 1,
|
|
1734
|
+
maximum: 4
|
|
1735
|
+
})),
|
|
1736
|
+
fileMaxWidth: Type.Optional(Type.Number({
|
|
1737
|
+
description: "Optional rendered-file max width in CSS pixels (640-2400).",
|
|
1738
|
+
minimum: 640,
|
|
1739
|
+
maximum: 2400
|
|
1740
|
+
})),
|
|
1741
|
+
/** @deprecated Use fileQuality. */
|
|
1742
|
+
imageQuality: Type.Optional(stringEnum(DIFF_IMAGE_QUALITY_PRESETS, {
|
|
1743
|
+
description: "Deprecated alias for fileQuality.",
|
|
1744
|
+
deprecated: true
|
|
1745
|
+
})),
|
|
1746
|
+
/** @deprecated Use fileFormat. */
|
|
1747
|
+
imageFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, {
|
|
1748
|
+
description: "Deprecated alias for fileFormat.",
|
|
1749
|
+
deprecated: true
|
|
1750
|
+
})),
|
|
1751
|
+
/** @deprecated Use fileScale. */
|
|
1752
|
+
imageScale: Type.Optional(Type.Number({
|
|
1753
|
+
description: "Deprecated alias for fileScale.",
|
|
1754
|
+
deprecated: true,
|
|
1755
|
+
minimum: 1,
|
|
1756
|
+
maximum: 4
|
|
1757
|
+
})),
|
|
1758
|
+
/** @deprecated Use fileMaxWidth. */
|
|
1759
|
+
imageMaxWidth: Type.Optional(Type.Number({
|
|
1760
|
+
description: "Deprecated alias for fileMaxWidth.",
|
|
1761
|
+
deprecated: true,
|
|
1762
|
+
minimum: 640,
|
|
1763
|
+
maximum: 2400
|
|
1764
|
+
})),
|
|
1765
|
+
expandUnchanged: Type.Optional(Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." })),
|
|
1766
|
+
ttlSeconds: Type.Optional(Type.Number({
|
|
1767
|
+
description: "Artifact lifetime in seconds. Default: 1800. Maximum: 21600.",
|
|
1768
|
+
minimum: 1,
|
|
1769
|
+
maximum: 21600
|
|
1770
|
+
})),
|
|
1771
|
+
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." }))
|
|
1772
|
+
}, { additionalProperties: false });
|
|
1773
|
+
function createDiffsTool(params) {
|
|
1774
|
+
return {
|
|
1775
|
+
name: "diffs",
|
|
1776
|
+
label: "Diffs",
|
|
1777
|
+
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.",
|
|
1778
|
+
parameters: DiffsToolSchema,
|
|
1779
|
+
execute: async (_toolCallId, rawParams) => {
|
|
1780
|
+
const toolParams = rawParams;
|
|
1781
|
+
const artifactContext = buildArtifactContext(params.context);
|
|
1782
|
+
const input = normalizeDiffInput(toolParams);
|
|
1783
|
+
const mode = normalizeMode(toolParams.mode, params.defaults.mode);
|
|
1784
|
+
const theme = normalizeTheme(toolParams.theme, params.defaults.theme);
|
|
1785
|
+
const layout = normalizeLayout(toolParams.layout, params.defaults.layout);
|
|
1786
|
+
const expandUnchanged = toolParams.expandUnchanged === true;
|
|
1787
|
+
const ttlMs = normalizeTtlMs(toolParams.ttlSeconds ?? params.defaults.ttlSeconds);
|
|
1788
|
+
const image = resolveDiffImageRenderOptions({
|
|
1789
|
+
defaults: params.defaults,
|
|
1790
|
+
fileFormat: normalizeOutputFormat(toolParams.fileFormat ?? toolParams.imageFormat ?? toolParams.format),
|
|
1791
|
+
fileQuality: normalizeFileQuality(toolParams.fileQuality ?? toolParams.imageQuality),
|
|
1792
|
+
fileScale: toolParams.fileScale ?? toolParams.imageScale,
|
|
1793
|
+
fileMaxWidth: toolParams.fileMaxWidth ?? toolParams.imageMaxWidth
|
|
1794
|
+
});
|
|
1795
|
+
const renderTarget = resolveRenderTarget(mode);
|
|
1796
|
+
const rendered = await renderDiffDocument(input, {
|
|
1797
|
+
presentation: {
|
|
1798
|
+
...params.defaults,
|
|
1799
|
+
layout,
|
|
1800
|
+
theme
|
|
1801
|
+
},
|
|
1802
|
+
image,
|
|
1803
|
+
expandUnchanged
|
|
1804
|
+
}, renderTarget);
|
|
1805
|
+
const screenshotter = params.screenshotter ?? new PlaywrightDiffScreenshotter({ config: params.api.config });
|
|
1806
|
+
if (isArtifactOnlyMode(mode)) {
|
|
1807
|
+
const artifactFile = await renderDiffArtifactFile({
|
|
1808
|
+
screenshotter,
|
|
1809
|
+
store: params.store,
|
|
1810
|
+
html: requireRenderedHtml(rendered.imageHtml, "image"),
|
|
1811
|
+
theme,
|
|
1812
|
+
image,
|
|
1813
|
+
ttlMs,
|
|
1814
|
+
context: artifactContext
|
|
1815
|
+
});
|
|
1816
|
+
return {
|
|
1817
|
+
content: [{
|
|
1818
|
+
type: "text",
|
|
1819
|
+
text: buildFileArtifactMessage({
|
|
1820
|
+
format: image.format,
|
|
1821
|
+
filePath: artifactFile.path
|
|
1822
|
+
})
|
|
1823
|
+
}],
|
|
1824
|
+
details: buildArtifactDetails({
|
|
1825
|
+
baseDetails: {
|
|
1826
|
+
...artifactFile.artifactId ? { artifactId: artifactFile.artifactId } : {},
|
|
1827
|
+
...artifactFile.expiresAt ? { expiresAt: artifactFile.expiresAt } : {},
|
|
1828
|
+
title: rendered.title,
|
|
1829
|
+
inputKind: rendered.inputKind,
|
|
1830
|
+
fileCount: rendered.fileCount,
|
|
1831
|
+
mode,
|
|
1832
|
+
...artifactContext ? { context: artifactContext } : {}
|
|
1833
|
+
},
|
|
1834
|
+
artifactFile,
|
|
1835
|
+
image
|
|
1836
|
+
})
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
const artifact = await params.store.createArtifact({
|
|
1840
|
+
html: requireRenderedHtml(rendered.html, "viewer"),
|
|
1841
|
+
title: rendered.title,
|
|
1842
|
+
inputKind: rendered.inputKind,
|
|
1843
|
+
fileCount: rendered.fileCount,
|
|
1844
|
+
ttlMs,
|
|
1845
|
+
context: artifactContext
|
|
1846
|
+
});
|
|
1847
|
+
const viewerUrl = buildViewerUrl({
|
|
1848
|
+
config: params.api.config,
|
|
1849
|
+
viewerPath: artifact.viewerPath,
|
|
1850
|
+
baseUrl: normalizeBaseUrl(toolParams.baseUrl) ?? params.viewerBaseUrl
|
|
1851
|
+
});
|
|
1852
|
+
const baseDetails = {
|
|
1853
|
+
artifactId: artifact.id,
|
|
1854
|
+
viewerUrl,
|
|
1855
|
+
viewerPath: artifact.viewerPath,
|
|
1856
|
+
title: artifact.title,
|
|
1857
|
+
expiresAt: artifact.expiresAt,
|
|
1858
|
+
inputKind: artifact.inputKind,
|
|
1859
|
+
fileCount: artifact.fileCount,
|
|
1860
|
+
mode,
|
|
1861
|
+
...artifactContext ? { context: artifactContext } : {}
|
|
1862
|
+
};
|
|
1863
|
+
if (mode === "view") return {
|
|
1864
|
+
content: [{
|
|
1865
|
+
type: "text",
|
|
1866
|
+
text: `Diff viewer ready.\n${viewerUrl}`
|
|
1867
|
+
}],
|
|
1868
|
+
details: baseDetails
|
|
1869
|
+
};
|
|
1870
|
+
try {
|
|
1871
|
+
const artifactFile = await renderDiffArtifactFile({
|
|
1872
|
+
screenshotter,
|
|
1873
|
+
store: params.store,
|
|
1874
|
+
artifactId: artifact.id,
|
|
1875
|
+
html: requireRenderedHtml(rendered.imageHtml, "image"),
|
|
1876
|
+
theme,
|
|
1877
|
+
image
|
|
1878
|
+
});
|
|
1879
|
+
await params.store.updateFilePath(artifact.id, artifactFile.path);
|
|
1880
|
+
return {
|
|
1881
|
+
content: [{
|
|
1882
|
+
type: "text",
|
|
1883
|
+
text: buildFileArtifactMessage({
|
|
1884
|
+
format: image.format,
|
|
1885
|
+
filePath: artifactFile.path,
|
|
1886
|
+
viewerUrl
|
|
1887
|
+
})
|
|
1888
|
+
}],
|
|
1889
|
+
details: buildArtifactDetails({
|
|
1890
|
+
baseDetails,
|
|
1891
|
+
artifactFile,
|
|
1892
|
+
image
|
|
1893
|
+
})
|
|
1894
|
+
};
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
if (mode === "both") {
|
|
1897
|
+
const errorMessage = formatErrorMessage(error);
|
|
1898
|
+
return {
|
|
1899
|
+
content: [{
|
|
1900
|
+
type: "text",
|
|
1901
|
+
text: `Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`
|
|
1902
|
+
}],
|
|
1903
|
+
details: {
|
|
1904
|
+
...baseDetails,
|
|
1905
|
+
fileError: errorMessage,
|
|
1906
|
+
imageError: errorMessage
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
throw error;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function normalizeFileQuality(fileQuality) {
|
|
1916
|
+
return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : void 0;
|
|
1917
|
+
}
|
|
1918
|
+
function normalizeOutputFormat(format) {
|
|
1919
|
+
return format && DIFF_OUTPUT_FORMATS.includes(format) ? format : void 0;
|
|
1920
|
+
}
|
|
1921
|
+
function isArtifactOnlyMode(mode) {
|
|
1922
|
+
return mode === "image" || mode === "file";
|
|
1923
|
+
}
|
|
1924
|
+
function resolveRenderTarget(mode) {
|
|
1925
|
+
if (mode === "view") return "viewer";
|
|
1926
|
+
if (isArtifactOnlyMode(mode)) return "image";
|
|
1927
|
+
return "both";
|
|
1928
|
+
}
|
|
1929
|
+
function requireRenderedHtml(html, target) {
|
|
1930
|
+
if (html !== void 0) return html;
|
|
1931
|
+
throw new Error(`Missing ${target} render output.`);
|
|
1932
|
+
}
|
|
1933
|
+
function buildArtifactDetails(params) {
|
|
1934
|
+
return {
|
|
1935
|
+
...params.baseDetails,
|
|
1936
|
+
filePath: params.artifactFile.path,
|
|
1937
|
+
imagePath: params.artifactFile.path,
|
|
1938
|
+
path: params.artifactFile.path,
|
|
1939
|
+
fileBytes: params.artifactFile.bytes,
|
|
1940
|
+
imageBytes: params.artifactFile.bytes,
|
|
1941
|
+
format: params.image.format,
|
|
1942
|
+
fileFormat: params.image.format,
|
|
1943
|
+
fileQuality: params.image.qualityPreset,
|
|
1944
|
+
imageQuality: params.image.qualityPreset,
|
|
1945
|
+
fileScale: params.image.scale,
|
|
1946
|
+
imageScale: params.image.scale,
|
|
1947
|
+
fileMaxWidth: params.image.maxWidth,
|
|
1948
|
+
imageMaxWidth: params.image.maxWidth
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
function buildFileArtifactMessage(params) {
|
|
1952
|
+
const lines = params.viewerUrl ? [`Diff viewer: ${params.viewerUrl}`] : [];
|
|
1953
|
+
lines.push(`Diff ${params.format.toUpperCase()} generated at: ${params.filePath}`);
|
|
1954
|
+
lines.push("Use the `message` tool with `path` or `filePath` to send this file.");
|
|
1955
|
+
return lines.join("\n");
|
|
1956
|
+
}
|
|
1957
|
+
async function renderDiffArtifactFile(params) {
|
|
1958
|
+
const standaloneArtifact = params.artifactId ? void 0 : await params.store.createStandaloneFileArtifact({
|
|
1959
|
+
format: params.image.format,
|
|
1960
|
+
ttlMs: params.ttlMs,
|
|
1961
|
+
context: params.context
|
|
1962
|
+
});
|
|
1963
|
+
const outputPath = params.artifactId ? params.store.allocateFilePath(params.artifactId, params.image.format) : standaloneArtifact.filePath;
|
|
1964
|
+
await params.screenshotter.screenshotHtml({
|
|
1965
|
+
html: params.html,
|
|
1966
|
+
outputPath,
|
|
1967
|
+
theme: params.theme,
|
|
1968
|
+
image: params.image
|
|
1969
|
+
});
|
|
1970
|
+
return {
|
|
1971
|
+
path: outputPath,
|
|
1972
|
+
bytes: (await fs.stat(outputPath)).size,
|
|
1973
|
+
...standaloneArtifact?.id ? { artifactId: standaloneArtifact.id } : {},
|
|
1974
|
+
...standaloneArtifact?.expiresAt ? { expiresAt: standaloneArtifact.expiresAt } : {}
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function buildArtifactContext(context) {
|
|
1978
|
+
if (!context) return;
|
|
1979
|
+
const artifactContext = {
|
|
1980
|
+
agentId: normalizeOptionalString(context.agentId),
|
|
1981
|
+
sessionId: normalizeOptionalString(context.sessionId),
|
|
1982
|
+
messageChannel: normalizeOptionalString(context.messageChannel),
|
|
1983
|
+
agentAccountId: normalizeOptionalString(context.agentAccountId)
|
|
1984
|
+
};
|
|
1985
|
+
return Object.values(artifactContext).some((value) => value !== void 0) ? artifactContext : void 0;
|
|
1986
|
+
}
|
|
1987
|
+
function normalizeDiffInput(params) {
|
|
1988
|
+
const patch = params.patch?.trim();
|
|
1989
|
+
const before = params.before;
|
|
1990
|
+
const after = params.after;
|
|
1991
|
+
if (patch) {
|
|
1992
|
+
assertMaxBytes(patch, "patch", MAX_PATCH_BYTES);
|
|
1993
|
+
if (before !== void 0 || after !== void 0) throw new PluginToolInputError("Provide either patch or before/after input, not both.");
|
|
1994
|
+
const title = params.title?.trim();
|
|
1995
|
+
if (title) assertMaxBytes(title, "title", MAX_TITLE_BYTES);
|
|
1996
|
+
return {
|
|
1997
|
+
kind: "patch",
|
|
1998
|
+
patch,
|
|
1999
|
+
title
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
if (before === void 0 || after === void 0) throw new PluginToolInputError("Provide patch or both before and after text.");
|
|
2003
|
+
assertMaxBytes(before, "before", MAX_BEFORE_AFTER_BYTES);
|
|
2004
|
+
assertMaxBytes(after, "after", MAX_BEFORE_AFTER_BYTES);
|
|
2005
|
+
const path = normalizeOptionalString(params.path);
|
|
2006
|
+
const lang = normalizeOptionalString(params.lang);
|
|
2007
|
+
const title = normalizeOptionalString(params.title);
|
|
2008
|
+
if (path) assertMaxBytes(path, "path", MAX_PATH_BYTES);
|
|
2009
|
+
if (lang) assertMaxBytes(lang, "lang", MAX_LANG_BYTES);
|
|
2010
|
+
if (title) assertMaxBytes(title, "title", MAX_TITLE_BYTES);
|
|
2011
|
+
return {
|
|
2012
|
+
kind: "before_after",
|
|
2013
|
+
before,
|
|
2014
|
+
after,
|
|
2015
|
+
path,
|
|
2016
|
+
lang,
|
|
2017
|
+
title
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function assertMaxBytes(value, label, maxBytes) {
|
|
2021
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return;
|
|
2022
|
+
throw new PluginToolInputError(`${label} exceeds maximum size (${maxBytes} bytes).`);
|
|
2023
|
+
}
|
|
2024
|
+
function normalizeBaseUrl(baseUrl) {
|
|
2025
|
+
const normalized = baseUrl?.trim();
|
|
2026
|
+
if (!normalized) return;
|
|
2027
|
+
try {
|
|
2028
|
+
return normalizeViewerBaseUrl(normalized);
|
|
2029
|
+
} catch {
|
|
2030
|
+
throw new PluginToolInputError(`Invalid baseUrl: ${normalized}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
function normalizeMode(mode, fallback) {
|
|
2034
|
+
return mode && DIFF_MODES.includes(mode) ? mode : fallback;
|
|
2035
|
+
}
|
|
2036
|
+
function normalizeTheme(theme, fallback) {
|
|
2037
|
+
return theme && DIFF_THEMES.includes(theme) ? theme : fallback;
|
|
2038
|
+
}
|
|
2039
|
+
function normalizeLayout(layout, fallback) {
|
|
2040
|
+
return layout && DIFF_LAYOUTS.includes(layout) ? layout : fallback;
|
|
2041
|
+
}
|
|
2042
|
+
function normalizeTtlMs(ttlSeconds) {
|
|
2043
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds === void 0) return;
|
|
2044
|
+
return Math.floor(ttlSeconds * 1e3);
|
|
2045
|
+
}
|
|
2046
|
+
var PluginToolInputError = class extends Error {
|
|
2047
|
+
constructor(message) {
|
|
2048
|
+
super(message);
|
|
2049
|
+
this.name = "ToolInputError";
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
//#endregion
|
|
2053
|
+
//#region extensions/diffs/src/plugin.ts
|
|
2054
|
+
function registerDiffsPlugin(api) {
|
|
2055
|
+
const store = new DiffArtifactStore({
|
|
2056
|
+
rootDir: path.join(resolvePreferredAutoBotTmpDir(), "autobot-diffs"),
|
|
2057
|
+
logger: api.logger
|
|
2058
|
+
});
|
|
2059
|
+
const resolveCurrentPluginConfig = () => resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "diffs", api.pluginConfig) ?? {};
|
|
2060
|
+
const resolveCurrentAccessConfig = () => {
|
|
2061
|
+
const currentConfig = api.runtime.config?.current?.() ?? api.config;
|
|
2062
|
+
return {
|
|
2063
|
+
allowRemoteViewer: resolveDiffsPluginSecurity(resolveCurrentPluginConfig()).allowRemoteViewer,
|
|
2064
|
+
trustedProxies: currentConfig.gateway?.trustedProxies,
|
|
2065
|
+
allowRealIpFallback: currentConfig.gateway?.allowRealIpFallback === true
|
|
2066
|
+
};
|
|
2067
|
+
};
|
|
2068
|
+
const initialAccessConfig = resolveCurrentAccessConfig();
|
|
2069
|
+
api.registerTool((ctx) => {
|
|
2070
|
+
const pluginConfig = resolveCurrentPluginConfig();
|
|
2071
|
+
return createDiffsTool({
|
|
2072
|
+
api,
|
|
2073
|
+
store,
|
|
2074
|
+
defaults: resolveDiffsPluginDefaults(pluginConfig),
|
|
2075
|
+
viewerBaseUrl: resolveDiffsPluginViewerBaseUrl(pluginConfig),
|
|
2076
|
+
context: ctx
|
|
2077
|
+
});
|
|
2078
|
+
}, { name: "diffs" });
|
|
2079
|
+
api.registerHttpRoute({
|
|
2080
|
+
path: "/plugins/diffs",
|
|
2081
|
+
auth: "plugin",
|
|
2082
|
+
match: "prefix",
|
|
2083
|
+
handler: createDiffsHttpHandler({
|
|
2084
|
+
store,
|
|
2085
|
+
logger: api.logger,
|
|
2086
|
+
allowRemoteViewer: initialAccessConfig.allowRemoteViewer,
|
|
2087
|
+
trustedProxies: initialAccessConfig.trustedProxies,
|
|
2088
|
+
allowRealIpFallback: initialAccessConfig.allowRealIpFallback,
|
|
2089
|
+
resolveAccessConfig: resolveCurrentAccessConfig
|
|
2090
|
+
})
|
|
2091
|
+
});
|
|
2092
|
+
api.on("before_prompt_build", async () => ({ prependSystemContext: DIFFS_AGENT_GUIDANCE }));
|
|
2093
|
+
}
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region extensions/diffs/index.ts
|
|
2096
|
+
var diffs_default = definePluginEntry({
|
|
2097
|
+
id: "diffs",
|
|
2098
|
+
name: "Diffs",
|
|
2099
|
+
description: "Read-only diff viewer and PNG/PDF renderer for agents.",
|
|
2100
|
+
configSchema: diffsPluginConfigSchema,
|
|
2101
|
+
register: registerDiffsPlugin
|
|
2102
|
+
});
|
|
2103
|
+
//#endregion
|
|
2104
|
+
export { diffs_default as default };
|