ptywright 0.6.1 → 0.6.3

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-XEimk7TO.mjs";
1
+ import { a as formatAgentLaunchPlan, i as runAgentSpecPath, n as replayAgentRecordPath, r as runAgentSpec, t as defaultSpecNameForPath } from "./runner-RhWYhus1.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-XLR4BPhA.mjs";
2
+ import { t as main } from "../cli-uj8xaanE.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-XEimk7TO.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-RhWYhus1.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-COuf3mW7.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-B4Bbuluz.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-XLR4BPhA.mjs";
1
+ import { t as main } from "./cli-uj8xaanE.mjs";
2
2
  export { main };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createPtywrightServer } from "./server-COuf3mW7.mjs";
1
+ import { t as createPtywrightServer } from "./server-B4Bbuluz.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-COuf3mW7.mjs";
1
+ import { t as createPtywrightServer } from "./server-B4Bbuluz.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);
@@ -1865,29 +1834,50 @@ function renderStableFrameDom(frame, targetCols) {
1865
1834
  let html = "";
1866
1835
  let totalRows = 0;
1867
1836
  let wideBlockId = 0;
1868
- for (const line of frame.lines) {
1869
- const lineCols = cellsDisplayWidth(line.cells);
1870
- if (shouldRenderViewportPan(line, lineCols, targetCols)) {
1871
- wideBlockId += 1;
1872
- const blockCols = Math.max(targetCols, lineCols);
1873
- html += `<div class="term-wide-row-block" data-aitty-wide-block="true" data-aitty-wide-block-id="${wideBlockId}" data-aitty-wide-block-kind="viewport-pan" style="--aitty-wide-block-cols: ${blockCols}">`;
1874
- html += renderStableFrameRow(line, blockCols, totalRows);
1875
- html += "</div>";
1876
- totalRows += 1;
1877
- } else for (const row of wrapStableLogicalLine(line, targetCols)) {
1837
+ let codeRunCols = 0;
1838
+ let codeRunRows = [];
1839
+ const renderPlainRow = (line) => {
1840
+ for (const row of wrapStableLogicalLine(line, targetCols)) {
1878
1841
  html += renderStableFrameRow(row, targetCols, totalRows);
1879
1842
  totalRows += 1;
1880
1843
  }
1844
+ };
1845
+ const flushCodeRun = () => {
1846
+ if (codeRunRows.length === 0) return;
1847
+ if (codeRunCols > targetCols) {
1848
+ wideBlockId += 1;
1849
+ const blockCols = Math.max(targetCols, codeRunCols);
1850
+ html += `<div class="term-wide-row-block" data-aitty-wide-block="true" data-aitty-wide-block-id="${wideBlockId}" data-aitty-wide-block-kind="guttered-code" style="--aitty-wide-block-cols: ${blockCols}">`;
1851
+ for (const row of codeRunRows) {
1852
+ html += renderStableFrameRow(row, blockCols, totalRows);
1853
+ totalRows += 1;
1854
+ }
1855
+ html += "</div>";
1856
+ } else for (const row of codeRunRows) renderPlainRow(row);
1857
+ codeRunCols = 0;
1858
+ codeRunRows = [];
1859
+ };
1860
+ for (const line of frame.lines) {
1861
+ const lineText = stableLineText(line);
1862
+ const lineCols = cellsDisplayWidth(line.cells);
1863
+ if (isGutteredCodeLine(lineText) || isDiffLikeLine(lineText)) {
1864
+ codeRunRows.push(line);
1865
+ codeRunCols = Math.max(codeRunCols, lineCols);
1866
+ continue;
1867
+ }
1868
+ flushCodeRun();
1869
+ renderPlainRow(line);
1881
1870
  }
1871
+ flushCodeRun();
1882
1872
  return `<div class="term-grid" data-cols="${targetCols}" data-rows="${totalRows}" style="--term-cols: ${targetCols}; --term-rows: ${totalRows};">${html}</div>`;
1883
1873
  }
1884
- function shouldRenderViewportPan(line, lineCols, targetCols) {
1885
- if (lineCols <= targetCols) return false;
1886
- return isDiffLikeLine(stableLineText(line));
1887
- }
1888
1874
  function stableLineText(line) {
1889
1875
  return line.cells.map((cell) => cell.text).join("").trimEnd();
1890
1876
  }
1877
+ function isGutteredCodeLine(text) {
1878
+ const normalized = text.replace(/[│┃┆┊▏▕]/g, " ").trimStart();
1879
+ return /^\d+\s+(?:[+-]|\s{2,}\S)/.test(normalized);
1880
+ }
1891
1881
  function isDiffLikeLine(text) {
1892
1882
  const normalized = text.replace(/[│┃┆┊▏▕]/g, " ").trimStart();
1893
1883
  if (normalized === "") return false;
@@ -1901,27 +1891,61 @@ function isCodeLikeText(text) {
1901
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);
1902
1892
  }
1903
1893
  function wrapStableLogicalLine(line, targetCols) {
1904
- if (cellsDisplayWidth(line.cells) <= targetCols) return [line];
1894
+ const cols = Math.max(1, targetCols);
1895
+ if (cellsDisplayWidth(line.cells) <= cols) return [line];
1905
1896
  const rows = [];
1906
- let cells = [];
1907
- let cols = 0;
1908
- 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
+ }
1909
1924
  rows.push({
1910
- cells,
1925
+ cells: trimTrailingStableCells(rowCells),
1911
1926
  live: line.live,
1912
1927
  physicalRows: 1
1913
1928
  });
1914
- cells = [];
1915
- cols = 0;
1916
- };
1917
- for (const cell of line.cells) {
1918
- if (cols > 0 && cols + cell.width > targetCols) flush();
1919
- cells.push(cell);
1920
- cols += cell.width;
1921
- if (cols >= targetCols) flush();
1929
+ remaining = dropLeadingStableSpaces(nextRemaining);
1922
1930
  }
1923
- if (cells.length > 0 || rows.length === 0) flush();
1924
- return rows;
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;
1937
+ }
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);
1925
1949
  }
