libretto 0.6.11 → 0.6.12
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 +4 -0
- package/README.template.md +4 -0
- package/dist/cli/cli.js +4 -3
- package/dist/cli/commands/ai.js +3 -2
- package/dist/cli/commands/browser.js +17 -17
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +20 -34
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +81 -9
- package/dist/cli/commands/status.js +5 -4
- package/dist/cli/core/ai-model.js +6 -3
- package/dist/cli/core/browser.js +300 -121
- package/dist/cli/core/config.js +4 -2
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +535 -89
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +72 -6
- package/dist/cli/core/experiments.js +66 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/snapshot-analyzer.js +4 -3
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/router.js +4 -1
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/package.json +4 -2
- package/skills/libretto/SKILL.md +3 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +4 -3
- package/src/cli/commands/ai.ts +3 -2
- package/src/cli/commands/browser.ts +13 -11
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +18 -36
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +99 -11
- package/src/cli/commands/status.ts +5 -4
- package/src/cli/core/ai-model.ts +6 -3
- package/src/cli/core/browser.ts +369 -147
- package/src/cli/core/config.ts +3 -1
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +686 -106
- package/src/cli/core/daemon/ipc.ts +330 -214
- package/src/cli/core/daemon/snapshot.ts +106 -8
- package/src/cli/core/experiments.ts +85 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/snapshot-analyzer.ts +4 -3
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +85 -0
- package/src/cli/router.ts +4 -1
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { scopeSnapshotToRef } from "./capture-snapshot.js";
|
|
2
|
+
const MAX_CHILDREN_PER_PARENT = 4;
|
|
3
|
+
const MAX_LABEL_CHARS = 140;
|
|
4
|
+
const MAX_SUMMARY_TEXT_CHARS = 80;
|
|
5
|
+
const MAX_HREF_CHARS = 96;
|
|
6
|
+
const MAX_ACTIONS_IN_SUMMARY = 3;
|
|
7
|
+
const MAX_ACTION_LABEL_CHARS = 80;
|
|
8
|
+
const PRESERVE_CHILDREN_BY_ROLE = /* @__PURE__ */ new Set([
|
|
9
|
+
"document",
|
|
10
|
+
"main",
|
|
11
|
+
"navigation",
|
|
12
|
+
"banner",
|
|
13
|
+
"contentinfo",
|
|
14
|
+
"form",
|
|
15
|
+
"search",
|
|
16
|
+
"list",
|
|
17
|
+
"table",
|
|
18
|
+
"tabpanel"
|
|
19
|
+
]);
|
|
20
|
+
const FLATTEN_ROLES = /* @__PURE__ */ new Set([
|
|
21
|
+
"none",
|
|
22
|
+
"presentation",
|
|
23
|
+
"LayoutTable",
|
|
24
|
+
"LayoutTableRow",
|
|
25
|
+
"LayoutTableCell"
|
|
26
|
+
]);
|
|
27
|
+
const SKIP_ROLES = /* @__PURE__ */ new Set(["InlineTextBox", "ListMarker"]);
|
|
28
|
+
const ACTION_ROLES = /* @__PURE__ */ new Set([
|
|
29
|
+
"button",
|
|
30
|
+
"link",
|
|
31
|
+
"textbox",
|
|
32
|
+
"checkbox",
|
|
33
|
+
"radio",
|
|
34
|
+
"switch",
|
|
35
|
+
"combobox",
|
|
36
|
+
"listbox",
|
|
37
|
+
"menuitem",
|
|
38
|
+
"tab",
|
|
39
|
+
"slider"
|
|
40
|
+
]);
|
|
41
|
+
const ACTION_STATE_ATTRS = /* @__PURE__ */ new Set([
|
|
42
|
+
"checked",
|
|
43
|
+
"disabled",
|
|
44
|
+
"expanded",
|
|
45
|
+
"pressed",
|
|
46
|
+
"selected",
|
|
47
|
+
"value",
|
|
48
|
+
"placeholder"
|
|
49
|
+
]);
|
|
50
|
+
const TEXT_ACTION_ROLES = /* @__PURE__ */ new Set(["button", "link", "menuitem", "tab"]);
|
|
51
|
+
const KEEP_ROLES = /* @__PURE__ */ new Set([
|
|
52
|
+
"document",
|
|
53
|
+
"main",
|
|
54
|
+
"navigation",
|
|
55
|
+
"banner",
|
|
56
|
+
"contentinfo",
|
|
57
|
+
"form",
|
|
58
|
+
"search",
|
|
59
|
+
"list",
|
|
60
|
+
"listitem",
|
|
61
|
+
"button",
|
|
62
|
+
"link",
|
|
63
|
+
"image",
|
|
64
|
+
"textbox",
|
|
65
|
+
"checkbox",
|
|
66
|
+
"radio",
|
|
67
|
+
"switch",
|
|
68
|
+
"combobox",
|
|
69
|
+
"listbox",
|
|
70
|
+
"menu",
|
|
71
|
+
"menuitem",
|
|
72
|
+
"option",
|
|
73
|
+
"tab",
|
|
74
|
+
"slider"
|
|
75
|
+
]);
|
|
76
|
+
const BLOCK_FLATTEN_ROLES = /* @__PURE__ */ new Set([
|
|
77
|
+
"paragraph",
|
|
78
|
+
"section",
|
|
79
|
+
"article",
|
|
80
|
+
"region",
|
|
81
|
+
"group",
|
|
82
|
+
"figure"
|
|
83
|
+
]);
|
|
84
|
+
const RENDERED_STATE_PROPERTIES = [
|
|
85
|
+
"disabled",
|
|
86
|
+
"checked",
|
|
87
|
+
"expanded",
|
|
88
|
+
"selected",
|
|
89
|
+
"pressed",
|
|
90
|
+
"required",
|
|
91
|
+
"invalid",
|
|
92
|
+
"readonly",
|
|
93
|
+
"multiline",
|
|
94
|
+
"autocomplete",
|
|
95
|
+
"haspopup",
|
|
96
|
+
"value"
|
|
97
|
+
];
|
|
98
|
+
function renderSnapshot(snapshot, refId) {
|
|
99
|
+
const snapshotTree = refId ? scopeSnapshotToRef(snapshot, refId) : snapshot;
|
|
100
|
+
const lines = [renderPageOpen(snapshotTree, "")];
|
|
101
|
+
for (const frame of renderSnapshotFrames(snapshotTree)) {
|
|
102
|
+
renderFrame(frame, 1, lines);
|
|
103
|
+
}
|
|
104
|
+
lines.push("</page>");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function renderSnapshotFrames(snapshot) {
|
|
108
|
+
return snapshot.frames.map(toRenderedFrame).filter(hasRenderedFrameContent);
|
|
109
|
+
}
|
|
110
|
+
function renderPageOpen(snapshot, prefix, selfClosing = false) {
|
|
111
|
+
return `${prefix}${formatTag(
|
|
112
|
+
"page",
|
|
113
|
+
[
|
|
114
|
+
["title", firstNonEmpty(snapshot.title, snapshot.url) ?? ""],
|
|
115
|
+
["url", snapshot.url]
|
|
116
|
+
],
|
|
117
|
+
!selfClosing
|
|
118
|
+
)}`;
|
|
119
|
+
}
|
|
120
|
+
function renderFrameLine(frame, depth, prefix, selfClosing) {
|
|
121
|
+
const attrs = [
|
|
122
|
+
["index", String(frame.index)],
|
|
123
|
+
["url", normalizeText(frame.url, MAX_LABEL_CHARS)]
|
|
124
|
+
];
|
|
125
|
+
if (frame.name)
|
|
126
|
+
attrs.push(["name", normalizeText(frame.name, MAX_LABEL_CHARS)]);
|
|
127
|
+
if (frame.parentId) attrs.push(["parent", frame.parentId]);
|
|
128
|
+
if (frame.status === "unavailable") {
|
|
129
|
+
attrs.push(["error", normalizeText(frame.error, 180)]);
|
|
130
|
+
}
|
|
131
|
+
return `${prefix}${indent(depth)}${formatTag("frame", attrs, !selfClosing)}`;
|
|
132
|
+
}
|
|
133
|
+
function renderTextNode(node, depth, prefix) {
|
|
134
|
+
return `${prefix}${indent(depth)}${escapeText(node.text)}`;
|
|
135
|
+
}
|
|
136
|
+
function indent(depth) {
|
|
137
|
+
return " ".repeat(depth);
|
|
138
|
+
}
|
|
139
|
+
function formatTag(tagName, attributes, hasChildren) {
|
|
140
|
+
const attrs = attributes.filter(([, value]) => value !== "").map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
|
|
141
|
+
return hasChildren ? `<${tagName}${attrs}>` : `<${tagName}${attrs} />`;
|
|
142
|
+
}
|
|
143
|
+
function renderChildrenTruncationNotice(children) {
|
|
144
|
+
const count = children.length;
|
|
145
|
+
const summaryActions = actionSummariesForChildren(children);
|
|
146
|
+
const textSnippet = previewForChildren(children, summaryActions.labels);
|
|
147
|
+
const elementLabel = count === 1 ? "element" : "elements";
|
|
148
|
+
const textSnippetPart = textSnippet ? `. Text snippet: ${JSON.stringify(textSnippet)}` : "";
|
|
149
|
+
const interactiveText = summaryActions.actions.length ? `. Interactive elements: ${summaryActions.actions.map((action) => action.markup).join(", ")}${summaryActions.hasMore ? ", ..." : ""}` : "";
|
|
150
|
+
return `[Truncated ${count} more ${elementLabel}${textSnippetPart}${interactiveText}]`;
|
|
151
|
+
}
|
|
152
|
+
function toRenderedFrame(frame) {
|
|
153
|
+
if (frame.status === "unavailable") return frame;
|
|
154
|
+
return {
|
|
155
|
+
status: "ok",
|
|
156
|
+
id: frame.id,
|
|
157
|
+
index: frame.index,
|
|
158
|
+
url: frame.url,
|
|
159
|
+
name: frame.name,
|
|
160
|
+
parentId: frame.parentId,
|
|
161
|
+
roots: frame.roots.flatMap((root) => toRenderedNodes(root, null))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function hasRenderedFrameContent(frame) {
|
|
165
|
+
if (frame.status === "unavailable") return true;
|
|
166
|
+
return frame.roots.length > 0;
|
|
167
|
+
}
|
|
168
|
+
function toRenderedNodes(node, parent) {
|
|
169
|
+
return toRenderedChildren(node, parent).filter(isRenderedNode);
|
|
170
|
+
}
|
|
171
|
+
function toRenderedChildren(node, parent) {
|
|
172
|
+
if (shouldSkipNode(node, parent)) return [];
|
|
173
|
+
if (isTextRole(node.role)) {
|
|
174
|
+
const text2 = firstNonEmpty(
|
|
175
|
+
node.name,
|
|
176
|
+
node.description,
|
|
177
|
+
primitiveToString(node.value)
|
|
178
|
+
);
|
|
179
|
+
if (text2 && text2 !== parent?.name && text2 !== nodeTextValue(parent)) {
|
|
180
|
+
return [{ kind: "text", text: text2 }];
|
|
181
|
+
}
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const children = renderableChildren(node);
|
|
185
|
+
const role = tagNameForRole(node.role);
|
|
186
|
+
if (role === "heading") return renderHeading(node, children);
|
|
187
|
+
const compactRole = roleForNode(node, role, children);
|
|
188
|
+
if (compactRole === "image" && !hasNonEmptyAttribute(node, "src"))
|
|
189
|
+
return [];
|
|
190
|
+
if (compactRole === "link" && !hasNonEmptyAttribute(node, "href")) {
|
|
191
|
+
return flattenedChildren(node, children).filter(
|
|
192
|
+
hasVisibleTextOrInteractive
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (node.ignored || FLATTEN_ROLES.has(node.role) || !KEEP_ROLES.has(compactRole)) {
|
|
196
|
+
return flattenedChildren(node, children).filter(
|
|
197
|
+
hasVisibleTextOrInteractive
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const text = normalizedText(children);
|
|
201
|
+
const suppressName = text.includes(normalizeRawText(node.name ?? "")) ? node.name : null;
|
|
202
|
+
const attrs = nodeAttributes(node, suppressName);
|
|
203
|
+
const content = nameAttributeAsContent(attrs, children);
|
|
204
|
+
const renderedChildren = removeDuplicateNestedActions(
|
|
205
|
+
compactRole,
|
|
206
|
+
content.attrs,
|
|
207
|
+
content.children
|
|
208
|
+
).filter(hasVisibleTextOrInteractive);
|
|
209
|
+
if (!ACTION_ROLES.has(compactRole) && !renderedChildren.some(hasVisibleTextOrInteractive)) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const rendered = {
|
|
213
|
+
kind: "node",
|
|
214
|
+
key: node.nodeId || node.ref || `${compactRole}:${content.attrs.map(([name, value]) => `${name}=${value}`).join(";")}`,
|
|
215
|
+
role: compactRole,
|
|
216
|
+
attrs: content.attrs,
|
|
217
|
+
children: renderedChildren
|
|
218
|
+
};
|
|
219
|
+
return [rendered];
|
|
220
|
+
}
|
|
221
|
+
function renderableChildren(node) {
|
|
222
|
+
const children = [];
|
|
223
|
+
for (const child of node.children)
|
|
224
|
+
children.push(...toRenderedChildren(child, node));
|
|
225
|
+
return mergeAdjacentText(children).filter(hasVisibleTextOrInteractive);
|
|
226
|
+
}
|
|
227
|
+
function renderHeading(node, children) {
|
|
228
|
+
const text = firstNonEmpty(node.name, normalizedText(children));
|
|
229
|
+
if (!text) return [];
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
kind: "text",
|
|
233
|
+
text: `${"#".repeat(headingLevel(node))} ${text}`,
|
|
234
|
+
block: true
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
function headingLevel(node) {
|
|
239
|
+
const rawLevel = node.properties.level;
|
|
240
|
+
const level = typeof rawLevel === "number" ? rawLevel : Number(rawLevel);
|
|
241
|
+
if (!Number.isFinite(level)) return 2;
|
|
242
|
+
return Math.min(6, Math.max(1, Math.round(level)));
|
|
243
|
+
}
|
|
244
|
+
function roleForNode(node, role, children) {
|
|
245
|
+
if (isPointerButtonCandidate(node, role, children)) return "button";
|
|
246
|
+
return role;
|
|
247
|
+
}
|
|
248
|
+
function isPointerButtonCandidate(node, role, children) {
|
|
249
|
+
if (KEEP_ROLES.has(role) && role !== "document") return false;
|
|
250
|
+
if (children.some(hasInteractiveNode)) return false;
|
|
251
|
+
if (!hasClickableHint(node)) return false;
|
|
252
|
+
return Boolean(firstNonEmpty(node.name, normalizedText(children)));
|
|
253
|
+
}
|
|
254
|
+
function hasClickableHint(node) {
|
|
255
|
+
if (node.attributes.cursor === "pointer") return true;
|
|
256
|
+
if (Object.hasOwn(node.attributes, "onclick")) return true;
|
|
257
|
+
const tabindex = node.attributes.tabindex;
|
|
258
|
+
return tabindex !== void 0 && Number(tabindex) >= 0;
|
|
259
|
+
}
|
|
260
|
+
function hasInteractiveNode(child) {
|
|
261
|
+
if (child.kind === "text") return false;
|
|
262
|
+
if (ACTION_ROLES.has(child.role)) return true;
|
|
263
|
+
return child.children.some(hasInteractiveNode);
|
|
264
|
+
}
|
|
265
|
+
function flattenedChildren(node, children) {
|
|
266
|
+
const fallbackText = fallbackTextForFlattenedNode(node);
|
|
267
|
+
const flattened = children.length > 0 || !fallbackText ? children : [{ kind: "text", text: fallbackText }];
|
|
268
|
+
if (!BLOCK_FLATTEN_ROLES.has(tagNameForRole(node.role))) return flattened;
|
|
269
|
+
return flattened.map(
|
|
270
|
+
(child) => child.kind === "text" ? { ...child, block: true } : child
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
function fallbackTextForFlattenedNode(node) {
|
|
274
|
+
const name = firstNonEmpty(node.name, primitiveToString(node.value));
|
|
275
|
+
if (!name) return null;
|
|
276
|
+
if (attributeMatchesName(node, "aria-label", name)) return null;
|
|
277
|
+
if (attributeMatchesName(node, "title", name)) return null;
|
|
278
|
+
if (attributeMatchesName(node, "alt", name)) return null;
|
|
279
|
+
return name;
|
|
280
|
+
}
|
|
281
|
+
function attributeMatchesName(node, attributeName, name) {
|
|
282
|
+
return normalizeRawText(node.attributes[attributeName] ?? "") === name;
|
|
283
|
+
}
|
|
284
|
+
function hasNonEmptyAttribute(node, attributeName) {
|
|
285
|
+
return normalizeRawText(node.attributes[attributeName] ?? "") !== "";
|
|
286
|
+
}
|
|
287
|
+
function removeDuplicateNestedActions(role, attrs, children) {
|
|
288
|
+
if (!ACTION_ROLES.has(role)) return children;
|
|
289
|
+
const label = firstNonEmpty(
|
|
290
|
+
attrFromAttrs(attrs, "name"),
|
|
291
|
+
normalizedText(children)
|
|
292
|
+
);
|
|
293
|
+
if (!label) return children;
|
|
294
|
+
return children.flatMap((child) => {
|
|
295
|
+
if (child.kind === "text") return [child];
|
|
296
|
+
if (!ACTION_ROLES.has(child.role)) return [child];
|
|
297
|
+
const childLabel = firstNonEmpty(
|
|
298
|
+
attrValue(child, "name"),
|
|
299
|
+
singleTextChild(child),
|
|
300
|
+
normalizedText(child.children)
|
|
301
|
+
);
|
|
302
|
+
return childLabel === label ? child.children : [child];
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function nameAttributeAsContent(attrs, children) {
|
|
306
|
+
const name = attrFromAttrs(attrs, "name");
|
|
307
|
+
if (!name) return { attrs, children };
|
|
308
|
+
const attrsWithoutName = attrs.filter(([attr]) => attr !== "name");
|
|
309
|
+
if (normalizedText(children).includes(normalizeRawText(name))) {
|
|
310
|
+
return { attrs: attrsWithoutName, children };
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
attrs: attrsWithoutName,
|
|
314
|
+
children: [{ kind: "text", text: name }, ...children]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function renderFrame(frame, depth, lines, prefix = "") {
|
|
318
|
+
if (frame.status === "unavailable") {
|
|
319
|
+
lines.push(renderFrameLine(frame, depth, prefix, true));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
lines.push(renderFrameLine(frame, depth, prefix, false));
|
|
323
|
+
for (const root of frame.roots) renderNode(root, depth + 1, lines, prefix);
|
|
324
|
+
lines.push(`${prefix}${indent(depth)}</frame>`);
|
|
325
|
+
}
|
|
326
|
+
function renderNode(node, depth, lines, prefix = "") {
|
|
327
|
+
if (renderFoldedSingleChildChain(node, depth, lines, prefix)) return;
|
|
328
|
+
if (node.children.length === 0) {
|
|
329
|
+
lines.push(
|
|
330
|
+
`${prefix}${indent(depth)}${formatTag(node.role, node.attrs, false)}`
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const singleText = singleTextChild(node);
|
|
335
|
+
if (singleText !== null) {
|
|
336
|
+
if (shouldRenderBareText(node)) {
|
|
337
|
+
lines.push(`${prefix}${indent(depth)}${escapeText(singleText)}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
lines.push(
|
|
341
|
+
`${prefix}${indent(depth)}${formatTag(node.role, node.attrs, true)}${escapeText(singleText)}</${node.role}>`
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
lines.push(
|
|
346
|
+
`${prefix}${indent(depth)}${formatTag(node.role, node.attrs, true)}`
|
|
347
|
+
);
|
|
348
|
+
renderChildren(node.children, depth + 1, lines, prefix);
|
|
349
|
+
lines.push(`${prefix}${indent(depth)}</${node.role}>`);
|
|
350
|
+
}
|
|
351
|
+
function renderChildren(children, depth, lines, prefix) {
|
|
352
|
+
const renderedChildren = children.slice(0, MAX_CHILDREN_PER_PARENT);
|
|
353
|
+
for (const child of renderedChildren) {
|
|
354
|
+
if (child.kind === "text") {
|
|
355
|
+
lines.push(`${prefix}${indent(depth)}${escapeText(child.text)}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
renderNode(child, depth, lines, prefix);
|
|
359
|
+
}
|
|
360
|
+
if (children.length > MAX_CHILDREN_PER_PARENT) {
|
|
361
|
+
const truncated = children.slice(MAX_CHILDREN_PER_PARENT);
|
|
362
|
+
lines.push(
|
|
363
|
+
`${prefix}${indent(depth)}${renderChildrenTruncationNotice(
|
|
364
|
+
truncated
|
|
365
|
+
)}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function renderFoldedSingleChildChain(node, depth, lines, prefix) {
|
|
370
|
+
const chain = singleChildChain(node);
|
|
371
|
+
if (chain.length <= 1) return false;
|
|
372
|
+
const keptIndexes = chain.map((chainNode, index) => ({ chainNode, index })).filter(
|
|
373
|
+
({ chainNode, index }) => shouldKeepFoldedChainNode(chainNode, index)
|
|
374
|
+
).map(({ index }) => index);
|
|
375
|
+
if (keptIndexes.length === chain.length) return false;
|
|
376
|
+
renderFoldedChainNode(chain, keptIndexes, 0, depth, lines, prefix);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function shouldKeepFoldedChainNode(node, index) {
|
|
380
|
+
return index === 0 || node.role === "list";
|
|
381
|
+
}
|
|
382
|
+
function renderFoldedChainNode(chain, keptIndexes, keptIndexPosition, depth, lines, prefix) {
|
|
383
|
+
const currentIndex = keptIndexes[keptIndexPosition];
|
|
384
|
+
const current = chain[currentIndex];
|
|
385
|
+
lines.push(
|
|
386
|
+
`${prefix}${indent(depth)}${formatTag(current.role, current.attrs, true)}`
|
|
387
|
+
);
|
|
388
|
+
renderFoldedChainNodeOwnContent(current, depth + 1, lines, prefix);
|
|
389
|
+
const nextKeptIndex = keptIndexes[keptIndexPosition + 1];
|
|
390
|
+
if (nextKeptIndex !== void 0) {
|
|
391
|
+
if (nextKeptIndex > currentIndex + 1)
|
|
392
|
+
lines.push(`${prefix}${indent(depth + 1)}...`);
|
|
393
|
+
renderFoldedChainNode(
|
|
394
|
+
chain,
|
|
395
|
+
keptIndexes,
|
|
396
|
+
keptIndexPosition + 1,
|
|
397
|
+
depth + 1,
|
|
398
|
+
lines,
|
|
399
|
+
prefix
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
const terminal = chain[chain.length - 1];
|
|
403
|
+
if (chain.length - 1 > currentIndex)
|
|
404
|
+
lines.push(`${prefix}${indent(depth + 1)}...`);
|
|
405
|
+
renderChildren(terminal.children, depth + 1, lines, prefix);
|
|
406
|
+
}
|
|
407
|
+
lines.push(`${prefix}${indent(depth)}</${current.role}>`);
|
|
408
|
+
}
|
|
409
|
+
function renderFoldedChainNodeOwnContent(node, depth, lines, prefix) {
|
|
410
|
+
for (const child of node.children) {
|
|
411
|
+
if (child.kind === "text")
|
|
412
|
+
lines.push(`${prefix}${indent(depth)}${escapeText(child.text)}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function singleChildChain(node) {
|
|
416
|
+
const chain = [node];
|
|
417
|
+
let current = node;
|
|
418
|
+
while (isDeprioritizedSingleChildParent(current)) {
|
|
419
|
+
const child = singleElementChild(current);
|
|
420
|
+
if (!child) break;
|
|
421
|
+
if (ACTION_ROLES.has(child.role)) break;
|
|
422
|
+
chain.push(child);
|
|
423
|
+
current = child;
|
|
424
|
+
}
|
|
425
|
+
return chain;
|
|
426
|
+
}
|
|
427
|
+
function isDeprioritizedSingleChildParent(node) {
|
|
428
|
+
if (node.role === "document") return false;
|
|
429
|
+
if (ACTION_ROLES.has(node.role)) return false;
|
|
430
|
+
return singleElementChild(node) !== null;
|
|
431
|
+
}
|
|
432
|
+
function singleElementChild(node) {
|
|
433
|
+
let result = null;
|
|
434
|
+
for (const child of node.children) {
|
|
435
|
+
if (child.kind === "text") continue;
|
|
436
|
+
if (result) return null;
|
|
437
|
+
result = child;
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
function shouldRenderBareText(node) {
|
|
442
|
+
if (ACTION_ROLES.has(node.role)) return false;
|
|
443
|
+
if (attrValue(node, "ref")) return false;
|
|
444
|
+
if (PRESERVE_CHILDREN_BY_ROLE.has(node.role)) return false;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
function nodeAttributes(node, suppressName) {
|
|
448
|
+
const attributes = [];
|
|
449
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
450
|
+
const push = (name, value) => {
|
|
451
|
+
if (value === void 0 || value === null || value === "") return;
|
|
452
|
+
if (value === false || value === "false") return;
|
|
453
|
+
const normalizedName = uniqueAttributeName(
|
|
454
|
+
sanitizeAttributeName(name),
|
|
455
|
+
usedNames
|
|
456
|
+
);
|
|
457
|
+
attributes.push([
|
|
458
|
+
normalizedName,
|
|
459
|
+
normalizeAttributeValue(normalizedName, value)
|
|
460
|
+
]);
|
|
461
|
+
usedNames.add(normalizedName);
|
|
462
|
+
};
|
|
463
|
+
push("ref", node.ref);
|
|
464
|
+
if (node.name !== suppressName) push("name", node.name);
|
|
465
|
+
const hasStateValue = node.properties.value !== void 0 && node.properties.value !== null && node.properties.value !== "";
|
|
466
|
+
for (const name of RENDERED_STATE_PROPERTIES) {
|
|
467
|
+
const value = node.properties[name];
|
|
468
|
+
if (value === true) push(name, "true");
|
|
469
|
+
else push(name, value);
|
|
470
|
+
}
|
|
471
|
+
if (!hasStateValue) push("value", node.value);
|
|
472
|
+
push("href", node.attributes.href);
|
|
473
|
+
push("placeholder", node.attributes.placeholder);
|
|
474
|
+
return attributes;
|
|
475
|
+
}
|
|
476
|
+
function normalizeAttributeValue(name, value) {
|
|
477
|
+
const normalized = normalizeRawText(String(value));
|
|
478
|
+
return name === "href" ? truncate(normalized, MAX_HREF_CHARS) : normalized;
|
|
479
|
+
}
|
|
480
|
+
function singleTextChild(node) {
|
|
481
|
+
if (node.children.length !== 1) return null;
|
|
482
|
+
const child = node.children[0];
|
|
483
|
+
return child.kind === "text" && !child.block ? child.text : null;
|
|
484
|
+
}
|
|
485
|
+
function mergeAdjacentText(children) {
|
|
486
|
+
const result = [];
|
|
487
|
+
for (const child of children) {
|
|
488
|
+
const previous = result[result.length - 1];
|
|
489
|
+
if (child.kind === "text" && previous?.kind === "text" && !child.block && !previous.block) {
|
|
490
|
+
previous.text = normalizeRawText(`${previous.text} ${child.text}`);
|
|
491
|
+
} else {
|
|
492
|
+
result.push(child);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
function normalizedText(children) {
|
|
498
|
+
return children.map(
|
|
499
|
+
(child) => child.kind === "text" ? child.text : normalizedText(child.children)
|
|
500
|
+
).join(" ");
|
|
501
|
+
}
|
|
502
|
+
function previewForChildren(children, excludedText) {
|
|
503
|
+
const labels = [];
|
|
504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
505
|
+
function visit(child, insideInteractive) {
|
|
506
|
+
if (child.kind === "text") {
|
|
507
|
+
if (!insideInteractive) pushLabel(child.text);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const nextInsideInteractive = insideInteractive || ACTION_ROLES.has(child.role);
|
|
511
|
+
if (labels.join(" \xB7 ").length > MAX_SUMMARY_TEXT_CHARS) return;
|
|
512
|
+
for (const grandchild of child.children)
|
|
513
|
+
visit(grandchild, nextInsideInteractive);
|
|
514
|
+
}
|
|
515
|
+
function pushLabel(value) {
|
|
516
|
+
const normalized = normalizeRawText(value ?? "");
|
|
517
|
+
if (!normalized || normalized === "no visible text" || seen.has(normalized) || excludedText.has(normalized)) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
seen.add(normalized);
|
|
521
|
+
labels.push(normalized);
|
|
522
|
+
}
|
|
523
|
+
for (const child of children) visit(child, false);
|
|
524
|
+
const preview = labels.join(" \xB7 ");
|
|
525
|
+
return preview ? truncate(preview, MAX_SUMMARY_TEXT_CHARS) : "";
|
|
526
|
+
}
|
|
527
|
+
function actionSummariesForChildren(children) {
|
|
528
|
+
const actions = [];
|
|
529
|
+
const labels = /* @__PURE__ */ new Set();
|
|
530
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
531
|
+
let hasMore = false;
|
|
532
|
+
function visit(child) {
|
|
533
|
+
if (child.kind === "text") return;
|
|
534
|
+
const ref = attrValue(child, "ref");
|
|
535
|
+
if (ref && ACTION_ROLES.has(child.role) && !seenRefs.has(ref)) {
|
|
536
|
+
seenRefs.add(ref);
|
|
537
|
+
const label = actionLabel(child);
|
|
538
|
+
if (label) labels.add(label);
|
|
539
|
+
if (actions.length < MAX_ACTIONS_IN_SUMMARY) {
|
|
540
|
+
actions.push({ markup: renderActionSummary(child, ref), label });
|
|
541
|
+
} else {
|
|
542
|
+
hasMore = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const grandchild of child.children) visit(grandchild);
|
|
546
|
+
}
|
|
547
|
+
for (const child of children) visit(child);
|
|
548
|
+
return { actions, labels, hasMore };
|
|
549
|
+
}
|
|
550
|
+
function renderActionSummary(node, ref) {
|
|
551
|
+
const label = actionLabel(node);
|
|
552
|
+
const attrs = [["ref", ref]];
|
|
553
|
+
for (const [name, value] of node.attrs) {
|
|
554
|
+
if (name === "ref" || !ACTION_STATE_ATTRS.has(name)) continue;
|
|
555
|
+
attrs.push([name, normalizeText(value, MAX_ACTION_LABEL_CHARS)]);
|
|
556
|
+
}
|
|
557
|
+
if (!label || !TEXT_ACTION_ROLES.has(node.role)) {
|
|
558
|
+
const name = attrValue(node, "name");
|
|
559
|
+
if (name) attrs.push(["name", normalizeText(name, MAX_ACTION_LABEL_CHARS)]);
|
|
560
|
+
return formatTag(node.role, attrs, false);
|
|
561
|
+
}
|
|
562
|
+
return `${formatTag(node.role, attrs, true)}${escapeText(
|
|
563
|
+
normalizeText(label, MAX_ACTION_LABEL_CHARS)
|
|
564
|
+
)}</${node.role}>`;
|
|
565
|
+
}
|
|
566
|
+
function actionLabel(node) {
|
|
567
|
+
return firstNonEmpty(
|
|
568
|
+
singleTextChild(node),
|
|
569
|
+
attrValue(node, "name"),
|
|
570
|
+
attrValue(node, "value"),
|
|
571
|
+
attrValue(node, "placeholder")
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
function attrValue(node, name) {
|
|
575
|
+
return node.attrs.find(([attr]) => attr === name)?.[1] ?? null;
|
|
576
|
+
}
|
|
577
|
+
function attrFromAttrs(attrs, name) {
|
|
578
|
+
return attrs.find(([attr]) => attr === name)?.[1] ?? null;
|
|
579
|
+
}
|
|
580
|
+
function shouldSkipNode(node, parent) {
|
|
581
|
+
if (SKIP_ROLES.has(node.role)) return true;
|
|
582
|
+
if (node.role !== "StaticText") return false;
|
|
583
|
+
return Boolean(parent?.name && node.name && parent.name === node.name);
|
|
584
|
+
}
|
|
585
|
+
function isTextRole(role) {
|
|
586
|
+
return role === "StaticText" || role === "InlineTextBox";
|
|
587
|
+
}
|
|
588
|
+
function isRenderedNode(child) {
|
|
589
|
+
return child.kind === "node";
|
|
590
|
+
}
|
|
591
|
+
function hasVisibleTextOrInteractive(child) {
|
|
592
|
+
if (child.kind === "text") return normalizeRawText(child.text) !== "";
|
|
593
|
+
if (ACTION_ROLES.has(child.role)) return true;
|
|
594
|
+
return child.children.some(hasVisibleTextOrInteractive);
|
|
595
|
+
}
|
|
596
|
+
function tagNameForRole(role) {
|
|
597
|
+
const normalized = normalizeRole(role).replace(/[^a-zA-Z0-9_.:-]/g, "-");
|
|
598
|
+
return /^[a-zA-Z_:]/.test(normalized) ? normalized : "node";
|
|
599
|
+
}
|
|
600
|
+
function normalizeRole(role) {
|
|
601
|
+
if (role === "RootWebArea") return "document";
|
|
602
|
+
if (role === "textField") return "textbox";
|
|
603
|
+
return role || "node";
|
|
604
|
+
}
|
|
605
|
+
function primitiveToString(value) {
|
|
606
|
+
return value === null ? null : String(value);
|
|
607
|
+
}
|
|
608
|
+
function nodeTextValue(node) {
|
|
609
|
+
if (!node) return null;
|
|
610
|
+
const value = primitiveToString(node.properties.value ?? node.value);
|
|
611
|
+
return value ? normalizeRawText(value) : null;
|
|
612
|
+
}
|
|
613
|
+
function firstNonEmpty(...values) {
|
|
614
|
+
for (const value of values) {
|
|
615
|
+
const normalized = normalizeRawText(value ?? "");
|
|
616
|
+
if (normalized) return truncate(normalized, MAX_LABEL_CHARS);
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function normalizeText(value, maxChars) {
|
|
621
|
+
return truncate(value.replace(/\s+/g, " ").trim(), maxChars);
|
|
622
|
+
}
|
|
623
|
+
function normalizeRawText(value) {
|
|
624
|
+
return value.replace(/\s+/g, " ").trim();
|
|
625
|
+
}
|
|
626
|
+
function truncate(value, maxChars) {
|
|
627
|
+
return value.length > maxChars ? `${value.slice(0, maxChars - 1)}\u2026` : value;
|
|
628
|
+
}
|
|
629
|
+
function uniqueAttributeName(name, usedNames) {
|
|
630
|
+
if (!usedNames.has(name)) return name;
|
|
631
|
+
let index = 2;
|
|
632
|
+
while (usedNames.has(`${name}-${index}`)) index += 1;
|
|
633
|
+
return `${name}-${index}`;
|
|
634
|
+
}
|
|
635
|
+
function sanitizeAttributeName(name) {
|
|
636
|
+
const sanitized = name.replace(/[^a-zA-Z0-9_.:-]/g, "-");
|
|
637
|
+
return /^[a-zA-Z_:]/.test(sanitized) ? sanitized : `attr-${sanitized}`;
|
|
638
|
+
}
|
|
639
|
+
function escapeText(value) {
|
|
640
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
641
|
+
}
|
|
642
|
+
function escapeAttribute(value) {
|
|
643
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
644
|
+
}
|
|
645
|
+
export {
|
|
646
|
+
renderChildrenTruncationNotice,
|
|
647
|
+
renderFrame,
|
|
648
|
+
renderNode,
|
|
649
|
+
renderSnapshot,
|
|
650
|
+
renderSnapshotFrames
|
|
651
|
+
};
|