ptywright 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as formatAgentLaunchPlan, i as runAgentSpecPath, n as replayAgentRecordPath, r as runAgentSpec, t as defaultSpecNameForPath } from "./runner-CF2ieCkx.mjs";
1
+ import { a as formatAgentLaunchPlan, i as runAgentSpecPath, n as replayAgentRecordPath, r as runAgentSpec, t as defaultSpecNameForPath } from "./runner-1gYMz8D5.mjs";
2
2
  export { defaultSpecNameForPath, formatAgentLaunchPlan, replayAgentRecordPath, runAgentSpec, runAgentSpecPath };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { t as main } from "../cli-CRGGdNy8.mjs";
2
+ import { t as main } from "../cli-DVDqUtha.mjs";
3
3
  //#region src/bin/ptywright.ts
4
4
  await main();
5
5
  //#endregion
@@ -1,9 +1,9 @@
1
1
  import { n as loadPtywrightConfig } from "./config-bGg636EW.mjs";
2
- import { C as writeAgentManifestPath, D as escapeHtml, E as escapeAttribute, O as normalizeAgentFlowSpec, S as validateAgentManifestFiles, T as readAgentCassettePath, _ as formatArgv, b as isAgentManifestLike, c as launchAgentBrowser, d as AGENT_RUN_RECORD_SCHEMA_URL, f as agentRunModeSchema, g as writeAgentRunRecordPath, h as readAgentRunRecordPath, i as runAgentSpecPath, k as sanitizeArtifactName, l as createAgentTemplateSpec, m as isAgentRunRecordLike, n as replayAgentRecordPath, o as resolveAgentLaunchTarget, p as formatAgentArgv, s as normalizeAgentFlowSpecWithConfig, u as loadAgentSpec, v as AGENT_MANIFEST_FILE_NAME, w as isAgentCassetteLike, x as readAgentManifestPath, y as agentManifestPath } from "./runner-CF2ieCkx.mjs";
2
+ import { C as writeAgentManifestPath, D as escapeHtml, E as escapeAttribute, O as normalizeAgentFlowSpec, S as validateAgentManifestFiles, T as readAgentCassettePath, _ as formatArgv, b as isAgentManifestLike, c as launchAgentBrowser, d as AGENT_RUN_RECORD_SCHEMA_URL, f as agentRunModeSchema, g as writeAgentRunRecordPath, h as readAgentRunRecordPath, i as runAgentSpecPath, k as sanitizeArtifactName, l as createAgentTemplateSpec, m as isAgentRunRecordLike, n as replayAgentRecordPath, o as resolveAgentLaunchTarget, p as formatAgentArgv, s as normalizeAgentFlowSpecWithConfig, u as loadAgentSpec, v as AGENT_MANIFEST_FILE_NAME, w as isAgentCassetteLike, x as readAgentManifestPath, y as agentManifestPath } from "./runner-1gYMz8D5.mjs";
3
3
  import { a as sameArgv, i as diffCommandMaps, r as formatZodIssues } from "./manifest_files-DW80c1H7.mjs";
4
4
  import { i as portableCliPath, n as mergeProcessEnv, o as relativeHref, s as samePath } from "./env-DPYHo-zH.mjs";
5
5
  import { d as resolvePtyBackend, u as createDefaultPtyAdapter } from "./runner-BHXXwxYp.mjs";
