ptywright 0.4.0 → 0.5.0
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/README.md +38 -31
- package/dist/agent.mjs +2 -2
- package/dist/bin/ptywright.mjs +1 -1
- package/dist/{cli-C40H_ElC.mjs → cli-PnG6UR43.mjs} +2519 -2472
- package/dist/cli.mjs +1 -1
- package/dist/config.mjs +1 -1
- package/dist/env-DPYHo-zH.mjs +36 -0
- package/dist/index.mjs +1 -1
- package/dist/manifest_files-DW80c1H7.mjs +77 -0
- package/dist/mcp.mjs +1 -1
- package/dist/pty-cassette.mjs +1 -1
- package/dist/{runner-CembqDgJ.mjs → runner-C1gPRyCM.mjs} +2042 -1127
- package/dist/{runner-zApMYWZx.mjs → runner-wW_DCBX7.mjs} +1576 -1422
- package/dist/script.mjs +1 -1
- package/dist/{server-h--2U0Ic.mjs → server-DMnnXjWv.mjs} +2643 -2527
- package/dist/session.mjs +1 -1
- package/dist/{terminal_session-DopC7Xg6.mjs → terminal_session-DJKr-O3X.mjs} +349 -328
- package/package.json +2 -1
- package/dist/{config-B0r-JCFI.mjs → config-bGg636EW.mjs} +1 -1
- package/dist/{pty_like-DqCo7XdB.mjs → pty_like-BjeBibSL.mjs} +2 -2
|
@@ -1,223 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as sameArgv, n as validateManifestFiles, r as formatZodIssues, t as collectManifestFiles } from "./manifest_files-DW80c1H7.mjs";
|
|
2
|
+
import { t as envTruthy } from "./env-DPYHo-zH.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
5
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
6
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { chromium } from "playwright";
|
|
7
|
+
import { z } from "zod";
|
|
7
8
|
import { createServer } from "node:http";
|
|
9
|
+
import { chromium } from "playwright";
|
|
8
10
|
import { spawn } from "node:child_process";
|
|
9
|
-
//#region src/agent/manifest.ts
|
|
10
|
-
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
11
|
-
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
12
|
-
const agentManifestKindSchema = z.enum([
|
|
13
|
-
"run",
|
|
14
|
-
"replay-suite",
|
|
15
|
-
"check",
|
|
16
|
-
"promote"
|
|
17
|
-
]);
|
|
18
|
-
const agentManifestFileKindSchema = z.enum([
|
|
19
|
-
"flow",
|
|
20
|
-
"cassette",
|
|
21
|
-
"run-record",
|
|
22
|
-
"replay-summary",
|
|
23
|
-
"check-summary",
|
|
24
|
-
"promote-summary",
|
|
25
|
-
"report",
|
|
26
|
-
"terminal",
|
|
27
|
-
"dom",
|
|
28
|
-
"screenshot",
|
|
29
|
-
"diff"
|
|
30
|
-
]);
|
|
31
|
-
const agentManifestCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
32
|
-
const agentManifestValidationStageSchema = z.object({
|
|
33
|
-
name: z.string().min(1),
|
|
34
|
-
ok: z.boolean(),
|
|
35
|
-
totalCount: z.number().int().nonnegative(),
|
|
36
|
-
failureCount: z.number().int().nonnegative()
|
|
37
|
-
}).strict();
|
|
38
|
-
const agentManifestValidationSchema = z.object({
|
|
39
|
-
ok: z.boolean(),
|
|
40
|
-
stages: z.array(agentManifestValidationStageSchema)
|
|
41
|
-
}).strict().superRefine((validation, ctx) => {
|
|
42
|
-
const ok = validation.stages.every((stage) => stage.ok && stage.failureCount === 0);
|
|
43
|
-
if (validation.ok !== ok) ctx.addIssue({
|
|
44
|
-
code: z.ZodIssueCode.custom,
|
|
45
|
-
path: ["ok"],
|
|
46
|
-
message: "validation ok must match validation stages"
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
const agentManifestFileSchema = z.object({
|
|
50
|
-
path: z.string().min(1),
|
|
51
|
-
kind: agentManifestFileKindSchema,
|
|
52
|
-
role: z.string().min(1).optional(),
|
|
53
|
-
ok: z.boolean().optional(),
|
|
54
|
-
bytes: z.number().int().nonnegative(),
|
|
55
|
-
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
56
|
-
}).strict();
|
|
57
|
-
const agentManifestSchema = z.object({
|
|
58
|
-
$schema: z.string().optional(),
|
|
59
|
-
version: z.literal(1),
|
|
60
|
-
kind: agentManifestKindSchema,
|
|
61
|
-
ok: z.boolean(),
|
|
62
|
-
generatedAt: z.string().min(1),
|
|
63
|
-
rootDir: z.string().min(1),
|
|
64
|
-
primaryPath: z.string().min(1),
|
|
65
|
-
commands: z.record(agentManifestCommandSchema),
|
|
66
|
-
validation: agentManifestValidationSchema.optional(),
|
|
67
|
-
files: z.array(agentManifestFileSchema)
|
|
68
|
-
}).strict().superRefine((manifest, ctx) => {
|
|
69
|
-
const seen = /* @__PURE__ */ new Set();
|
|
70
|
-
for (const file of manifest.files) {
|
|
71
|
-
if (seen.has(file.path)) ctx.addIssue({
|
|
72
|
-
code: z.ZodIssueCode.custom,
|
|
73
|
-
path: ["files"],
|
|
74
|
-
message: `duplicate manifest file path: ${file.path}`
|
|
75
|
-
});
|
|
76
|
-
seen.add(file.path);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
function agentManifestPath(rootDir) {
|
|
80
|
-
return join(rootDir, AGENT_MANIFEST_FILE_NAME);
|
|
81
|
-
}
|
|
82
|
-
function createAgentManifest(options) {
|
|
83
|
-
return normalizeAgentManifest({
|
|
84
|
-
$schema: AGENT_MANIFEST_SCHEMA_URL,
|
|
85
|
-
version: 1,
|
|
86
|
-
kind: options.kind,
|
|
87
|
-
ok: options.ok,
|
|
88
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
-
rootDir: options.rootDir,
|
|
90
|
-
primaryPath: options.primaryPath,
|
|
91
|
-
commands: options.commands,
|
|
92
|
-
validation: options.validation,
|
|
93
|
-
files: collectManifestFiles(options.files, options.rootDir)
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function readAgentManifestPath(path) {
|
|
97
|
-
return normalizeAgentManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
98
|
-
}
|
|
99
|
-
function writeAgentManifestPath(path, options) {
|
|
100
|
-
const manifest = createAgentManifest(options);
|
|
101
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
103
|
-
return manifest;
|
|
104
|
-
}
|
|
105
|
-
function normalizeAgentManifest(input) {
|
|
106
|
-
try {
|
|
107
|
-
const parsed = agentManifestSchema.parse(input);
|
|
108
|
-
return {
|
|
109
|
-
...parsed,
|
|
110
|
-
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json"
|
|
111
|
-
};
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (error instanceof z.ZodError) throw new Error(`invalid agent manifest: ${formatZodIssues$1(error)}`);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function validateAgentManifestFiles(manifest, manifestPath) {
|
|
118
|
-
const failures = [];
|
|
119
|
-
const baseDir = manifestPath ? dirname(resolve(process.cwd(), manifestPath)) : resolve(process.cwd(), manifest.rootDir);
|
|
120
|
-
for (const file of manifest.files) {
|
|
121
|
-
let current = null;
|
|
122
|
-
try {
|
|
123
|
-
current = readManifestFile(file, baseDir);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
failures.push(`${file.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (current.bytes !== file.bytes) failures.push(`${file.path}: bytes ${current.bytes} !== ${file.bytes}`);
|
|
129
|
-
if (current.sha256 !== file.sha256) failures.push(`${file.path}: sha256 ${current.sha256} !== ${file.sha256}`);
|
|
130
|
-
}
|
|
131
|
-
if (failures.length > 0) throw new Error(`invalid agent manifest files: ${failures.join("; ")}`);
|
|
132
|
-
}
|
|
133
|
-
function isAgentManifestLike(input) {
|
|
134
|
-
return typeof input === "object" && input !== null && input.version === 1 && typeof input.kind === "string" && Array.isArray(input.files) && typeof input.commands === "object";
|
|
135
|
-
}
|
|
136
|
-
function collectManifestFiles(files, rootDir) {
|
|
137
|
-
const out = [];
|
|
138
|
-
const seen = /* @__PURE__ */ new Set();
|
|
139
|
-
const rootAbs = resolve(process.cwd(), rootDir);
|
|
140
|
-
for (const file of files) {
|
|
141
|
-
if (!file.path || seen.has(file.path)) continue;
|
|
142
|
-
seen.add(file.path);
|
|
143
|
-
try {
|
|
144
|
-
out.push(readManifestFile(file, rootAbs, { portableRoot: true }));
|
|
145
|
-
} catch {}
|
|
146
|
-
}
|
|
147
|
-
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
148
|
-
}
|
|
149
|
-
function readManifestFile(file, baseDir, options = {}) {
|
|
150
|
-
if (!file.path) throw new Error("missing file path");
|
|
151
|
-
const absPath = isAbsolute(file.path) ? file.path : resolve(options.portableRoot ? process.cwd() : baseDir, file.path);
|
|
152
|
-
if (!statSync(absPath).isFile()) throw new Error("not a file");
|
|
153
|
-
const bytes = readFileSync(absPath);
|
|
154
|
-
return {
|
|
155
|
-
path: options.portableRoot ? portableManifestPath(absPath, baseDir) : file.path,
|
|
156
|
-
kind: file.kind,
|
|
157
|
-
role: file.role,
|
|
158
|
-
ok: file.ok,
|
|
159
|
-
bytes: bytes.byteLength,
|
|
160
|
-
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
function portableManifestPath(absPath, rootAbs) {
|
|
164
|
-
const rel = relative(rootAbs, absPath);
|
|
165
|
-
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return rel;
|
|
166
|
-
return absPath;
|
|
167
|
-
}
|
|
168
|
-
function formatZodIssues$1(error) {
|
|
169
|
-
return error.issues.map((issue) => {
|
|
170
|
-
return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
171
|
-
}).join("; ");
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
174
|
-
//#region src/agent/browser.ts
|
|
175
|
-
async function launchAgentBrowser(args) {
|
|
176
|
-
let lastError;
|
|
177
|
-
for (let attempt = 0; attempt < 5; attempt += 1) try {
|
|
178
|
-
const browser = await chromium.launch({ headless: args.headless });
|
|
179
|
-
await verifyBrowserLaunch(browser);
|
|
180
|
-
return browser;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
lastError = error;
|
|
183
|
-
if (!isTransientBrowserLaunchError(error) || attempt === 4) throw error;
|
|
184
|
-
await new Promise((resolveRetry) => setTimeout(resolveRetry, 250 * (attempt + 1)));
|
|
185
|
-
}
|
|
186
|
-
throw lastError;
|
|
187
|
-
}
|
|
188
|
-
async function verifyBrowserLaunch(browser) {
|
|
189
|
-
const context = await browser.newContext();
|
|
190
|
-
try {
|
|
191
|
-
const page = await context.newPage();
|
|
192
|
-
await page.goto("data:text/html,<title>ptywright</title>", { waitUntil: "load" });
|
|
193
|
-
await page.close();
|
|
194
|
-
} catch (error) {
|
|
195
|
-
await browser.close();
|
|
196
|
-
throw error;
|
|
197
|
-
} finally {
|
|
198
|
-
await context.close().catch(() => void 0);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function isTransientBrowserLaunchError(error) {
|
|
202
|
-
const fields = errorFields(error);
|
|
203
|
-
return fields.message.includes("Failed to connect") || fields.message.includes("Target page, context or browser has been closed") || fields.message.includes("browserType.launch") || fields.code === "ENOENT" || fields.code === "ECONNREFUSED" || fields.syscall === "connect";
|
|
204
|
-
}
|
|
205
|
-
function errorFields(error) {
|
|
206
|
-
const record = typeof error === "object" && error !== null ? error : {};
|
|
207
|
-
return {
|
|
208
|
-
message: error instanceof Error ? error.message : String(error),
|
|
209
|
-
code: typeof record.code === "string" ? record.code : "",
|
|
210
|
-
syscall: typeof record.syscall === "string" ? record.syscall : ""
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
//#endregion
|
|
214
11
|
//#region src/agent/normalize.ts
|
|
215
|
-
function applyAgentMasks(input, rules = []) {
|
|
12
|
+
function applyAgentMasks(input, rules = [], options = {}) {
|
|
216
13
|
let out = input;
|
|
217
14
|
for (const rule of rules) {
|
|
218
15
|
const flags = rule.flags ?? "g";
|
|
219
16
|
const regex = new RegExp(rule.regex, flags.includes("g") ? flags : `${flags}g`);
|
|
220
|
-
const replacement = rule.replacement ?? "<masked>";
|
|
17
|
+
const replacement = options.replacement?.(rule.replacement ?? "<masked>") ?? rule.replacement ?? "<masked>";
|
|
221
18
|
out = out.replace(regex, (match) => {
|
|
222
19
|
if (!rule.preserveLength) return replacement;
|
|
223
20
|
if (replacement.length === match.length) return replacement;
|
|
@@ -234,7 +31,7 @@ function normalizeTerminalText(input, rules = []) {
|
|
|
234
31
|
return lines.join("\n");
|
|
235
32
|
}
|
|
236
33
|
function normalizeDomSnapshot(input, rules = []) {
|
|
237
|
-
return applyAgentMasks(input.replace(/\sdata-v-[a-z0-9-]+="[^"]*"/g, "").replace(/\sstyle="[^"]*--term-(?:cell-width|row-height):[^"]*"/g, "").replace(/\s+/g, " ").replace(/>\s+</g, "><").replace(/<div class="[^"]*\bterm-row\b[^"]*\bterm-scrollback-row\b[^"]*"[^>]*><span[^>]*><\/span><\/div>/g, "").trim(), rules);
|
|
34
|
+
return applyAgentMasks(input.replace(/\sdata-v-[a-z0-9-]+="[^"]*"/g, "").replace(/\sstyle="[^"]*--term-(?:cell-width|row-height):[^"]*"/g, "").replace(/\s+/g, " ").replace(/>\s+</g, "><").replace(/<div class="[^"]*\bterm-row\b[^"]*\bterm-scrollback-row\b[^"]*"[^>]*><span[^>]*><\/span><\/div>/g, "").trim(), rules, { replacement: escapeHtmlText });
|
|
238
35
|
}
|
|
239
36
|
function sanitizeArtifactName(input) {
|
|
240
37
|
return input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
|
|
@@ -247,6 +44,9 @@ function shortHash(input) {
|
|
|
247
44
|
}
|
|
248
45
|
return hash.toString(16).padStart(8, "0");
|
|
249
46
|
}
|
|
47
|
+
function escapeHtmlText(input) {
|
|
48
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
49
|
+
}
|
|
250
50
|
//#endregion
|
|
251
51
|
//#region src/agent/schema.ts
|
|
252
52
|
const agentTextMaskRuleSchema = z.object({
|
|
@@ -394,73 +194,15 @@ function resolveAgentLaunchMode(launch) {
|
|
|
394
194
|
return inferAgentLaunchMode(launch);
|
|
395
195
|
}
|
|
396
196
|
//#endregion
|
|
397
|
-
//#region src/agent/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
viewport: agentViewportSchema,
|
|
401
|
-
phase: z.number().int().nonnegative(),
|
|
402
|
-
stepIndex: z.number().int().nonnegative().nullable(),
|
|
403
|
-
stepType: z.string().min(1),
|
|
404
|
-
terminalText: z.string(),
|
|
405
|
-
terminalHash: z.string().min(1),
|
|
406
|
-
dom: z.string(),
|
|
407
|
-
domHash: z.string().min(1),
|
|
408
|
-
capturedAt: z.string().min(1)
|
|
409
|
-
});
|
|
410
|
-
const agentCassetteSchema = z.object({
|
|
411
|
-
$schema: z.string().optional(),
|
|
412
|
-
version: z.literal(1),
|
|
413
|
-
name: z.string().min(1),
|
|
414
|
-
createdAt: z.string().min(1),
|
|
415
|
-
spec: agentFlowSpecSchema.optional(),
|
|
416
|
-
frames: z.array(agentCassetteFrameSchema).min(1)
|
|
417
|
-
});
|
|
418
|
-
function createAgentCassette(name, spec) {
|
|
419
|
-
return {
|
|
420
|
-
$schema: AGENT_CASSETTE_SCHEMA_URL,
|
|
421
|
-
version: 1,
|
|
422
|
-
name,
|
|
423
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
424
|
-
spec: normalizeAgentFlowSpec(spec),
|
|
425
|
-
frames: []
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function normalizeAgentCassette(input, fallbackSpec) {
|
|
429
|
-
const parsed = agentCassetteSchema.parse(input);
|
|
430
|
-
const specInput = parsed.spec ?? fallbackSpec;
|
|
431
|
-
if (!specInput) throw new Error("invalid agent cassette: missing spec");
|
|
432
|
-
validateCassetteFrameHashes(parsed.frames);
|
|
433
|
-
return {
|
|
434
|
-
...parsed,
|
|
435
|
-
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json",
|
|
436
|
-
spec: normalizeAgentFlowSpec(specInput)
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
function validateCassetteFrameHashes(frames) {
|
|
440
|
-
for (const frame of frames) {
|
|
441
|
-
if (shortHash(frame.terminalText) !== frame.terminalHash) throw new Error(`invalid agent cassette: terminal hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
442
|
-
if (shortHash(frame.dom) !== frame.domHash) throw new Error(`invalid agent cassette: dom hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function readAgentCassettePath(path, fallbackSpec) {
|
|
446
|
-
return normalizeAgentCassette(JSON.parse(readFileSync(path, "utf8")), fallbackSpec);
|
|
447
|
-
}
|
|
448
|
-
function isAgentCassetteLike(input) {
|
|
449
|
-
return typeof input === "object" && input !== null && input.version === 1 && Array.isArray(input.frames);
|
|
197
|
+
//#region src/agent/html_escape.ts
|
|
198
|
+
function escapeHtml(input) {
|
|
199
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
450
200
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
...frame,
|
|
454
|
-
terminalHash: shortHash(frame.terminalText),
|
|
455
|
-
domHash: shortHash(frame.dom)
|
|
456
|
-
};
|
|
457
|
-
const index = cassette.frames.findIndex((candidate) => candidate.viewport.name === next.viewport.name && candidate.phase === next.phase);
|
|
458
|
-
if (index >= 0) {
|
|
459
|
-
cassette.frames[index] = next;
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
cassette.frames.push(next);
|
|
201
|
+
function escapeAttribute(input) {
|
|
202
|
+
return escapeHtml(input).replace(/'/g, "'");
|
|
463
203
|
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/agent/cassette_server.ts
|
|
464
206
|
async function startAgentCassetteServer(cassette) {
|
|
465
207
|
const sockets = /* @__PURE__ */ new Set();
|
|
466
208
|
const server = createServer((request, response) => {
|
|
@@ -496,7 +238,7 @@ async function startAgentCassetteServer(cassette) {
|
|
|
496
238
|
});
|
|
497
239
|
const address = server.address();
|
|
498
240
|
if (!address || typeof address === "string") {
|
|
499
|
-
await closeServer(server);
|
|
241
|
+
await closeServer(server, sockets);
|
|
500
242
|
throw new Error("failed to bind cassette replay server");
|
|
501
243
|
}
|
|
502
244
|
return {
|
|
@@ -511,7 +253,7 @@ function renderCassetteHtml(cassette) {
|
|
|
511
253
|
<head>
|
|
512
254
|
<meta charset="utf-8" />
|
|
513
255
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
514
|
-
<title>${escapeHtml
|
|
256
|
+
<title>${escapeHtml(cassette.name)} cassette replay</title>
|
|
515
257
|
<style>
|
|
516
258
|
:root {
|
|
517
259
|
color-scheme: dark;
|
|
@@ -542,10 +284,25 @@ function renderCassetteHtml(cassette) {
|
|
|
542
284
|
const root = document.querySelector("[data-terminal-root]");
|
|
543
285
|
let phase = 0;
|
|
544
286
|
|
|
287
|
+
function queryViewport() {
|
|
288
|
+
const params = new URLSearchParams(window.location.search);
|
|
289
|
+
const width = Number.parseInt(params.get("viewportWidth") || "", 10);
|
|
290
|
+
const height = Number.parseInt(params.get("viewportHeight") || "", 10);
|
|
291
|
+
const name = params.get("viewportName") || "";
|
|
292
|
+
return {
|
|
293
|
+
height: Number.isFinite(height) && height > 0 ? height : window.innerHeight,
|
|
294
|
+
name,
|
|
295
|
+
width: Number.isFinite(width) && width > 0 ? width : window.innerWidth,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
545
299
|
function viewportScore(frame) {
|
|
546
300
|
const viewport = frame.viewport || {};
|
|
547
|
-
|
|
548
|
-
|
|
301
|
+
const target = queryViewport();
|
|
302
|
+
const namePenalty = target.name && viewport.name !== target.name ? 100000 : 0;
|
|
303
|
+
return namePenalty +
|
|
304
|
+
Math.abs((viewport.width || target.width) - target.width) +
|
|
305
|
+
Math.abs((viewport.height || target.height) - target.height);
|
|
549
306
|
}
|
|
550
307
|
|
|
551
308
|
function chooseFrame() {
|
|
@@ -620,288 +377,193 @@ async function closeServer(server, sockets) {
|
|
|
620
377
|
});
|
|
621
378
|
});
|
|
622
379
|
}
|
|
623
|
-
function escapeHtml$1(input) {
|
|
624
|
-
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
625
|
-
}
|
|
626
380
|
//#endregion
|
|
627
|
-
//#region src/agent/
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
function resolveNamedDir(root, name, configRoot) {
|
|
652
|
-
if (!root) return void 0;
|
|
653
|
-
const namedDir = join(root, name);
|
|
654
|
-
return isAbsolute(namedDir) ? namedDir : resolve(configRoot, namedDir);
|
|
655
|
-
}
|
|
656
|
-
function cloneViewports(viewports) {
|
|
657
|
-
return Array.isArray(viewports) && viewports.length > 0 ? viewports.map((viewport) => ({ ...viewport })) : void 0;
|
|
658
|
-
}
|
|
659
|
-
function mergeMaskRules(configMask, specMask) {
|
|
660
|
-
const merged = [...configMask ?? [], ...specMask ?? []];
|
|
661
|
-
return merged.length > 0 ? merged : void 0;
|
|
662
|
-
}
|
|
663
|
-
//#endregion
|
|
664
|
-
//#region src/agent/command_launch.ts
|
|
665
|
-
const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
|
|
666
|
-
function buildCommandLaunchCommand(launch, options = {}) {
|
|
667
|
-
if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
|
|
668
|
-
const rootDir = options.rootDir ?? process.cwd();
|
|
669
|
-
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
381
|
+
//#region src/agent/cassette.ts
|
|
382
|
+
const AGENT_CASSETTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json";
|
|
383
|
+
const agentCassetteFrameSchema = z.object({
|
|
384
|
+
viewport: agentViewportSchema,
|
|
385
|
+
phase: z.number().int().nonnegative(),
|
|
386
|
+
stepIndex: z.number().int().nonnegative().nullable(),
|
|
387
|
+
stepType: z.string().min(1),
|
|
388
|
+
terminalText: z.string(),
|
|
389
|
+
terminalHash: z.string().min(1),
|
|
390
|
+
dom: z.string(),
|
|
391
|
+
domHash: z.string().min(1),
|
|
392
|
+
capturedAt: z.string().min(1)
|
|
393
|
+
});
|
|
394
|
+
const agentCassetteSchema = z.object({
|
|
395
|
+
$schema: z.string().optional(),
|
|
396
|
+
version: z.literal(1),
|
|
397
|
+
name: z.string().min(1),
|
|
398
|
+
createdAt: z.string().min(1),
|
|
399
|
+
spec: agentFlowSpecSchema.optional(),
|
|
400
|
+
frames: z.array(agentCassetteFrameSchema).min(1)
|
|
401
|
+
});
|
|
402
|
+
function createAgentCassette(name, spec) {
|
|
670
403
|
return {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
},
|
|
678
|
-
label: launch.command,
|
|
679
|
-
urlRegex: launch.urlRegex,
|
|
680
|
-
waitForUrlMs: launch.waitForUrlMs
|
|
404
|
+
$schema: AGENT_CASSETTE_SCHEMA_URL,
|
|
405
|
+
version: 1,
|
|
406
|
+
name,
|
|
407
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
408
|
+
spec: normalizeAgentFlowSpec(spec),
|
|
409
|
+
frames: []
|
|
681
410
|
};
|
|
682
411
|
}
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
stdio: [
|
|
689
|
-
"ignore",
|
|
690
|
-
"pipe",
|
|
691
|
-
"pipe"
|
|
692
|
-
]
|
|
693
|
-
});
|
|
694
|
-
const stdoutChunks = [];
|
|
695
|
-
const stderrChunks = [];
|
|
412
|
+
function normalizeAgentCassette(input, fallbackSpec) {
|
|
413
|
+
const parsed = agentCassetteSchema.parse(input);
|
|
414
|
+
const specInput = parsed.spec ?? fallbackSpec;
|
|
415
|
+
if (!specInput) throw new Error("invalid agent cassette: missing spec");
|
|
416
|
+
validateCassetteFrameHashes(parsed.frames);
|
|
696
417
|
return {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for ${command.label ?? command.file} session URL\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
701
|
-
}, timeoutMs);
|
|
702
|
-
const finish = (result) => {
|
|
703
|
-
if (settled) return;
|
|
704
|
-
settled = true;
|
|
705
|
-
clearTimeout(timer);
|
|
706
|
-
child.stdout.off("data", onStdout);
|
|
707
|
-
child.stderr.off("data", onStderr);
|
|
708
|
-
child.off("error", onError);
|
|
709
|
-
child.off("exit", onExit);
|
|
710
|
-
if (result instanceof Error) reject(result);
|
|
711
|
-
else resolveUrl(result);
|
|
712
|
-
};
|
|
713
|
-
const readUrl = () => {
|
|
714
|
-
const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
|
|
715
|
-
if (found) finish(found);
|
|
716
|
-
};
|
|
717
|
-
const onStdout = (chunk) => {
|
|
718
|
-
stdoutChunks.push(chunk.toString("utf8"));
|
|
719
|
-
readUrl();
|
|
720
|
-
};
|
|
721
|
-
const onStderr = (chunk) => {
|
|
722
|
-
stderrChunks.push(chunk.toString("utf8"));
|
|
723
|
-
readUrl();
|
|
724
|
-
};
|
|
725
|
-
const onError = (error) => finish(error);
|
|
726
|
-
const onExit = (code, signal) => {
|
|
727
|
-
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
728
|
-
};
|
|
729
|
-
child.stdout.on("data", onStdout);
|
|
730
|
-
child.stderr.on("data", onStderr);
|
|
731
|
-
child.once("error", onError);
|
|
732
|
-
child.once("exit", onExit);
|
|
733
|
-
}),
|
|
734
|
-
process: child,
|
|
735
|
-
close: () => closeChild(child)
|
|
418
|
+
...parsed,
|
|
419
|
+
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json",
|
|
420
|
+
spec: normalizeAgentFlowSpec(specInput)
|
|
736
421
|
};
|
|
737
422
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
function formatBrowserLaunchCommand(command) {
|
|
744
|
-
return [command.file, ...command.args].join(" ");
|
|
423
|
+
function validateCassetteFrameHashes(frames) {
|
|
424
|
+
for (const frame of frames) {
|
|
425
|
+
if (shortHash(frame.terminalText) !== frame.terminalHash) throw new Error(`invalid agent cassette: terminal hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
426
|
+
if (shortHash(frame.dom) !== frame.domHash) throw new Error(`invalid agent cassette: dom hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
427
|
+
}
|
|
745
428
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
await new Promise((resolveClose) => {
|
|
749
|
-
const timer = setTimeout(() => {
|
|
750
|
-
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
751
|
-
resolveClose();
|
|
752
|
-
}, 2e3);
|
|
753
|
-
child.once("exit", () => {
|
|
754
|
-
clearTimeout(timer);
|
|
755
|
-
resolveClose();
|
|
756
|
-
});
|
|
757
|
-
child.kill("SIGTERM");
|
|
758
|
-
});
|
|
429
|
+
function readAgentCassettePath(path, fallbackSpec) {
|
|
430
|
+
return normalizeAgentCassette(JSON.parse(readFileSync(path, "utf8")), fallbackSpec);
|
|
759
431
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
function buildAgentLaunchCommand(launch, options = {}) {
|
|
763
|
-
if (resolveAgentLaunchMode(launch) === "url") return null;
|
|
764
|
-
return buildCommandLaunchCommand(launch, options);
|
|
432
|
+
function isAgentCassetteLike(input) {
|
|
433
|
+
return typeof input === "object" && input !== null && input.version === 1 && Array.isArray(input.frames);
|
|
765
434
|
}
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
session: null
|
|
772
|
-
};
|
|
773
|
-
const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
|
|
774
|
-
return {
|
|
775
|
-
mode,
|
|
776
|
-
url: session.url,
|
|
777
|
-
session
|
|
435
|
+
function upsertAgentCassetteFrame(cassette, frame) {
|
|
436
|
+
const next = {
|
|
437
|
+
...frame,
|
|
438
|
+
terminalHash: shortHash(frame.terminalText),
|
|
439
|
+
domHash: shortHash(frame.dom)
|
|
778
440
|
};
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
441
|
+
const index = cassette.frames.findIndex((candidate) => candidate.viewport.name === next.viewport.name && candidate.phase === next.phase);
|
|
442
|
+
if (index >= 0) {
|
|
443
|
+
cassette.frames[index] = next;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
cassette.frames.push(next);
|
|
783
447
|
}
|
|
784
448
|
//#endregion
|
|
785
|
-
//#region src/agent/
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (command === "claude" || command === "claude-code" || command.startsWith("claude-")) return "claude";
|
|
858
|
-
if (command === "droid" || command === "droidx" || command.startsWith("droid")) return "droid";
|
|
859
|
-
return "generic";
|
|
449
|
+
//#region src/agent/manifest.ts
|
|
450
|
+
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
451
|
+
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
452
|
+
const agentManifestKindSchema = z.enum([
|
|
453
|
+
"run",
|
|
454
|
+
"replay-suite",
|
|
455
|
+
"check",
|
|
456
|
+
"promote"
|
|
457
|
+
]);
|
|
458
|
+
const agentManifestFileKindSchema = z.enum([
|
|
459
|
+
"flow",
|
|
460
|
+
"cassette",
|
|
461
|
+
"run-record",
|
|
462
|
+
"replay-summary",
|
|
463
|
+
"check-summary",
|
|
464
|
+
"promote-summary",
|
|
465
|
+
"report",
|
|
466
|
+
"terminal",
|
|
467
|
+
"dom",
|
|
468
|
+
"screenshot",
|
|
469
|
+
"diff"
|
|
470
|
+
]);
|
|
471
|
+
const agentManifestCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
472
|
+
const agentManifestValidationStageSchema = z.object({
|
|
473
|
+
name: z.string().min(1),
|
|
474
|
+
ok: z.boolean(),
|
|
475
|
+
totalCount: z.number().int().nonnegative(),
|
|
476
|
+
failureCount: z.number().int().nonnegative()
|
|
477
|
+
}).strict();
|
|
478
|
+
const agentManifestValidationSchema = z.object({
|
|
479
|
+
ok: z.boolean(),
|
|
480
|
+
stages: z.array(agentManifestValidationStageSchema)
|
|
481
|
+
}).strict().superRefine((validation, ctx) => {
|
|
482
|
+
const ok = validation.stages.every((stage) => stage.ok && stage.failureCount === 0);
|
|
483
|
+
if (validation.ok !== ok) ctx.addIssue({
|
|
484
|
+
code: z.ZodIssueCode.custom,
|
|
485
|
+
path: ["ok"],
|
|
486
|
+
message: "validation ok must match validation stages"
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
const agentManifestFileSchema = z.object({
|
|
490
|
+
path: z.string().min(1),
|
|
491
|
+
kind: agentManifestFileKindSchema,
|
|
492
|
+
role: z.string().min(1).optional(),
|
|
493
|
+
ok: z.boolean().optional(),
|
|
494
|
+
bytes: z.number().int().nonnegative(),
|
|
495
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
496
|
+
}).strict();
|
|
497
|
+
const agentManifestSchema = z.object({
|
|
498
|
+
$schema: z.string().optional(),
|
|
499
|
+
version: z.literal(1),
|
|
500
|
+
kind: agentManifestKindSchema,
|
|
501
|
+
ok: z.boolean(),
|
|
502
|
+
generatedAt: z.string().min(1),
|
|
503
|
+
rootDir: z.string().min(1),
|
|
504
|
+
primaryPath: z.string().min(1),
|
|
505
|
+
commands: z.record(agentManifestCommandSchema),
|
|
506
|
+
validation: agentManifestValidationSchema.optional(),
|
|
507
|
+
files: z.array(agentManifestFileSchema)
|
|
508
|
+
}).strict().superRefine((manifest, ctx) => {
|
|
509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
510
|
+
for (const file of manifest.files) {
|
|
511
|
+
if (seen.has(file.path)) ctx.addIssue({
|
|
512
|
+
code: z.ZodIssueCode.custom,
|
|
513
|
+
path: ["files"],
|
|
514
|
+
message: `duplicate manifest file path: ${file.path}`
|
|
515
|
+
});
|
|
516
|
+
seen.add(file.path);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
function agentManifestPath(rootDir) {
|
|
520
|
+
return join(rootDir, AGENT_MANIFEST_FILE_NAME);
|
|
860
521
|
}
|
|
861
|
-
function
|
|
862
|
-
return
|
|
522
|
+
function createAgentManifest(options) {
|
|
523
|
+
return normalizeAgentManifest({
|
|
524
|
+
$schema: AGENT_MANIFEST_SCHEMA_URL,
|
|
525
|
+
version: 1,
|
|
526
|
+
kind: options.kind,
|
|
527
|
+
ok: options.ok,
|
|
528
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
529
|
+
rootDir: options.rootDir,
|
|
530
|
+
primaryPath: options.primaryPath,
|
|
531
|
+
commands: options.commands,
|
|
532
|
+
validation: options.validation,
|
|
533
|
+
files: collectManifestFiles(options.files, options.rootDir)
|
|
534
|
+
});
|
|
863
535
|
}
|
|
864
|
-
function
|
|
865
|
-
return
|
|
536
|
+
function readAgentManifestPath(path) {
|
|
537
|
+
return normalizeAgentManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
866
538
|
}
|
|
867
|
-
function
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}, {
|
|
896
|
-
type: "snapshot",
|
|
897
|
-
name: "launch",
|
|
898
|
-
targets: [
|
|
899
|
-
"terminal",
|
|
900
|
-
"dom",
|
|
901
|
-
"screenshot"
|
|
902
|
-
]
|
|
903
|
-
}]
|
|
904
|
-
};
|
|
539
|
+
function writeAgentManifestPath(path, options) {
|
|
540
|
+
const manifest = createAgentManifest(options);
|
|
541
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
542
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
543
|
+
return manifest;
|
|
544
|
+
}
|
|
545
|
+
function normalizeAgentManifest(input) {
|
|
546
|
+
try {
|
|
547
|
+
const parsed = agentManifestSchema.parse(input);
|
|
548
|
+
return {
|
|
549
|
+
...parsed,
|
|
550
|
+
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json"
|
|
551
|
+
};
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (error instanceof z.ZodError) throw new Error(`invalid agent manifest: ${formatZodIssues(error)}`);
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function validateAgentManifestFiles(manifest, manifestPath) {
|
|
558
|
+
validateManifestFiles({
|
|
559
|
+
files: manifest.files,
|
|
560
|
+
manifestPath,
|
|
561
|
+
rootDir: manifest.rootDir,
|
|
562
|
+
label: "agent"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function isAgentManifestLike(input) {
|
|
566
|
+
return typeof input === "object" && input !== null && input.version === 1 && typeof input.kind === "string" && Array.isArray(input.files) && typeof input.commands === "object";
|
|
905
567
|
}
|
|
906
568
|
//#endregion
|
|
907
569
|
//#region src/common/argv.ts
|
|
@@ -1011,9 +673,6 @@ function normalizeAgentRunRecord(input) {
|
|
|
1011
673
|
function formatAgentArgv(argv) {
|
|
1012
674
|
return formatArgv(argv);
|
|
1013
675
|
}
|
|
1014
|
-
function sameArgv(left, right) {
|
|
1015
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1016
|
-
}
|
|
1017
676
|
function isReplayArgv(argv) {
|
|
1018
677
|
return argv.length >= 4 && argv[0] === "ptywright" && argv[1] === "agent" && argv[2] === "replay";
|
|
1019
678
|
}
|
|
@@ -1032,175 +691,1227 @@ function isAgentRunRecordLike(input) {
|
|
|
1032
691
|
const candidate = input;
|
|
1033
692
|
return candidate.version === 1 && ("cassettePath" in candidate || "flowPath" in candidate || "spec" in candidate);
|
|
1034
693
|
}
|
|
1035
|
-
function formatZodIssues(error) {
|
|
1036
|
-
return error.issues.map((issue) => {
|
|
1037
|
-
return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
1038
|
-
}).join("; ");
|
|
1039
|
-
}
|
|
1040
694
|
//#endregion
|
|
1041
|
-
//#region src/agent/
|
|
1042
|
-
function
|
|
1043
|
-
|
|
1044
|
-
|
|
695
|
+
//#region src/agent/spec_loader.ts
|
|
696
|
+
async function loadAgentSpec(specPath) {
|
|
697
|
+
const resolved = resolve(process.cwd(), specPath);
|
|
698
|
+
if (resolved.endsWith(".json")) {
|
|
699
|
+
const raw = JSON.parse(readFileSync(resolved, "utf8"));
|
|
700
|
+
return {
|
|
701
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
702
|
+
raw,
|
|
703
|
+
path: resolved
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const mod = await import(`${pathToFileURL(resolved).href}?t=${Date.now()}`);
|
|
707
|
+
const raw = mod.default ?? mod.spec;
|
|
708
|
+
return {
|
|
709
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
710
|
+
raw,
|
|
711
|
+
path: resolved
|
|
712
|
+
};
|
|
1045
713
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/agent/presets.ts
|
|
716
|
+
const COMMON_AGENT_MASKS = [
|
|
717
|
+
{
|
|
718
|
+
regex: "\\b\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?\\b",
|
|
719
|
+
replacement: "<timestamp>"
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
regex: "\\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\b",
|
|
723
|
+
flags: "gi",
|
|
724
|
+
replacement: "<uuid>"
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
regex: "\\b(?:req|run|msg|chatcmpl|call|toolu|session)_[A-Za-z0-9_-]{8,}\\b",
|
|
728
|
+
replacement: "<id>"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
regex: "\\b(?:[0-9a-f]{7,40})\\b",
|
|
732
|
+
flags: "gi",
|
|
733
|
+
replacement: "<hex>"
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
regex: "\\$\\d+(?:\\.\\d{2,6})?\\b",
|
|
737
|
+
replacement: "$<amount>"
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
regex: "\\b(?:\\d+\\.\\d+|\\d+)\\s*(?:s|ms|tokens?|tok)\\b",
|
|
741
|
+
flags: "gi",
|
|
742
|
+
replacement: "<metric>"
|
|
743
|
+
}
|
|
744
|
+
];
|
|
745
|
+
const FLAVOR_MASKS = {
|
|
746
|
+
codex: [{
|
|
747
|
+
regex: "\\b(?:gpt-[A-Za-z0-9._:-]+|o[0-9][A-Za-z0-9._:-]*)\\b",
|
|
748
|
+
flags: "gi",
|
|
749
|
+
replacement: "<model>"
|
|
750
|
+
}, {
|
|
751
|
+
regex: "\\b(?:context|tokens?)\\s*[:=]\\s*[0-9,]+\\b",
|
|
752
|
+
flags: "gi",
|
|
753
|
+
replacement: "<token-count>"
|
|
754
|
+
}],
|
|
755
|
+
claude: [{
|
|
756
|
+
regex: "\\bclaude-[A-Za-z0-9._-]+\\b",
|
|
757
|
+
flags: "gi",
|
|
758
|
+
replacement: "<model>"
|
|
759
|
+
}, {
|
|
760
|
+
regex: "\\b(?:Opus|Sonnet|Haiku)\\s+[0-9.]+\\b",
|
|
761
|
+
flags: "gi",
|
|
762
|
+
replacement: "<model>"
|
|
763
|
+
}],
|
|
764
|
+
droid: [{
|
|
765
|
+
regex: "\\bdroidx?-[A-Za-z0-9._-]+\\b",
|
|
766
|
+
flags: "gi",
|
|
767
|
+
replacement: "<droid-id>"
|
|
768
|
+
}],
|
|
769
|
+
generic: []
|
|
770
|
+
};
|
|
771
|
+
const DEFAULT_VIEWPORTS = [{
|
|
772
|
+
name: "desktop",
|
|
773
|
+
width: 1280,
|
|
774
|
+
height: 820
|
|
775
|
+
}, {
|
|
776
|
+
name: "mobile",
|
|
777
|
+
width: 390,
|
|
778
|
+
height: 844,
|
|
779
|
+
isMobile: true,
|
|
780
|
+
hasTouch: true
|
|
781
|
+
}];
|
|
782
|
+
function resolveAgentFlavor(spec) {
|
|
783
|
+
const explicit = spec.launch.agentFlavor;
|
|
784
|
+
if (explicit) return explicit;
|
|
785
|
+
const command = spec.launch.command?.split(/[\\/]/).at(-1)?.toLowerCase() ?? "";
|
|
786
|
+
if (command === "codex" || command.startsWith("codex-")) return "codex";
|
|
787
|
+
if (command === "claude" || command === "claude-code" || command.startsWith("claude-")) return "claude";
|
|
788
|
+
if (command === "droid" || command === "droidx" || command.startsWith("droid")) return "droid";
|
|
789
|
+
return "generic";
|
|
790
|
+
}
|
|
791
|
+
function getAgentMaskPreset(flavor) {
|
|
792
|
+
return [...COMMON_AGENT_MASKS, ...FLAVOR_MASKS[flavor]];
|
|
793
|
+
}
|
|
794
|
+
function resolveAgentMasks(spec) {
|
|
795
|
+
return [...getAgentMaskPreset(resolveAgentFlavor(spec)), ...spec.defaults?.mask ?? []];
|
|
796
|
+
}
|
|
797
|
+
function createAgentTemplateSpec(flavor) {
|
|
798
|
+
const command = flavor === "droid" ? "droidx" : flavor === "generic" ? "agent" : flavor;
|
|
799
|
+
const name = flavor === "generic" ? "agent_browser_smoke" : `${flavor}_browser_smoke`;
|
|
800
|
+
return {
|
|
801
|
+
name,
|
|
802
|
+
artifactsDir: `.tmp/agent/${name}`,
|
|
803
|
+
snapshotDir: `tests/agent-snapshots/${name}`,
|
|
804
|
+
launch: {
|
|
805
|
+
mode: "command",
|
|
806
|
+
agentFlavor: flavor,
|
|
807
|
+
command: "your-browser-terminal-launcher",
|
|
808
|
+
args: [
|
|
809
|
+
"--agent",
|
|
810
|
+
command,
|
|
811
|
+
"--print-url"
|
|
812
|
+
],
|
|
813
|
+
waitForUrlMs: 15e3
|
|
814
|
+
},
|
|
815
|
+
viewports: DEFAULT_VIEWPORTS.map((viewport) => ({ ...viewport })),
|
|
816
|
+
defaults: {
|
|
817
|
+
timeoutMs: 45e3,
|
|
818
|
+
screenshot: true
|
|
819
|
+
},
|
|
820
|
+
steps: [{
|
|
821
|
+
type: "waitForStableDom",
|
|
822
|
+
timeoutMs: 45e3,
|
|
823
|
+
quietMs: 600,
|
|
824
|
+
intervalMs: 150
|
|
825
|
+
}, {
|
|
826
|
+
type: "snapshot",
|
|
827
|
+
name: "launch",
|
|
828
|
+
targets: [
|
|
829
|
+
"terminal",
|
|
830
|
+
"dom",
|
|
831
|
+
"screenshot"
|
|
832
|
+
]
|
|
833
|
+
}]
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/agent/browser.ts
|
|
838
|
+
async function launchAgentBrowser(args) {
|
|
839
|
+
let lastError;
|
|
840
|
+
for (let attempt = 0; attempt < 5; attempt += 1) try {
|
|
841
|
+
const browser = await chromium.launch({ headless: args.headless });
|
|
842
|
+
await verifyBrowserLaunch(browser);
|
|
843
|
+
return browser;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
lastError = error;
|
|
846
|
+
if (!isTransientBrowserLaunchError(error) || attempt === 4) throw error;
|
|
847
|
+
await new Promise((resolveRetry) => setTimeout(resolveRetry, 250 * (attempt + 1)));
|
|
848
|
+
}
|
|
849
|
+
throw lastError;
|
|
850
|
+
}
|
|
851
|
+
async function verifyBrowserLaunch(browser) {
|
|
852
|
+
const context = await browser.newContext();
|
|
853
|
+
try {
|
|
854
|
+
const page = await context.newPage();
|
|
855
|
+
await page.goto("data:text/html,<title>ptywright</title>", { waitUntil: "load" });
|
|
856
|
+
await page.close();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
await browser.close();
|
|
859
|
+
throw error;
|
|
860
|
+
} finally {
|
|
861
|
+
await context.close().catch(() => void 0);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function isTransientBrowserLaunchError(error) {
|
|
865
|
+
const fields = errorFields(error);
|
|
866
|
+
return fields.message.includes("Failed to connect") || fields.message.includes("Target page, context or browser has been closed") || fields.message.includes("browserType.launch") || fields.code === "ENOENT" || fields.code === "ECONNREFUSED" || fields.syscall === "connect";
|
|
867
|
+
}
|
|
868
|
+
function errorFields(error) {
|
|
869
|
+
const record = typeof error === "object" && error !== null ? error : {};
|
|
870
|
+
return {
|
|
871
|
+
message: error instanceof Error ? error.message : String(error),
|
|
872
|
+
code: typeof record.code === "string" ? record.code : "",
|
|
873
|
+
syscall: typeof record.syscall === "string" ? record.syscall : ""
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/agent/aitty_report_assets.ts
|
|
878
|
+
function prepareAittyReportAssets(context) {
|
|
879
|
+
const sources = resolveAittyReportAssetSources(context);
|
|
880
|
+
if (!existsSync(sources.scriptSource) || !existsSync(sources.styleSource)) throw new Error("@aitty/snapshot report assets are missing. Run the package build before generating reports.");
|
|
881
|
+
const assetDir = join(context.artifactsDir, "assets");
|
|
882
|
+
const scriptPath = join(assetDir, "aitty-web-component.js");
|
|
883
|
+
const stylePath = join(assetDir, "aitty-terminal.css");
|
|
884
|
+
mkdirSync(assetDir, { recursive: true });
|
|
885
|
+
copyFileSync(sources.scriptSource, scriptPath);
|
|
886
|
+
copyFileSync(sources.styleSource, stylePath);
|
|
887
|
+
return {
|
|
888
|
+
scriptPath,
|
|
889
|
+
scriptType: sources.scriptType,
|
|
890
|
+
stylePath
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function resolveAittyReportAssetSources(context) {
|
|
894
|
+
for (const resolverBase of resolveAittyReportResolverBases(context)) {
|
|
895
|
+
const sources = tryResolveAittyReportAssetSources(createRequire(resolverBase));
|
|
896
|
+
if (sources) return sources;
|
|
897
|
+
}
|
|
898
|
+
const fallback = tryResolveAittyReportAssetSources(createRequire(import.meta.url));
|
|
899
|
+
if (fallback) return fallback;
|
|
900
|
+
throw new Error("@aitty/snapshot report assets are missing. Install @aitty/snapshot or run the package build before generating reports.");
|
|
901
|
+
}
|
|
902
|
+
function resolveAittyReportResolverBases(context) {
|
|
903
|
+
const candidates = [
|
|
904
|
+
findNearestPackageJson(dirname(resolve(context.flowPath))),
|
|
905
|
+
findNearestPackageJson(dirname(resolve(context.reportPath))),
|
|
906
|
+
findNearestPackageJson(dirname(resolve(context.artifactsDir))),
|
|
907
|
+
findNearestPackageJson(process.cwd())
|
|
908
|
+
].filter((path) => Boolean(path));
|
|
909
|
+
return Array.from(new Set(candidates));
|
|
910
|
+
}
|
|
911
|
+
function findNearestPackageJson(startDir) {
|
|
912
|
+
let currentDir = resolve(startDir);
|
|
913
|
+
while (true) {
|
|
914
|
+
const packagePath = join(currentDir, "package.json");
|
|
915
|
+
if (existsSync(packagePath)) return packagePath;
|
|
916
|
+
const parentDir = dirname(currentDir);
|
|
917
|
+
if (parentDir === currentDir) return null;
|
|
918
|
+
currentDir = parentDir;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function tryResolveAittyReportAssetSources(resolver) {
|
|
922
|
+
return tryResolveAittyPackageAssetSources(resolver, "@aitty/snapshot") ?? tryResolveAittyPackageAssetSources(resolver, "@aitty/browser");
|
|
923
|
+
}
|
|
924
|
+
function tryResolveAittyPackageAssetSources(resolver, packageName) {
|
|
925
|
+
let scriptSource;
|
|
926
|
+
let scriptType = "classic";
|
|
927
|
+
let styleSource;
|
|
928
|
+
try {
|
|
929
|
+
scriptSource = resolver.resolve(`${packageName}/web-component.global.js`);
|
|
930
|
+
} catch {
|
|
931
|
+
try {
|
|
932
|
+
scriptSource = resolver.resolve(`${packageName}/web-component.js`);
|
|
933
|
+
scriptType = "module";
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
styleSource = resolver.resolve(`${packageName}/style.css`);
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
scriptSource,
|
|
945
|
+
scriptType,
|
|
946
|
+
styleSource
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/agent/report_paths.ts
|
|
951
|
+
function relativeHref(fromPath, targetPath, artifactsDir) {
|
|
952
|
+
if (targetPath.startsWith(artifactsDir)) return relative(dirname(fromPath), targetPath).replaceAll("\\", "/") || ".";
|
|
953
|
+
return targetPath;
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/agent/report_aitty_preview.ts
|
|
957
|
+
function resolveAittyPreviewAssets(previewPath, assets, artifactsDir) {
|
|
958
|
+
return {
|
|
959
|
+
scriptHref: relativeHref(previewPath, assets.scriptPath, artifactsDir),
|
|
960
|
+
scriptType: assets.scriptType,
|
|
961
|
+
styleHref: relativeHref(previewPath, assets.stylePath, artifactsDir)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function renderAittyPreviewAssetTags(assets) {
|
|
965
|
+
return ` <link rel="stylesheet" href="${escapeAttribute(assets.styleHref)}" />
|
|
966
|
+
<script${assets.scriptType === "module" ? " type=\"module\"" : ""} src="${escapeAttribute(assets.scriptHref)}"><\/script>
|
|
967
|
+
`;
|
|
968
|
+
}
|
|
969
|
+
function renderAittyPreviewBody(args) {
|
|
970
|
+
const { snapshot, snapshotLayout, viewOptions } = args;
|
|
971
|
+
return ` <aitty-snapshot
|
|
972
|
+
cols="${snapshotLayout.cols}"
|
|
973
|
+
rows="${snapshotLayout.rows}"
|
|
974
|
+
screen-mode="${escapeAttribute(viewOptions.screenMode)}"
|
|
975
|
+
theme="${escapeAttribute(viewOptions.theme)}"
|
|
976
|
+
font-size="${snapshotLayout.fontSize}"
|
|
977
|
+
line-height="${snapshotLayout.lineHeight}"
|
|
978
|
+
>${snapshot}</aitty-snapshot>`;
|
|
979
|
+
}
|
|
980
|
+
function renderAittyPreviewCss(viewOptions) {
|
|
981
|
+
return ` :root {
|
|
982
|
+
color-scheme: ${viewOptions.theme};
|
|
983
|
+
}
|
|
984
|
+
html,
|
|
985
|
+
body {
|
|
986
|
+
width: 100%;
|
|
987
|
+
height: 100%;
|
|
988
|
+
margin: 0;
|
|
989
|
+
background: var(--theme-term-bg, Canvas);
|
|
990
|
+
}
|
|
991
|
+
aitty-snapshot {
|
|
992
|
+
display: block;
|
|
993
|
+
width: 100%;
|
|
994
|
+
height: 100%;
|
|
995
|
+
}`;
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/agent/report_artifact_paths.ts
|
|
999
|
+
function artifactViewerPath(artifact) {
|
|
1000
|
+
if (artifact.kind === "terminal") return artifact.path.endsWith(".terminal.txt") ? artifact.path.replace(/\.terminal\.txt$/, ".terminal.viewer.html") : `${artifact.path}.viewer.html`;
|
|
1001
|
+
if (artifact.kind === "dom") return artifact.path.endsWith(".dom.html") ? artifact.path.replace(/\.dom\.html$/, ".dom.viewer.html") : `${artifact.path}.viewer.html`;
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
function artifactDomPreviewPath(artifact) {
|
|
1005
|
+
return artifact.path.endsWith(".dom.html") ? artifact.path.replace(/\.dom\.html$/, ".dom.preview.html") : `${artifact.path}.preview.html`;
|
|
1006
|
+
}
|
|
1007
|
+
function artifactSnapshotKey(artifact) {
|
|
1008
|
+
return `${artifact.viewport}\u0000${artifact.name}`;
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/agent/report_dom_artifact_viewer.ts
|
|
1012
|
+
function resolveReportDomPreview(path) {
|
|
1013
|
+
return path ? { path } : null;
|
|
1014
|
+
}
|
|
1015
|
+
function renderDomArtifactViewer(args) {
|
|
1016
|
+
const { artifactsDir, mobile, preview, viewerPath, viewOptions } = args;
|
|
1017
|
+
const src = relativeHref(viewerPath, preview.path, artifactsDir);
|
|
1018
|
+
return `<div class="viewer-viewport dom-viewport" data-mobile="${mobile ? "true" : "false"}" data-screen-mode="${escapeAttribute(viewOptions.screenMode)}"><iframe class="dom-viewer-frame" sandbox="allow-same-origin allow-scripts" src="${escapeAttribute(src)}" title="terminal DOM artifact"></iframe></div>`;
|
|
1019
|
+
}
|
|
1020
|
+
function renderDomArtifactViewerFragment(args) {
|
|
1021
|
+
return {
|
|
1022
|
+
css: "",
|
|
1023
|
+
html: renderDomArtifactViewer(args),
|
|
1024
|
+
script: ""
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/agent/report_raw_ansi_text.ts
|
|
1029
|
+
function renderRawAnsiTextHtml(input) {
|
|
1030
|
+
const out = [];
|
|
1031
|
+
let style = {};
|
|
1032
|
+
let segmentStart = 0;
|
|
1033
|
+
let index = 0;
|
|
1034
|
+
while (index < input.length) {
|
|
1035
|
+
if (!isCsiStart(input, index)) {
|
|
1036
|
+
index += 1;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const end = findCsiEnd(input, index + 2);
|
|
1040
|
+
if (end === -1) {
|
|
1041
|
+
index += 1;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
out.push(renderAnsiTextSegment(input.slice(segmentStart, index), style));
|
|
1045
|
+
if (input[end] === "m") style = applySgrCodes(style, parseSgrCodes(input.slice(index + 2, end)));
|
|
1046
|
+
index = end + 1;
|
|
1047
|
+
segmentStart = index;
|
|
1048
|
+
}
|
|
1049
|
+
out.push(renderAnsiTextSegment(input.slice(segmentStart), style));
|
|
1050
|
+
return out.join("");
|
|
1051
|
+
}
|
|
1052
|
+
function isCsiStart(input, index) {
|
|
1053
|
+
return input.charCodeAt(index) === 27 && input[index + 1] === "[";
|
|
1054
|
+
}
|
|
1055
|
+
function findCsiEnd(input, start) {
|
|
1056
|
+
for (let index = start; index < input.length; index += 1) {
|
|
1057
|
+
const code = input.charCodeAt(index);
|
|
1058
|
+
if (code >= 64 && code <= 126) return index;
|
|
1059
|
+
}
|
|
1060
|
+
return -1;
|
|
1061
|
+
}
|
|
1062
|
+
function parseSgrCodes(input) {
|
|
1063
|
+
if (!input) return [0];
|
|
1064
|
+
return input.split(";").map((value) => {
|
|
1065
|
+
const parsed = Number.parseInt(value, 10);
|
|
1066
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function applySgrCodes(current, codes) {
|
|
1070
|
+
const next = { ...current };
|
|
1071
|
+
for (let index = 0; index < codes.length; index += 1) {
|
|
1072
|
+
const code = codes[index] ?? 0;
|
|
1073
|
+
if (code === 0) {
|
|
1074
|
+
resetStyle(next);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (code === 1) next.bold = true;
|
|
1078
|
+
else if (code === 2) next.dim = true;
|
|
1079
|
+
else if (code === 3) next.italic = true;
|
|
1080
|
+
else if (code === 4) next.underline = true;
|
|
1081
|
+
else if (code === 7) next.inverse = true;
|
|
1082
|
+
else if (code === 9) next.strikethrough = true;
|
|
1083
|
+
else if (code === 22) {
|
|
1084
|
+
next.bold = false;
|
|
1085
|
+
next.dim = false;
|
|
1086
|
+
} else if (code === 23) next.italic = false;
|
|
1087
|
+
else if (code === 24) next.underline = false;
|
|
1088
|
+
else if (code === 27) next.inverse = false;
|
|
1089
|
+
else if (code === 29) next.strikethrough = false;
|
|
1090
|
+
else if (code === 39) next.fg = void 0;
|
|
1091
|
+
else if (code === 49) next.bg = void 0;
|
|
1092
|
+
else if (code >= 30 && code <= 37) next.fg = ansiPaletteColor(code - 30);
|
|
1093
|
+
else if (code >= 90 && code <= 97) next.fg = ansiPaletteColor(code - 90 + 8);
|
|
1094
|
+
else if (code >= 40 && code <= 47) next.bg = ansiPaletteColor(code - 40);
|
|
1095
|
+
else if (code >= 100 && code <= 107) next.bg = ansiPaletteColor(code - 100 + 8);
|
|
1096
|
+
else if ((code === 38 || code === 48) && codes[index + 1] === 5) {
|
|
1097
|
+
const color = ansiPaletteColor(codes[index + 2] ?? 0);
|
|
1098
|
+
if (code === 38) next.fg = color;
|
|
1099
|
+
else next.bg = color;
|
|
1100
|
+
index += 2;
|
|
1101
|
+
} else if ((code === 38 || code === 48) && codes[index + 1] === 2) {
|
|
1102
|
+
const color = `rgb(${clampColor(codes[index + 2] ?? 0)} ${clampColor(codes[index + 3] ?? 0)} ${clampColor(codes[index + 4] ?? 0)})`;
|
|
1103
|
+
if (code === 38) next.fg = color;
|
|
1104
|
+
else next.bg = color;
|
|
1105
|
+
index += 4;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return next;
|
|
1109
|
+
}
|
|
1110
|
+
function resetStyle(style) {
|
|
1111
|
+
style.fg = void 0;
|
|
1112
|
+
style.bg = void 0;
|
|
1113
|
+
style.bold = false;
|
|
1114
|
+
style.dim = false;
|
|
1115
|
+
style.italic = false;
|
|
1116
|
+
style.underline = false;
|
|
1117
|
+
style.inverse = false;
|
|
1118
|
+
style.strikethrough = false;
|
|
1119
|
+
}
|
|
1120
|
+
function renderAnsiTextSegment(text, style) {
|
|
1121
|
+
if (!text) return "";
|
|
1122
|
+
const safeText = escapeHtml(text);
|
|
1123
|
+
const css = ansiStyleToCss(style);
|
|
1124
|
+
return css ? `<span style="${escapeAttribute(css)}">${safeText}</span>` : safeText;
|
|
1125
|
+
}
|
|
1126
|
+
function ansiStyleToCss(style) {
|
|
1127
|
+
const decls = [];
|
|
1128
|
+
const fg = style.inverse ? style.bg : style.fg;
|
|
1129
|
+
const bg = style.inverse ? style.fg : style.bg;
|
|
1130
|
+
if (fg) decls.push(`color: ${fg}`);
|
|
1131
|
+
if (bg) decls.push(`background-color: ${bg}`);
|
|
1132
|
+
if (style.bold) decls.push("font-weight: 700");
|
|
1133
|
+
if (style.italic) decls.push("font-style: italic");
|
|
1134
|
+
if (style.dim) decls.push("opacity: 0.72");
|
|
1135
|
+
const decorations = [];
|
|
1136
|
+
if (style.underline) decorations.push("underline");
|
|
1137
|
+
if (style.strikethrough) decorations.push("line-through");
|
|
1138
|
+
if (decorations.length > 0) decls.push(`text-decoration: ${decorations.join(" ")}`);
|
|
1139
|
+
return decls.join("; ");
|
|
1140
|
+
}
|
|
1141
|
+
function ansiPaletteColor(index) {
|
|
1142
|
+
const table16 = [
|
|
1143
|
+
"#151b23",
|
|
1144
|
+
"#ff7b72",
|
|
1145
|
+
"#7ee787",
|
|
1146
|
+
"#f2cc60",
|
|
1147
|
+
"#79c0ff",
|
|
1148
|
+
"#d2a8ff",
|
|
1149
|
+
"#70e1e8",
|
|
1150
|
+
"#e6edf7",
|
|
1151
|
+
"#6e7681",
|
|
1152
|
+
"#ffa198",
|
|
1153
|
+
"#aff5b4",
|
|
1154
|
+
"#ffdf80",
|
|
1155
|
+
"#a5d6ff",
|
|
1156
|
+
"#e2c5ff",
|
|
1157
|
+
"#96f0f5",
|
|
1158
|
+
"#ffffff"
|
|
1159
|
+
];
|
|
1160
|
+
const normalized = clampColor(index);
|
|
1161
|
+
if (normalized < table16.length) return table16[normalized] ?? "#e6edf7";
|
|
1162
|
+
if (normalized <= 231) {
|
|
1163
|
+
const values = [
|
|
1164
|
+
0,
|
|
1165
|
+
95,
|
|
1166
|
+
135,
|
|
1167
|
+
175,
|
|
1168
|
+
215,
|
|
1169
|
+
255
|
|
1170
|
+
];
|
|
1171
|
+
const offset = normalized - 16;
|
|
1172
|
+
return `rgb(${values[Math.trunc(offset / 36) % 6] ?? 0} ${values[Math.trunc(offset / 6) % 6] ?? 0} ${values[offset % 6] ?? 0})`;
|
|
1173
|
+
}
|
|
1174
|
+
const gray = clampColor(8 + (normalized - 232) * 10);
|
|
1175
|
+
return `rgb(${gray} ${gray} ${gray})`;
|
|
1176
|
+
}
|
|
1177
|
+
function clampColor(value) {
|
|
1178
|
+
if (!Number.isFinite(value)) return 0;
|
|
1179
|
+
return Math.max(0, Math.min(255, Math.trunc(value)));
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/agent/report_viewer_pan.ts
|
|
1183
|
+
function renderReportViewportPanCss() {
|
|
1184
|
+
return ` [data-ptywright-report-pan="true"] {
|
|
1185
|
+
cursor: grab;
|
|
1186
|
+
}
|
|
1187
|
+
[data-ptywright-report-panning="true"] {
|
|
1188
|
+
cursor: grabbing;
|
|
1189
|
+
}`;
|
|
1190
|
+
}
|
|
1191
|
+
function renderReportViewportPanScript(body) {
|
|
1192
|
+
return ` (() => {
|
|
1193
|
+
const isHtmlElement = (value) => {
|
|
1194
|
+
return Boolean(
|
|
1195
|
+
value &&
|
|
1196
|
+
value.nodeType === 1 &&
|
|
1197
|
+
value.dataset &&
|
|
1198
|
+
value.style &&
|
|
1199
|
+
typeof value.addEventListener === "function"
|
|
1200
|
+
);
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const scrollToBottom = (element) => {
|
|
1204
|
+
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const enableViewportPan = (element) => {
|
|
1208
|
+
if (!isHtmlElement(element) || element.dataset.ptywrightReportPan === "true") {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
element.dataset.ptywrightReportPan = "true";
|
|
1213
|
+
let activePointerId = null;
|
|
1214
|
+
let startX = 0;
|
|
1215
|
+
let startY = 0;
|
|
1216
|
+
let startScrollLeft = 0;
|
|
1217
|
+
let startScrollTop = 0;
|
|
1218
|
+
let moved = false;
|
|
1219
|
+
|
|
1220
|
+
element.addEventListener("pointerdown", (event) => {
|
|
1221
|
+
if (event.button !== 0) return;
|
|
1222
|
+
activePointerId = event.pointerId;
|
|
1223
|
+
startX = event.clientX;
|
|
1224
|
+
startY = event.clientY;
|
|
1225
|
+
startScrollLeft = element.scrollLeft;
|
|
1226
|
+
startScrollTop = element.scrollTop;
|
|
1227
|
+
moved = false;
|
|
1228
|
+
element.dataset.ptywrightReportPanning = "true";
|
|
1229
|
+
element.setPointerCapture?.(event.pointerId);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
element.addEventListener("pointermove", (event) => {
|
|
1233
|
+
if (activePointerId !== event.pointerId) return;
|
|
1234
|
+
const deltaX = event.clientX - startX;
|
|
1235
|
+
const deltaY = event.clientY - startY;
|
|
1236
|
+
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
|
1237
|
+
moved = true;
|
|
1238
|
+
}
|
|
1239
|
+
element.scrollLeft = startScrollLeft - deltaX;
|
|
1240
|
+
element.scrollTop = startScrollTop - deltaY;
|
|
1241
|
+
if (moved) event.preventDefault();
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const finish = (event) => {
|
|
1245
|
+
if (activePointerId !== event.pointerId) return;
|
|
1246
|
+
activePointerId = null;
|
|
1247
|
+
element.dataset.ptywrightReportPanning = "false";
|
|
1248
|
+
element.releasePointerCapture?.(event.pointerId);
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
element.addEventListener("pointerup", finish);
|
|
1252
|
+
element.addEventListener("pointercancel", finish);
|
|
1253
|
+
};
|
|
1254
|
+
${body}
|
|
1255
|
+
})();`;
|
|
1256
|
+
}
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/agent/report_raw_artifact_viewer.ts
|
|
1259
|
+
function renderRawArtifactViewer(content, mobile, viewOptions) {
|
|
1260
|
+
return `<div class="viewer-viewport raw-artifact-viewport" data-mobile="${mobile ? "true" : "false"}" data-screen-mode="${escapeAttribute(viewOptions.screenMode)}"><pre class="raw-artifact-text">${renderRawAnsiTextHtml(content)}</pre></div>`;
|
|
1261
|
+
}
|
|
1262
|
+
function renderRawArtifactViewerCss() {
|
|
1263
|
+
return ` .raw-artifact-text {
|
|
1264
|
+
min-width: 100%;
|
|
1265
|
+
width: max-content;
|
|
1266
|
+
min-height: 100%;
|
|
1267
|
+
margin: 0;
|
|
1268
|
+
background:
|
|
1269
|
+
linear-gradient(180deg, color-mix(in srgb, #162033 92%, black), #0c111d);
|
|
1270
|
+
color: #e6edf7;
|
|
1271
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1272
|
+
font-size: 13px;
|
|
1273
|
+
line-height: 1.45;
|
|
1274
|
+
overflow: visible;
|
|
1275
|
+
padding: 12px;
|
|
1276
|
+
white-space: pre;
|
|
1277
|
+
}
|
|
1278
|
+
.viewer-page[data-theme="light"] .raw-artifact-text {
|
|
1279
|
+
background: #ffffff;
|
|
1280
|
+
color: #4c4f69;
|
|
1281
|
+
}
|
|
1282
|
+
${renderReportViewportPanCss()}`;
|
|
1283
|
+
}
|
|
1284
|
+
function renderRawArtifactViewerScript() {
|
|
1285
|
+
return renderReportViewportPanScript(`
|
|
1286
|
+
document.querySelectorAll(".raw-artifact-viewport").forEach((viewport) => {
|
|
1287
|
+
enableViewportPan(viewport);
|
|
1288
|
+
if (viewport.getAttribute("data-screen-mode") === "termvision") {
|
|
1289
|
+
requestAnimationFrame(() => scrollToBottom(viewport));
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
`);
|
|
1293
|
+
}
|
|
1294
|
+
function renderRawArtifactViewerFragment(args) {
|
|
1295
|
+
const { content, mobile, viewOptions } = args;
|
|
1296
|
+
return {
|
|
1297
|
+
css: renderRawArtifactViewerCss(),
|
|
1298
|
+
html: renderRawArtifactViewer(content, mobile, viewOptions),
|
|
1299
|
+
script: renderRawArtifactViewerScript()
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/agent/report_artifact_viewer_fragment.ts
|
|
1304
|
+
function renderArtifactViewerFragment(args) {
|
|
1305
|
+
const { artifactsDir, content, domViewerPreview, mobile, viewerPath, viewOptions } = args;
|
|
1306
|
+
if (domViewerPreview) return renderDomArtifactViewerFragment({
|
|
1307
|
+
artifactsDir,
|
|
1308
|
+
mobile,
|
|
1309
|
+
preview: domViewerPreview,
|
|
1310
|
+
viewerPath,
|
|
1311
|
+
viewOptions
|
|
1312
|
+
});
|
|
1313
|
+
return renderRawArtifactViewerFragment({
|
|
1314
|
+
content,
|
|
1315
|
+
mobile,
|
|
1316
|
+
viewOptions
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/agent/report_artifact_viewer_shell_css.ts
|
|
1321
|
+
function renderArtifactViewerShellCss() {
|
|
1322
|
+
return ` :root {
|
|
1323
|
+
color-scheme: dark;
|
|
1324
|
+
--bg: #080d16;
|
|
1325
|
+
--panel: #0c111d;
|
|
1326
|
+
--line: rgba(148, 163, 184, 0.26);
|
|
1327
|
+
--ink: #e6edf7;
|
|
1328
|
+
--muted: #91a0b8;
|
|
1329
|
+
--focus: #79c0ff;
|
|
1330
|
+
font-family:
|
|
1331
|
+
ui-sans-serif,
|
|
1332
|
+
system-ui,
|
|
1333
|
+
-apple-system,
|
|
1334
|
+
BlinkMacSystemFont,
|
|
1335
|
+
"Segoe UI",
|
|
1336
|
+
sans-serif;
|
|
1337
|
+
}
|
|
1338
|
+
* {
|
|
1339
|
+
box-sizing: border-box;
|
|
1340
|
+
}
|
|
1341
|
+
html,
|
|
1342
|
+
body {
|
|
1343
|
+
width: 100%;
|
|
1344
|
+
height: 100%;
|
|
1345
|
+
margin: 0;
|
|
1346
|
+
overflow: hidden;
|
|
1347
|
+
background: var(--bg);
|
|
1348
|
+
color: var(--ink);
|
|
1349
|
+
}
|
|
1350
|
+
.viewer-page {
|
|
1351
|
+
display: grid;
|
|
1352
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1353
|
+
width: 100%;
|
|
1354
|
+
height: 100dvh;
|
|
1355
|
+
min-width: 0;
|
|
1356
|
+
min-height: 0;
|
|
1357
|
+
}
|
|
1358
|
+
.viewer-toolbar {
|
|
1359
|
+
display: flex;
|
|
1360
|
+
flex-wrap: wrap;
|
|
1361
|
+
gap: 8px;
|
|
1362
|
+
align-items: center;
|
|
1363
|
+
min-width: 0;
|
|
1364
|
+
border-bottom: 1px solid var(--line);
|
|
1365
|
+
background: color-mix(in srgb, var(--panel) 88%, black);
|
|
1366
|
+
padding: 10px 12px;
|
|
1367
|
+
}
|
|
1368
|
+
.viewer-title {
|
|
1369
|
+
min-width: min(100%, 220px);
|
|
1370
|
+
margin-right: auto;
|
|
1371
|
+
overflow-wrap: anywhere;
|
|
1372
|
+
font-size: 14px;
|
|
1373
|
+
font-weight: 700;
|
|
1374
|
+
}
|
|
1375
|
+
.viewer-link,
|
|
1376
|
+
.viewer-pill {
|
|
1377
|
+
display: inline-flex;
|
|
1378
|
+
min-height: 30px;
|
|
1117
1379
|
align-items: center;
|
|
1118
1380
|
border: 1px solid var(--line);
|
|
1119
1381
|
border-radius: 999px;
|
|
1120
|
-
padding: 0
|
|
1382
|
+
padding: 0 10px;
|
|
1121
1383
|
color: var(--muted);
|
|
1122
|
-
font-size:
|
|
1384
|
+
font-size: 12px;
|
|
1385
|
+
text-decoration: none;
|
|
1123
1386
|
}
|
|
1124
|
-
.
|
|
1125
|
-
|
|
1126
|
-
color: ${result.ok ? "var(--good)" : "var(--bad)"};
|
|
1387
|
+
.viewer-link {
|
|
1388
|
+
color: var(--focus);
|
|
1127
1389
|
font-weight: 700;
|
|
1128
1390
|
}
|
|
1129
|
-
.
|
|
1391
|
+
.viewer-stage {
|
|
1130
1392
|
display: grid;
|
|
1131
|
-
|
|
1132
|
-
|
|
1393
|
+
grid-template-columns: max-content;
|
|
1394
|
+
grid-template-rows: max-content;
|
|
1395
|
+
min-width: 0;
|
|
1396
|
+
min-height: 0;
|
|
1397
|
+
justify-content: start;
|
|
1398
|
+
align-content: start;
|
|
1399
|
+
overflow: auto;
|
|
1400
|
+
background:
|
|
1401
|
+
radial-gradient(circle at top left, rgba(121, 192, 255, 0.1), transparent 32%),
|
|
1402
|
+
#060a13;
|
|
1403
|
+
padding: 14px;
|
|
1404
|
+
}
|
|
1405
|
+
.viewer-viewport {
|
|
1406
|
+
width: var(--config-viewport-width);
|
|
1407
|
+
height: var(--config-viewport-height);
|
|
1408
|
+
max-width: none;
|
|
1409
|
+
max-height: none;
|
|
1410
|
+
overflow: auto;
|
|
1411
|
+
overscroll-behavior: contain;
|
|
1412
|
+
border: 0;
|
|
1133
1413
|
border-radius: 8px;
|
|
1134
|
-
background:
|
|
1135
|
-
|
|
1414
|
+
background: #0c111d;
|
|
1415
|
+
outline: 1px solid var(--line);
|
|
1416
|
+
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.34);
|
|
1136
1417
|
}
|
|
1137
|
-
.
|
|
1138
|
-
|
|
1139
|
-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
1140
|
-
gap: 12px;
|
|
1418
|
+
.viewer-viewport[data-mobile="true"] {
|
|
1419
|
+
width: var(--config-viewport-width);
|
|
1141
1420
|
}
|
|
1142
|
-
.
|
|
1143
|
-
|
|
1144
|
-
border-radius: 8px;
|
|
1145
|
-
padding: 14px;
|
|
1146
|
-
background: color-mix(in oklch, var(--panel) 82%, var(--bg));
|
|
1421
|
+
.dom-viewport {
|
|
1422
|
+
overflow: hidden;
|
|
1147
1423
|
}
|
|
1148
|
-
.
|
|
1424
|
+
.dom-viewer-frame {
|
|
1149
1425
|
display: block;
|
|
1150
|
-
|
|
1151
|
-
|
|
1426
|
+
width: 100%;
|
|
1427
|
+
height: 100%;
|
|
1428
|
+
border: 0;
|
|
1429
|
+
background: #0c111d;
|
|
1152
1430
|
}
|
|
1153
|
-
.
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1431
|
+
.viewer-page[data-theme="light"] {
|
|
1432
|
+
--bg: #f8fafc;
|
|
1433
|
+
--panel: #ffffff;
|
|
1434
|
+
--line: rgba(15, 23, 42, 0.14);
|
|
1435
|
+
--ink: #0f172a;
|
|
1436
|
+
--muted: #64748b;
|
|
1437
|
+
--focus: #1e66f5;
|
|
1158
1438
|
}
|
|
1159
|
-
.
|
|
1439
|
+
.viewer-page[data-theme="light"] .viewer-stage {
|
|
1440
|
+
background: #f8fafc;
|
|
1441
|
+
}
|
|
1442
|
+
.viewer-page[data-theme="light"] .viewer-viewport,
|
|
1443
|
+
.viewer-page[data-theme="light"] .dom-viewer-frame {
|
|
1444
|
+
background: #ffffff;
|
|
1445
|
+
}
|
|
1446
|
+
@media (max-width: 720px) {
|
|
1447
|
+
.viewer-toolbar {
|
|
1448
|
+
padding: 8px;
|
|
1449
|
+
}
|
|
1450
|
+
.viewer-title {
|
|
1451
|
+
flex-basis: 100%;
|
|
1452
|
+
order: -1;
|
|
1453
|
+
}
|
|
1454
|
+
.viewer-stage {
|
|
1455
|
+
padding: 0;
|
|
1456
|
+
}
|
|
1457
|
+
.viewer-viewport {
|
|
1458
|
+
width: var(--config-viewport-width);
|
|
1459
|
+
height: var(--config-viewport-height);
|
|
1460
|
+
border-radius: 0;
|
|
1461
|
+
}
|
|
1462
|
+
}`;
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/agent/report_artifact_viewer_shell.ts
|
|
1466
|
+
function renderArtifactViewerShellHtml(args) {
|
|
1467
|
+
const { artifact, artifactsDir, contentFragment, mobile, reportPath, viewOptions, viewerPath } = args;
|
|
1468
|
+
const title = `${artifact.viewport} ${artifact.name} ${artifact.kind}`;
|
|
1469
|
+
const viewportStyle = renderConfiguredViewportStyle(args.viewport);
|
|
1470
|
+
const viewportLabel = args.viewport ? `${args.viewport.name} ${args.viewport.width}x${args.viewport.height}` : `${artifact.viewport} viewport`;
|
|
1471
|
+
const backHref = relativeHref(viewerPath, reportPath, artifactsDir);
|
|
1472
|
+
const rawHref = relativeHref(viewerPath, artifact.path, artifactsDir);
|
|
1473
|
+
const baselineHref = artifact.baselinePath ? relativeHref(viewerPath, artifact.baselinePath, artifactsDir) : "";
|
|
1474
|
+
const diffHref = artifact.diffPath ? relativeHref(viewerPath, artifact.diffPath, artifactsDir) : "";
|
|
1475
|
+
const scriptTag = contentFragment.script ? ` <script>
|
|
1476
|
+
${contentFragment.script}
|
|
1477
|
+
<\/script>
|
|
1478
|
+
` : "";
|
|
1479
|
+
return `<!doctype html>
|
|
1480
|
+
<html lang="en" data-theme="${escapeAttribute(viewOptions.theme)}">
|
|
1481
|
+
<head>
|
|
1482
|
+
<meta charset="utf-8" />
|
|
1483
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1484
|
+
<title>${escapeHtml(title)}</title>
|
|
1485
|
+
<style>
|
|
1486
|
+
${renderArtifactViewerShellCss()}
|
|
1487
|
+
${contentFragment.css}
|
|
1488
|
+
</style>
|
|
1489
|
+
</head>
|
|
1490
|
+
<body>
|
|
1491
|
+
<main class="viewer-page" data-report-screen-mode="${escapeAttribute(viewOptions.screenMode)}" data-theme="${escapeAttribute(viewOptions.theme)}" style="${escapeAttribute(viewportStyle)}">
|
|
1492
|
+
<header class="viewer-toolbar">
|
|
1493
|
+
<a class="viewer-link" href="${escapeAttribute(backHref)}">Report</a>
|
|
1494
|
+
<span class="viewer-title">${escapeHtml(title)}</span>
|
|
1495
|
+
<span class="viewer-pill">${escapeHtml(viewportLabel)}</span>
|
|
1496
|
+
<span class="viewer-pill">${escapeHtml(viewOptions.screenMode)}</span>
|
|
1497
|
+
<a class="viewer-link" href="${escapeAttribute(rawHref)}">Raw</a>
|
|
1498
|
+
${baselineHref ? `<a class="viewer-link" href="${escapeAttribute(baselineHref)}">Baseline</a>` : ""}
|
|
1499
|
+
${diffHref ? `<a class="viewer-link" href="${escapeAttribute(diffHref)}">Diff</a>` : ""}
|
|
1500
|
+
</header>
|
|
1501
|
+
<section class="viewer-stage" data-mobile="${mobile ? "true" : "false"}">
|
|
1502
|
+
${contentFragment.html}
|
|
1503
|
+
</section>
|
|
1504
|
+
</main>
|
|
1505
|
+
${scriptTag}
|
|
1506
|
+
</body>
|
|
1507
|
+
</html>`;
|
|
1508
|
+
}
|
|
1509
|
+
function renderConfiguredViewportStyle(viewport) {
|
|
1510
|
+
if (!viewport) return "--config-viewport-width: 1280px; --config-viewport-height: 760px;";
|
|
1511
|
+
return `--config-viewport-width: ${viewport.width}px; --config-viewport-height: ${viewport.height}px;`;
|
|
1512
|
+
}
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/agent/report_view_options.ts
|
|
1515
|
+
function isMobileViewport(viewport) {
|
|
1516
|
+
return Boolean(viewport?.isMobile || viewport?.hasTouch || (viewport?.width ?? 9999) <= 720);
|
|
1517
|
+
}
|
|
1518
|
+
function resolveReportViewOptions(result) {
|
|
1519
|
+
const launchArgSets = [readFlowLaunchArgs(result.flowPath), readCassetteLaunchArgs(result.replaySourceCassettePath ?? result.cassettePath)];
|
|
1520
|
+
const screenModeArg = readFlagValueFromArgSets(launchArgSets, "--experimental-screen-mode");
|
|
1521
|
+
const themeArg = readFlagValueFromArgSets(launchArgSets, "--theme");
|
|
1522
|
+
const fontSizeArg = readFlagValueFromArgSets(launchArgSets, "--font-size");
|
|
1523
|
+
const lineHeightArg = readFlagValueFromArgSets(launchArgSets, "--line-height");
|
|
1524
|
+
return {
|
|
1525
|
+
fontSize: parsePositiveNumber(fontSizeArg) ?? 15,
|
|
1526
|
+
lineHeight: parsePositiveNumber(lineHeightArg) ?? 1.6,
|
|
1527
|
+
screenMode: screenModeArg && screenModeArg !== "termvision" ? "plain" : "termvision",
|
|
1528
|
+
theme: themeArg === "light" ? "light" : "dark"
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function readCassetteLaunchArgs(cassettePath) {
|
|
1532
|
+
try {
|
|
1533
|
+
return normalizeStringArray(JSON.parse(readFileSync(cassettePath, "utf8")).spec?.launch?.args);
|
|
1534
|
+
} catch {
|
|
1535
|
+
return [];
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function readFlowLaunchArgs(flowPath) {
|
|
1539
|
+
try {
|
|
1540
|
+
return normalizeStringArray(JSON.parse(readFileSync(flowPath, "utf8")).launch?.args);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return [];
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
function normalizeStringArray(value) {
|
|
1546
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1547
|
+
}
|
|
1548
|
+
function readFlagValueFromArgSets(argSets, flag) {
|
|
1549
|
+
for (const args of argSets) {
|
|
1550
|
+
const value = readFlagValue(args, flag);
|
|
1551
|
+
if (value !== void 0) return value;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function readFlagValue(args, flag) {
|
|
1555
|
+
const index = args.indexOf(flag);
|
|
1556
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
1557
|
+
}
|
|
1558
|
+
function parsePositiveNumber(value) {
|
|
1559
|
+
if (value === void 0) return void 0;
|
|
1560
|
+
const parsed = Number(value);
|
|
1561
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1562
|
+
}
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/agent/report_artifact_viewer.ts
|
|
1565
|
+
function renderArtifactViewerHtml(args) {
|
|
1566
|
+
const { artifact, artifactsDir, content, domPreview, reportPath, terminalDomPreview, viewOptions, viewerPath, viewport } = args;
|
|
1567
|
+
const mobile = isMobileViewport(viewport);
|
|
1568
|
+
return renderArtifactViewerShellHtml({
|
|
1569
|
+
artifact,
|
|
1570
|
+
artifactsDir,
|
|
1571
|
+
contentFragment: renderArtifactViewerFragment({
|
|
1572
|
+
artifactsDir,
|
|
1573
|
+
content,
|
|
1574
|
+
domViewerPreview: artifact.kind === "dom" ? domPreview : artifact.kind === "terminal" ? terminalDomPreview : null,
|
|
1575
|
+
mobile,
|
|
1576
|
+
viewerPath,
|
|
1577
|
+
viewOptions
|
|
1578
|
+
}),
|
|
1579
|
+
mobile,
|
|
1580
|
+
reportPath,
|
|
1581
|
+
viewOptions,
|
|
1582
|
+
viewerPath,
|
|
1583
|
+
viewport
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region src/agent/report_terminal_layout.ts
|
|
1588
|
+
function resolveTerminalSnapshotLayout(snapshot, viewport, viewOptions) {
|
|
1589
|
+
const cols = inferTerminalCols(snapshot);
|
|
1590
|
+
const rows = inferTerminalRows(snapshot);
|
|
1591
|
+
const mobile = isMobileViewport(viewport);
|
|
1592
|
+
return {
|
|
1593
|
+
cols,
|
|
1594
|
+
fontSize: viewOptions.fontSize,
|
|
1595
|
+
lineHeight: viewOptions.lineHeight,
|
|
1596
|
+
paddingInline: mobile ? 16 : 32,
|
|
1597
|
+
rows
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function inferTerminalCols(snapshot) {
|
|
1601
|
+
const dataCols = /data-cols="(\d+)"/.exec(snapshot)?.[1];
|
|
1602
|
+
const styleCols = /--term-cols:\s*(\d+)/.exec(snapshot)?.[1];
|
|
1603
|
+
const parsed = Number.parseInt(dataCols ?? styleCols ?? "", 10);
|
|
1604
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 80;
|
|
1605
|
+
}
|
|
1606
|
+
function inferTerminalRows(snapshot) {
|
|
1607
|
+
const dataRows = /data-rows="(\d+)"/.exec(snapshot)?.[1];
|
|
1608
|
+
const styleRows = /--term-rows:\s*(\d+)/.exec(snapshot)?.[1];
|
|
1609
|
+
const parsed = Number.parseInt(dataRows ?? styleRows ?? "", 10);
|
|
1610
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 24;
|
|
1611
|
+
}
|
|
1612
|
+
//#endregion
|
|
1613
|
+
//#region src/agent/report_terminal_preview.ts
|
|
1614
|
+
function renderDomPreviewDocument(snapshot, viewport, viewOptions, aittyAssets) {
|
|
1615
|
+
const body = renderAittyPreviewBody({
|
|
1616
|
+
snapshot,
|
|
1617
|
+
snapshotLayout: resolveTerminalSnapshotLayout(snapshot, viewport, viewOptions),
|
|
1618
|
+
viewOptions
|
|
1619
|
+
});
|
|
1620
|
+
const style = renderAittyPreviewCss(viewOptions);
|
|
1621
|
+
const assetTags = renderAittyPreviewAssetTags(aittyAssets);
|
|
1622
|
+
return `<!doctype html>
|
|
1623
|
+
<html lang="en" data-theme="${escapeAttribute(viewOptions.theme)}">
|
|
1624
|
+
<head>
|
|
1625
|
+
<meta charset="utf-8" />
|
|
1626
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
1627
|
+
${assetTags} <style>
|
|
1628
|
+
${style}
|
|
1629
|
+
</style>
|
|
1630
|
+
</head>
|
|
1631
|
+
<body>
|
|
1632
|
+
${body}
|
|
1633
|
+
</body>
|
|
1634
|
+
</html>`;
|
|
1635
|
+
}
|
|
1636
|
+
//#endregion
|
|
1637
|
+
//#region src/agent/report_artifact_writer.ts
|
|
1638
|
+
function writeArtifactViewerPages(result) {
|
|
1639
|
+
const viewportsByName = new Map(result.viewports.map((viewport) => [viewport.name, viewport]));
|
|
1640
|
+
const viewOptions = resolveReportViewOptions(result);
|
|
1641
|
+
const readableArtifacts = [];
|
|
1642
|
+
for (const artifact of result.artifacts) {
|
|
1643
|
+
const viewerPath = artifactViewerPath(artifact);
|
|
1644
|
+
if (!viewerPath) continue;
|
|
1645
|
+
const content = readArtifactText(artifact.path);
|
|
1646
|
+
if (content === null) continue;
|
|
1647
|
+
readableArtifacts.push({
|
|
1648
|
+
artifact,
|
|
1649
|
+
content,
|
|
1650
|
+
viewerPath
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
const domPreviewPathsBySnapshot = new Map(readableArtifacts.filter(({ artifact }) => artifact.kind === "dom").map(({ artifact }) => [artifactSnapshotKey(artifact), artifactDomPreviewPath(artifact)]));
|
|
1654
|
+
const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({
|
|
1655
|
+
artifactsDir: result.artifactsDir,
|
|
1656
|
+
flowPath: result.flowPath,
|
|
1657
|
+
reportPath: result.reportPath
|
|
1658
|
+
}) : null;
|
|
1659
|
+
const writtenDomPreviewPaths = /* @__PURE__ */ new Set();
|
|
1660
|
+
if (aittyAssets) for (const { artifact, content } of readableArtifacts) {
|
|
1661
|
+
if (artifact.kind !== "dom") continue;
|
|
1662
|
+
const viewport = viewportsByName.get(artifact.viewport);
|
|
1663
|
+
const domPreviewPath = artifactDomPreviewPath(artifact);
|
|
1664
|
+
mkdirSync(dirname(domPreviewPath), { recursive: true });
|
|
1665
|
+
writeFileSync(domPreviewPath, renderDomPreviewDocument(content, viewport, viewOptions, resolveAittyPreviewAssets(domPreviewPath, aittyAssets, result.artifactsDir)), "utf8");
|
|
1666
|
+
writtenDomPreviewPaths.add(domPreviewPath);
|
|
1667
|
+
}
|
|
1668
|
+
for (const { artifact, content, viewerPath } of readableArtifacts) {
|
|
1669
|
+
const viewport = viewportsByName.get(artifact.viewport);
|
|
1670
|
+
const domPreviewPath = artifact.kind === "dom" ? artifactDomPreviewPath(artifact) : null;
|
|
1671
|
+
mkdirSync(dirname(viewerPath), { recursive: true });
|
|
1672
|
+
writeFileSync(viewerPath, renderArtifactViewerHtml({
|
|
1673
|
+
artifact,
|
|
1674
|
+
artifactsDir: result.artifactsDir,
|
|
1675
|
+
content,
|
|
1676
|
+
reportPath: result.reportPath,
|
|
1677
|
+
viewerPath,
|
|
1678
|
+
domPreview: resolveReportDomPreview(domPreviewPath && writtenDomPreviewPaths.has(domPreviewPath) ? domPreviewPath : null),
|
|
1679
|
+
viewOptions,
|
|
1680
|
+
viewport,
|
|
1681
|
+
terminalDomPreview: artifact.kind === "terminal" ? resolveReportDomPreview(resolveWrittenDomPreviewPath(domPreviewPathsBySnapshot, writtenDomPreviewPaths, artifact)) : null
|
|
1682
|
+
}), "utf8");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function resolveWrittenDomPreviewPath(domPreviewPathsBySnapshot, writtenDomPreviewPaths, artifact) {
|
|
1686
|
+
const path = domPreviewPathsBySnapshot.get(artifactSnapshotKey(artifact));
|
|
1687
|
+
return path && writtenDomPreviewPaths.has(path) ? path : null;
|
|
1688
|
+
}
|
|
1689
|
+
function readArtifactText(path) {
|
|
1690
|
+
try {
|
|
1691
|
+
return readFileSync(path, "utf8");
|
|
1692
|
+
} catch {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/agent/report_index_artifacts.ts
|
|
1698
|
+
function renderAgentReportArtifacts(args) {
|
|
1699
|
+
return args.artifacts.map((artifact) => renderArtifactRow({
|
|
1700
|
+
artifact,
|
|
1701
|
+
artifactsDir: args.artifactsDir,
|
|
1702
|
+
reportPath: args.reportPath
|
|
1703
|
+
})).join("\n") || "<p>No artifacts were captured.</p>";
|
|
1704
|
+
}
|
|
1705
|
+
function renderArtifactRow(args) {
|
|
1706
|
+
const { artifact, artifactsDir, reportPath } = args;
|
|
1707
|
+
const state = artifact.ok ? "pass" : "fail";
|
|
1708
|
+
const href = relativeHref(reportPath, artifact.path, artifactsDir);
|
|
1709
|
+
const baselineHref = artifact.baselinePath ? relativeHref(reportPath, artifact.baselinePath, artifactsDir) : "";
|
|
1710
|
+
const diffHref = artifact.diffPath ? relativeHref(reportPath, artifact.diffPath, artifactsDir) : "";
|
|
1711
|
+
const viewerPath = artifactViewerPath(artifact);
|
|
1712
|
+
const viewerHref = viewerPath ? relativeHref(reportPath, viewerPath, artifactsDir) : "";
|
|
1713
|
+
return `<article class="artifact">
|
|
1714
|
+
<div class="artifact-summary">
|
|
1715
|
+
<span class="badge ${state}">${state}</span>
|
|
1716
|
+
<div class="artifact-meta">
|
|
1717
|
+
<div class="artifact-links">
|
|
1718
|
+
<a href="${escapeAttribute(viewerHref || href)}">${escapeHtml(artifact.kind)}</a>
|
|
1719
|
+
${viewerHref ? `<a href="${escapeAttribute(href)}">raw</a>` : ""}
|
|
1720
|
+
${baselineHref ? `<a href="${escapeAttribute(baselineHref)}">baseline</a>` : ""}
|
|
1721
|
+
${diffHref ? `<a href="${escapeAttribute(diffHref)}">diff</a>` : ""}
|
|
1722
|
+
</div>
|
|
1723
|
+
<div><code>${escapeHtml(artifact.viewport)} / ${escapeHtml(artifact.name)}</code></div>
|
|
1724
|
+
${artifact.error ? `<div><code>${escapeHtml(artifact.error)}</code></div>` : ""}
|
|
1725
|
+
</div>
|
|
1726
|
+
<code class="artifact-hash">${escapeHtml(artifact.hash ?? "")}</code>
|
|
1727
|
+
</div>
|
|
1728
|
+
</article>`;
|
|
1729
|
+
}
|
|
1730
|
+
//#endregion
|
|
1731
|
+
//#region src/agent/report_index_commands.ts
|
|
1732
|
+
function renderAgentReportCommandBlock(label, argv) {
|
|
1733
|
+
return `<div class="command">
|
|
1734
|
+
<span>${escapeHtml(label)}</span>
|
|
1735
|
+
<pre>${escapeHtml(formatAgentArgv(argv))}</pre>
|
|
1736
|
+
</div>`;
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/agent/report_index_artifacts_css.ts
|
|
1740
|
+
function renderAgentReportArtifactsCss() {
|
|
1741
|
+
return ` .artifacts {
|
|
1160
1742
|
display: grid;
|
|
1161
|
-
gap:
|
|
1743
|
+
gap: 14px;
|
|
1162
1744
|
}
|
|
1163
1745
|
.artifact {
|
|
1164
1746
|
display: grid;
|
|
1165
|
-
|
|
1166
|
-
gap: 14px;
|
|
1167
|
-
align-items: center;
|
|
1747
|
+
gap: 12px;
|
|
1168
1748
|
border: 1px solid var(--line);
|
|
1169
1749
|
border-radius: 8px;
|
|
1170
1750
|
padding: 12px;
|
|
1171
1751
|
background: var(--panel);
|
|
1172
1752
|
}
|
|
1753
|
+
.artifact-summary {
|
|
1754
|
+
display: grid;
|
|
1755
|
+
grid-template-columns: auto minmax(0, 1fr) minmax(96px, auto);
|
|
1756
|
+
gap: 14px;
|
|
1757
|
+
align-items: start;
|
|
1758
|
+
}
|
|
1759
|
+
.artifact-meta {
|
|
1760
|
+
display: grid;
|
|
1761
|
+
gap: 4px;
|
|
1762
|
+
min-width: 0;
|
|
1763
|
+
}
|
|
1764
|
+
.artifact-links {
|
|
1765
|
+
display: flex;
|
|
1766
|
+
flex-wrap: wrap;
|
|
1767
|
+
gap: 8px 12px;
|
|
1768
|
+
}
|
|
1173
1769
|
.artifact a {
|
|
1174
1770
|
color: var(--focus);
|
|
1175
1771
|
font-weight: 700;
|
|
1176
1772
|
text-decoration: none;
|
|
1177
1773
|
}
|
|
1178
|
-
.artifact code,
|
|
1179
|
-
pre {
|
|
1180
|
-
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
1181
|
-
}
|
|
1182
1774
|
.artifact code {
|
|
1183
1775
|
color: var(--muted);
|
|
1184
1776
|
overflow-wrap: anywhere;
|
|
1185
1777
|
}
|
|
1186
|
-
.
|
|
1187
|
-
justify-self:
|
|
1778
|
+
.artifact-hash {
|
|
1779
|
+
justify-self: end;
|
|
1780
|
+
text-align: right;
|
|
1781
|
+
}
|
|
1782
|
+
.badge {
|
|
1783
|
+
justify-self: start;
|
|
1784
|
+
border-radius: 999px;
|
|
1785
|
+
padding: 5px 9px;
|
|
1786
|
+
background: color-mix(in oklch, var(--line) 52%, transparent);
|
|
1787
|
+
color: var(--muted);
|
|
1788
|
+
font-size: 12px;
|
|
1789
|
+
font-weight: 700;
|
|
1790
|
+
}
|
|
1791
|
+
.badge.fail {
|
|
1792
|
+
background: color-mix(in oklch, var(--bad) 12%, var(--panel));
|
|
1793
|
+
color: var(--bad);
|
|
1794
|
+
}
|
|
1795
|
+
.badge.pass {
|
|
1796
|
+
background: color-mix(in oklch, var(--good) 12%, var(--panel));
|
|
1797
|
+
color: var(--good);
|
|
1798
|
+
}
|
|
1799
|
+
@media (max-width: 720px) {
|
|
1800
|
+
.artifact-summary {
|
|
1801
|
+
grid-template-columns: 1fr;
|
|
1802
|
+
}
|
|
1803
|
+
.artifact-hash {
|
|
1804
|
+
justify-self: start;
|
|
1805
|
+
text-align: left;
|
|
1806
|
+
}
|
|
1807
|
+
}`;
|
|
1808
|
+
}
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region src/agent/report_index_base_css.ts
|
|
1811
|
+
function renderAgentReportBaseCss() {
|
|
1812
|
+
return ` :root {
|
|
1813
|
+
color-scheme: light;
|
|
1814
|
+
--bg: oklch(97.5% 0.008 210);
|
|
1815
|
+
--ink: oklch(19% 0.018 230);
|
|
1816
|
+
--muted: oklch(48% 0.02 230);
|
|
1817
|
+
--line: oklch(86% 0.018 230);
|
|
1818
|
+
--panel: oklch(99% 0.006 210);
|
|
1819
|
+
--good: oklch(55% 0.15 155);
|
|
1820
|
+
--bad: oklch(58% 0.19 25);
|
|
1821
|
+
--focus: oklch(55% 0.14 235);
|
|
1822
|
+
font-family:
|
|
1823
|
+
ui-sans-serif,
|
|
1824
|
+
system-ui,
|
|
1825
|
+
-apple-system,
|
|
1826
|
+
BlinkMacSystemFont,
|
|
1827
|
+
"Segoe UI",
|
|
1828
|
+
sans-serif;
|
|
1829
|
+
}
|
|
1830
|
+
* { box-sizing: border-box; }
|
|
1831
|
+
body {
|
|
1832
|
+
margin: 0;
|
|
1833
|
+
background: var(--bg);
|
|
1834
|
+
color: var(--ink);
|
|
1835
|
+
}
|
|
1836
|
+
main {
|
|
1837
|
+
display: grid;
|
|
1838
|
+
gap: 24px;
|
|
1839
|
+
width: min(1180px, calc(100vw - 32px));
|
|
1840
|
+
margin: 0 auto;
|
|
1841
|
+
padding: 32px 0 48px;
|
|
1842
|
+
}
|
|
1843
|
+
header {
|
|
1844
|
+
display: grid;
|
|
1845
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1846
|
+
gap: 20px;
|
|
1847
|
+
align-items: start;
|
|
1848
|
+
border-bottom: 1px solid var(--line);
|
|
1849
|
+
padding-bottom: 24px;
|
|
1850
|
+
}
|
|
1851
|
+
h1 {
|
|
1852
|
+
margin: 0;
|
|
1853
|
+
font-size: 28px;
|
|
1854
|
+
line-height: 1.15;
|
|
1855
|
+
letter-spacing: 0;
|
|
1856
|
+
}
|
|
1857
|
+
h2 {
|
|
1858
|
+
margin: 0;
|
|
1859
|
+
font-size: 18px;
|
|
1860
|
+
line-height: 1.25;
|
|
1861
|
+
}
|
|
1862
|
+
.meta {
|
|
1863
|
+
display: flex;
|
|
1864
|
+
flex-wrap: wrap;
|
|
1865
|
+
gap: 8px;
|
|
1866
|
+
margin-top: 12px;
|
|
1867
|
+
}
|
|
1868
|
+
.pill,
|
|
1869
|
+
.status {
|
|
1870
|
+
display: inline-flex;
|
|
1871
|
+
min-height: 32px;
|
|
1872
|
+
align-items: center;
|
|
1873
|
+
border: 1px solid var(--line);
|
|
1188
1874
|
border-radius: 999px;
|
|
1189
|
-
padding:
|
|
1190
|
-
background: color-mix(in oklch, var(--line) 52%, transparent);
|
|
1875
|
+
padding: 0 12px;
|
|
1191
1876
|
color: var(--muted);
|
|
1192
|
-
font-size:
|
|
1877
|
+
font-size: 13px;
|
|
1878
|
+
}
|
|
1879
|
+
.status {
|
|
1193
1880
|
font-weight: 700;
|
|
1194
1881
|
}
|
|
1195
|
-
.
|
|
1196
|
-
|
|
1882
|
+
.status.pass {
|
|
1883
|
+
border-color: color-mix(in oklch, var(--good) 42%, var(--line));
|
|
1884
|
+
color: var(--good);
|
|
1885
|
+
}
|
|
1886
|
+
.status.fail {
|
|
1887
|
+
border-color: color-mix(in oklch, var(--bad) 44%, var(--line));
|
|
1197
1888
|
color: var(--bad);
|
|
1198
1889
|
}
|
|
1199
|
-
.
|
|
1200
|
-
|
|
1201
|
-
|
|
1890
|
+
.panel {
|
|
1891
|
+
display: grid;
|
|
1892
|
+
gap: 16px;
|
|
1893
|
+
border: 1px solid var(--line);
|
|
1894
|
+
border-radius: 8px;
|
|
1895
|
+
background: var(--panel);
|
|
1896
|
+
padding: 18px;
|
|
1202
1897
|
}
|
|
1203
|
-
|
|
1898
|
+
@media (max-width: 720px) {
|
|
1899
|
+
main {
|
|
1900
|
+
width: min(100vw - 20px, 1180px);
|
|
1901
|
+
padding-top: 18px;
|
|
1902
|
+
}
|
|
1903
|
+
header {
|
|
1904
|
+
grid-template-columns: 1fr;
|
|
1905
|
+
}
|
|
1906
|
+
.status {
|
|
1907
|
+
justify-self: start;
|
|
1908
|
+
}
|
|
1909
|
+
}`;
|
|
1910
|
+
}
|
|
1911
|
+
//#endregion
|
|
1912
|
+
//#region src/agent/report_index_commands_css.ts
|
|
1913
|
+
function renderAgentReportCommandsCss() {
|
|
1914
|
+
return ` .commands {
|
|
1204
1915
|
display: grid;
|
|
1205
1916
|
gap: 10px;
|
|
1206
1917
|
}
|
|
@@ -1213,6 +1924,10 @@ function renderAgentReportHtml(result) {
|
|
|
1213
1924
|
font-size: 13px;
|
|
1214
1925
|
font-weight: 700;
|
|
1215
1926
|
}
|
|
1927
|
+
.artifact code,
|
|
1928
|
+
pre {
|
|
1929
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
1930
|
+
}
|
|
1216
1931
|
pre {
|
|
1217
1932
|
overflow: auto;
|
|
1218
1933
|
margin: 0;
|
|
@@ -1221,20 +1936,63 @@ function renderAgentReportHtml(result) {
|
|
|
1221
1936
|
color: oklch(92% 0.012 230);
|
|
1222
1937
|
padding: 14px;
|
|
1223
1938
|
line-height: 1.5;
|
|
1939
|
+
}`;
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/agent/report_index_summary_css.ts
|
|
1943
|
+
function renderAgentReportSummaryCss() {
|
|
1944
|
+
return ` .summary {
|
|
1945
|
+
display: grid;
|
|
1946
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
1947
|
+
gap: 12px;
|
|
1224
1948
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
.
|
|
1235
|
-
justify-self: start;
|
|
1236
|
-
}
|
|
1949
|
+
.metric {
|
|
1950
|
+
border: 1px solid var(--line);
|
|
1951
|
+
border-radius: 8px;
|
|
1952
|
+
padding: 14px;
|
|
1953
|
+
background: color-mix(in oklch, var(--panel) 82%, var(--bg));
|
|
1954
|
+
}
|
|
1955
|
+
.metric strong {
|
|
1956
|
+
display: block;
|
|
1957
|
+
font-size: 24px;
|
|
1958
|
+
line-height: 1.1;
|
|
1237
1959
|
}
|
|
1960
|
+
.metric span {
|
|
1961
|
+
display: block;
|
|
1962
|
+
margin-top: 4px;
|
|
1963
|
+
color: var(--muted);
|
|
1964
|
+
font-size: 13px;
|
|
1965
|
+
}`;
|
|
1966
|
+
}
|
|
1967
|
+
//#endregion
|
|
1968
|
+
//#region src/agent/report_index_css.ts
|
|
1969
|
+
function renderAgentReportCss() {
|
|
1970
|
+
return [
|
|
1971
|
+
renderAgentReportBaseCss(),
|
|
1972
|
+
renderAgentReportSummaryCss(),
|
|
1973
|
+
renderAgentReportArtifactsCss(),
|
|
1974
|
+
renderAgentReportCommandsCss()
|
|
1975
|
+
].join("\n");
|
|
1976
|
+
}
|
|
1977
|
+
//#endregion
|
|
1978
|
+
//#region src/agent/report_index.ts
|
|
1979
|
+
function renderAgentReportHtml(result) {
|
|
1980
|
+
const artifacts = renderAgentReportArtifacts({
|
|
1981
|
+
artifacts: result.artifacts,
|
|
1982
|
+
artifactsDir: result.artifactsDir,
|
|
1983
|
+
reportPath: result.reportPath
|
|
1984
|
+
});
|
|
1985
|
+
const viewportTabs = result.viewports.map((viewport) => `<span class="pill">${escapeHtml(viewport.name)} ${viewport.width}x${viewport.height}</span>`).join("");
|
|
1986
|
+
const status = result.ok ? "passed" : "failed";
|
|
1987
|
+
const statusClass = result.ok ? "pass" : "fail";
|
|
1988
|
+
return `<!doctype html>
|
|
1989
|
+
<html lang="en">
|
|
1990
|
+
<head>
|
|
1991
|
+
<meta charset="utf-8" />
|
|
1992
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1993
|
+
<title>${escapeHtml(`${result.name} terminal-agent report`)}</title>
|
|
1994
|
+
<style>
|
|
1995
|
+
${renderAgentReportCss()}
|
|
1238
1996
|
</style>
|
|
1239
1997
|
</head>
|
|
1240
1998
|
<body>
|
|
@@ -1243,7 +2001,7 @@ function renderAgentReportHtml(result) {
|
|
|
1243
2001
|
<div>
|
|
1244
2002
|
<h1>${escapeHtml(result.name)}</h1>
|
|
1245
2003
|
<div class="meta">
|
|
1246
|
-
<span class="status">${status}</span>
|
|
2004
|
+
<span class="status ${statusClass}">${status}</span>
|
|
1247
2005
|
<span class="pill">${escapeHtml(result.mode)}</span>
|
|
1248
2006
|
<span class="pill">${escapeHtml(result.agentFlavor)}</span>
|
|
1249
2007
|
${viewportTabs}
|
|
@@ -1262,9 +2020,9 @@ function renderAgentReportHtml(result) {
|
|
|
1262
2020
|
<section class="panel">
|
|
1263
2021
|
<h2>Commands</h2>
|
|
1264
2022
|
<div class="commands">
|
|
1265
|
-
${
|
|
1266
|
-
${
|
|
1267
|
-
${
|
|
2023
|
+
${renderAgentReportCommandBlock("replay", result.commands.replay.argv)}
|
|
2024
|
+
${renderAgentReportCommandBlock("update snapshots", result.commands.updateSnapshots.argv)}
|
|
2025
|
+
${renderAgentReportCommandBlock("inspect commands", [
|
|
1268
2026
|
"ptywright",
|
|
1269
2027
|
"agent",
|
|
1270
2028
|
"commands",
|
|
@@ -1278,93 +2036,263 @@ function renderAgentReportHtml(result) {
|
|
|
1278
2036
|
<section class="panel">
|
|
1279
2037
|
<h2>Terminal Agent Artifacts</h2>
|
|
1280
2038
|
<div class="artifacts">
|
|
1281
|
-
${artifacts
|
|
2039
|
+
${artifacts}
|
|
1282
2040
|
</div>
|
|
1283
2041
|
</section>
|
|
1284
2042
|
</main>
|
|
1285
2043
|
</body>
|
|
1286
2044
|
</html>`;
|
|
1287
2045
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
2046
|
+
//#endregion
|
|
2047
|
+
//#region src/agent/report.ts
|
|
2048
|
+
function writeAgentReport(path, result) {
|
|
2049
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2050
|
+
writeArtifactViewerPages(result);
|
|
2051
|
+
writeFileSync(path, renderAgentReportHtml(result), "utf8");
|
|
1293
2052
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2053
|
+
//#endregion
|
|
2054
|
+
//#region src/agent/run_artifacts.ts
|
|
2055
|
+
function writeRunRecord(result, spec) {
|
|
2056
|
+
const record = {
|
|
2057
|
+
$schema: AGENT_RUN_RECORD_SCHEMA_URL,
|
|
2058
|
+
version: 1,
|
|
2059
|
+
name: result.name,
|
|
2060
|
+
ok: result.ok,
|
|
2061
|
+
startedAt: new Date(result.startedAt).toISOString(),
|
|
2062
|
+
durationMs: result.durationMs,
|
|
2063
|
+
mode: result.mode,
|
|
2064
|
+
spec,
|
|
2065
|
+
flowPath: relative(dirname(result.recordPath), result.flowPath),
|
|
2066
|
+
artifactsDir: result.artifactsDir,
|
|
2067
|
+
snapshotDir: result.snapshotDir,
|
|
2068
|
+
reportPath: result.reportPath,
|
|
2069
|
+
cassettePath: relative(dirname(result.recordPath), result.cassettePath),
|
|
2070
|
+
cassetteFrameCount: result.cassetteFrameCount,
|
|
2071
|
+
replayCommand: result.replayCommand,
|
|
2072
|
+
commands: result.commands,
|
|
2073
|
+
steps: result.steps,
|
|
2074
|
+
artifacts: result.artifacts,
|
|
2075
|
+
errors: result.errors
|
|
2076
|
+
};
|
|
2077
|
+
writeAgentRunRecordPath(result.recordPath, record);
|
|
2078
|
+
}
|
|
2079
|
+
function writeRunManifest(result) {
|
|
2080
|
+
writeAgentManifestPath(agentManifestPath(result.artifactsDir), {
|
|
2081
|
+
kind: "run",
|
|
2082
|
+
ok: result.ok,
|
|
2083
|
+
rootDir: result.artifactsDir,
|
|
2084
|
+
primaryPath: result.recordPath,
|
|
2085
|
+
commands: result.commands,
|
|
2086
|
+
validation: {
|
|
2087
|
+
ok: result.ok,
|
|
2088
|
+
stages: [{
|
|
2089
|
+
name: "run",
|
|
2090
|
+
ok: result.ok,
|
|
2091
|
+
totalCount: result.artifacts.length,
|
|
2092
|
+
failureCount: result.artifacts.filter((artifact) => !artifact.ok).length
|
|
2093
|
+
}]
|
|
2094
|
+
},
|
|
2095
|
+
files: [
|
|
2096
|
+
{
|
|
2097
|
+
path: result.flowPath,
|
|
2098
|
+
kind: "flow",
|
|
2099
|
+
role: "flow"
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
path: result.cassettePath,
|
|
2103
|
+
kind: "cassette",
|
|
2104
|
+
role: "cassette"
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
path: result.recordPath,
|
|
2108
|
+
kind: "run-record",
|
|
2109
|
+
role: "record",
|
|
2110
|
+
ok: result.ok
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
path: result.reportPath,
|
|
2114
|
+
kind: "report",
|
|
2115
|
+
role: "report",
|
|
2116
|
+
ok: result.ok
|
|
2117
|
+
},
|
|
2118
|
+
...result.artifacts.flatMap((artifact) => [{
|
|
2119
|
+
path: artifact.path,
|
|
2120
|
+
kind: artifact.kind,
|
|
2121
|
+
role: "artifact",
|
|
2122
|
+
ok: artifact.ok
|
|
2123
|
+
}, {
|
|
2124
|
+
path: artifact.diffPath,
|
|
2125
|
+
kind: "diff",
|
|
2126
|
+
role: "diff",
|
|
2127
|
+
ok: artifact.ok
|
|
2128
|
+
}])
|
|
2129
|
+
]
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
function writeFlowArtifact(path, spec) {
|
|
2133
|
+
writeFileSync(path, JSON.stringify(spec, null, 2) + "\n", "utf8");
|
|
2134
|
+
}
|
|
2135
|
+
//#endregion
|
|
2136
|
+
//#region src/agent/config_defaults.ts
|
|
2137
|
+
function normalizeAgentFlowSpecWithConfig(input, config) {
|
|
2138
|
+
return normalizeAgentFlowSpec(applyAgentConfigDefaults(agentFlowSpecSchema.parse(input), config));
|
|
2139
|
+
}
|
|
2140
|
+
function applyAgentConfigDefaults(input, config) {
|
|
2141
|
+
const agent = config?.agent;
|
|
2142
|
+
if (!agent) return input;
|
|
2143
|
+
const name = sanitizeArtifactName(input.name ?? "agent-flow");
|
|
2144
|
+
const configDefaults = agent.defaults ?? {};
|
|
2145
|
+
const specDefaults = input.defaults ?? {};
|
|
2146
|
+
const viewports = input.viewports ? void 0 : cloneViewports(configDefaults.viewports);
|
|
2147
|
+
return {
|
|
2148
|
+
...input,
|
|
2149
|
+
artifactsDir: input.artifactsDir ?? resolveNamedDir(agent.artifactsRoot, name, config.rootDir),
|
|
2150
|
+
snapshotDir: input.snapshotDir ?? resolveNamedDir(agent.snapshotDir, name, config.rootDir),
|
|
2151
|
+
viewports: viewports ?? input.viewports,
|
|
2152
|
+
defaults: {
|
|
2153
|
+
...specDefaults,
|
|
2154
|
+
timeoutMs: specDefaults.timeoutMs ?? configDefaults.timeoutMs,
|
|
2155
|
+
screenshot: specDefaults.screenshot ?? configDefaults.screenshot,
|
|
2156
|
+
mask: mergeMaskRules(configDefaults.mask, specDefaults.mask)
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function resolveNamedDir(root, name, configRoot) {
|
|
2161
|
+
if (!root) return void 0;
|
|
2162
|
+
const namedDir = join(root, name);
|
|
2163
|
+
return isAbsolute(namedDir) ? namedDir : resolve(configRoot, namedDir);
|
|
2164
|
+
}
|
|
2165
|
+
function cloneViewports(viewports) {
|
|
2166
|
+
return Array.isArray(viewports) && viewports.length > 0 ? viewports.map((viewport) => ({ ...viewport })) : void 0;
|
|
2167
|
+
}
|
|
2168
|
+
function mergeMaskRules(configMask, specMask) {
|
|
2169
|
+
const merged = [...configMask ?? [], ...specMask ?? []];
|
|
2170
|
+
return merged.length > 0 ? merged : void 0;
|
|
2171
|
+
}
|
|
2172
|
+
//#endregion
|
|
2173
|
+
//#region src/agent/command_launch.ts
|
|
2174
|
+
const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
|
|
2175
|
+
function buildCommandLaunchCommand(launch, options = {}) {
|
|
2176
|
+
if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
|
|
2177
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
2178
|
+
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
2179
|
+
return {
|
|
2180
|
+
file: launch.command,
|
|
2181
|
+
args: launch.args ?? [],
|
|
2182
|
+
cwd,
|
|
2183
|
+
env: {
|
|
2184
|
+
...options.env ?? process.env,
|
|
2185
|
+
...launch.env
|
|
2186
|
+
},
|
|
2187
|
+
label: launch.command,
|
|
2188
|
+
urlRegex: launch.urlRegex,
|
|
2189
|
+
waitForUrlMs: launch.waitForUrlMs
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
async function launchBrowserSessionFromCommand(command) {
|
|
2193
|
+
const timeoutMs = command.waitForUrlMs ?? 15e3;
|
|
2194
|
+
const child = spawn(command.file, command.args, {
|
|
2195
|
+
cwd: command.cwd,
|
|
2196
|
+
env: command.env,
|
|
2197
|
+
stdio: [
|
|
2198
|
+
"ignore",
|
|
2199
|
+
"pipe",
|
|
2200
|
+
"pipe"
|
|
2201
|
+
]
|
|
2202
|
+
});
|
|
2203
|
+
const stdoutChunks = [];
|
|
2204
|
+
const stderrChunks = [];
|
|
2205
|
+
return {
|
|
2206
|
+
url: await new Promise((resolveUrl, reject) => {
|
|
2207
|
+
let settled = false;
|
|
2208
|
+
const timer = setTimeout(() => {
|
|
2209
|
+
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for ${command.label ?? command.file} session URL\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
2210
|
+
}, timeoutMs);
|
|
2211
|
+
const finish = (result) => {
|
|
2212
|
+
if (settled) return;
|
|
2213
|
+
settled = true;
|
|
2214
|
+
clearTimeout(timer);
|
|
2215
|
+
child.stdout.off("data", onStdout);
|
|
2216
|
+
child.stderr.off("data", onStderr);
|
|
2217
|
+
child.off("error", onError);
|
|
2218
|
+
child.off("exit", onExit);
|
|
2219
|
+
if (result instanceof Error) reject(result);
|
|
2220
|
+
else resolveUrl(result);
|
|
2221
|
+
};
|
|
2222
|
+
const readUrl = () => {
|
|
2223
|
+
const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
|
|
2224
|
+
if (found) finish(found);
|
|
2225
|
+
};
|
|
2226
|
+
const onStdout = (chunk) => {
|
|
2227
|
+
stdoutChunks.push(chunk.toString("utf8"));
|
|
2228
|
+
readUrl();
|
|
2229
|
+
};
|
|
2230
|
+
const onStderr = (chunk) => {
|
|
2231
|
+
stderrChunks.push(chunk.toString("utf8"));
|
|
2232
|
+
readUrl();
|
|
2233
|
+
};
|
|
2234
|
+
const onError = (error) => finish(error);
|
|
2235
|
+
const onExit = (code, signal) => {
|
|
2236
|
+
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
2237
|
+
};
|
|
2238
|
+
child.stdout.on("data", onStdout);
|
|
2239
|
+
child.stderr.on("data", onStderr);
|
|
2240
|
+
child.once("error", onError);
|
|
2241
|
+
child.once("exit", onExit);
|
|
2242
|
+
}),
|
|
2243
|
+
process: child,
|
|
2244
|
+
close: () => closeChild(child)
|
|
2245
|
+
};
|
|
1310
2246
|
}
|
|
1311
|
-
function
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
2247
|
+
function extractUrlFromOutput(output, regexSource) {
|
|
2248
|
+
if (!regexSource) return output.match(DEFAULT_URL_REGEX)?.[0] ?? null;
|
|
2249
|
+
const match = output.match(new RegExp(regexSource, "m"));
|
|
2250
|
+
return match?.[1] ?? match?.[0] ?? null;
|
|
1314
2251
|
}
|
|
1315
|
-
function
|
|
1316
|
-
return
|
|
2252
|
+
function formatBrowserLaunchCommand(command) {
|
|
2253
|
+
return [command.file, ...command.args].join(" ");
|
|
1317
2254
|
}
|
|
1318
|
-
function
|
|
1319
|
-
|
|
2255
|
+
async function closeChild(child) {
|
|
2256
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
2257
|
+
await new Promise((resolveClose) => {
|
|
2258
|
+
const timer = setTimeout(() => {
|
|
2259
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
2260
|
+
resolveClose();
|
|
2261
|
+
}, 2e3);
|
|
2262
|
+
child.once("exit", () => {
|
|
2263
|
+
clearTimeout(timer);
|
|
2264
|
+
resolveClose();
|
|
2265
|
+
});
|
|
2266
|
+
child.kill("SIGTERM");
|
|
2267
|
+
});
|
|
1320
2268
|
}
|
|
1321
2269
|
//#endregion
|
|
1322
|
-
//#region src/agent/
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const
|
|
2270
|
+
//#region src/agent/launch.ts
|
|
2271
|
+
function buildAgentLaunchCommand(launch, options = {}) {
|
|
2272
|
+
if (resolveAgentLaunchMode(launch) === "url") return null;
|
|
2273
|
+
return buildCommandLaunchCommand(launch, options);
|
|
2274
|
+
}
|
|
2275
|
+
async function resolveAgentLaunchTarget(launch, options = {}) {
|
|
2276
|
+
const mode = resolveAgentLaunchMode(launch);
|
|
2277
|
+
if (mode === "url") return {
|
|
2278
|
+
mode,
|
|
2279
|
+
url: launch.url,
|
|
2280
|
+
session: null
|
|
2281
|
+
};
|
|
2282
|
+
const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
|
|
1335
2283
|
return {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
2284
|
+
mode,
|
|
2285
|
+
url: session.url,
|
|
2286
|
+
session
|
|
1339
2287
|
};
|
|
1340
2288
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
return runAgentSpec((await loadAgentSpec(specPath)).raw, options);
|
|
1345
|
-
}
|
|
1346
|
-
async function replayAgentRecordPath(recordPath, options = {}) {
|
|
1347
|
-
const raw = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
1348
|
-
if (isAgentCassetteLike(raw)) return replayAgentCassette(normalizeAgentCassette(raw), recordPath, options);
|
|
1349
|
-
const record = readAgentRunRecordPath(recordPath);
|
|
1350
|
-
if (record.cassettePath) {
|
|
1351
|
-
const cassettePath = isAbsolute(record.cassettePath) ? record.cassettePath : resolve(dirname(recordPath), record.cassettePath);
|
|
1352
|
-
return replayAgentCassette(readAgentCassettePath(cassettePath, record.spec), cassettePath, {
|
|
1353
|
-
...options,
|
|
1354
|
-
artifactsDir: options.artifactsDir ?? join(dirname(recordPath), "replay")
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
if (record.spec) return runAgentSpec(record.spec, {
|
|
1358
|
-
...options,
|
|
1359
|
-
config: void 0
|
|
1360
|
-
});
|
|
1361
|
-
if (!record.flowPath) throw new Error(`invalid agent run record: missing replay source in ${recordPath}`);
|
|
1362
|
-
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath), {
|
|
1363
|
-
...options,
|
|
1364
|
-
config: void 0
|
|
1365
|
-
});
|
|
2289
|
+
function formatAgentLaunchCommand(launch) {
|
|
2290
|
+
const command = buildAgentLaunchCommand(launch);
|
|
2291
|
+
return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
|
|
1366
2292
|
}
|
|
1367
|
-
|
|
2293
|
+
//#endregion
|
|
2294
|
+
//#region src/agent/runner_setup.ts
|
|
2295
|
+
function prepareAgentRun(input, options) {
|
|
1368
2296
|
const startedAt = Date.now();
|
|
1369
2297
|
const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
|
|
1370
2298
|
const spec = normalizeAgentFlowSpecWithConfig(input, options.replayCassette ? void 0 : options.config);
|
|
@@ -1385,161 +2313,51 @@ async function runAgentSpec(input, options = {}) {
|
|
|
1385
2313
|
"replay",
|
|
1386
2314
|
relative(process.cwd(), recordPath)
|
|
1387
2315
|
];
|
|
1388
|
-
|
|
1389
|
-
ok: true,
|
|
1390
|
-
name,
|
|
1391
|
-
mode: options.replayCassette ? "replay" : "live",
|
|
2316
|
+
return {
|
|
1392
2317
|
startedAt,
|
|
1393
|
-
|
|
2318
|
+
rootDir,
|
|
2319
|
+
spec,
|
|
2320
|
+
name,
|
|
1394
2321
|
artifactsDir,
|
|
1395
2322
|
snapshotDir,
|
|
1396
2323
|
reportPath,
|
|
1397
2324
|
recordPath,
|
|
1398
2325
|
flowPath,
|
|
1399
2326
|
cassettePath,
|
|
1400
|
-
replaySourceCassettePath: options.replaySourceCassettePath,
|
|
1401
|
-
replayCommand: formatAgentArgv(replayArgv),
|
|
1402
|
-
commands: {
|
|
1403
|
-
replay: { argv: replayArgv },
|
|
1404
|
-
updateSnapshots: { argv: [...replayArgv, "--update-snapshots"] }
|
|
1405
|
-
},
|
|
1406
|
-
agentFlavor: resolveAgentFlavor(spec),
|
|
1407
|
-
viewports: spec.viewports ?? [],
|
|
1408
|
-
cassetteFrameCount: cassette.frames.length,
|
|
1409
|
-
steps: [],
|
|
1410
|
-
artifacts: [],
|
|
1411
|
-
errors: []
|
|
1412
|
-
};
|
|
1413
|
-
writeFileSync(flowPath, JSON.stringify(spec, null, 2) + "\n", "utf8");
|
|
1414
|
-
let browser = null;
|
|
1415
|
-
try {
|
|
1416
|
-
browser = await launchAgentBrowser({ headless: options.headless ?? true });
|
|
1417
|
-
for (const viewport of spec.viewports ?? []) await runViewport({
|
|
1418
|
-
browser,
|
|
1419
|
-
spec,
|
|
1420
|
-
viewport,
|
|
1421
|
-
rootDir,
|
|
1422
|
-
artifactsDir,
|
|
1423
|
-
snapshotDir,
|
|
1424
|
-
updateSnapshots,
|
|
1425
|
-
recordCassette: !options.replayCassette,
|
|
1426
|
-
cassette,
|
|
1427
|
-
result
|
|
1428
|
-
});
|
|
1429
|
-
} catch (error) {
|
|
1430
|
-
result.ok = false;
|
|
1431
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1432
|
-
} finally {
|
|
1433
|
-
await closeBrowserSafely(browser);
|
|
1434
|
-
result.durationMs = Date.now() - startedAt;
|
|
1435
|
-
if (options.replaySourceCassettePath) writeFileSync(cassettePath, readFileSync(options.replaySourceCassettePath, "utf8"), "utf8");
|
|
1436
|
-
else writeFileSync(cassettePath, JSON.stringify(cassette, null, 2) + "\n", "utf8");
|
|
1437
|
-
result.cassetteFrameCount = cassette.frames.length;
|
|
1438
|
-
writeRunRecord(result, spec);
|
|
1439
|
-
writeAgentReport(reportPath, result);
|
|
1440
|
-
writeRunManifest(result);
|
|
1441
|
-
}
|
|
1442
|
-
return result;
|
|
1443
|
-
}
|
|
1444
|
-
async function runViewport(args) {
|
|
1445
|
-
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
1446
|
-
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
1447
|
-
const context = await browser.newContext({
|
|
1448
|
-
viewport: {
|
|
1449
|
-
width: viewport.width,
|
|
1450
|
-
height: viewport.height
|
|
1451
|
-
},
|
|
1452
|
-
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
1453
|
-
isMobile: viewport.isMobile,
|
|
1454
|
-
hasTouch: viewport.hasTouch
|
|
1455
|
-
});
|
|
1456
|
-
const page = await context.newPage();
|
|
1457
|
-
const ctx = {
|
|
1458
|
-
spec,
|
|
1459
|
-
viewport,
|
|
1460
|
-
page,
|
|
1461
|
-
artifactsDir,
|
|
1462
|
-
snapshotDir,
|
|
1463
2327
|
updateSnapshots,
|
|
1464
|
-
recordCassette,
|
|
1465
|
-
masks: resolveAgentMasks(spec),
|
|
1466
|
-
artifacts: result.artifacts,
|
|
1467
|
-
replay: Boolean(args.cassette && !args.recordCassette),
|
|
1468
2328
|
cassette,
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
stepType: step.type
|
|
1496
|
-
});
|
|
1497
|
-
} catch (error) {
|
|
1498
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1499
|
-
result.ok = false;
|
|
1500
|
-
result.errors.push(`${viewport.name} step ${i + 1} ${step.type}: ${message}`);
|
|
1501
|
-
result.steps.push({
|
|
1502
|
-
index: i,
|
|
1503
|
-
type: step.type,
|
|
1504
|
-
label: formatStepLabel(step),
|
|
1505
|
-
durationMs: Date.now() - started,
|
|
1506
|
-
ok: false,
|
|
1507
|
-
error: message
|
|
1508
|
-
});
|
|
1509
|
-
break;
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
} finally {
|
|
1513
|
-
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
1514
|
-
await launchTarget.session?.close();
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
1518
|
-
const server = await startAgentCassetteServer(cassette);
|
|
1519
|
-
try {
|
|
1520
|
-
const replaySpec = structuredClone(cassette.spec);
|
|
1521
|
-
const replayCassette = structuredClone(cassette);
|
|
1522
|
-
return await runAgentSpec({
|
|
1523
|
-
...replaySpec,
|
|
1524
|
-
launch: {
|
|
1525
|
-
mode: "url",
|
|
1526
|
-
url: server.url,
|
|
1527
|
-
agentFlavor: replaySpec.launch.agentFlavor
|
|
1528
|
-
}
|
|
1529
|
-
}, {
|
|
1530
|
-
...options,
|
|
1531
|
-
artifactsDir: options.artifactsDir ?? join(dirname(cassettePath), "replay"),
|
|
1532
|
-
replayCassette,
|
|
1533
|
-
replaySourceCassettePath: cassettePath
|
|
1534
|
-
});
|
|
1535
|
-
} finally {
|
|
1536
|
-
await server.close();
|
|
1537
|
-
}
|
|
2329
|
+
result: {
|
|
2330
|
+
ok: true,
|
|
2331
|
+
name,
|
|
2332
|
+
mode: options.replayCassette ? "replay" : "live",
|
|
2333
|
+
startedAt,
|
|
2334
|
+
durationMs: 0,
|
|
2335
|
+
artifactsDir,
|
|
2336
|
+
snapshotDir,
|
|
2337
|
+
reportPath,
|
|
2338
|
+
recordPath,
|
|
2339
|
+
flowPath,
|
|
2340
|
+
cassettePath,
|
|
2341
|
+
replaySourceCassettePath: options.replaySourceCassettePath,
|
|
2342
|
+
replayCommand: formatAgentArgv(replayArgv),
|
|
2343
|
+
commands: {
|
|
2344
|
+
replay: { argv: replayArgv },
|
|
2345
|
+
updateSnapshots: { argv: [...replayArgv, "--update-snapshots"] }
|
|
2346
|
+
},
|
|
2347
|
+
agentFlavor: resolveAgentFlavor(spec),
|
|
2348
|
+
viewports: spec.viewports ?? [],
|
|
2349
|
+
cassetteFrameCount: cassette.frames.length,
|
|
2350
|
+
steps: [],
|
|
2351
|
+
artifacts: [],
|
|
2352
|
+
errors: []
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
1538
2355
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
await withTimeout(browser.close().catch(() => void 0), 5e3);
|
|
2356
|
+
function formatAgentLaunchPlan(input) {
|
|
2357
|
+
return formatAgentLaunchCommand(normalizeAgentFlowSpec(input).launch);
|
|
1542
2358
|
}
|
|
2359
|
+
//#endregion
|
|
2360
|
+
//#region src/agent/timeout.ts
|
|
1543
2361
|
async function withTimeout(promise, timeoutMs) {
|
|
1544
2362
|
let timer;
|
|
1545
2363
|
try {
|
|
@@ -1550,85 +2368,95 @@ async function withTimeout(promise, timeoutMs) {
|
|
|
1550
2368
|
if (timer) clearTimeout(timer);
|
|
1551
2369
|
}
|
|
1552
2370
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2371
|
+
//#endregion
|
|
2372
|
+
//#region src/agent/step_label.ts
|
|
2373
|
+
function formatAgentStepLabel(step) {
|
|
2374
|
+
if (step.type === "snapshot") return `snapshot ${step.name}`;
|
|
2375
|
+
if (step.type === "waitForText") return `wait ${step.text ?? step.regex ?? ""}`;
|
|
2376
|
+
if (step.type === "typeText") return `type ${step.text.slice(0, 24)}`;
|
|
2377
|
+
if (step.type === "pressKey") return `press ${step.key}`;
|
|
2378
|
+
if (step.type === "click") return `click ${step.selector ?? step.text ?? `${step.x},${step.y}`}`;
|
|
2379
|
+
if (step.type === "mark") return `mark ${step.label ?? ""}`;
|
|
2380
|
+
if (step.type === "sleep") return `sleep ${step.ms}ms`;
|
|
2381
|
+
return step.type;
|
|
2382
|
+
}
|
|
2383
|
+
//#endregion
|
|
2384
|
+
//#region src/agent/snapshot_diff.ts
|
|
2385
|
+
function renderSnapshotDiff(expected, received) {
|
|
2386
|
+
const expectedLines = expected.split("\n");
|
|
2387
|
+
const receivedLines = received.split("\n");
|
|
2388
|
+
const max = Math.max(expectedLines.length, receivedLines.length);
|
|
2389
|
+
const out = ["--- expected", "+++ received"];
|
|
2390
|
+
for (let i = 0; i < max; i += 1) {
|
|
2391
|
+
const before = expectedLines[i];
|
|
2392
|
+
const after = receivedLines[i];
|
|
2393
|
+
if (before === after) {
|
|
2394
|
+
if (before !== void 0) out.push(` ${before}`);
|
|
2395
|
+
continue;
|
|
1569
2396
|
}
|
|
1570
|
-
|
|
2397
|
+
if (before !== void 0) out.push(`- ${before}`);
|
|
2398
|
+
if (after !== void 0) out.push(`+ ${after}`);
|
|
1571
2399
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2400
|
+
return out.join("\n") + "\n";
|
|
2401
|
+
}
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/agent/terminal_dom.ts
|
|
2404
|
+
async function waitForTerminalRoot(page, timeoutMs) {
|
|
2405
|
+
await page.locator("[data-terminal-root]").first().waitFor({
|
|
2406
|
+
state: "attached",
|
|
2407
|
+
timeout: timeoutMs
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
async function waitForTerminalText(page, args) {
|
|
2411
|
+
const started = Date.now();
|
|
2412
|
+
const matcher = args.regex ? new RegExp(args.regex) : null;
|
|
2413
|
+
while (Date.now() - started < args.timeoutMs) {
|
|
2414
|
+
const text = await readTerminalText(page);
|
|
2415
|
+
if (args.text && text.includes(args.text)) return;
|
|
2416
|
+
if (matcher?.test(text)) return;
|
|
2417
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
1576
2418
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2419
|
+
throw new Error(`timed out waiting for terminal text ${args.text ?? args.regex ?? ""}`);
|
|
2420
|
+
}
|
|
2421
|
+
async function waitForStableDom(page, args) {
|
|
2422
|
+
const started = Date.now();
|
|
2423
|
+
let last = "";
|
|
2424
|
+
let stableSince = Date.now();
|
|
2425
|
+
while (Date.now() - started < args.timeoutMs) {
|
|
2426
|
+
const current = await readTerminalDomIfPresent(page);
|
|
2427
|
+
if (current === null) {
|
|
2428
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
2429
|
+
continue;
|
|
1587
2430
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
await
|
|
1593
|
-
timeoutMs: step.timeoutMs ?? timeout,
|
|
1594
|
-
quietMs: step.quietMs ?? 350,
|
|
1595
|
-
intervalMs: step.intervalMs ?? 100
|
|
1596
|
-
});
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (step.type === "snapshot") {
|
|
1600
|
-
await captureSnapshotStep(ctx, step);
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
if (step.type === "sleep") {
|
|
1604
|
-
if (!ctx.replay) await new Promise((resolveSleep) => setTimeout(resolveSleep, step.ms));
|
|
1605
|
-
return;
|
|
2431
|
+
if (current !== last) {
|
|
2432
|
+
last = current;
|
|
2433
|
+
stableSince = Date.now();
|
|
2434
|
+
} else if (Date.now() - stableSince >= args.quietMs) return;
|
|
2435
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
1606
2436
|
}
|
|
1607
|
-
|
|
2437
|
+
throw new Error(`timed out waiting for stable terminal DOM`);
|
|
1608
2438
|
}
|
|
1609
|
-
async function
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
stepType: frame.stepType,
|
|
1617
|
-
terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
|
|
1618
|
-
dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
|
|
1619
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2439
|
+
async function readTerminalText(page) {
|
|
2440
|
+
const text = await page.evaluate(() => {
|
|
2441
|
+
const node = document.querySelector("[data-terminal-root]");
|
|
2442
|
+
if (!node) return null;
|
|
2443
|
+
const rows = Array.from(node.querySelectorAll(".term-grid .term-row"));
|
|
2444
|
+
if (rows.length > 0) return rows.map((row) => row.textContent ?? "").join("\n");
|
|
2445
|
+
return node.textContent ?? "";
|
|
1620
2446
|
});
|
|
2447
|
+
if (text === null) throw new Error("terminal root is not attached");
|
|
2448
|
+
return text;
|
|
1621
2449
|
}
|
|
1622
|
-
async function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
window.__ptywrightReplaySetPhase?.(phase);
|
|
1630
|
-
}, ctx.nextReplayPhase);
|
|
2450
|
+
async function readTerminalDom(page) {
|
|
2451
|
+
const dom = await readTerminalDomIfPresent(page);
|
|
2452
|
+
if (dom === null) throw new Error("terminal root is not attached");
|
|
2453
|
+
return dom;
|
|
2454
|
+
}
|
|
2455
|
+
async function readTerminalDomIfPresent(page) {
|
|
2456
|
+
return page.evaluate(() => document.querySelector("[data-terminal-root]")?.innerHTML ?? null);
|
|
1631
2457
|
}
|
|
2458
|
+
//#endregion
|
|
2459
|
+
//#region src/agent/snapshot_artifacts.ts
|
|
1632
2460
|
async function captureSnapshotStep(ctx, step) {
|
|
1633
2461
|
const targets = step.targets ?? [
|
|
1634
2462
|
"terminal",
|
|
@@ -1636,7 +2464,8 @@ async function captureSnapshotStep(ctx, step) {
|
|
|
1636
2464
|
...ctx.spec.defaults?.screenshot ? ["screenshot"] : []
|
|
1637
2465
|
];
|
|
1638
2466
|
const base = `${sanitizeArtifactName(ctx.viewport.name)}.${sanitizeArtifactName(step.name)}`;
|
|
1639
|
-
|
|
2467
|
+
const errors = [];
|
|
2468
|
+
for (const target of targets) try {
|
|
1640
2469
|
if (target === "terminal") {
|
|
1641
2470
|
const text = normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks);
|
|
1642
2471
|
await writeComparableArtifact(ctx, {
|
|
@@ -1675,7 +2504,10 @@ async function captureSnapshotStep(ctx, step) {
|
|
|
1675
2504
|
path: screenshotPath,
|
|
1676
2505
|
ok: true
|
|
1677
2506
|
});
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
1678
2509
|
}
|
|
2510
|
+
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
1679
2511
|
}
|
|
1680
2512
|
async function writeComparableArtifact(ctx, artifact) {
|
|
1681
2513
|
const artifactPath = join(ctx.artifactsDir, artifact.relativePath);
|
|
@@ -1752,172 +2584,255 @@ async function writeComparableArtifact(ctx, artifact) {
|
|
|
1752
2584
|
ok: true
|
|
1753
2585
|
});
|
|
1754
2586
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region src/agent/step_runner.ts
|
|
2589
|
+
async function runAgentStep(ctx, step) {
|
|
2590
|
+
const timeout = ctx.spec.defaults?.timeoutMs ?? 3e4;
|
|
2591
|
+
if (step.type === "waitForText") {
|
|
2592
|
+
await waitForTerminalText(ctx.page, {
|
|
2593
|
+
text: step.text,
|
|
2594
|
+
regex: step.regex,
|
|
2595
|
+
timeoutMs: step.timeoutMs ?? timeout
|
|
2596
|
+
});
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (step.type === "typeText") {
|
|
2600
|
+
await advanceReplayPhase(ctx);
|
|
2601
|
+
if (!ctx.replay) {
|
|
2602
|
+
await ctx.page.locator("[data-terminal-root]").first().click({ timeout });
|
|
2603
|
+
await ctx.page.keyboard.type(step.text, { delay: step.delayMs });
|
|
2604
|
+
if (step.enter) await ctx.page.keyboard.press("Enter");
|
|
1766
2605
|
}
|
|
1767
|
-
|
|
1768
|
-
if (after !== void 0) out.push(`+ ${after}`);
|
|
2606
|
+
return;
|
|
1769
2607
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
state: "attached",
|
|
1775
|
-
timeout: timeoutMs
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
async function waitForTerminalText(page, args) {
|
|
1779
|
-
const started = Date.now();
|
|
1780
|
-
const matcher = args.regex ? new RegExp(args.regex) : null;
|
|
1781
|
-
while (Date.now() - started < args.timeoutMs) {
|
|
1782
|
-
const text = await readTerminalText(page);
|
|
1783
|
-
if (args.text && text.includes(args.text)) return;
|
|
1784
|
-
if (matcher?.test(text)) return;
|
|
1785
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
2608
|
+
if (step.type === "pressKey") {
|
|
2609
|
+
await advanceReplayPhase(ctx);
|
|
2610
|
+
if (!ctx.replay) await ctx.page.keyboard.press(step.key);
|
|
2611
|
+
return;
|
|
1786
2612
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
while (Date.now() - started < args.timeoutMs) {
|
|
1794
|
-
const current = await readTerminalDomIfPresent(page);
|
|
1795
|
-
if (current === null) {
|
|
1796
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
1797
|
-
continue;
|
|
2613
|
+
if (step.type === "click") {
|
|
2614
|
+
await advanceReplayPhase(ctx);
|
|
2615
|
+
if (ctx.replay) return;
|
|
2616
|
+
if (step.selector) {
|
|
2617
|
+
await ctx.page.locator(step.selector).first().click({ timeout });
|
|
2618
|
+
return;
|
|
1798
2619
|
}
|
|
1799
|
-
if (
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
}
|
|
1803
|
-
await
|
|
2620
|
+
if (step.text) {
|
|
2621
|
+
await ctx.page.getByText(step.text, { exact: false }).first().click({ timeout });
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
await ctx.page.mouse.click(step.x, step.y);
|
|
2625
|
+
return;
|
|
1804
2626
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2627
|
+
if (step.type === "waitForStableDom") {
|
|
2628
|
+
await waitForStableDom(ctx.page, {
|
|
2629
|
+
timeoutMs: step.timeoutMs ?? timeout,
|
|
2630
|
+
quietMs: step.quietMs ?? 350,
|
|
2631
|
+
intervalMs: step.intervalMs ?? 100
|
|
2632
|
+
});
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
if (step.type === "snapshot") {
|
|
2636
|
+
await captureSnapshotStep(ctx, step);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (step.type === "sleep") {
|
|
2640
|
+
if (!ctx.replay) await new Promise((resolveSleep) => setTimeout(resolveSleep, step.ms));
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (step.type === "mark") return;
|
|
1817
2644
|
}
|
|
1818
|
-
async function
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2645
|
+
async function captureCassetteFrame(ctx, frame) {
|
|
2646
|
+
if (!ctx.cassette || !ctx.recordCassette) return;
|
|
2647
|
+
const phase = ctx.nextReplayPhase;
|
|
2648
|
+
upsertAgentCassetteFrame(ctx.cassette, {
|
|
2649
|
+
viewport: ctx.viewport,
|
|
2650
|
+
phase,
|
|
2651
|
+
stepIndex: frame.stepIndex,
|
|
2652
|
+
stepType: frame.stepType,
|
|
2653
|
+
terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
|
|
2654
|
+
dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
|
|
2655
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2656
|
+
});
|
|
1822
2657
|
}
|
|
1823
|
-
async function
|
|
1824
|
-
|
|
2658
|
+
async function advanceReplayPhase(ctx) {
|
|
2659
|
+
if (!ctx.replay) {
|
|
2660
|
+
ctx.nextReplayPhase += 1;
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
ctx.nextReplayPhase += 1;
|
|
2664
|
+
await ctx.page.evaluate((phase) => {
|
|
2665
|
+
window.__ptywrightReplaySetPhase?.(phase);
|
|
2666
|
+
}, ctx.nextReplayPhase);
|
|
1825
2667
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/agent/viewport_runner.ts
|
|
2670
|
+
async function runAgentViewport(args) {
|
|
2671
|
+
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
2672
|
+
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
2673
|
+
const context = await browser.newContext({
|
|
2674
|
+
viewport: {
|
|
2675
|
+
width: viewport.width,
|
|
2676
|
+
height: viewport.height
|
|
2677
|
+
},
|
|
2678
|
+
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
2679
|
+
isMobile: viewport.isMobile,
|
|
2680
|
+
hasTouch: viewport.hasTouch
|
|
2681
|
+
});
|
|
2682
|
+
const page = await context.newPage();
|
|
2683
|
+
const ctx = {
|
|
1835
2684
|
spec,
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
commands: result.commands,
|
|
1844
|
-
steps: result.steps,
|
|
2685
|
+
viewport,
|
|
2686
|
+
page,
|
|
2687
|
+
artifactsDir,
|
|
2688
|
+
snapshotDir,
|
|
2689
|
+
updateSnapshots,
|
|
2690
|
+
recordCassette,
|
|
2691
|
+
masks: resolveAgentMasks(spec),
|
|
1845
2692
|
artifacts: result.artifacts,
|
|
1846
|
-
|
|
2693
|
+
replay: Boolean(args.cassette && !args.recordCassette),
|
|
2694
|
+
cassette,
|
|
2695
|
+
nextReplayPhase: 0
|
|
1847
2696
|
};
|
|
1848
|
-
|
|
2697
|
+
try {
|
|
2698
|
+
await page.goto(resolveViewportUrl(launchTarget.url, viewport), {
|
|
2699
|
+
waitUntil: "domcontentloaded",
|
|
2700
|
+
timeout: spec.defaults?.timeoutMs ?? 3e4
|
|
2701
|
+
});
|
|
2702
|
+
await waitForTerminalRoot(page, spec.defaults?.timeoutMs ?? 3e4);
|
|
2703
|
+
await captureCassetteFrame(ctx, {
|
|
2704
|
+
stepIndex: null,
|
|
2705
|
+
stepType: "initial"
|
|
2706
|
+
});
|
|
2707
|
+
for (let i = 0; i < spec.steps.length; i += 1) {
|
|
2708
|
+
const step = spec.steps[i];
|
|
2709
|
+
const started = Date.now();
|
|
2710
|
+
try {
|
|
2711
|
+
await runAgentStep(ctx, step);
|
|
2712
|
+
result.steps.push({
|
|
2713
|
+
index: i,
|
|
2714
|
+
type: step.type,
|
|
2715
|
+
label: formatAgentStepLabel(step),
|
|
2716
|
+
durationMs: Date.now() - started,
|
|
2717
|
+
ok: true
|
|
2718
|
+
});
|
|
2719
|
+
await captureCassetteFrame(ctx, {
|
|
2720
|
+
stepIndex: i,
|
|
2721
|
+
stepType: step.type
|
|
2722
|
+
});
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2725
|
+
result.ok = false;
|
|
2726
|
+
result.errors.push(`${viewport.name} step ${i + 1} ${step.type}: ${message}`);
|
|
2727
|
+
result.steps.push({
|
|
2728
|
+
index: i,
|
|
2729
|
+
type: step.type,
|
|
2730
|
+
label: formatAgentStepLabel(step),
|
|
2731
|
+
durationMs: Date.now() - started,
|
|
2732
|
+
ok: false,
|
|
2733
|
+
error: message
|
|
2734
|
+
});
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
} finally {
|
|
2739
|
+
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
2740
|
+
await launchTarget.session?.close();
|
|
2741
|
+
}
|
|
1849
2742
|
}
|
|
1850
|
-
function
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
{
|
|
1878
|
-
path: result.recordPath,
|
|
1879
|
-
kind: "run-record",
|
|
1880
|
-
role: "record",
|
|
1881
|
-
ok: result.ok
|
|
1882
|
-
},
|
|
1883
|
-
{
|
|
1884
|
-
path: result.reportPath,
|
|
1885
|
-
kind: "report",
|
|
1886
|
-
role: "report",
|
|
1887
|
-
ok: result.ok
|
|
1888
|
-
},
|
|
1889
|
-
...result.artifacts.flatMap((artifact) => [{
|
|
1890
|
-
path: artifact.path,
|
|
1891
|
-
kind: artifact.kind,
|
|
1892
|
-
role: "artifact",
|
|
1893
|
-
ok: artifact.ok
|
|
1894
|
-
}, {
|
|
1895
|
-
path: artifact.diffPath,
|
|
1896
|
-
kind: "diff",
|
|
1897
|
-
role: "diff",
|
|
1898
|
-
ok: artifact.ok
|
|
1899
|
-
}])
|
|
1900
|
-
]
|
|
2743
|
+
function resolveViewportUrl(url, viewport) {
|
|
2744
|
+
return url.replaceAll("{viewportName}", encodeURIComponent(viewport.name)).replaceAll("{viewportWidth}", encodeURIComponent(String(viewport.width))).replaceAll("{viewportHeight}", encodeURIComponent(String(viewport.height)));
|
|
2745
|
+
}
|
|
2746
|
+
//#endregion
|
|
2747
|
+
//#region src/agent/runner.ts
|
|
2748
|
+
async function runAgentSpecPath(specPath, options = {}) {
|
|
2749
|
+
return runAgentSpec((await loadAgentSpec(specPath)).raw, options);
|
|
2750
|
+
}
|
|
2751
|
+
async function replayAgentRecordPath(recordPath, options = {}) {
|
|
2752
|
+
const raw = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
2753
|
+
if (isAgentCassetteLike(raw)) return replayAgentCassette(normalizeAgentCassette(raw), recordPath, options);
|
|
2754
|
+
const record = readAgentRunRecordPath(recordPath);
|
|
2755
|
+
if (record.cassettePath) {
|
|
2756
|
+
const cassettePath = isAbsolute(record.cassettePath) ? record.cassettePath : resolve(dirname(recordPath), record.cassettePath);
|
|
2757
|
+
return replayAgentCassette(readAgentCassettePath(cassettePath, record.spec), cassettePath, {
|
|
2758
|
+
...options,
|
|
2759
|
+
artifactsDir: options.artifactsDir ?? join(dirname(recordPath), "replay")
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
if (record.spec) return runAgentSpec(record.spec, {
|
|
2763
|
+
...options,
|
|
2764
|
+
config: void 0
|
|
2765
|
+
});
|
|
2766
|
+
if (!record.flowPath) throw new Error(`invalid agent run record: missing replay source in ${recordPath}`);
|
|
2767
|
+
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath), {
|
|
2768
|
+
...options,
|
|
2769
|
+
config: void 0
|
|
1901
2770
|
});
|
|
1902
2771
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2772
|
+
async function runAgentSpec(input, options = {}) {
|
|
2773
|
+
const { artifactsDir, cassette, cassettePath, flowPath, reportPath, result, rootDir, snapshotDir, spec, startedAt, updateSnapshots } = prepareAgentRun(input, options);
|
|
2774
|
+
writeFlowArtifact(flowPath, spec);
|
|
2775
|
+
let browser = null;
|
|
2776
|
+
try {
|
|
2777
|
+
browser = await launchAgentBrowser({ headless: options.headless ?? true });
|
|
2778
|
+
for (const viewport of spec.viewports ?? []) await runAgentViewport({
|
|
2779
|
+
browser,
|
|
2780
|
+
spec,
|
|
2781
|
+
viewport,
|
|
2782
|
+
rootDir,
|
|
2783
|
+
artifactsDir,
|
|
2784
|
+
snapshotDir,
|
|
2785
|
+
updateSnapshots,
|
|
2786
|
+
recordCassette: !options.replayCassette,
|
|
2787
|
+
cassette,
|
|
2788
|
+
result
|
|
2789
|
+
});
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
result.ok = false;
|
|
2792
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
2793
|
+
} finally {
|
|
2794
|
+
await closeBrowserSafely(browser);
|
|
2795
|
+
result.durationMs = Date.now() - startedAt;
|
|
2796
|
+
if (options.replaySourceCassettePath) writeFileSync(cassettePath, readFileSync(options.replaySourceCassettePath, "utf8"), "utf8");
|
|
2797
|
+
else writeFileSync(cassettePath, JSON.stringify(cassette, null, 2) + "\n", "utf8");
|
|
2798
|
+
result.cassetteFrameCount = cassette.frames.length;
|
|
2799
|
+
writeRunRecord(result, spec);
|
|
2800
|
+
writeAgentReport(reportPath, result);
|
|
2801
|
+
writeRunManifest(result);
|
|
2802
|
+
}
|
|
2803
|
+
return result;
|
|
2804
|
+
}
|
|
2805
|
+
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
2806
|
+
const server = await startAgentCassetteServer(cassette);
|
|
2807
|
+
try {
|
|
2808
|
+
const replaySpec = structuredClone(cassette.spec);
|
|
2809
|
+
const replayCassette = structuredClone(cassette);
|
|
2810
|
+
return await runAgentSpec({
|
|
2811
|
+
...replaySpec,
|
|
2812
|
+
launch: {
|
|
2813
|
+
mode: "url",
|
|
2814
|
+
url: withReplayViewportQuery(server.url),
|
|
2815
|
+
agentFlavor: replaySpec.launch.agentFlavor
|
|
2816
|
+
}
|
|
2817
|
+
}, {
|
|
2818
|
+
...options,
|
|
2819
|
+
artifactsDir: options.artifactsDir ?? join(dirname(cassettePath), "replay"),
|
|
2820
|
+
replayCassette,
|
|
2821
|
+
replaySourceCassettePath: cassettePath
|
|
2822
|
+
});
|
|
2823
|
+
} finally {
|
|
2824
|
+
await server.close();
|
|
2825
|
+
}
|
|
1912
2826
|
}
|
|
1913
|
-
function
|
|
1914
|
-
return
|
|
2827
|
+
function withReplayViewportQuery(url) {
|
|
2828
|
+
return `${url}${url.includes("?") ? "&" : "?"}viewportName={viewportName}&viewportWidth={viewportWidth}&viewportHeight={viewportHeight}`;
|
|
1915
2829
|
}
|
|
1916
|
-
function
|
|
1917
|
-
|
|
2830
|
+
async function closeBrowserSafely(browser) {
|
|
2831
|
+
if (!browser) return;
|
|
2832
|
+
await withTimeout(browser.close().catch(() => void 0), 5e3);
|
|
1918
2833
|
}
|
|
1919
2834
|
function defaultSpecNameForPath(path) {
|
|
1920
2835
|
return sanitizeArtifactName(basename(path, extname(path)));
|
|
1921
2836
|
}
|
|
1922
2837
|
//#endregion
|
|
1923
|
-
export {
|
|
2838
|
+
export { writeAgentManifestPath as C, escapeHtml as D, escapeAttribute as E, normalizeAgentFlowSpec as O, validateAgentManifestFiles as S, readAgentCassettePath as T, formatArgv as _, formatAgentLaunchPlan as a, isAgentManifestLike as b, launchAgentBrowser as c, AGENT_RUN_RECORD_SCHEMA_URL as d, agentRunModeSchema as f, writeAgentRunRecordPath as g, readAgentRunRecordPath as h, runAgentSpecPath as i, sanitizeArtifactName as k, createAgentTemplateSpec as l, isAgentRunRecordLike as m, replayAgentRecordPath as n, resolveAgentLaunchTarget as o, formatAgentArgv as p, runAgentSpec as r, normalizeAgentFlowSpecWithConfig as s, defaultSpecNameForPath as t, loadAgentSpec as u, AGENT_MANIFEST_FILE_NAME as v, isAgentCassetteLike as w, readAgentManifestPath as x, agentManifestPath as y };
|