1926
1950
  function renderStableFrameRow(line, lineCols, lineIndex) {
1927
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>`;
@@ -2089,13 +2113,13 @@ async function writeArtifactViewerPages(result, options = {}) {
2089
2113
  });
2090
2114
  }
2091
2115
  const domPreviewPathsBySnapshot = new Map(readableArtifacts.filter(({ artifact }) => artifact.kind === "dom").map(({ artifact }) => [artifactSnapshotKey(artifact), artifactDomPreviewPath(artifact)]));
2092
- const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({
2093
- artifactsDir: result.artifactsDir,
2094
- flowPath: result.flowPath,
2095
- reportPath: result.reportPath
2096
- }) : null;
2116
+ const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({ artifactsDir: result.artifactsDir }) : null;
2097
2117
  const writtenDomPreviewPaths = /* @__PURE__ */ new Set();
2098
- 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({
2099
2123
  config: options.config,
2100
2124
  result
2101
2125
  }) : null;
@@ -2888,15 +2912,101 @@ async function waitForStableDom(page, args) {
2888
2912
  throw new Error(`timed out waiting for stable terminal DOM`);
2889
2913
  }
2890
2914
  async function readTerminalText(page) {
2891
- const text = await page.evaluate(() => {
2915
+ const text = await readTerminalProjection(page, "text");
2916
+ if (text === null) throw new Error("terminal root is not attached");
2917
+ return text;
2918
+ }
2919
+ async function readTerminalLayout(page) {
2920
+ const layout = await readTerminalProjection(page, "layout");
2921
+ if (layout === null) throw new Error("terminal root is not attached");
2922
+ return layout;
2923
+ }
2924
+ async function readTerminalProjection(page, mode) {
2925
+ return page.evaluate((projectionMode) => {
2892
2926
  const node = document.querySelector("[data-terminal-root]");
2893
2927
  if (!node) return null;
2894
2928
  const rows = Array.from(node.querySelectorAll(".term-grid .term-row"));
2895
- if (rows.length > 0) return rows.map((row) => row.textContent ?? "").join("\n");
2896
- return node.textContent ?? "";
2897
- });
2898
- if (text === null) throw new Error("terminal root is not attached");
2899
- return text;
2929
+ if (projectionMode === "text") {
2930
+ if (rows.length > 0) return rows.map((row) => serializeTerminalRowText(row)).join("\n");
2931
+ return node.textContent ?? "";
2932
+ }
2933
+ return serializeTerminalLayout(node);
2934
+ function serializeTerminalLayout(root) {
2935
+ const grid = root.querySelector(".term-grid");
2936
+ const target = grid ?? root;
2937
+ const lines = [`# terminal-layout v1 cols=${readElementInteger(grid, "data-cols") ?? "unknown"} rows=${readElementInteger(grid, "data-rows") ?? "unknown"}`];
2938
+ for (const child of Array.from(target.children)) serializeTerminalLayoutElement(child, "", lines);
2939
+ return lines.join("\n");
2940
+ }
2941
+ function serializeTerminalLayoutElement(element, indent, lines) {
2942
+ if (element.classList.contains("term-wide-row-block")) {
2943
+ lines.push(`${indent}wide-block kind=${JSON.stringify(readWideBlockKind(element))} cols=${readWideBlockCols(element) ?? "unknown"}`);
2944
+ for (const child of Array.from(element.children)) serializeTerminalLayoutElement(child, `${indent} `, lines);
2945
+ lines.push(`${indent}end-wide-block`);
2946
+ return;
2947
+ }
2948
+ if (element.classList.contains("term-row")) {
2949
+ lines.push(`${indent}row cols=${readElementInteger(element, "data-aitty-line-cols") ?? "unknown"} text=${JSON.stringify(serializeTerminalRowText(element))}`);
2950
+ return;
2951
+ }
2952
+ for (const child of Array.from(element.children)) serializeTerminalLayoutElement(child, indent, lines);
2953
+ }
2954
+ function readWideBlockKind(element) {
2955
+ if (!(element instanceof HTMLElement)) return "wide";
2956
+ return element.dataset.aittyWideBlockKind || "wide";
2957
+ }
2958
+ function readWideBlockCols(element) {
2959
+ if (!(element instanceof HTMLElement)) return null;
2960
+ const styleCols = element.style.getPropertyValue("--aitty-wide-block-cols").trim();
2961
+ const cols = Number.parseInt(styleCols, 10);
2962
+ return Number.isFinite(cols) && cols > 0 ? cols : null;
2963
+ }
2964
+ function readElementInteger(element, attribute) {
2965
+ const raw = element?.getAttribute(attribute) ?? "";
2966
+ const value = Number.parseInt(raw, 10);
2967
+ return Number.isFinite(value) && value >= 0 ? value : null;
2968
+ }
2969
+ function serializeTerminalRowText(row) {
2970
+ let text = "";
2971
+ for (const child of Array.from(row.childNodes)) text += serializeTerminalNodeText(child);
2972
+ return text.trimEnd();
2973
+ }
2974
+ function serializeTerminalNodeText(node) {
2975
+ if (node.nodeType === Node.TEXT_NODE) {
2976
+ const text = normalizeTerminalTextSegment(node.textContent ?? "");
2977
+ return text.trim().length === 0 ? "" : text;
2978
+ }
2979
+ if (!(node instanceof HTMLElement)) return "";
2980
+ const text = normalizeTerminalTextSegment(node.textContent ?? "");
2981
+ const cols = parseTerminalCellWidth(node);
2982
+ if (cols === null) return text;
2983
+ const width = terminalTextDisplayWidth(text);
2984
+ const padding = Math.max(0, cols - width);
2985
+ return text + " ".repeat(padding);
2986
+ }
2987
+ function normalizeTerminalTextSegment(text) {
2988
+ return text.replace(/\u00a0/g, " ");
2989
+ }
2990
+ function parseTerminalCellWidth(element) {
2991
+ const width = element.style.getPropertyValue("width");
2992
+ if (/^var\(--term-cell-width\b/.test(width)) return 1;
2993
+ const calcMatch = /calc\(\s*var\(--term-cell-width[^)]*\)\s*\*\s*(\d+(?:\.\d+)?)\s*\)/.exec(width);
2994
+ if (!calcMatch) return null;
2995
+ const cols = Number.parseFloat(calcMatch[1] ?? "");
2996
+ return Number.isFinite(cols) && cols > 0 ? Math.round(cols) : null;
2997
+ }
2998
+ function terminalTextDisplayWidth(text) {
2999
+ let width = 0;
3000
+ for (const char of text) {
3001
+ if (/[\u0300-\u036f]/u.test(char)) continue;
3002
+ width += isWideTerminalChar(char) ? 2 : 1;
3003
+ }
3004
+ return width;
3005
+ }
3006
+ function isWideTerminalChar(char) {
3007
+ 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);
3008
+ }
3009
+ }, mode);
2900
3010
  }
