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,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderChildrenTruncationNotice,
|
|
3
|
+
renderFrame,
|
|
4
|
+
renderNode,
|
|
5
|
+
renderSnapshotFrames
|
|
6
|
+
} from "./render-snapshot.js";
|
|
7
|
+
const MAX_DIFF_CHILDREN_PER_PARENT = 4;
|
|
8
|
+
const MAX_LABEL_CHARS = 140;
|
|
9
|
+
const LOW_SIGNAL_DIFF_ATTRS = /* @__PURE__ */ new Set(["ref"]);
|
|
10
|
+
function diffSnapshots(before, after) {
|
|
11
|
+
const beforeFrames = renderSnapshotFrames(before);
|
|
12
|
+
const afterFrames = renderSnapshotFrames(after);
|
|
13
|
+
const pageChanged = before.title !== after.title || before.url !== after.url;
|
|
14
|
+
const frames = diffFrames(beforeFrames, afterFrames);
|
|
15
|
+
return {
|
|
16
|
+
before,
|
|
17
|
+
after,
|
|
18
|
+
pageChanged,
|
|
19
|
+
frames,
|
|
20
|
+
changed: pageChanged || frames.length > 0
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function renderSnapshotDiff(diff) {
|
|
24
|
+
if (!diff.changed) return "";
|
|
25
|
+
if (diff.pageChanged && diff.frames.length === 0) {
|
|
26
|
+
return [
|
|
27
|
+
renderPageOpen(diff.before, "- ", true),
|
|
28
|
+
renderPageOpen(diff.after, "+ ", true)
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
const lines = [renderPageOpen(diff.after, "")];
|
|
32
|
+
for (const frameDiff of diff.frames) renderFrameDiff(frameDiff, 1, lines);
|
|
33
|
+
lines.push("</page>");
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
function renderPageOpen(snapshot, prefix, selfClosing = false) {
|
|
37
|
+
return `${prefix}${formatTag(
|
|
38
|
+
"page",
|
|
39
|
+
[
|
|
40
|
+
["title", firstNonEmpty(snapshot.title, snapshot.url) ?? ""],
|
|
41
|
+
["url", snapshot.url]
|
|
42
|
+
],
|
|
43
|
+
!selfClosing
|
|
44
|
+
)}`;
|
|
45
|
+
}
|
|
46
|
+
function renderFrameLine(frame, depth, prefix, selfClosing) {
|
|
47
|
+
const attrs = [
|
|
48
|
+
["index", String(frame.index)],
|
|
49
|
+
["url", normalizeText(frame.url, MAX_LABEL_CHARS)]
|
|
50
|
+
];
|
|
51
|
+
if (frame.name)
|
|
52
|
+
attrs.push(["name", normalizeText(frame.name, MAX_LABEL_CHARS)]);
|
|
53
|
+
if (frame.parentId) attrs.push(["parent", frame.parentId]);
|
|
54
|
+
if (frame.status === "unavailable") {
|
|
55
|
+
attrs.push(["error", normalizeText(frame.error, 180)]);
|
|
56
|
+
}
|
|
57
|
+
return `${prefix}${indent(depth)}${formatTag("frame", attrs, !selfClosing)}`;
|
|
58
|
+
}
|
|
59
|
+
function renderTextNode(node, depth, prefix) {
|
|
60
|
+
return `${prefix}${indent(depth)}${escapeText(node.text)}`;
|
|
61
|
+
}
|
|
62
|
+
function indent(depth) {
|
|
63
|
+
return " ".repeat(depth);
|
|
64
|
+
}
|
|
65
|
+
function formatTag(tagName, attributes, hasChildren) {
|
|
66
|
+
const attrs = attributes.filter(([, value]) => value !== "").map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
|
|
67
|
+
return hasChildren ? `<${tagName}${attrs}>` : `<${tagName}${attrs} />`;
|
|
68
|
+
}
|
|
69
|
+
function diffFrames(beforeFrames, afterFrames) {
|
|
70
|
+
const diffs = [];
|
|
71
|
+
const maxLength = Math.max(beforeFrames.length, afterFrames.length);
|
|
72
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
73
|
+
const before = beforeFrames[index];
|
|
74
|
+
const after = afterFrames[index];
|
|
75
|
+
if (before && !after) {
|
|
76
|
+
diffs.push({ type: "removed", frame: before });
|
|
77
|
+
} else if (!before && after) {
|
|
78
|
+
diffs.push({ type: "added", frame: after });
|
|
79
|
+
} else if (before && after) {
|
|
80
|
+
const diff = diffFrame(before, after);
|
|
81
|
+
if (diff) diffs.push(diff);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return diffs;
|
|
85
|
+
}
|
|
86
|
+
function diffFrame(before, after) {
|
|
87
|
+
if (before.status !== after.status)
|
|
88
|
+
return { type: "modified", before, after };
|
|
89
|
+
if (before.status === "unavailable" || after.status === "unavailable") {
|
|
90
|
+
return comparableFrame(before) === comparableFrame(after) ? null : { type: "modified", before, after };
|
|
91
|
+
}
|
|
92
|
+
const children = diffChildren(before.roots, after.roots);
|
|
93
|
+
const frameChanged = comparableFrame(before) !== comparableFrame(after);
|
|
94
|
+
if (!frameChanged && children.length === 0) return null;
|
|
95
|
+
if (frameChanged && children.length === 0)
|
|
96
|
+
return { type: "modified", before, after };
|
|
97
|
+
return { type: "context", frame: after, children };
|
|
98
|
+
}
|
|
99
|
+
function diffChildren(beforeChildren, afterChildren) {
|
|
100
|
+
const diffs = [];
|
|
101
|
+
const usedBefore = /* @__PURE__ */ new Set();
|
|
102
|
+
for (let afterIndex = 0; afterIndex < afterChildren.length; afterIndex += 1) {
|
|
103
|
+
const after = afterChildren[afterIndex];
|
|
104
|
+
const beforeIndex = findMatchingBeforeChild(
|
|
105
|
+
after,
|
|
106
|
+
afterIndex,
|
|
107
|
+
beforeChildren,
|
|
108
|
+
usedBefore
|
|
109
|
+
);
|
|
110
|
+
if (beforeIndex === -1) {
|
|
111
|
+
diffs.push(addedChild(after));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
usedBefore.add(beforeIndex);
|
|
115
|
+
const childDiff = diffChild(beforeChildren[beforeIndex], after);
|
|
116
|
+
if (childDiff) diffs.push(childDiff);
|
|
117
|
+
}
|
|
118
|
+
for (let beforeIndex = 0; beforeIndex < beforeChildren.length; beforeIndex += 1) {
|
|
119
|
+
if (!usedBefore.has(beforeIndex))
|
|
120
|
+
diffs.push(removedChild(beforeChildren[beforeIndex]));
|
|
121
|
+
}
|
|
122
|
+
return diffs;
|
|
123
|
+
}
|
|
124
|
+
function diffChild(before, after) {
|
|
125
|
+
if (before.kind === "text" || after.kind === "text") {
|
|
126
|
+
if (before.kind === "text" && after.kind === "text") {
|
|
127
|
+
return before.text === after.text ? null : { kind: "text", type: "modified", before, after };
|
|
128
|
+
}
|
|
129
|
+
return after.kind === "text" ? addedChild(after) : removedChild(before);
|
|
130
|
+
}
|
|
131
|
+
const children = diffChildren(before.children, after.children);
|
|
132
|
+
const selfChanged = comparableNode(before) !== comparableNode(after);
|
|
133
|
+
const directTextChanged = children.some((child) => child.kind === "text");
|
|
134
|
+
if (!selfChanged && children.length === 0) return null;
|
|
135
|
+
if (selfChanged || sameRef(before, after) && directTextChanged) {
|
|
136
|
+
return { kind: "node", type: "modified", before, after, children };
|
|
137
|
+
}
|
|
138
|
+
return { kind: "node", type: "context", node: after, children };
|
|
139
|
+
}
|
|
140
|
+
function findMatchingBeforeChild(after, afterIndex, beforeChildren, usedBefore) {
|
|
141
|
+
const beforeAtSameIndex = beforeChildren[afterIndex];
|
|
142
|
+
if (beforeAtSameIndex && !usedBefore.has(afterIndex) && arePositionallySimilarChildren(beforeAtSameIndex, after)) {
|
|
143
|
+
return afterIndex;
|
|
144
|
+
}
|
|
145
|
+
if (after.kind === "node") {
|
|
146
|
+
const byKey = beforeChildren.findIndex(
|
|
147
|
+
(before, index) => !usedBefore.has(index) && before.kind === "node" && before.key === after.key
|
|
148
|
+
);
|
|
149
|
+
if (byKey !== -1) return byKey;
|
|
150
|
+
const afterFingerprint = childFingerprint(after);
|
|
151
|
+
const byFingerprint = beforeChildren.findIndex(
|
|
152
|
+
(before, index) => !usedBefore.has(index) && before.kind === "node" && childFingerprint(before) === afterFingerprint
|
|
153
|
+
);
|
|
154
|
+
if (byFingerprint !== -1) return byFingerprint;
|
|
155
|
+
}
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
158
|
+
function addedChild(child) {
|
|
159
|
+
return child.kind === "text" ? { kind: "text", type: "added", node: child } : { kind: "node", type: "added", node: child };
|
|
160
|
+
}
|
|
161
|
+
function removedChild(child) {
|
|
162
|
+
return child.kind === "text" ? { kind: "text", type: "removed", node: child } : { kind: "node", type: "removed", node: child };
|
|
163
|
+
}
|
|
164
|
+
function arePositionallySimilarChildren(before, after) {
|
|
165
|
+
if (before.kind !== after.kind) return false;
|
|
166
|
+
if (before.kind === "text" && after.kind === "text") return true;
|
|
167
|
+
if (before.kind === "node" && after.kind === "node") {
|
|
168
|
+
return before.key === after.key || before.role === after.role;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function renderFrameDiff(diff, depth, lines) {
|
|
173
|
+
if (diff.type === "added") {
|
|
174
|
+
renderFrame(diff.frame, depth, lines, "+ ");
|
|
175
|
+
} else if (diff.type === "removed") {
|
|
176
|
+
renderFrame(diff.frame, depth, lines, "- ");
|
|
177
|
+
} else if (diff.type === "modified") {
|
|
178
|
+
renderFrame(diff.before, depth, lines, "- ");
|
|
179
|
+
renderFrame(diff.after, depth, lines, "+ ");
|
|
180
|
+
} else if (diff.frame.status === "ok") {
|
|
181
|
+
lines.push(renderFrameLine(diff.frame, depth, "", false));
|
|
182
|
+
if (diff.frame.roots.length > diff.children.length && diff.children.length > 0) {
|
|
183
|
+
lines.push(`${indent(depth + 1)}...`);
|
|
184
|
+
}
|
|
185
|
+
renderChildDiffs(diff.children, depth + 1, lines);
|
|
186
|
+
lines.push(`${indent(depth)}</frame>`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function renderChildDiffs(diffs, depth, lines) {
|
|
190
|
+
for (const diff of diffs.slice(0, MAX_DIFF_CHILDREN_PER_PARENT)) {
|
|
191
|
+
renderChildDiff(diff, depth, lines);
|
|
192
|
+
}
|
|
193
|
+
if (diffs.length > MAX_DIFF_CHILDREN_PER_PARENT) {
|
|
194
|
+
const truncated = diffs.slice(MAX_DIFF_CHILDREN_PER_PARENT);
|
|
195
|
+
const prefix = diffPrefixForSummary(truncated);
|
|
196
|
+
lines.push(
|
|
197
|
+
`${prefix}${indent(depth)}${renderChildrenTruncationNotice(
|
|
198
|
+
diffSummaryChildren(truncated)
|
|
199
|
+
)}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function renderChildDiff(diff, depth, lines) {
|
|
204
|
+
if (diff.kind === "text") {
|
|
205
|
+
if (diff.type === "added")
|
|
206
|
+
lines.push(renderTextNode(diff.node, depth, "+ "));
|
|
207
|
+
else if (diff.type === "removed")
|
|
208
|
+
lines.push(renderTextNode(diff.node, depth, "- "));
|
|
209
|
+
else {
|
|
210
|
+
lines.push(renderTextNode(diff.before, depth, "- "));
|
|
211
|
+
lines.push(renderTextNode(diff.after, depth, "+ "));
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
renderNodeDiff(diff, depth, lines);
|
|
216
|
+
}
|
|
217
|
+
function renderNodeDiff(diff, depth, lines) {
|
|
218
|
+
if (diff.type === "added") {
|
|
219
|
+
renderNode(diff.node, depth, lines, "+ ");
|
|
220
|
+
} else if (diff.type === "removed") {
|
|
221
|
+
renderRemovedNode(diff.node, depth, lines);
|
|
222
|
+
} else if (diff.type === "modified") {
|
|
223
|
+
if (sameRef(diff.before, diff.after)) {
|
|
224
|
+
renderModifiedSameRefNode(diff, depth, lines);
|
|
225
|
+
} else {
|
|
226
|
+
renderRemovedNode(diff.before, depth, lines);
|
|
227
|
+
renderNode(diff.after, depth, lines, "+ ");
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
if (diff.node.children.length === 0) {
|
|
231
|
+
lines.push(
|
|
232
|
+
`${indent(depth)}${formatTag(diff.node.role, diff.node.attrs, false)}`
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
lines.push(
|
|
237
|
+
`${indent(depth)}${formatTag(diff.node.role, diff.node.attrs, true)}`
|
|
238
|
+
);
|
|
239
|
+
if (diff.node.children.length > diff.children.length)
|
|
240
|
+
lines.push(`${indent(depth + 1)}...`);
|
|
241
|
+
renderChildDiffs(diff.children, depth + 1, lines);
|
|
242
|
+
lines.push(`${indent(depth)}</${diff.node.role}>`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function renderModifiedSameRefNode(diff, depth, lines) {
|
|
246
|
+
if (diff.children.length === 0 || singleTextChild(diff.after) !== null) {
|
|
247
|
+
renderNode(diff.after, depth, lines, "~ ");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (diff.after.children.length === 0) {
|
|
251
|
+
lines.push(
|
|
252
|
+
`~ ${indent(depth)}${formatTag(diff.after.role, diff.after.attrs, false)}`
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
lines.push(
|
|
257
|
+
`~ ${indent(depth)}${formatTag(diff.after.role, diff.after.attrs, true)}`
|
|
258
|
+
);
|
|
259
|
+
if (diff.after.children.length > diff.children.length)
|
|
260
|
+
lines.push(`~ ${indent(depth + 1)}...`);
|
|
261
|
+
renderChildDiffs(diff.children, depth + 1, lines);
|
|
262
|
+
lines.push(`~ ${indent(depth)}</${diff.after.role}>`);
|
|
263
|
+
}
|
|
264
|
+
function renderRemovedNode(node, depth, lines) {
|
|
265
|
+
const attrs = node.attrs.filter(([name]) => name === "ref");
|
|
266
|
+
if (node.children.length === 0) {
|
|
267
|
+
lines.push(`- ${indent(depth)}${formatTag(node.role, attrs, false)}`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
lines.push(
|
|
271
|
+
`- ${indent(depth)}${formatTag(node.role, attrs, true)}...</${node.role}>`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function comparableFrame(frame) {
|
|
275
|
+
return JSON.stringify({
|
|
276
|
+
status: frame.status,
|
|
277
|
+
id: frame.id,
|
|
278
|
+
index: frame.index,
|
|
279
|
+
url: frame.url,
|
|
280
|
+
name: frame.name,
|
|
281
|
+
parentId: frame.parentId,
|
|
282
|
+
error: frame.status === "unavailable" ? frame.error : void 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function comparableNode(node) {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
role: node.role,
|
|
288
|
+
attrs: comparableAttrs(node.attrs)
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function comparableAttrs(attrs) {
|
|
292
|
+
return attrs.flatMap(([name, value]) => {
|
|
293
|
+
if (LOW_SIGNAL_DIFF_ATTRS.has(name)) return [];
|
|
294
|
+
if (name === "href") return [[name, normalizeComparableHref(value)]];
|
|
295
|
+
return [[name, value]];
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function sameRef(before, after) {
|
|
299
|
+
const beforeRef = attrValue(before, "ref");
|
|
300
|
+
return beforeRef !== null && beforeRef === attrValue(after, "ref");
|
|
301
|
+
}
|
|
302
|
+
function attrValue(node, name) {
|
|
303
|
+
return node.attrs.find(([attr]) => attr === name)?.[1] ?? null;
|
|
304
|
+
}
|
|
305
|
+
function singleTextChild(node) {
|
|
306
|
+
if (node.children.length !== 1) return null;
|
|
307
|
+
const child = node.children[0];
|
|
308
|
+
return child.kind === "text" && !child.block ? child.text : null;
|
|
309
|
+
}
|
|
310
|
+
function childFingerprint(child) {
|
|
311
|
+
if (child.kind === "text") return `text:${child.text}`;
|
|
312
|
+
return comparableNode(child);
|
|
313
|
+
}
|
|
314
|
+
function normalizeComparableHref(value) {
|
|
315
|
+
const withoutEllipsis = value.endsWith("\u2026") ? value.slice(0, -1) : value;
|
|
316
|
+
try {
|
|
317
|
+
const url = new URL(withoutEllipsis);
|
|
318
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
319
|
+
} catch {
|
|
320
|
+
return withoutEllipsis.split(/[?#]/, 1)[0] ?? withoutEllipsis;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function firstNonEmpty(...values) {
|
|
324
|
+
for (const value of values) {
|
|
325
|
+
const normalized = normalizeRawText(value ?? "");
|
|
326
|
+
if (normalized) return truncate(normalized, MAX_LABEL_CHARS);
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
function normalizeText(value, maxChars) {
|
|
331
|
+
return truncate(value.replace(/\s+/g, " ").trim(), maxChars);
|
|
332
|
+
}
|
|
333
|
+
function normalizeRawText(value) {
|
|
334
|
+
return value.replace(/\s+/g, " ").trim();
|
|
335
|
+
}
|
|
336
|
+
function truncate(value, maxChars) {
|
|
337
|
+
return value.length > maxChars ? `${value.slice(0, maxChars - 1)}\u2026` : value;
|
|
338
|
+
}
|
|
339
|
+
function escapeText(value) {
|
|
340
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
341
|
+
}
|
|
342
|
+
function escapeAttribute(value) {
|
|
343
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
344
|
+
}
|
|
345
|
+
function diffPrefixForSummary(diffs) {
|
|
346
|
+
if (diffs.every((diff) => diff.type === "added")) return "+ ";
|
|
347
|
+
if (diffs.every((diff) => diff.type === "removed")) return "- ";
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
function diffSummaryChildren(diffs) {
|
|
351
|
+
return diffs.map(
|
|
352
|
+
(diff) => diff.type === "modified" ? diff.after : diff.node
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
export {
|
|
356
|
+
diffSnapshots,
|
|
357
|
+
renderSnapshotDiff
|
|
358
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Snapshot } from './types.js';
|
|
2
|
+
|
|
3
|
+
type SnapshotTextNode = {
|
|
4
|
+
kind: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
block?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type RenderedSnapshotNode = {
|
|
9
|
+
kind: "node";
|
|
10
|
+
key: string;
|
|
11
|
+
role: string;
|
|
12
|
+
attrs: Array<[string, string]>;
|
|
13
|
+
children: RenderedSnapshotChild[];
|
|
14
|
+
};
|
|
15
|
+
type RenderedSnapshotChild = RenderedSnapshotNode | SnapshotTextNode;
|
|
16
|
+
type RenderedSnapshotFrame = {
|
|
17
|
+
status: "ok";
|
|
18
|
+
id: string;
|
|
19
|
+
index: number;
|
|
20
|
+
url: string;
|
|
21
|
+
name: string | null;
|
|
22
|
+
parentId: string | null;
|
|
23
|
+
roots: RenderedSnapshotNode[];
|
|
24
|
+
} | {
|
|
25
|
+
status: "unavailable";
|
|
26
|
+
id: string;
|
|
27
|
+
index: number;
|
|
28
|
+
url: string;
|
|
29
|
+
name: string | null;
|
|
30
|
+
parentId: string | null;
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
declare function renderSnapshot(snapshot: Snapshot, refId?: string): string;
|
|
34
|
+
declare function renderSnapshotFrames(snapshot: Snapshot): RenderedSnapshotFrame[];
|
|
35
|
+
declare function renderChildrenTruncationNotice(children: RenderedSnapshotChild[]): string;
|
|
36
|
+
declare function renderFrame(frame: RenderedSnapshotFrame, depth: number, lines: string[], prefix?: string): void;
|
|
37
|
+
declare function renderNode(node: RenderedSnapshotNode, depth: number, lines: string[], prefix?: string): void;
|
|
38
|
+
|
|
39
|
+
export { type RenderedSnapshotChild, type RenderedSnapshotFrame, type RenderedSnapshotNode, renderChildrenTruncationNotice, renderFrame, renderNode, renderSnapshot, renderSnapshotFrames };
|