6
- import { a as resolveScriptManifestPath, c as relocateScriptManifestCommands, d as resolveScriptRunSummaryPath, f as runScriptPath, i as readScriptManifestPath, l as resolveManifestPrimaryPath$1, n as runAllScripts, o as validateScriptManifest, r as findScriptSummaryManifest, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-DpsZDVu2.mjs";
6
+ import { a as resolveScriptManifestPath, c as relocateScriptManifestCommands, d as resolveScriptRunSummaryPath, f as runScriptPath, i as readScriptManifestPath, l as resolveManifestPrimaryPath$1, n as runAllScripts, o as validateScriptManifest, r as findScriptSummaryManifest, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-OujH4CEo.mjs";
7
7
  import { c as createPtyCassetteReplay, i as formatPtyCassetteInspectLines, l as readPtyCassettePath, o as inspectPtyCassettePath, r as createPtyCassetteRecorder, t as wrapPtyLike, v as validatePtyCassette } from "./pty_like-DWIlWGgA.mjs";
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
9
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -418,6 +418,7 @@ const agentReplayFailedArtifactSchema = z.object({
418
418
  kind: z.enum([
419
419
  "terminal",
420
420
  "dom",
421
+ "layout",
421
422
  "screenshot"
422
423
  ]),
423
424
  path: z.string().min(1),
@@ -1808,6 +1809,7 @@ async function replayRecordEntry(filePath, artifactsDir, options) {
1808
1809
  try {
1809
1810
  return await replayAgentRecordPath(filePath, {
1810
1811
  artifactsDir,
1812
+ config: options.config,
1811
1813
  headless: options.headless,
1812
1814
  updateSnapshots: options.updateSnapshots
1813
1815
  });
@@ -2023,6 +2025,7 @@ async function replayAllAgentRecords(options = {}) {
2023
2025
  const artifactsDir = join(suiteDir, "tests", safeArtifactsDirName(relative(dir, filePath)));
2024
2026
  const entryStartedAt = Date.now();
2025
2027
  const result = await replayRecordEntry(filePath, artifactsDir, {
2028
+ config: options.config,
2026
2029
  headless: options.headless ?? true,
2027
2030
  updateSnapshots
2028
2031
  });
@@ -2089,6 +2092,7 @@ async function checkAgentRegression(options = {}) {
2089
2092
  validationAfter: emptyValidationResult(artifactsRoot)
2090
2093
  });
2091
2094
  const replay = await replayAllAgentRecords({
2095
+ config: options.config,
2092
2096
  dir: cassetteDir,
2093
2097
  artifactsRoot,
2094
2098
  headless: options.headless ?? true,
@@ -2721,6 +2725,7 @@ async function runAgentRecord(args, context) {
2721
2725
  }
2722
2726
  async function runAgentCheck(args, context) {
2723
2727
  return printAgentCheckResult(await checkAgentRegression({
2728
+ config: context.config,
2724
2729
  cassetteDir: args.path ?? args.cassetteDir ?? resolveAgentConfigPath(context.config, context.config?.agent?.cassetteDir),
2725
2730
  artifactsRoot: args.artifactsRoot ?? resolveAgentConfigPath(context.config, context.config?.agent?.artifactsRoot),
2726
2731
  headless: context.headless,
@@ -2748,6 +2753,7 @@ async function runAgentRerun(args, context) {
2748
2753
  }
2749
2754
  async function runAgentReplayAll(args, context) {
2750
2755
  return printAgentReplayAllResult(await replayAllAgentRecords({
2756
+ config: context.config,
2751
2757
  dir: args.path ?? resolveAgentConfigPath(context.config, context.config?.agent?.cassetteDir),
2752
2758
  artifactsRoot: args.artifactsRoot ?? resolveAgentConfigPath(context.config, context.config?.agent?.artifactsRoot),
2753
2759
  headless: context.headless,
package/dist/cli.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as main } from "./cli-CRGGdNy8.mjs";
1
+ import { t as main } from "./cli-DVDqUtha.mjs";
2
2
  export { main };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createPtywrightServer } from "./server-DpsZDVu2.mjs";
1
+ import { t as createPtywrightServer } from "./server-OujH4CEo.mjs";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  //#region src/index.ts
4
4
  const { server, sessions } = createPtywrightServer();
package/dist/mcp.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createPtywrightServer } from "./server-DpsZDVu2.mjs";
1
+ import { t as createPtywrightServer } from "./server-OujH4CEo.mjs";
2
2
  export { createPtywrightServer };
@@ -36,6 +36,9 @@ function normalizeTerminalText(input, rules = []) {
36
36
  function normalizeDomSnapshot(input, rules = []) {
37
37
  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 });
38
38
  }
39
+ function normalizeReplayDom(input, rules = []) {
40
+ return applyAgentMasks(input.replace(/\r\n?/g, "\n"), rules, { replacement: escapeHtmlText });
41
+ }
39
42
  function sanitizeArtifactName(input) {
40
43
  return input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
41
44
  }
@@ -144,6 +147,7 @@ const snapshotStepSchema = z.object({
144
147
  targets: z.array(z.enum([
145
148
  "terminal",
146
149
  "dom",
150
+ "layout",
147
151
  "screenshot"
148
152
  ])).optional(),
149
153
  fullPage: z.boolean().optional()
@@ -468,6 +472,7 @@ const agentManifestFileKindSchema = z.enum([
468
472
  "report",
469
473
  "terminal",
470
474
  "dom",
475
+ "layout",
471
476
  "screenshot",
472
477
  "diff"
473
478
  ]);
@@ -595,6 +600,7 @@ const agentRunArtifactSchema = z.object({
595
600
  kind: z.enum([
596
601
  "terminal",
597
602
  "dom",
603
+ "layout",
598
604
  "screenshot"
599
605
  ]),
600
606
  path: z.string().min(1),
@@ -879,8 +885,8 @@ function errorFields(error) {
879
885
  //#endregion
880
886
  //#region src/agent/aitty_report_assets.ts
881
887
  function prepareAittyReportAssets(context) {
882
- const sources = resolveAittyReportAssetSources(context);
883
- if (!existsSync(sources.scriptSource) || !existsSync(sources.styleSource)) throw new Error("@aitty/snapshot report assets are missing. Run the package build before generating reports.");
888
+ const sources = resolveAittyReportAssetSources();
889
+ if (!existsSync(sources.scriptSource) || !existsSync(sources.styleSource)) throw new Error("@aitty/snapshot report assets are missing. Reinstall ptywright dependencies before generating reports.");
884
890
  const assetDir = join(context.artifactsDir, "assets");
885
891
  const scriptPath = join(assetDir, "aitty-web-component.js");
886
892
  const stylePath = join(assetDir, "aitty-terminal.css");
@@ -889,63 +895,25 @@ function prepareAittyReportAssets(context) {
889
895
  copyFileSync(sources.styleSource, stylePath);
890
896
  return {
891
897
  scriptPath,
892
- scriptType: sources.scriptType,
893
898
  stylePath
894
899
  };
895
900
  }
896
- function resolveAittyReportAssetSources(context) {
897
- for (const resolverBase of resolveAittyReportResolverBases(context)) {
898
- const sources = tryResolveAittyReportAssetSources(createRequire(resolverBase));
899
- if (sources) return sources;
900
- }
901
- const fallback = tryResolveAittyReportAssetSources(createRequire(import.meta.url));
902
- if (fallback) return fallback;
903
- throw new Error("@aitty/snapshot report assets are missing. Install @aitty/snapshot or run the package build before generating reports.");
904
- }
905
- function resolveAittyReportResolverBases(context) {
906
- const candidates = [
907
- findNearestPackageJson(dirname(resolve(context.flowPath))),
908
- findNearestPackageJson(dirname(resolve(context.reportPath))),
909
- findNearestPackageJson(dirname(resolve(context.artifactsDir))),
910
- findNearestPackageJson(process.cwd())
911
- ].filter((path) => Boolean(path));
912
- return Array.from(new Set(candidates));
913
- }
914
- function findNearestPackageJson(startDir) {
915
- let currentDir = resolve(startDir);
916
- while (true) {
917
- const packagePath = join(currentDir, "package.json");
918
- if (existsSync(packagePath)) return packagePath;
919
- const parentDir = dirname(currentDir);
920
- if (parentDir === currentDir) return null;
921
- currentDir = parentDir;
922
- }
901
+ function resolveAittyReportAssetSources() {
902
+ const sources = tryResolveAittyReportAssetSources(createRequire(import.meta.url));
903
+ if (!sources) throw new Error("@aitty/snapshot report assets are missing. Install @aitty/snapshot before generating reports.");
904
+ return sources;
923
905
  }
924
906
  function tryResolveAittyReportAssetSources(resolver) {
925
- return tryResolveAittyPackageAssetSources(resolver, "@aitty/snapshot") ?? tryResolveAittyPackageAssetSources(resolver, "@aitty/browser");
926
- }
927
- function tryResolveAittyPackageAssetSources(resolver, packageName) {
928
907
  let scriptSource;
929
- let scriptType = "classic";
930
908
  let styleSource;
931
909
  try {
932
- scriptSource = resolver.resolve(`${packageName}/web-component.global.js`);
933
- } catch {
934
- try {
935
- scriptSource = resolver.resolve(`${packageName}/web-component.js`);
936
- scriptType = "module";
937
- } catch {
938
- return null;
939
- }
940
- }
941
- try {
942
- styleSource = resolver.resolve(`${packageName}/style.css`);
910
+ scriptSource = resolver.resolve("@aitty/snapshot/web-component.global.js");
911
+ styleSource = resolver.resolve("@aitty/snapshot/style.css");
943
912
  } catch {
944
913
  return null;
945
914
  }
946
915
  return {
947
916
  scriptSource,
948
- scriptType,
949
917
  styleSource
950
918
  };
951
919
  }
@@ -960,13 +928,12 @@ function relativeHref(fromPath, targetPath, artifactsDir) {
960
928
  function resolveAittyPreviewAssets(previewPath, assets, artifactsDir) {
961
929
  return {
962
930
  scriptHref: relativeHref(previewPath, assets.scriptPath, artifactsDir),
963
- scriptType: assets.scriptType,
964
931
  styleHref: relativeHref(previewPath, assets.stylePath, artifactsDir)
965
932
  };
966
933
  }
967
934
  function renderAittyPreviewAssetTags(assets) {
968
935
  return ` <link rel="stylesheet" href="${escapeAttribute(assets.styleHref)}" />
969
- <script${assets.scriptType === "module" ? " type=\"module\"" : ""} src="${escapeAttribute(assets.scriptHref)}"><\/script>
936
+ <script src="${escapeAttribute(assets.scriptHref)}"><\/script>
970
937
  `;
971
938
  }
972
939
  function renderAittyPreviewBody(args) {
@@ -1001,6 +968,7 @@ function renderAittyPreviewCss(viewOptions) {
1001
968
  //#region src/agent/report_artifact_paths.ts
1002
969
  function artifactViewerPath(artifact) {
1003
970
  if (artifact.kind === "terminal") return artifact.path.endsWith(".terminal.txt") ? artifact.path.replace(/\.terminal\.txt$/, ".terminal.viewer.html") : `${artifact.path}.viewer.html`;
971
+ if (artifact.kind === "layout") return artifact.path.endsWith(".layout.txt") ? artifact.path.replace(/\.layout\.txt$/, ".layout.viewer.html") : `${artifact.path}.viewer.html`;
1004
972
  if (artifact.kind === "dom") return artifact.path.endsWith(".dom.html") ? artifact.path.replace(/\.dom\.html$/, ".dom.viewer.html") : `${artifact.path}.viewer.html`;
1005
973
  return null;
1006
974
  }
@@ -1546,6 +1514,28 @@ function normalizeStringArray(value) {
1546
1514
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1547
1515
  }
1548
1516
  //#endregion
1517
+ //#region src/agent/report_stable_frame_config.ts
1518
+ function resolveStableFrameConfig(config, flowName) {
1519
+ const stableFrames = config?.agent?.report?.stableFrames;
1520
+ const flowConfig = stableFrames?.flows?.[flowName];
1521
+ return {
1522
+ ...stableFrames,
1523
+ ...flowConfig,
1524
+ previewSource: flowConfig?.previewSource ?? stableFrames?.previewSource ?? "captured-dom",
1525
+ stableMs: flowConfig?.stableMs ?? stableFrames?.stableMs ?? 200,
1526
+ theme: flowConfig?.theme ?? stableFrames?.theme ?? "dark",
1527
+ viewportOnly: flowConfig?.viewportOnly ?? stableFrames?.viewportOnly ?? false,
1528
+ viewportTargets: {
1529
+ ...stableFrames?.viewportTargets,
1530
+ ...flowConfig?.viewportTargets
1531
+ }
1532
+ };
1533
+ }
1534
+ function shouldUsePtyReplayStableFrameDomPreview(args) {
1535
+ const config = resolveStableFrameConfig(args.config, args.flowName);
1536
+ return config.enabled !== false && !config.skip && config.previewSource === "pty-replay";
1537
+ }
1538
+ //#endregion
1549
1539
  //#region src/agent/report_view_options.ts
1550
1540
  function isMobileViewport(viewport) {
1551
1541
  return Boolean(viewport?.isMobile || viewport?.hasTouch || (viewport?.width ?? 9999) <= 720);
@@ -1557,8 +1547,11 @@ function resolveReportViewOptions(result, config) {
1557
1547
  const themeArg = readFlagValueFromArgSets(launchArgSets, "--theme");
1558
1548
  const fontSizeArg = readFlagValueFromArgSets(launchArgSets, "--font-size");
1559
1549
  const lineHeightArg = readFlagValueFromArgSets(launchArgSets, "--line-height");
1560
- const stableFrameConfig = resolveStableFrameConfig$1(config, result.name);
1561
- const themeOverride = ptyReplayArg && stableFrameConfig.enabled !== false && !stableFrameConfig.skip ? stableFrameConfig.theme : void 0;
1550
+ const stableFrameConfig = resolveStableFrameConfig(config, result.name);
1551
+ const themeOverride = ptyReplayArg && shouldUsePtyReplayStableFrameDomPreview({
1552
+ config,
1553
+ flowName: result.name
1554
+ }) ? stableFrameConfig.theme : void 0;
1562
1555
  return {
1563
1556
  fontSize: parsePositiveNumber(fontSizeArg) ?? 15,
1564
1557
  lineHeight: parsePositiveNumber(lineHeightArg) ?? 1.6,
@@ -1571,15 +1564,6 @@ function parsePositiveNumber(value) {
1571
1564
  const parsed = Number(value);
1572
1565
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
1573
1566
  }
1574
- function resolveStableFrameConfig$1(config, flowName) {
1575
- const stableFrames = config?.agent?.report?.stableFrames;
1576
- const flowConfig = stableFrames?.flows?.[flowName];
1577
- return {
1578
- enabled: flowConfig?.enabled ?? stableFrames?.enabled,
1579
- skip: flowConfig?.skip ?? stableFrames?.skip,
1580
- theme: flowConfig?.theme ?? stableFrames?.theme ?? "dark"
1581
- };
1582
- }
1583
1567
  //#endregion
1584
1568
  //#region src/agent/report_artifact_viewer.ts
1585
1569
  function renderArtifactViewerHtml(args) {
@@ -1667,21 +1651,6 @@ ${body}
1667
1651
  </body>
1668
1652
  </html>`;
1669
1653
  }
1670
- function resolveStableFrameConfig(config, flowName) {
1671
- const stableFrames = config?.agent?.report?.stableFrames;
1672
- const flowConfig = stableFrames?.flows?.[flowName];
1673
- return {
1674
- ...stableFrames,
1675
- ...flowConfig,
1676
- stableMs: flowConfig?.stableMs ?? stableFrames?.stableMs ?? 200,
1677
- theme: flowConfig?.theme ?? stableFrames?.theme ?? "dark",
1678
- viewportOnly: flowConfig?.viewportOnly ?? stableFrames?.viewportOnly ?? false,
1679
- viewportTargets: {
1680
- ...stableFrames?.viewportTargets,
1681
- ...flowConfig?.viewportTargets
1682
- }
1683
- };
1684
- }
1685
1654
  function resolvePtyReplayPath(replayPathArg, config) {
1686
1655
  if (isAbsolute(replayPathArg)) return replayPathArg;
1687
1656
  return resolve(config?.rootDir ?? process.cwd(), replayPathArg);
@@ -1922,27 +1891,61 @@ function isCodeLikeText(text) {
1922
1891
  return /[{}()[\];=<>]/.test(text) || /\b(?:async|await|class|const|def|export|from|function|if|import|interface|let|return|type|var)\b/.test(text) || /(?:^|\s)(?:--?[a-z][\w-]*|#[\w-]+|\/[A-Za-z0-9._/-]+)(?:\s|$)/.test(text);
1923
1892
  }
1924
1893
  function wrapStableLogicalLine(line, targetCols) {
1925
- if (cellsDisplayWidth(line.cells) <= targetCols) return [line];
1894
+ const cols = Math.max(1, targetCols);
1895
+ if (cellsDisplayWidth(line.cells) <= cols) return [line];
1926
1896
  const rows = [];
1927
- let cells = [];
1928
- let cols = 0;
1929
- const flush = () => {
1897
+ let remaining = [...line.cells];
1898
+ while (remaining.length > 0) {
1899
+ const chunk = [];
1900
+ let usedCols = 0;
1901
+ let consumed = 0;
1902
+ for (const cell of remaining) {
1903
+ if (chunk.length > 0 && usedCols + cell.width > cols) break;
1904
+ chunk.push(cell);
1905
+ usedCols += cell.width;
1906
+ consumed += 1;
1907
+ if (usedCols >= cols) break;
1908
+ }
1909
+ if (chunk.length === 0) {
1910
+ chunk.push(remaining[0] ?? {
1911
+ style: DEFAULT_STYLE,
1912
+ text: "",
1913
+ width: 1
1914
+ });
1915
+ consumed = 1;
1916
+ }
1917
+ let rowCells = chunk;
1918
+ let nextRemaining = remaining.slice(consumed);
1919
+ const breakIndex = findStableLineBreakIndex(chunk);
1920
+ if (breakIndex > 0) {
1921
+ rowCells = chunk.slice(0, breakIndex);
1922
+ nextRemaining = [...chunk.slice(breakIndex + 1), ...nextRemaining];
1923
+ }
1930
1924
  rows.push({
1931
- cells,
1925
+ cells: trimTrailingStableCells(rowCells),
1932
1926
  live: line.live,
1933
1927
  physicalRows: 1
1934
1928
  });
1935
- cells = [];
1936
- cols = 0;
1937
- };
1938
- for (const cell of line.cells) {
1939
- if (cols > 0 && cols + cell.width > targetCols) flush();
1940
- cells.push(cell);
1941
- cols += cell.width;
1942
- if (cols >= targetCols) flush();
1929
+ remaining = dropLeadingStableSpaces(nextRemaining);
1930
+ }
1931
+ return rows.length > 0 ? rows : [line];
1932
+ }
1933
+ function findStableLineBreakIndex(cells) {
1934
+ for (let index = cells.length - 2; index > 0; index -= 1) {
1935
+ const cell = cells[index];
1936
+ if (cell?.text === " " && cell.width === 1) return index;
1943
1937
  }
1944
- if (cells.length > 0 || rows.length === 0) flush();
1945
- return rows;
1938
+ return -1;
1939
+ }
1940
+ function trimTrailingStableCells(cells) {
1941
+ let end = cells.length;
1942
+ while (end > 0 && cells[end - 1]?.text === " " && cells[end - 1]?.width === 1) end -= 1;
1943
+ return cells.slice(0, end);
1944
+ }
1945
+ function dropLeadingStableSpaces(cells) {
1946
+ let start = 0;
1947
+ while (start < cells.length && cells[start]?.text === " " && cells[start]?.width === 1) start += 1;
1948
+ return cells.slice(start);
1946
1949
  }
1947
1950
  function renderStableFrameRow(line, lineCols, lineIndex) {
1948
1951
  return `<div class="${line.live ? "term-row" : "term-row term-scrollback-row"}"${line.live ? ` data-aitty-live-grid-row="${lineIndex + 1}"` : ""} data-aitty-line-cols="${lineCols}">${renderStableFrameCells(line.cells, lineCols)}</div>`;
@@ -2110,13 +2113,13 @@ async function writeArtifactViewerPages(result, options = {}) {
2110
2113
  });
2111
2114
  }
2112
2115
  const domPreviewPathsBySnapshot = new Map(readableArtifacts.filter(({ artifact }) => artifact.kind === "dom").map(({ artifact }) => [artifactSnapshotKey(artifact), artifactDomPreviewPath(artifact)]));
2113
- const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({
2114
- artifactsDir: result.artifactsDir,
2115
- flowPath: result.flowPath,
2116
- reportPath: result.reportPath
2117
- }) : null;
2116
+ const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({ artifactsDir: result.artifactsDir }) : null;
2118
2117
  const writtenDomPreviewPaths = /* @__PURE__ */ new Set();
2119
- const ptyReplayStableFrame = aittyAssets ? await extractPtyReplayStableFrameForReport({
2118
+ const usePtyReplayStableFrameDomPreview = shouldUsePtyReplayStableFrameDomPreview({
2119
+ config: options.config,
2120
+ flowName: result.name
2121
+ });
2122
+ const ptyReplayStableFrame = aittyAssets && usePtyReplayStableFrameDomPreview ? await extractPtyReplayStableFrameForReport({
2120
2123
  config: options.config,
2121
2124
  result
2122
2125
  }) : null;
@@ -2213,6 +2216,7 @@ function renderAgentReportArtifactsCss() {
2213
2216
  return ` .artifacts {
2214
2217
  display: grid;
2215
2218
  gap: 14px;
2219
+ min-width: 0;
2216
2220
  }
2217
2221
  .artifact {
2218
2222
  display: grid;
@@ -2221,12 +2225,14 @@ function renderAgentReportArtifactsCss() {
2221
2225
  border-radius: 8px;
2222
2226
  padding: 12px;
2223
2227
  background: var(--panel);
2228
+ min-width: 0;
2224
2229
  }
2225
2230
  .artifact-summary {
2226
2231
  display: grid;
2227
- grid-template-columns: auto minmax(0, 1fr) minmax(96px, auto);
2232
+ grid-template-columns: auto minmax(0, 1fr) minmax(0, auto);
2228
2233
  gap: 14px;
2229
2234
  align-items: start;
2235
+ min-width: 0;
2230
2236
  }
2231
2237
  .artifact-meta {
2232
2238
  display: grid;
@@ -2237,19 +2243,23 @@ function renderAgentReportArtifactsCss() {
2237
2243
  display: flex;
2238
2244
  flex-wrap: wrap;
2239
2245
  gap: 8px 12px;
2246
+ min-width: 0;
2240
2247
  }
2241
2248
  .artifact a {
2242
2249
  color: var(--focus);
2243
2250
  font-weight: 700;
2244
2251
  text-decoration: none;
2252
+ overflow-wrap: anywhere;
2245
2253
  }
2246
2254
  .artifact code {
2247
2255
  color: var(--muted);
2248
2256
  overflow-wrap: anywhere;
2257
+ min-width: 0;
2249
2258
  }
2250
2259
  .artifact-hash {
2251
2260
  justify-self: end;
2252
2261
  text-align: right;
2262
+ max-width: 100%;
2253
2263
  }
2254
2264
  .badge {
2255
2265
  justify-self: start;
@@ -2304,6 +2314,7 @@ function renderAgentReportBaseCss() {
2304
2314
  margin: 0;
2305
2315
  background: var(--bg);
2306
2316
  color: var(--ink);
2317
+ overflow-wrap: anywhere;
2307
2318
  }
2308
2319
  main {
2309
2320
  display: grid;
@@ -2311,6 +2322,10 @@ function renderAgentReportBaseCss() {
2311
2322
  width: min(1180px, calc(100vw - 32px));
2312
2323
  margin: 0 auto;
2313
2324
  padding: 32px 0 48px;
2325
+ min-width: 0;
2326
+ }
2327
+ main > * {
2328
+ min-width: 0;
2314
2329
  }
2315
2330
  header {
2316
2331
  display: grid;
@@ -2319,27 +2334,37 @@ function renderAgentReportBaseCss() {
2319
2334
  align-items: start;
2320
2335
  border-bottom: 1px solid var(--line);
2321
2336
  padding-bottom: 24px;
2337
+ min-width: 0;
2338
+ }
2339
+ header > * {
2340
+ min-width: 0;
2322
2341
  }
2323
2342
  h1 {
2324
2343
  margin: 0;
2325
2344
  font-size: 28px;
2326
2345
  line-height: 1.15;
2327
2346
  letter-spacing: 0;
2347
+ max-width: 100%;
2348
+ overflow-wrap: anywhere;
2328
2349
  }
2329
2350
  h2 {
2330
2351
  margin: 0;
2331
2352
  font-size: 18px;
2332
2353
  line-height: 1.25;
2354
+ overflow-wrap: anywhere;
2333
2355
  }
2334
2356
  .meta {
2335
2357
  display: flex;
2336
2358
  flex-wrap: wrap;
2337
2359
  gap: 8px;
2338
2360
  margin-top: 12px;
2361
+ min-width: 0;
2339
2362
  }
2340
2363
  .pill,
2341
2364
  .status {
2342
2365
  display: inline-flex;
2366
+ max-width: 100%;
2367
+ min-width: 0;
2343
2368
  min-height: 32px;
2344
2369
  align-items: center;
2345
2370
  border: 1px solid var(--line);
@@ -2347,6 +2372,7 @@ function renderAgentReportBaseCss() {
2347
2372
  padding: 0 12px;
2348
2373
  color: var(--muted);
2349
2374
  font-size: 13px;
2375
+ overflow-wrap: anywhere;
2350
2376
  }
2351
2377
  .status {
2352
2378
  font-weight: 700;
@@ -2366,6 +2392,14 @@ function renderAgentReportBaseCss() {
2366
2392
  border-radius: 8px;
2367
2393
  background: var(--panel);
2368
2394
  padding: 18px;
2395
+ min-width: 0;
2396
+ }
2397
+ .panel > * {
2398
+ min-width: 0;
2399
+ }
2400
+ .panel p {
2401
+ margin: 0;
2402
+ max-width: 100%;
2369
2403
  }
2370
2404
  @media (max-width: 720px) {
2371
2405
  main {
@@ -2386,15 +2420,18 @@ function renderAgentReportCommandsCss() {
2386
2420
  return ` .commands {
2387
2421
  display: grid;
2388
2422
  gap: 10px;
2423
+ min-width: 0;
2389
2424
  }
2390
2425
  .command {
2391
2426
  display: grid;
2392
2427
  gap: 5px;
2428
+ min-width: 0;
2393
2429
  }
2394
2430
  .command span {
2395
2431
  color: var(--muted);
2396
2432
  font-size: 13px;
2397
2433
  font-weight: 700;
2434
+ overflow-wrap: anywhere;
2398
2435
  }
2399
2436
  .artifact code,
2400
2437
  pre {
@@ -2402,6 +2439,8 @@ function renderAgentReportCommandsCss() {
2402
2439
  }
2403
2440
  pre {
2404
2441
  overflow: auto;
2442
+ max-width: 100%;
2443
+ min-width: 0;
2405
2444
  margin: 0;
2406
2445
  border-radius: 8px;
2407
2446
  background: oklch(20% 0.015 230);
@@ -2415,25 +2454,29 @@ function renderAgentReportCommandsCss() {
2415
2454
  function renderAgentReportSummaryCss() {
2416
2455
  return ` .summary {
2417
2456
  display: grid;
2418
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2457
+ grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
2419
2458
  gap: 12px;
2459
+ min-width: 0;
2420
2460
  }
2421
2461
  .metric {
2422
2462
  border: 1px solid var(--line);
2423
2463
  border-radius: 8px;
2424
2464
  padding: 14px;
2425
2465
  background: color-mix(in oklch, var(--panel) 82%, var(--bg));
2466
+ min-width: 0;
2426
2467
  }
2427
2468
  .metric strong {
2428
2469
  display: block;
2429
2470
  font-size: 24px;
2430
2471
  line-height: 1.1;
2472
+ overflow-wrap: anywhere;
2431
2473
  }
2432
2474
  .metric span {
2433
2475
  display: block;
2434
2476
  margin-top: 4px;
2435
2477
  color: var(--muted);
2436
2478
  font-size: 13px;
2479
+ overflow-wrap: anywhere;
2437
2480
  }`;
2438
2481
  }
2439
2482
  //#endregion
@@ -2909,15 +2952,101 @@ async function waitForStableDom(page, args) {
2909
2952
  throw new Error(`timed out waiting for stable terminal DOM`);
2910
2953
  }
2911
2954
  async function readTerminalText(page) {
2912
- const text = await page.evaluate(() => {
2955
+ const text = await readTerminalProjection(page, "text");
2956
+ if (text === null) throw new Error("terminal root is not attached");
2957
+ return text;
2958
+ }
2959
+ async function readTerminalLayout(page) {
2960
+ const layout = await readTerminalProjection(page, "layout");
2961
+ if (layout === null) throw new Error("terminal root is not attached");
2962
+ return layout;
2963
+ }
2964
+ async function readTerminalProjection(page, mode) {
2965
+ return page.evaluate((projectionMode) => {
2913
2966
  const node = document.querySelector("[data-terminal-root]");
2914
2967
  if (!node) return null;
2915
2968
  const rows = Array.from(node.querySelectorAll(".term-grid .term-row"));
2916
- if (rows.length > 0) return rows.map((row) => row.textContent ?? "").join("\n");
2917
- return node.textContent ?? "";
2918
- });
2919
- if (text === null) throw new Error("terminal root is not attached");
2920
- return text;
2969
+ if (projectionMode === "text") {
2970
+ if (rows.length > 0) return rows.map((row) => serializeTerminalRowText(row)).join("\n");
2971
+ return node.textContent ?? "";
2972
+ }
2973
+ return serializeTerminalLayout(node);
2974
+ function serializeTerminalLayout(root) {
2975
+ const grid = root.querySelector(".term-grid");
2976
+ const target = grid ?? root;
2977
+ const lines = [`# terminal-layout v1 cols=${readElementInteger(grid, "data-cols") ?? "unknown"} rows=${readElementInteger(grid, "data-rows") ?? "unknown"}`];
2978
+ for (const child of Array.from(target.children)) serializeTerminalLayoutElement(child, "", lines);
2979
+ return lines.join("\n");
2980
+ }
2981
+ function serializeTerminalLayoutElement(element, indent, lines) {
2982
+ if (element.classList.contains("term-wide-row-block")) {
2983
+ lines.push(`${indent}wide-block kind=${JSON.stringify(readWideBlockKind(element))} cols=${readWideBlockCols(element) ?? "unknown"}`);
2984
+ for (const child of Array.from(element.children)) serializeTerminalLayoutElement(child, `${indent} `, lines);
2985
+ lines.push(`${indent}end-wide-block`);
2986
+ return;
2987
+ }
2988
+ if (element.classList.contains("term-row")) {
2989
+ lines.push(`${indent}row cols=${readElementInteger(element, "data-aitty-line-cols") ?? "unknown"} text=${JSON.stringify(serializeTerminalRowText(element))}`);
2990
+ return;
2991
+ }
2992
+ for (const child of Array.from(element.children)) serializeTerminalLayoutElement(child, indent, lines);
2993
+ }
2994
+ function readWideBlockKind(element) {
2995
+ if (!(element instanceof HTMLElement)) return "wide";
2996
+ return element.dataset.aittyWideBlockKind || "wide";
2997
+ }
2998
+ function readWideBlockCols(element) {
2999
+ if (!(element instanceof HTMLElement)) return null;
3000
+ const styleCols = element.style.getPropertyValue("--aitty-wide-block-cols").trim();
3001
+ const cols = Number.parseInt(styleCols, 10);
3002
+ return Number.isFinite(cols) && cols > 0 ? cols : null;
3003
+ }
3004
+ function readElementInteger(element, attribute) {
3005
+ const raw = element?.getAttribute(attribute) ?? "";
3006
+ const value = Number.parseInt(raw, 10);
3007
+ return Number.isFinite(value) && value >= 0 ? value : null;
3008
+ }
3009
+ function serializeTerminalRowText(row) {
3010
+ let text = "";
3011
+ for (const child of Array.from(row.childNodes)) text += serializeTerminalNodeText(child);
3012
+ return text.trimEnd();
3013
+ }
3014
+ function serializeTerminalNodeText(node) {
3015
+ if (node.nodeType === Node.TEXT_NODE) {
3016
+ const text = normalizeTerminalTextSegment(node.textContent ?? "");
3017
+ return text.trim().length === 0 ? "" : text;
3018
+ }
3019
+ if (!(node instanceof HTMLElement)) return "";
3020
+ const text = normalizeTerminalTextSegment(node.textContent ?? "");
3021
+ const cols = parseTerminalCellWidth(node);
3022
+ if (cols === null) return text;
3023
+ const width = terminalTextDisplayWidth(text);
3024
+ const padding = Math.max(0, cols - width);
3025
+ return text + " ".repeat(padding);
3026
+ }
3027
+ function normalizeTerminalTextSegment(text) {
3028
+ return text.replace(/\u00a0/g, " ");
3029
+ }
3030
+ function parseTerminalCellWidth(element) {
3031
+ const width = element.style.getPropertyValue("width");
3032
+ if (/^var\(--term-cell-width\b/.test(width)) return 1;
3033
+ const calcMatch = /calc\(\s*var\(--term-cell-width[^)]*\)\s*\*\s*(\d+(?:\.\d+)?)\s*\)/.exec(width);
3034
+ if (!calcMatch) return null;
3035
+ const cols = Number.parseFloat(calcMatch[1] ?? "");
3036
+ return Number.isFinite(cols) && cols > 0 ? Math.round(cols) : null;
3037
+ }
3038
+ function terminalTextDisplayWidth(text) {
3039
+ let width = 0;
3040
+ for (const char of text) {
3041
+ if (/[\u0300-\u036f]/u.test(char)) continue;
3042
+ width += isWideTerminalChar(char) ? 2 : 1;
3043
+ }
3044
+ return width;
3045
+ }
3046
+ function isWideTerminalChar(char) {
3047
+ return /[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/u.test(char) || /\p{Extended_Pictographic}/u.test(char);
3048
+ }
3049
+ }, mode);
2921
3050
  }
2922
3051
  async function readTerminalDom(page) {
2923
3052
  const dom = await readTerminalDomIfPresent(page);
@@ -2964,6 +3093,19 @@ async function captureSnapshotStep(ctx, step) {
2964
3093
  });
2965
3094
  continue;
2966
3095
  }
3096
+ if (target === "layout") {
3097
+ const layout = normalizeTerminalText(await readTerminalLayout(ctx.page), ctx.masks);
3098
+ await writeComparableArtifact(ctx, {
3099
+ name: step.name,
3100
+ kind: "layout",
3101
+ relativePath: `${base}.layout.txt`,
3102
+ baselineRelativePath: `${base}.layout.snap.txt`,
3103
+ diffRelativePath: `${base}.layout.diff.txt`,
3104
+ content: layout + "\n",
3105
+ compare: step.compare ?? true
3106
+ });
3107
+ continue;
3108
+ }
2967
3109
  const screenshotPath = join(ctx.artifactsDir, `${base}.png`);
2968
3110
  await ctx.page.screenshot({
2969
3111
  path: screenshotPath,
@@ -3123,7 +3265,7 @@ async function captureCassetteFrame(ctx, frame) {
3123
3265
  stepIndex: frame.stepIndex,
3124
3266
  stepType: frame.stepType,
3125
3267
  terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
3126
- dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
3268
+ dom: normalizeReplayDom(await readTerminalDom(ctx.page), ctx.masks),
3127
3269
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3128
3270
  });
3129
3271
  }
@@ -144,7 +144,7 @@ var ScriptRecordingManager = class {
144
144
  };
145
145
  //#endregion
146
146
  //#region package.json
147
- var version = "0.6.2";
147
+ var version = "0.6.4";
148
148
  //#endregion
149
149
  //#region src/mcp/tool_result.ts
150
150
  function toolError(message, extra = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ptywright",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Terminal/TUI automation driver over PTY + xterm, exposed as MCP tools",
5
5
  "keywords": [
6
6
  "agent",
@@ -93,6 +93,7 @@
93
93
  "report",
94
94
  "terminal",
95
95
  "dom",
96
+ "layout",
96
97
  "screenshot",
97
98
  "diff"
98
99
  ]
@@ -129,7 +129,7 @@
129
129
  "properties": {
130
130
  "name": { "type": "string", "minLength": 1 },
131
131
  "viewport": { "type": "string", "minLength": 1 },
132
- "kind": { "type": "string", "enum": ["terminal", "dom", "screenshot"] },
132
+ "kind": { "type": "string", "enum": ["terminal", "dom", "layout", "screenshot"] },
133
133
  "path": { "type": "string", "minLength": 1 },
134
134
  "baselinePath": { "type": "string", "minLength": 1 },
135
135
  "diffPath": { "type": "string", "minLength": 1 },
@@ -113,7 +113,7 @@
113
113
  "properties": {
114
114
  "name": { "type": "string", "minLength": 1 },
115
115
  "viewport": { "type": "string", "minLength": 1 },
116
- "kind": { "type": "string", "enum": ["terminal", "dom", "screenshot"] },
116
+ "kind": { "type": "string", "enum": ["terminal", "dom", "layout", "screenshot"] },
117
117
  "path": { "type": "string", "minLength": 1 },
118
118
  "baselinePath": { "type": "string", "minLength": 1 },
119
119
  "diffPath": { "type": "string", "minLength": 1 },
@@ -123,7 +123,7 @@
123
123
  "compare": { "type": "boolean" },
124
124
  "targets": {
125
125
  "type": "array",
126
- "items": { "type": "string", "enum": ["terminal", "dom", "screenshot"] }
126
+ "items": { "type": "string", "enum": ["terminal", "dom", "layout", "screenshot"] }
127
127
  },
128
128
  "fullPage": { "type": "boolean" }
129
129
  }