2901
3011
  async function readTerminalDom(page) {
2902
3012
  const dom = await readTerminalDomIfPresent(page);
@@ -2943,6 +3053,19 @@ async function captureSnapshotStep(ctx, step) {
2943
3053
  });
2944
3054
  continue;
2945
3055
  }
3056
+ if (target === "layout") {
3057
+ const layout = normalizeTerminalText(await readTerminalLayout(ctx.page), ctx.masks);
3058
+ await writeComparableArtifact(ctx, {
3059
+ name: step.name,
3060
+ kind: "layout",
3061
+ relativePath: `${base}.layout.txt`,
3062
+ baselineRelativePath: `${base}.layout.snap.txt`,
3063
+ diffRelativePath: `${base}.layout.diff.txt`,
3064
+ content: layout + "\n",
3065
+ compare: step.compare ?? true
3066
+ });
3067
+ continue;
3068
+ }
2946
3069
  const screenshotPath = join(ctx.artifactsDir, `${base}.png`);
2947
3070
  await ctx.page.screenshot({
2948
3071
  path: screenshotPath,
@@ -3102,7 +3225,7 @@ async function captureCassetteFrame(ctx, frame) {
3102
3225
  stepIndex: frame.stepIndex,
3103
3226
  stepType: frame.stepType,
3104
3227
  terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
3105
- dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
3228
+ dom: normalizeReplayDom(await readTerminalDom(ctx.page), ctx.masks),
3106
3229
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3107
3230
  });
3108
3231
  }
@@ -144,7 +144,7 @@ var ScriptRecordingManager = class {
144
144
  };
145
145
  //#endregion
146
146
  //#region package.json
147
- var version = "0.6.1";
147
+ var version = "0.6.3";
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.1",
3
+ "version": "0.6.3",
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
  }