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.
Files changed (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -0,0 +1,333 @@
1
+ import { chromium } from "playwright";
2
+ import outdent from "outdent";
3
+ import { describe, expect, test as base } from "vitest";
4
+ import { snapshot } from "./capture-snapshot.js";
5
+ import {
6
+ diffSnapshots,
7
+ renderSnapshotDiff
8
+ } from "./diff-snapshots.js";
9
+ import { renderSnapshot } from "./render-snapshot.js";
10
+ const test = base.extend({
11
+ page: async ({}, use) => {
12
+ const browser = await chromium.launch({ headless: true });
13
+ const page = await browser.newPage();
14
+ await use(page);
15
+ await page.close();
16
+ await browser.close();
17
+ },
18
+ expectSnapshot: async ({ page }, use) => {
19
+ await use(async (html, expected) => {
20
+ await page.setContent(outdent.string(html));
21
+ const raw = await snapshot(page);
22
+ expect(normalizeIncidentalUrls(renderSnapshot(raw))).toBe(
23
+ outdent.string(expected)
24
+ );
25
+ });
26
+ },
27
+ expectScopedSnapshot: async ({ page }, use) => {
28
+ await use(async (html, refId, expected) => {
29
+ await page.setContent(outdent.string(html));
30
+ const raw = await snapshot(page);
31
+ expect(normalizeIncidentalUrls(renderSnapshot(raw, refId))).toBe(
32
+ outdent.string(expected)
33
+ );
34
+ });
35
+ },
36
+ expectSnapshotDiff: async ({ page }, use) => {
37
+ await use(async (beforeHtml, afterHtml, expected) => {
38
+ await page.setContent(outdent.string(beforeHtml));
39
+ const before = await snapshot(page);
40
+ await page.setContent(outdent.string(afterHtml));
41
+ const after = await snapshot(page);
42
+ expect(
43
+ normalizeIncidentalUrls(
44
+ renderSnapshotDiff(diffSnapshots(before, after))
45
+ )
46
+ ).toBe(outdent.string(expected));
47
+ });
48
+ }
49
+ });
50
+ function normalizeIncidentalUrls(rendered) {
51
+ return rendered.replaceAll(/url="[^"]*"/g, `url="<page-url>"`);
52
+ }
53
+ function makeNode(input) {
54
+ const node = {
55
+ nodeId: input.nodeId,
56
+ ignored: input.ignored ?? false,
57
+ role: input.role,
58
+ name: input.name ?? null,
59
+ value: input.value ?? null,
60
+ description: input.description ?? null,
61
+ properties: input.properties ?? {},
62
+ attributes: input.attributes ?? {},
63
+ children: input.children ?? [],
64
+ ref: input.ref ?? null,
65
+ subtreeSize: 1
66
+ };
67
+ node.subtreeSize = countSubtree(node);
68
+ return node;
69
+ }
70
+ function countSubtree(node) {
71
+ return 1 + node.children.reduce((sum, child) => sum + countSubtree(child), 0);
72
+ }
73
+ function makeSnapshot(roots, options = {}) {
74
+ const url = options.url ?? "about:blank";
75
+ return {
76
+ title: options.title ?? "Demo Page",
77
+ url,
78
+ frames: [
79
+ {
80
+ status: "ok",
81
+ id: "main",
82
+ index: 0,
83
+ url,
84
+ name: null,
85
+ parentId: null,
86
+ roots
87
+ }
88
+ ]
89
+ };
90
+ }
91
+ describe("renderSnapshot", () => {
92
+ test("renders page, frame, semantic roles, heading text, refs, and no command hint", async ({
93
+ expectSnapshot
94
+ }) => {
95
+ await expectSnapshot(
96
+ `
97
+ <!doctype html>
98
+ <html>
99
+ <head><title>Product Docs</title></head>
100
+ <body>
101
+ <header>
102
+ <nav aria-label="Primary">
103
+ <a href="/docs">Docs</a>
104
+ </nav>
105
+ </header>
106
+ <main>
107
+ <h1>Welcome</h1>
108
+ <button>Save</button>
109
+ <input placeholder="Search docs" value="query" />
110
+ </main>
111
+ </body>
112
+ </html>
113
+ `,
114
+ `
115
+ <page title="Product Docs" url="<page-url>">
116
+ <frame index="0" url="<page-url>">
117
+ <document ref="l1">
118
+ Product Docs
119
+ <banner ref="l2">
120
+ ...
121
+ Primary
122
+ <link ref="l4" href="/docs">Docs</link>
123
+ </banner>
124
+ <main ref="l5">
125
+ # Welcome
126
+ <button ref="l7">Save</button>
127
+ <textbox ref="l8" value="query" placeholder="Search docs">
128
+ Search docs
129
+ query
130
+ </textbox>
131
+ </main>
132
+ </document>
133
+ </frame>
134
+ </page>
135
+ `
136
+ );
137
+ });
138
+ test("scopes an already-captured tree by ref with numeric-suffix fallback", async ({
139
+ expectScopedSnapshot
140
+ }) => {
141
+ await expectScopedSnapshot(
142
+ `
143
+ <!doctype html>
144
+ <title>Scoped Snapshot</title>
145
+ <main>
146
+ <button>Sibling</button>
147
+ <button>Target</button>
148
+ </main>
149
+ `,
150
+ "e4",
151
+ `
152
+ <page title="Scoped Snapshot" url="<page-url>">
153
+ <frame index="0" url="<page-url>">
154
+ <button ref="l4">Target</button>
155
+ </frame>
156
+ </page>
157
+ `
158
+ );
159
+ });
160
+ test("compacts low-value wrappers, clickable generics, single-child chains, and long child lists", async ({
161
+ expectSnapshot
162
+ }) => {
163
+ await expectSnapshot(
164
+ `
165
+ <!doctype html>
166
+ <title>Compact Demo</title>
167
+ <style>.card { cursor: pointer; }</style>
168
+ <main>
169
+ <div><section><p>Flattened wrapper text</p></section></div>
170
+ <div class="card" onclick="void 0">Open card</div>
171
+ <main aria-label="Outer">
172
+ <nav aria-label="Navigation">
173
+ <form aria-label="Lookup"><button>Submit chain</button></form>
174
+ </nav>
175
+ </main>
176
+ <ul>
177
+ <li><button>One</button></li>
178
+ <li><button>Two</button></li>
179
+ <li><button>Three</button></li>
180
+ <li><button>Four</button></li>
181
+ <li><button>Five</button></li>
182
+ <li><button>Six</button></li>
183
+ </ul>
184
+ </main>
185
+ `,
186
+ `
187
+ <page title="Compact Demo" url="<page-url>">
188
+ <frame index="0" url="<page-url>">
189
+ <document ref="l1">
190
+ Compact Demo
191
+ <main ref="l2">
192
+ Flattened wrapper text
193
+ <button ref="l3">Open card</button>
194
+ <main ref="l4">
195
+ Outer
196
+ ...
197
+ Lookup
198
+ <button ref="l7">Submit chain</button>
199
+ </main>
200
+ <list>
201
+ <listitem>
202
+ <button ref="l9">One</button>
203
+ </listitem>
204
+ <listitem>
205
+ <button ref="l11">Two</button>
206
+ </listitem>
207
+ <listitem>
208
+ <button ref="l13">Three</button>
209
+ </listitem>
210
+ <listitem>
211
+ <button ref="l15">Four</button>
212
+ </listitem>
213
+ [Truncated 2 more elements. Interactive elements: <button ref="l17">Five</button>, <button ref="l19">Six</button>]
214
+ </list>
215
+ </main>
216
+ </document>
217
+ </frame>
218
+ </page>
219
+ `
220
+ );
221
+ });
222
+ });
223
+ describe("diffSnapshots", () => {
224
+ test("returns no rendered diff for unchanged browser-rendered snapshots", async ({
225
+ expectSnapshotDiff
226
+ }) => {
227
+ const html = `
228
+ <!-- BEFORE -->
229
+ <!doctype html>
230
+ <title>Stable Demo</title>
231
+ <main><button>Stable</button></main>
232
+ `;
233
+ await expectSnapshotDiff(html, html, "");
234
+ });
235
+ test("tracks added, removed, and modified nodes under context ancestors", async ({
236
+ expectSnapshotDiff
237
+ }) => {
238
+ await expectSnapshotDiff(
239
+ `
240
+ <!-- BEFORE -->
241
+ <!doctype html>
242
+ <title>Diff Demo</title>
243
+ <main>
244
+ <h1>Tasks</h1>
245
+ <button id="stable">Stable</button>
246
+ <button id="save">Save</button>
247
+ <button id="delete">Delete</button>
248
+ </main>
249
+ `,
250
+ `
251
+ <!-- AFTER -->
252
+ <!doctype html>
253
+ <title>Diff Demo</title>
254
+ <main>
255
+ <h1>Tasks</h1>
256
+ <button id="stable">Stable</button>
257
+ <button id="save">Saved</button>
258
+ <a id="docs" href="https://example.test/docs">Docs</a>
259
+ </main>
260
+ `,
261
+ `
262
+ <page title="Diff Demo" url="<page-url>">
263
+ <frame index="0" url="<page-url>">
264
+ <document ref="l1">
265
+ ...
266
+ <main ref="l2">
267
+ ...
268
+ ~ <button ref="l5">Saved</button>
269
+ + <link ref="l6" href="https://example.test/docs">Docs</link>
270
+ - <button ref="l6">...</button>
271
+ </main>
272
+ </document>
273
+ </frame>
274
+ </page>
275
+ `
276
+ );
277
+ });
278
+ test("suppresses href query and hash changes from browser-rendered links", async ({
279
+ expectSnapshotDiff
280
+ }) => {
281
+ await expectSnapshotDiff(
282
+ `
283
+ <!-- BEFORE -->
284
+ <!doctype html>
285
+ <title>Href Demo</title>
286
+ <main><a id="docs" href="https://example.test/docs?utm=old#intro">Docs</a></main>
287
+ `,
288
+ `
289
+ <!-- AFTER -->
290
+ <!doctype html>
291
+ <title>Href Demo</title>
292
+ <main><a id="docs" href="https://example.test/docs?utm=new#api">Docs</a></main>
293
+ `,
294
+ ""
295
+ );
296
+ });
297
+ test("suppresses ref-only changes that browser HTML cannot express directly", () => {
298
+ const before = makeSnapshot([
299
+ makeNode({
300
+ nodeId: "root",
301
+ role: "RootWebArea",
302
+ ref: "l1",
303
+ children: [
304
+ makeNode({
305
+ nodeId: "docs",
306
+ role: "link",
307
+ name: "Docs",
308
+ ref: "l2",
309
+ attributes: { href: "https://example.test/docs" }
310
+ })
311
+ ]
312
+ })
313
+ ]);
314
+ const after = makeSnapshot([
315
+ makeNode({
316
+ nodeId: "root",
317
+ role: "RootWebArea",
318
+ ref: "l10",
319
+ children: [
320
+ makeNode({
321
+ nodeId: "docs",
322
+ role: "link",
323
+ name: "Docs",
324
+ ref: "l20",
325
+ attributes: { href: "https://example.test/docs" }
326
+ })
327
+ ]
328
+ })
329
+ ]);
330
+ const diff = diffSnapshots(before, after);
331
+ expect(renderSnapshotDiff(diff)).toBe("");
332
+ });
333
+ });
@@ -0,0 +1,40 @@
1
+ type SnapshotPrimitive = string | number | boolean | null;
2
+ type Snapshot = {
3
+ title: string;
4
+ url: string;
5
+ frames: SnapshotFrame[];
6
+ };
7
+ type SnapshotFrame = SnapshotAvailableFrame | SnapshotUnavailableFrame;
8
+ type SnapshotAvailableFrame = {
9
+ status: "ok";
10
+ id: string;
11
+ index: number;
12
+ url: string;
13
+ name: string | null;
14
+ parentId: string | null;
15
+ roots: SnapshotNode[];
16
+ };
17
+ type SnapshotUnavailableFrame = {
18
+ status: "unavailable";
19
+ id: string;
20
+ index: number;
21
+ url: string;
22
+ name: string | null;
23
+ parentId: string | null;
24
+ error: string;
25
+ };
26
+ type SnapshotNode = {
27
+ nodeId: string;
28
+ ignored: boolean;
29
+ role: string;
30
+ name: string | null;
31
+ value: SnapshotPrimitive;
32
+ description: string | null;
33
+ properties: Record<string, SnapshotPrimitive>;
34
+ attributes: Record<string, string>;
35
+ children: SnapshotNode[];
36
+ ref: string | null;
37
+ subtreeSize: number;
38
+ };
39
+
40
+ export type { Snapshot, SnapshotAvailableFrame, SnapshotFrame, SnapshotNode, SnapshotPrimitive, SnapshotUnavailableFrame };
File without changes
@@ -0,0 +1,17 @@
1
+ import { Page } from 'playwright';
2
+
3
+ type PageStabilityWaitOptions = {
4
+ timeoutMs?: number;
5
+ mutationIdleMs?: number;
6
+ minimumWaitMs?: number;
7
+ pollIntervalMs?: number;
8
+ };
9
+ type PageStabilityWaitResult = {
10
+ ok: boolean;
11
+ diagnostics: string[];
12
+ };
13
+ declare function preparePageStabilityWait(page: Page, options?: Pick<PageStabilityWaitOptions, "timeoutMs">): Promise<PageStabilityWaitResult>;
14
+ declare function waitForPageStable(page: Page, options?: PageStabilityWaitOptions): Promise<PageStabilityWaitResult>;
15
+ declare function installPageStabilityWaiter(): void;
16
+
17
+ export { type PageStabilityWaitOptions, type PageStabilityWaitResult, installPageStabilityWaiter, preparePageStabilityWait, waitForPageStable };
@@ -0,0 +1,281 @@
1
+ const DEFAULT_TIMEOUT_MS = 1e4;
2
+ const DEFAULT_MUTATION_IDLE_MS = 400;
3
+ const DEFAULT_MINIMUM_WAIT_MS = 800;
4
+ const DEFAULT_POLL_INTERVAL_MS = 100;
5
+ async function preparePageStabilityWait(page, options = {}) {
6
+ const diagnostic = await installBrowserStabilityWaiterOnPage(
7
+ page,
8
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
9
+ );
10
+ return {
11
+ ok: diagnostic === null,
12
+ diagnostics: diagnostic === null ? [] : [diagnostic]
13
+ };
14
+ }
15
+ async function waitForPageStable(page, options = {}) {
16
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17
+ const mutationIdleMs = options.mutationIdleMs ?? DEFAULT_MUTATION_IDLE_MS;
18
+ const minimumWaitMs = options.minimumWaitMs ?? DEFAULT_MINIMUM_WAIT_MS;
19
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
20
+ const deadline = Date.now() + timeoutMs;
21
+ const loadDiagnostics = await Promise.all([
22
+ waitForLoadState(page, "domcontentloaded", deadline),
23
+ waitForLoadState(page, "load", deadline)
24
+ ]);
25
+ const browserDiagnostic = await waitForBrowserStability(page, {
26
+ timeoutMs: Math.max(0, deadline - Date.now()),
27
+ mutationIdleMs,
28
+ minimumWaitMs,
29
+ pollIntervalMs
30
+ });
31
+ const diagnostics = [...loadDiagnostics, browserDiagnostic].filter(
32
+ (diagnostic) => diagnostic !== null
33
+ );
34
+ return { ok: diagnostics.length === 0, diagnostics };
35
+ }
36
+ async function waitForLoadState(page, state, deadline) {
37
+ const timeout = Math.max(0, deadline - Date.now());
38
+ if (timeout === 0) return `Timed out waiting for ${state}`;
39
+ try {
40
+ await page.waitForLoadState(state, { timeout });
41
+ return null;
42
+ } catch (error) {
43
+ return `Failed to wait for ${state}: ${errorMessage(error)}`;
44
+ }
45
+ }
46
+ async function waitForBrowserStability(page, args) {
47
+ const deadline = Date.now() + args.timeoutMs;
48
+ let lastError = null;
49
+ while (Date.now() < deadline) {
50
+ const installDiagnostic = await installBrowserStabilityWaiterOnPage(
51
+ page,
52
+ Math.max(0, deadline - Date.now())
53
+ );
54
+ if (installDiagnostic) return installDiagnostic;
55
+ try {
56
+ return await page.evaluate(runBrowserStabilityWait, {
57
+ ...args,
58
+ timeoutMs: Math.max(0, deadline - Date.now())
59
+ });
60
+ } catch (error) {
61
+ lastError = errorMessage(error);
62
+ if (!isRetryableExecutionContextError(lastError)) {
63
+ return `Failed to wait for page stability: ${lastError}`;
64
+ }
65
+ await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
66
+ }
67
+ }
68
+ return lastError ? `Failed to wait for page stability: ${lastError}` : "Timed out waiting for page stability";
69
+ }
70
+ async function installBrowserStabilityWaiterOnPage(page, timeoutMs) {
71
+ const deadline = Date.now() + timeoutMs;
72
+ let lastError = null;
73
+ while (Date.now() < deadline) {
74
+ try {
75
+ await page.evaluate(installPageStabilityWaiter);
76
+ return null;
77
+ } catch (error) {
78
+ lastError = errorMessage(error);
79
+ if (!isRetryableExecutionContextError(lastError)) {
80
+ return `Failed to install page stability waiter: ${lastError}`;
81
+ }
82
+ await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
83
+ }
84
+ }
85
+ return lastError ? `Failed to install page stability waiter: ${lastError}` : "Timed out installing page stability waiter";
86
+ }
87
+ function installPageStabilityWaiter() {
88
+ const symbol = /* @__PURE__ */ Symbol.for("libretto.pageStabilityWaiter");
89
+ const windowWithWaiter = window;
90
+ if (windowWithWaiter[symbol]) return;
91
+ const state = {
92
+ pendingRequests: 0,
93
+ pendingUrls: /* @__PURE__ */ new Set()
94
+ };
95
+ const requestStarted = (url) => {
96
+ state.pendingRequests += 1;
97
+ state.pendingUrls.add(url);
98
+ };
99
+ const requestFinished = (url) => {
100
+ state.pendingRequests = Math.max(0, state.pendingRequests - 1);
101
+ state.pendingUrls.delete(url);
102
+ };
103
+ const originalFetch = window.fetch;
104
+ if (typeof originalFetch === "function") {
105
+ window.fetch = function trackedFetch(input, init) {
106
+ const url = requestUrl(input);
107
+ requestStarted(url);
108
+ return originalFetch.call(this, input, init).finally(() => {
109
+ requestFinished(url);
110
+ });
111
+ };
112
+ }
113
+ const originalOpen = XMLHttpRequest.prototype.open;
114
+ const originalSend = XMLHttpRequest.prototype.send;
115
+ const requestUrls = /* @__PURE__ */ new WeakMap();
116
+ const startedRequests = /* @__PURE__ */ new WeakSet();
117
+ XMLHttpRequest.prototype.open = function trackedOpen(method, url, async, username, password) {
118
+ requestUrls.set(this, String(url));
119
+ return originalOpen.call(
120
+ this,
121
+ method,
122
+ url,
123
+ async ?? true,
124
+ username,
125
+ password
126
+ );
127
+ };
128
+ XMLHttpRequest.prototype.send = function trackedSend(body) {
129
+ const url = requestUrls.get(this) ?? "XMLHttpRequest";
130
+ requestStarted(url);
131
+ startedRequests.add(this);
132
+ const finish = () => {
133
+ if (!startedRequests.has(this)) return;
134
+ startedRequests.delete(this);
135
+ requestFinished(url);
136
+ };
137
+ this.addEventListener("loadend", finish, { once: true });
138
+ try {
139
+ return originalSend.call(this, body);
140
+ } catch (error) {
141
+ finish();
142
+ throw error;
143
+ }
144
+ };
145
+ const waitForStability = async (args) => {
146
+ const sleepInPage = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
147
+ const startedAt = Date.now();
148
+ let lastActivityAt = Date.now();
149
+ let lastResources = {
150
+ pendingResources: 0,
151
+ pendingResourceLabels: []
152
+ };
153
+ let lastPendingRequests = state.pendingRequests;
154
+ let lastPendingUrls = [...state.pendingUrls];
155
+ const markActivity = () => {
156
+ lastActivityAt = Date.now();
157
+ };
158
+ const observer = new MutationObserver(markActivity);
159
+ const root = document.documentElement ?? document.body;
160
+ if (root) {
161
+ observer.observe(root, {
162
+ attributes: true,
163
+ childList: true,
164
+ characterData: true,
165
+ subtree: true
166
+ });
167
+ }
168
+ try {
169
+ while (Date.now() - startedAt < args.timeoutMs) {
170
+ lastResources = countPendingResourceElements();
171
+ lastPendingRequests = state.pendingRequests;
172
+ lastPendingUrls = [...state.pendingUrls];
173
+ const pageLoaded = document.readyState === "complete";
174
+ const mutationIdle = Date.now() - lastActivityAt >= args.mutationIdleMs;
175
+ const waitedLongEnough = Date.now() - startedAt >= args.minimumWaitMs;
176
+ const requestIdle = lastPendingRequests === 0;
177
+ const resourceIdle = lastResources.pendingResources === 0;
178
+ if (pageLoaded && mutationIdle && waitedLongEnough && requestIdle && resourceIdle) {
179
+ return null;
180
+ }
181
+ await sleepInPage(args.pollIntervalMs);
182
+ }
183
+ } finally {
184
+ observer.disconnect();
185
+ }
186
+ return formatStabilityTimeout({
187
+ timeoutMs: args.timeoutMs,
188
+ readyState: document.readyState,
189
+ pendingRequests: lastPendingRequests,
190
+ pendingUrls: lastPendingUrls,
191
+ pendingResources: lastResources.pendingResources,
192
+ pendingResourceLabels: lastResources.pendingResourceLabels
193
+ });
194
+ };
195
+ Object.defineProperty(windowWithWaiter, symbol, {
196
+ value: { waitForStability },
197
+ configurable: false,
198
+ enumerable: false,
199
+ writable: false
200
+ });
201
+ function countPendingResourceElements() {
202
+ const elements = Array.from(
203
+ document.querySelectorAll(
204
+ 'img,video,audio,embed,object,iframe[src],link[rel="stylesheet"][href]'
205
+ )
206
+ );
207
+ let pendingResources = 0;
208
+ const pendingResourceLabels = [];
209
+ const markPending = (element) => {
210
+ pendingResources += 1;
211
+ if (pendingResourceLabels.length < 5) {
212
+ pendingResourceLabels.push(resourceLabel(element));
213
+ }
214
+ };
215
+ for (const element of elements) {
216
+ const tagName = element.tagName.toLowerCase();
217
+ if (tagName === "img") {
218
+ const image = element;
219
+ if (image.loading !== "lazy" && !image.complete) markPending(element);
220
+ continue;
221
+ }
222
+ if (tagName === "video" || tagName === "audio") {
223
+ const media = element;
224
+ if (media.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
225
+ markPending(element);
226
+ }
227
+ continue;
228
+ }
229
+ if (tagName === "iframe") {
230
+ const iframe = element;
231
+ try {
232
+ if (iframe.contentDocument && iframe.contentDocument.readyState !== "complete") {
233
+ markPending(element);
234
+ }
235
+ } catch {
236
+ }
237
+ continue;
238
+ }
239
+ if (tagName === "link") {
240
+ const link = element;
241
+ if (!link.sheet) markPending(element);
242
+ }
243
+ }
244
+ return { pendingResources, pendingResourceLabels };
245
+ }
246
+ function requestUrl(input) {
247
+ if (typeof input === "string") return input;
248
+ if (input instanceof URL) return input.href;
249
+ return input.url;
250
+ }
251
+ function resourceLabel(element) {
252
+ const tagName = element.tagName.toLowerCase();
253
+ const source = element.getAttribute("src") ?? element.getAttribute("href") ?? "";
254
+ return source ? `${tagName}:${source}` : tagName;
255
+ }
256
+ function formatStabilityTimeout(args) {
257
+ const urls = args.pendingUrls.slice(0, 5).join(", ");
258
+ const resources = args.pendingResourceLabels.join(", ");
259
+ return `Timed out waiting for page stability after ${args.timeoutMs}ms (readyState=${args.readyState}, pendingRequests=${args.pendingRequests}${urls ? `, pendingUrls=${urls}` : ""}, pendingResources=${args.pendingResources}${resources ? `, pendingResourceLabels=${resources}` : ""})`;
260
+ }
261
+ }
262
+ async function runBrowserStabilityWait(args) {
263
+ const symbol = /* @__PURE__ */ Symbol.for("libretto.pageStabilityWaiter");
264
+ const waiter = window[symbol];
265
+ if (!waiter) return "Page stability waiter was not installed.";
266
+ return waiter.waitForStability(args);
267
+ }
268
+ function sleep(ms) {
269
+ return new Promise((resolve) => setTimeout(resolve, ms));
270
+ }
271
+ function errorMessage(error) {
272
+ return error instanceof Error ? error.message : String(error);
273
+ }
274
+ function isRetryableExecutionContextError(message) {
275
+ return message.includes("Execution context was destroyed") || message.includes("Cannot find context with specified id") || message.includes("Most likely the page has been closed");
276
+ }
277
+ export {
278
+ installPageStabilityWaiter,
279
+ preparePageStabilityWait,
280
+ waitForPageStable
281
+ };
@@ -44,6 +44,7 @@ declare const SessionStateFileSchema: z.ZodObject<{
44
44
  width: z.ZodNumber;
45
45
  height: z.ZodNumber;
46
46
  }, z.core.$strip>>;
47
+ stayOpenOnSuccess: z.ZodOptional<z.ZodBoolean>;
47
48
  provider: z.ZodOptional<z.ZodObject<{
48
49
  name: z.ZodString;
49
50
  sessionId: z.ZodString;
@@ -27,6 +27,7 @@ const SessionStateFileSchema = z.object({
27
27
  status: SessionStatusSchema.optional(),
28
28
  mode: SessionAccessModeSchema.default("write-access"),
29
29
  viewport: SessionViewportSchema.optional(),
30
+ stayOpenOnSuccess: z.boolean().optional(),
30
31
  provider: ProviderStateSchema.optional(),
31
32
  daemonSocketPath: z.string().optional()
32
33
  });