@vosjs/render-core 0.7.0 → 0.9.0

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.
@@ -1,4 +1,5 @@
1
- import { RecordingMeta, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc } from '@vosjs/studio-core';
1
+ import { RecordingMeta, PeerStepSpan, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc, RecordingPageMedia } from '@vosjs/studio-core';
2
+ export { PeerStepSpan } from '@vosjs/studio-core';
2
3
 
3
4
  /**
4
5
  * The recorder's driver: what it needs from a browser, as a STRUCTURAL
@@ -54,6 +55,11 @@ interface RecorderApiResponse {
54
55
  headers(): Record<string, string>;
55
56
  body(): Promise<Uint8Array>;
56
57
  }
58
+ /** Which button a press is, and which press of a multi-click. */
59
+ interface RecorderButtonOpts {
60
+ button?: 'left' | 'right' | 'middle';
61
+ clickCount?: number;
62
+ }
57
63
  interface RecorderLocatorRoot {
58
64
  first(): RecorderLocator;
59
65
  }
@@ -66,14 +72,14 @@ interface RecorderConsoleMessage {
66
72
  }
67
73
  interface RecorderPage {
68
74
  goto(url: string, opts: {
69
- waitUntil: 'networkidle';
75
+ waitUntil: 'networkidle' | 'load';
70
76
  timeout: number;
71
77
  }): Promise<RecorderResponse | null>;
72
78
  url(): string;
73
79
  title(): Promise<string>;
74
80
  /** An `await` with `reload: true`; absent, the recorder opens the page's URL again. */
75
81
  reload?(opts: {
76
- waitUntil: 'networkidle';
82
+ waitUntil: 'networkidle' | 'load';
77
83
  timeout: number;
78
84
  }): Promise<RecorderResponse | null>;
79
85
  /** A `download` step's click; absent, the recorder says the browser reports none. */
@@ -91,13 +97,16 @@ interface RecorderPage {
91
97
  locator(selector: string): RecorderLocatorRoot;
92
98
  mouse: {
93
99
  move(x: number, y: number): Promise<void>;
94
- down(): Promise<void>;
95
- up(): Promise<void>;
100
+ down(opts?: RecorderButtonOpts): Promise<void>;
101
+ up(opts?: RecorderButtonOpts): Promise<void>;
96
102
  wheel(dx: number, dy: number): Promise<void>;
97
103
  };
98
104
  keyboard: {
99
105
  type(text: string): Promise<void>;
100
106
  press(key: string): Promise<void>;
107
+ /** A drag's modifiers, held from the press to the release. */
108
+ down(key: string): Promise<void>;
109
+ up(key: string): Promise<void>;
101
110
  };
102
111
  }
103
112
  interface RecorderCdp {
@@ -131,6 +140,13 @@ interface RecorderContext {
131
140
  */
132
141
  route?(pattern: string, handler: (route: RecorderRoute) => Promise<unknown> | unknown): Promise<unknown>;
133
142
  newCDPSession(page: unknown): Promise<RecorderCdp>;
143
+ /**
144
+ * A `paste` step's clipboard access, when the driver grants it; absent,
145
+ * the paste is dispatched in the page and the recorder says so.
146
+ */
147
+ grantPermissions?(permissions: string[], opts?: {
148
+ origin?: string;
149
+ }): Promise<unknown>;
134
150
  close(): Promise<void>;
135
151
  }
136
152
  interface RecorderBrowser {
@@ -162,6 +178,25 @@ interface FrameSink {
162
178
  interface OutputSink {
163
179
  save(name: string, bytes: Uint8Array, contentType?: string): Promise<void> | void;
164
180
  }
181
+ /**
182
+ * Where the sound a page played during the take is kept: the CLI writes
183
+ * `page-sound/<name>` in the take directory and answers that take-relative
184
+ * key, which the plan's audio clips name. A host that hands in none (the
185
+ * fleet) still records `meta.media`; the plan then places nothing.
186
+ */
187
+ interface SoundSink {
188
+ save(name: string, bytes: Uint8Array, contentType?: string): Promise<string> | string;
189
+ }
190
+ /**
191
+ * Where the files a script names beside itself come from: a drag's path
192
+ * file, a paste's file, by a plain name. The CLI reads the actions file's
193
+ * directory. A host that hands in none (a hosted recorder carries the
194
+ * script alone) cannot run such a step, and the recorder refuses it before
195
+ * capturing anything.
196
+ */
197
+ interface ScriptFiles {
198
+ read(name: string): Promise<Uint8Array>;
199
+ }
165
200
  /** The clock and the pauses, injectable so a rehearsal runs fast. */
166
201
  interface RecorderClock {
167
202
  now(): number;
@@ -200,6 +235,7 @@ declare const SETTLE_MS: {
200
235
  readonly click: 150;
201
236
  readonly type: 150;
202
237
  readonly press: 150;
238
+ readonly paste: 150;
203
239
  readonly scroll: 200;
204
240
  readonly drag: 80;
205
241
  };
@@ -437,6 +473,9 @@ declare class PageErrorLog {
437
473
  type ExposureKind = 'email' | 'key' | 'card' | 'card-tail';
438
474
  type Exposure = NonNullable<RecordingMeta['exposures']>[number];
439
475
  type MaskReport = NonNullable<RecordingMeta['masks']>[number];
476
+ type Notice = NonNullable<RecordingMeta['notices']>[number];
477
+ /** A notice as one scan reports it. */
478
+ type ScannedNotice = Omit<Notice, 'step' | 'seen'>;
440
479
  interface MaskRule {
441
480
  selector: string;
442
481
  /** `blur` (default) blurs the element; `text` swaps its words for `text`. */
@@ -456,12 +495,28 @@ interface MaskRule {
456
495
  * - card-tail: a masked number's visible tail (•••• 4242).
457
496
  */
458
497
  declare const EXPOSURE_MATCHERS_SRC = "(() => {\n const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g\n const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i\n const KEY = new RegExp([\n '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',\n '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',\n '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',\n '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',\n '\\\\bAKIA[0-9A-Z]{16}\\\\b',\n '\\\\bAIza[0-9A-Za-z_-]{30,}',\n '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',\n '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',\n ].join('|'))\n const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g\n const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/\n const luhn = (digits) => {\n let sum = 0, alt = false\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48\n if (alt) { d *= 2; if (d > 9) d -= 9 }\n sum += d; alt = !alt\n }\n return sum % 10 === 0\n }\n const kindsIn = (text) => {\n const kinds = []\n if (!text || text.length < 6) return kinds\n EMAIL.lastIndex = 0\n for (let m; (m = EMAIL.exec(text)); ) {\n if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }\n }\n if (KEY.test(text)) kinds.push('key')\n CARD.lastIndex = 0\n for (let m; (m = CARD.exec(text)); ) {\n const digits = m[1].replace(/[ -]/g, '')\n if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }\n }\n if (TAIL.test(text)) kinds.push('card-tail')\n return kinds\n }\n return { kindsIn }\n})()";
498
+ /**
499
+ * The INFORMATION the scan reports beside the exposures (never a failure):
500
+ * the signed-in account's own display name, wherever the frame shows it,
501
+ * and a list of the account's recent items. Runs inside EXPOSURE_PROBE,
502
+ * after `seen` and before any selector is made: it sets `acct`, which
503
+ * `quiet` then keeps out of every selector. Never `$` + `{` in here: it is
504
+ * spliced into a template.
505
+ *
506
+ * The name is read conservatively, from where apps say whose account it
507
+ * is: a line that greets or names the account ("Signed in as …",
508
+ * "Welcome back, …"), the label of an account menu ("Account menu for …"),
509
+ * or the words on the account menu's own trigger (the button that opens
510
+ * a menu and holds the avatar), which are SURE; or a name-like label on an avatar (an image's alt, a
511
+ * button holding the picture), which MAY be the name.
512
+ */
513
+ declare const NOTICE_SRC: string;
459
514
  /**
460
515
  * The scan: visible text in the viewport, plus the values of text inputs.
461
516
  * Skips anything inside a masked element, password fields, and text the eye
462
517
  * cannot see. Reports kind + a selector-ish name for the element + its rect.
463
518
  */
464
- declare const EXPOSURE_PROBE = "(() => {\n const { kindsIn } = (() => {\n const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g\n const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i\n const KEY = new RegExp([\n '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',\n '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',\n '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',\n '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',\n '\\\\bAKIA[0-9A-Z]{16}\\\\b',\n '\\\\bAIza[0-9A-Za-z_-]{30,}',\n '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',\n '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',\n ].join('|'))\n const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g\n const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/\n const luhn = (digits) => {\n let sum = 0, alt = false\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48\n if (alt) { d *= 2; if (d > 9) d -= 9 }\n sum += d; alt = !alt\n }\n return sum % 10 === 0\n }\n const kindsIn = (text) => {\n const kinds = []\n if (!text || text.length < 6) return kinds\n EMAIL.lastIndex = 0\n for (let m; (m = EMAIL.exec(text)); ) {\n if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }\n }\n if (KEY.test(text)) kinds.push('key')\n CARD.lastIndex = 0\n for (let m; (m = CARD.exec(text)); ) {\n const digits = m[1].replace(/[ -]/g, '')\n if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }\n }\n if (TAIL.test(text)) kinds.push('card-tail')\n return kinds\n }\n return { kindsIn }\n})()\n const vw = innerWidth, vh = innerHeight\n // A selector that reaches THIS element and no other, so it can be pasted\n // into \"mask\" as it is: climb until the path is unique, and say which\n // sibling wherever a tag-and-class alone would match several. What the\n // page wrote for people and tests comes first (an id, a test id, an\n // aria-label, a field's name or placeholder); utility classes are a\n // layout's, and they come last. A value that itself holds what the scan\n // looks for is never used: the selector would quote it.\n const attr = (v) => '\"' + v.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"') + '\"'\n const quiet = (v) => !!v && v.length <= 80 && !kindsIn(v).length\n const segment = (el) => {\n const tag = el.tagName.toLowerCase()\n if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id) && !/\\d{2,}/.test(el.id) && quiet(el.id)) return tag + '#' + el.id\n const tid = el.getAttribute('data-testid')\n if (quiet(tid)) return tag + '[data-testid=' + attr(tid) + ']'\n const label = el.getAttribute('aria-label')\n if (quiet(label)) return tag + '[aria-label=' + attr(label) + ']'\n if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) {\n const nm = el.getAttribute('name')\n if (quiet(nm)) return tag + '[name=' + attr(nm) + ']'\n const ph = el.getAttribute('placeholder')\n if (quiet(ph)) return tag + '[placeholder=' + attr(ph) + ']'\n }\n const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)\n let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')\n const p = el.parentElement\n if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)\n seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'\n return seg\n }\n const name = (el) => {\n const parts = []\n for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {\n parts.unshift(segment(cur))\n let n = 2\n try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}\n // a bare tag is unique HERE and nowhere else: give it its parent too\n const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])\n if ((n === 1 && !bare) || parts.length >= 5) break\n }\n return parts.join(' > ')\n }\n const seen = (el) => {\n if (el.closest('[data-vos-masked]')) return null\n const r = el.getBoundingClientRect()\n if (r.width < 2 || r.height < 2) return null\n if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null\n const s = getComputedStyle(el)\n if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null\n return r\n }\n const out = [], keys = new Set()\n const add = (el, text) => {\n const kinds = kindsIn(text)\n if (!kinds.length) return\n const r = seen(el)\n if (!r) return\n const selector = name(el)\n for (const kind of kinds) {\n const k = kind + '|' + selector\n if (keys.has(k) || out.length >= 40) continue\n keys.add(k)\n out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })\n }\n }\n const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)\n for (let n; (n = walker.nextNode()); ) {\n const el = n.parentElement\n if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue\n add(el, n.nodeValue)\n }\n for (const el of document.querySelectorAll('input, textarea')) {\n if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue\n add(el, el.value)\n }\n const hits = {}\n for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n\n return { exposures: out, maskHits: hits }\n})()";
519
+ declare const EXPOSURE_PROBE: string;
465
520
  /**
466
521
  * The mask, as an init script: it runs at document start on EVERY navigation,
467
522
  * so a masked value is hidden before the page's first paint, and a
@@ -488,6 +543,26 @@ declare class ExposureLog {
488
543
  /** One exposure, in words. `step` -1 is the page as it opened. */
489
544
  declare function exposureLine(e: Exposure): string;
490
545
  declare const EXPOSURE_ADVICE = "The recording shows these. Record from a demo account, or hide them before the camera rolls with \"mask\" in actions.json: { \"selector\": \"\u2026\", \"as\": \"text\", \"text\": \"demo@acme.test\" } swaps the words, \"as\": \"blur\" blurs. Then re-record.";
546
+ /**
547
+ * Accumulates the notices across a take: the account's name is ONE notice,
548
+ * its places gathered from every scan; a recent-items list is one notice
549
+ * per list. Each keeps the first step it was seen at.
550
+ */
551
+ declare class NoticeLog {
552
+ private byKey;
553
+ add(step: number, notices: ScannedNotice[] | undefined | null): void;
554
+ notices(): Notice[];
555
+ }
556
+ /**
557
+ * One notice, in words. The account's name is never printed: the line says
558
+ * where it showed, so each place can be masked.
559
+ */
560
+ declare function noticeLine(n: Notice): string;
561
+ /**
562
+ * What to do about the notices, in one line: the swap hint is about the
563
+ * account's name, so it is said only when the name was noticed.
564
+ */
565
+ declare function noticeAdvice(notices: readonly Pick<Notice, 'kind'>[]): string;
491
566
 
492
567
  /**
493
568
  * `setup`: the steps that run BEFORE the camera rolls. A sign-in form, a
@@ -567,6 +642,8 @@ declare function runSetup(page: RecorderPage, steps: SetupStep[], opts: {
567
642
  sleep: (ms: number) => Promise<void>;
568
643
  /** Absent means no env at all: a `{ env }` text then fails in words. */
569
644
  env?: Record<string, string | undefined>;
645
+ /** How a `goto` opens its page and waits for it (the take's ready mode); absent, network idle. */
646
+ goto?: (url: string) => Promise<unknown>;
570
647
  }): Promise<SetupResult>;
571
648
  /** A `{ env }` the shell did not provide: a usage error, not a page's. */
572
649
  declare class SetupEnvError extends Error {
@@ -580,6 +657,494 @@ declare class SetupError extends Error {
580
657
  /** `--header name=value`, repeatable: extra request headers on every request. */
581
658
  declare function parseHeaders(given: string[] | undefined): Record<string, string>;
582
659
 
660
+ /**
661
+ * When a page counts as ready for the take to begin. The default is the
662
+ * DOM: after `load`, the page must make no DOM change for a beat (counted
663
+ * from outside, the way an `await` reload's settle is), at most a few
664
+ * seconds. Network idle is the opt-in: plenty of real apps hold a socket
665
+ * open or keep an analytics beacon going and never go network-idle, so a
666
+ * take that waited for it started after its whole timeout, every time.
667
+ */
668
+
669
+ /** How a page counts as ready: its DOM settled (default), or its network went idle. */
670
+ type ReadyMode = 'networkidle' | 'dom';
671
+ declare const READY_MODES: readonly ReadyMode[];
672
+ declare const DEFAULT_READY: ReadyMode;
673
+ /** The longest a navigation waits for `load` or network idle. */
674
+ declare const NAV_TIMEOUT_MS = 45000;
675
+ /** The longest the settle waits on a page that never stops changing. */
676
+ declare const READY_SETTLE_CAP_MS = 8000;
677
+ /** After network idle, the beat a hydration takes (what the recorder always gave it). */
678
+ declare const NETWORKIDLE_HYDRATION_MS = 800;
679
+ /** How the first load settled: the done event's `ready`. */
680
+ interface ReadyReport {
681
+ mode: ReadyMode;
682
+ /** ms from the navigation's start until the page counted as ready */
683
+ ms: number;
684
+ /**
685
+ * The wait gave up instead of the page settling: the DOM never stopped
686
+ * changing within the cap (dom), or network idle never came within
687
+ * the navigation timeout (networkidle). Recorded anyway either way.
688
+ */
689
+ capped: boolean;
690
+ }
691
+ /** Why a ready mode is not one, or null. */
692
+ declare function readyProblem(v: unknown): string | null;
693
+ interface ReadyOpts {
694
+ sleep: (ms: number) => Promise<void>;
695
+ now?: () => number;
696
+ /** the settle's cap (default `READY_SETTLE_CAP_MS`) */
697
+ capMs?: number;
698
+ /** the navigation's own timeout (default `NAV_TIMEOUT_MS`) */
699
+ timeoutMs?: number;
700
+ }
701
+ /**
702
+ * Wait for a page that is navigating (or has just navigated) to be ready:
703
+ * `navigate` starts the navigation with the `waitUntil` the mode asks for;
704
+ * in `dom` mode the DOM settle follows `load`. A navigation that times
705
+ * out is not fatal: the take goes on from whatever the page shows, and the
706
+ * report says it was capped.
707
+ */
708
+ declare function untilReady(page: RecorderPage, mode: ReadyMode, navigate: (opts: {
709
+ waitUntil: 'networkidle' | 'load';
710
+ timeout: number;
711
+ }) => Promise<RecorderResponse | null>, opts: ReadyOpts): Promise<{
712
+ response: RecorderResponse | null;
713
+ ready: ReadyReport;
714
+ }>;
715
+ /**
716
+ * An `await`'s reload between polls, under the take's ready mode: the
717
+ * reload waits for `load` (dom) or network idle (networkidle), at most
718
+ * the await's own timeout, and then the DOM settles either way, because
719
+ * network quiet is not a drawn page (a list rendered from its data lands
720
+ * after it). A page that never goes network-idle no longer costs a whole
721
+ * navigation timeout per poll.
722
+ */
723
+ declare function reloadUntilReady(page: RecorderPage, mode: ReadyMode, timeoutMs: number, opts?: {
724
+ sleep?: (ms: number) => Promise<void>;
725
+ }): Promise<void>;
726
+ /** The ready report in words, for the take's log. */
727
+ declare function readyLine(r: ReadyReport): string;
728
+
729
+ /**
730
+ * The canvas gestures, the pure parts: key chords and sequences, the
731
+ * modifiers a drag holds, a drawn path and its timing, a click's count and
732
+ * button, and the overrun report. What a whiteboard, a design tool or a
733
+ * timeline is recorded with, said as data so a script can carry it and
734
+ * both hosts validate it in the same words.
735
+ *
736
+ * None of it reaches the cursor track as key identity: a key is a typing
737
+ * ping (when and where), a modifier is nothing at all, and a path is its
738
+ * points.
739
+ */
740
+ /** The modifiers a chord or a drag may hold. `Mod` is Meta on macOS, Control elsewhere. */
741
+ declare const MODIFIERS: readonly ["Shift", "Control", "Alt", "Meta", "Mod", "ControlOrMeta"];
742
+ type Modifier = (typeof MODIFIERS)[number];
743
+ /** The modifiers a drag names (the platform-neutral one is `Mod`). */
744
+ declare const DRAG_MODIFIERS: readonly ["Alt", "Shift", "Meta", "Control", "Mod"];
745
+ type DragModifier = (typeof DRAG_MODIFIERS)[number];
746
+ /** A key name the browser's keyboard answers: a character, a code, a named key. */
747
+ declare function isKeyName(k: string): boolean;
748
+ /**
749
+ * A chord split into its modifiers and its key: `Shift+D` is
750
+ * `{ mods: ['Shift'], key: 'D' }`, a bare `r` has no modifiers, and `+`
751
+ * names the plus key when it ends the chord (`Shift++`).
752
+ */
753
+ declare function splitChord(chord: string): {
754
+ mods: string[];
755
+ key: string;
756
+ };
757
+ /** Why a chord is not one the recorder can press, or null. */
758
+ declare function chordProblem(chord: unknown): string | null;
759
+ /**
760
+ * A chord as the browser's keyboard takes it: `Mod` becomes
761
+ * `ControlOrMeta`, which the browser resolves to Meta on macOS and to
762
+ * Control elsewhere, where the browser runs.
763
+ */
764
+ declare function resolveChord(chord: string): string;
765
+ /** A `press` step's keys as a list: one chord or a sequence. */
766
+ declare function pressSequence(step: {
767
+ key?: string;
768
+ keys?: string | string[];
769
+ }): string[];
770
+ /** The pause between two keys of a sequence, a person's hand: ms. */
771
+ declare const KEY_SEQUENCE_GAP_MS = 150;
772
+ /**
773
+ * What one key costs beyond the script's gaps: the browser's round trip for
774
+ * the press (a keydown and a keyup the page answers). Part of the plan, so a
775
+ * sequence on an idle page is never "slower than scripted".
776
+ */
777
+ declare const KEY_ROUNDTRIP_MS = 50;
778
+ /** A key sequence's plan, ms: its gaps, and each key's round trip. */
779
+ declare const keySequencePlanMs: (keys: number) => number;
780
+ /** The hold after a drag's release, ms: the recorder's, part of the gesture. */
781
+ declare const DRAG_HOLD_MS = 400;
782
+ /** The most keys one press step may name. */
783
+ declare const MAX_KEYS = 50;
784
+ /** A drag modifier as the keyboard holds it. */
785
+ declare function resolveModifier(m: DragModifier): string;
786
+ /**
787
+ * The modifier bits of a raw pointer event (the pen path): Alt 1, Control
788
+ * 2, Meta 4, Shift 8, with `Mod` read for the platform the browser runs on.
789
+ */
790
+ declare function modifierBits(mods: readonly DragModifier[], platform: 'mac' | 'linux' | 'windows' | undefined): number;
791
+ /** A click's button, and the pointer button number the cursor track records. */
792
+ declare const CLICK_BUTTONS: readonly ["left", "right", "middle"];
793
+ type ClickButton = (typeof CLICK_BUTTONS)[number];
794
+ declare const buttonNumber: (b: ClickButton | undefined) => 0 | 1 | 2;
795
+ /**
796
+ * The pause between the presses of a double or triple click, well inside
797
+ * every platform's double-click interval (500 ms on macOS and Windows by
798
+ * default), so the page reads one gesture.
799
+ */
800
+ declare const MULTI_CLICK_GAP_MS = 90;
801
+ /**
802
+ * One point of a drawn path: viewport CSS px, with an optional time in ms
803
+ * (from the path's start) and an optional pressure in [0, 1]. Written as a
804
+ * tuple `[x, y]`, `[x, y, t]`, `[x, y, t, pressure]`, or an object.
805
+ */
806
+ type PathPoint = [number, number] | [number, number, number] | [number, number, number, number] | {
807
+ x: number;
808
+ y: number;
809
+ t?: number;
810
+ pressure?: number;
811
+ };
812
+ /** A path point as the recorder replays it. */
813
+ interface PathSample {
814
+ x: number;
815
+ y: number;
816
+ /** ms from the path's first point */
817
+ at: number;
818
+ pressure?: number;
819
+ }
820
+ /** The most points one path may hold. */
821
+ declare const MAX_PATH_POINTS = 20000;
822
+ /** An untimed path's travel when the step names no ms. */
823
+ declare const PATH_DEFAULT_MS = 700;
824
+ /**
825
+ * A path's points read and checked, or the problems in words. The same
826
+ * reader takes an inline `path` and the contents of a path file (an array
827
+ * of points, or `{ "points": [...] }`).
828
+ */
829
+ declare function readPath(raw: unknown): {
830
+ points: PathSample[];
831
+ timed: boolean;
832
+ } | {
833
+ problems: string[];
834
+ };
835
+ /**
836
+ * The path's schedule: a timed path keeps its own times (from its first
837
+ * point), an untimed one spreads its points evenly over `ms` (default
838
+ * 700). The last point is at the path's whole duration.
839
+ */
840
+ declare function schedulePath(path: {
841
+ points: PathSample[];
842
+ timed: boolean;
843
+ }, ms: number | undefined): PathSample[];
844
+ /** How long a scheduled path takes, ms. */
845
+ declare const pathDurationMs: (s: readonly PathSample[]) => number;
846
+ /**
847
+ * A paced gesture that ran past its script: the wall time it took against
848
+ * the time the script planned for it (a drag's travel, a path's timing,
849
+ * typing's characters × delay, a key sequence's gaps). Reported when the
850
+ * gesture took more than a fifth longer, the sign of a page that could not
851
+ * keep up with what it was sent.
852
+ */
853
+ interface StepOverrun {
854
+ step: number;
855
+ id?: string;
856
+ do: string;
857
+ plannedMs: number;
858
+ tookMs: number;
859
+ overrunMs: number;
860
+ }
861
+ /** The share past the plan that counts as an overrun. */
862
+ declare const OVERRUN_SHARE = 0.2;
863
+ /** A plan shorter than this is not measured: its noise is the round trip. */
864
+ declare const OVERRUN_MIN_PLANNED_MS = 100;
865
+ /**
866
+ * The least a gesture must run over to count, ms: below it the difference
867
+ * is one slow sample or round trip on a busy machine, never a page that
868
+ * fell behind, however short the gesture.
869
+ */
870
+ declare const OVERRUN_MIN_MS = 100;
871
+ /** The overrun, ms, or null when the gesture kept to its plan. */
872
+ declare function overrunOf(plannedMs: number, tookMs: number): number | null;
873
+ /** The overruns in one line for the log and the done event, or null when none. */
874
+ declare function overrunLine(overruns: readonly StepOverrun[]): string | null;
875
+
876
+ /**
877
+ * What an `expect` and an `await` wait for: a condition on the page, read
878
+ * without touching it (no cursor, no input), polled until it holds or the
879
+ * time runs out. `expect` polls on camera for a few seconds; `await` polls
880
+ * off camera for as long as a slow result takes. One reader for both, so
881
+ * "appeared" means the same thing in either.
882
+ */
883
+
884
+ interface WaitCondition {
885
+ /** an element matching this selector is visible */
886
+ selector?: string;
887
+ /** some visible text on the page contains this */
888
+ text?: string;
889
+ /** no element matching this selector is visible */
890
+ gone?: string;
891
+ /** with `selector`: the element is enabled (true) or disabled (false) */
892
+ enabled?: boolean;
893
+ /**
894
+ * A JavaScript expression evaluated in the page (awaited when it answers
895
+ * a promise): the condition holds when its value is truthy, or, with
896
+ * `equals`, when its value equals that one. For an app that keeps its
897
+ * state in script rather than in the DOM (a canvas, a document model).
898
+ */
899
+ js?: string;
900
+ /** with `js`: the value the expression must equal (compared as JSON) */
901
+ equals?: unknown;
902
+ }
903
+ /** What the condition found the last time it looked: what a miss is said from. */
904
+ interface ConditionState {
905
+ holds: boolean;
906
+ /** with `selector`: whether a match was visible */
907
+ visible?: boolean;
908
+ /**
909
+ * with `js`: what the expression answered the last time it was read, as
910
+ * JSON when it has a JSON form (absent for `undefined`), and in words
911
+ * either way (`said`), or the error it threw (`threw`).
912
+ */
913
+ value?: unknown;
914
+ said?: string;
915
+ threw?: string;
916
+ }
917
+ /**
918
+ * The visible text of the page contains `text`: `innerText` is what the
919
+ * page renders, so a hidden element's words never count.
920
+ */
921
+ declare function visibleTextScript(text: string): string;
922
+ /** The longest a js value is said in a line, in characters. */
923
+ declare const JS_SAID_MAX = 120;
924
+ /** The most a js value kept on the step holds, in characters of JSON. */
925
+ declare const JS_VALUE_MAX = 2048;
926
+ /**
927
+ * The page script that evaluates a step's expression and answers what it
928
+ * found, as a string (the one evaluate shape every driver shares). The
929
+ * expression is the script author's own, run on their own page: it is
930
+ * wrapped, never parsed, and a newline closes it before the paren so a
931
+ * trailing line comment cannot swallow the wrapper.
932
+ */
933
+ declare function jsConditionScript(expr: string): string;
934
+ /** Two JSON values alike: the same keys and values whatever the key order. */
935
+ declare function sameJson(a: unknown, b: unknown): boolean;
936
+ /**
937
+ * A js value as a step keeps it (`meta.steps[].value`): the value itself
938
+ * while its JSON fits, else its JSON cut short as a string.
939
+ */
940
+ declare function keptValue(value: unknown): unknown;
941
+ /** Read the condition once. A page mid-navigation reads as "not yet". */
942
+ declare function readCondition(page: RecorderPage, c: WaitCondition): Promise<ConditionState>;
943
+ /** What a poll found: whether the condition held, after how long, and the last read. */
944
+ interface PollResult {
945
+ ok: boolean;
946
+ ms: number;
947
+ last: ConditionState;
948
+ }
949
+ /**
950
+ * Poll the condition every `everyMs` until it holds or `timeoutMs` pass.
951
+ * `between` runs before each look but the first (an `await`'s reload).
952
+ * With `confirm`, a hold counts only once it has held on that many reads
953
+ * in a row, `confirm.gapMs` apart: a page that renders after network quiet
954
+ * reads half-built for a moment, and a `gone` condition holds on a list
955
+ * that has not been drawn yet. The clock is the wall's: a rehearsal waits
956
+ * as long as a take would, because how long a result takes is what a
957
+ * rehearsal is for.
958
+ */
959
+ declare function pollCondition(page: RecorderPage, c: WaitCondition, opts: {
960
+ timeoutMs: number;
961
+ everyMs: number;
962
+ sleep: (ms: number) => Promise<void>;
963
+ now?: () => number;
964
+ between?: () => Promise<void>;
965
+ confirm?: {
966
+ reads: number;
967
+ gapMs: number;
968
+ };
969
+ }): Promise<PollResult>;
970
+ /** The DOM counts as settled after this long with no change. */
971
+ declare const DOM_QUIET_MS = 800;
972
+ /** The longest a settle waits on a page that never stops changing. */
973
+ declare const DOM_SETTLE_CAP_MS = 5000;
974
+ /**
975
+ * The page's own count of DOM changes: an observer installed once per
976
+ * document, read back as a number. A string, the one evaluate shape every
977
+ * driver shares.
978
+ */
979
+ declare const DOM_CHANGE_PROBE = "(() => {\n if (!window.__vosDomChanges) {\n window.__vosDomChanges = { n: 0 };\n new MutationObserver((list) => { window.__vosDomChanges.n += list.length })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n }\n return window.__vosDomChanges.n;\n})()";
980
+ /**
981
+ * Wait until the DOM has not changed for `quietMs`, at most `capMs`: what
982
+ * a page does after its network went quiet (a list rendered from data, a
983
+ * hydration). The count is polled from here rather than timed in the
984
+ * page, because a background tab throttles its own timers.
985
+ */
986
+ declare function domSettled(page: RecorderPage, opts: {
987
+ sleep: (ms: number) => Promise<void>;
988
+ now?: () => number;
989
+ quietMs?: number;
990
+ capMs?: number;
991
+ tickMs?: number;
992
+ }): Promise<{
993
+ settled: boolean;
994
+ ms: number;
995
+ }>;
996
+ /** Seconds as the recorder says them: `5 s`, `2.5 s`, `3 min 20 s`. */
997
+ declare function sayDuration(ms: number): string;
998
+ /** What the condition waits for, in words: `'#result' to appear`. */
999
+ declare function sayCondition(c: WaitCondition): string;
1000
+ /** Why the condition did not hold, in words: `'Generate' stayed disabled for 5 s`. */
1001
+ declare function conditionMiss(c: WaitCondition, last: ConditionState, ms: number): string;
1002
+ /** An expression as a line names it: in backticks, cut short. */
1003
+ declare function sayJs(expr: string): string;
1004
+ /** A kept value as a line says it: its JSON, cut short. */
1005
+ declare function sayValue(value: unknown): string;
1006
+
1007
+ /**
1008
+ * `peers`: other people on the same page, off camera. A collaboration
1009
+ * product shows best with a second person on the canvas: their cursor, the
1010
+ * shape they drew, the comment they left, arriving in the filmed page the
1011
+ * way the app syncs them. A peer is a second browser context (its own
1012
+ * cookies, so its own user, from its own storage state or none) on the
1013
+ * same URL, driven by its own step list, interleaved with the take's.
1014
+ *
1015
+ * What a peer is, and is not:
1016
+ *
1017
+ * - NOT FILMED. Only the take's own page is captured. A peer's page is
1018
+ * never screencast, scanned, masked or listened to; what it does reaches
1019
+ * the footage only through the app (its sync, its presence, its remote
1020
+ * cursors), which is what a viewer would see.
1021
+ * - NOT ON THE CURSOR TRACK. A peer's pointer moves its own page; the take's
1022
+ * CursorTrack is synthesized from the take's own steps alone, so a zoom,
1023
+ * a click ring or a dwell is never planned on a peer's gesture.
1024
+ * - ORDERED BY THE TAKE'S STEPS. A peer step may name a take step by id:
1025
+ * `with` starts it as that step starts (the two run together), `after` as
1026
+ * that step ends. A step naming neither follows the peer's previous step
1027
+ * at once; a peer's first step with neither starts as the camera rolls.
1028
+ * The take never waits on a peer: it waits on what the peer's work
1029
+ * SHOWS, with an `expect` or an `await` on its own page, so the footage
1030
+ * holds exactly the moment the sync landed. When the take's steps end,
1031
+ * a peer step still waiting on a trigger never runs (said), and one
1032
+ * already running finishes, bounded, before the trailing hold.
1033
+ * - PACED, NOT INSTANT. A peer's pointer glides on the same clock as the
1034
+ * take's (an app that draws remote cursors shows it travelling) and its
1035
+ * typing is paced per key, so what it does reads as a person.
1036
+ * - BOUNDED. Every peer step runs inside its own bound, as a take step
1037
+ * does; a selector that never appears or a check that never holds is a
1038
+ * skip with its reason, listed with the take's (`--strict` fails on it).
1039
+ *
1040
+ * A peer takes the take's gestures and checks (`wait`, `move`, `hover`,
1041
+ * `click`, `type`, `press`, `scroll`, `drag` with an inline `path`, and
1042
+ * `expect`), never a `paste`, a `download`, an `await` (a peer that must
1043
+ * wait for a result is the take's `await`), a guard, a `read`, a `caption`
1044
+ * or `settle`, which all belong to the filmed page.
1045
+ */
1046
+
1047
+ /** One other person on the page: their own context, their own steps. */
1048
+ interface Peer {
1049
+ /** How the peer is named in lines and in `meta.peers`: a short word. */
1050
+ id: string;
1051
+ /** The page the peer opens; absent, the take's own `url`. */
1052
+ url?: string;
1053
+ /** The peer's viewport in CSS px; absent, the take's. */
1054
+ viewport?: {
1055
+ width: number;
1056
+ height: number;
1057
+ };
1058
+ /**
1059
+ * A Playwright storage state the peer's context is made from: the second
1060
+ * person's own sign-in. The host resolves the name (the CLI: a path from
1061
+ * the actions file's directory) and never sends the file anywhere.
1062
+ * Absent, the peer is signed out.
1063
+ */
1064
+ storageState?: string;
1065
+ steps: PeerStep[];
1066
+ }
1067
+ /** A peer's step: one of the take's gestures or checks, and when it starts. */
1068
+ type PeerStep = ActionStep & {
1069
+ /**
1070
+ * Start as the take's step with this id starts: the two run together.
1071
+ * (`with`, never `at`: a `type` step's `at` is where its caret goes.)
1072
+ */
1073
+ with?: string;
1074
+ /** Start as the take's step with this id ends. */
1075
+ after?: string;
1076
+ };
1077
+ /** The verbs a peer takes: the gestures and the on-page check. */
1078
+ declare const PEER_VERBS: readonly ["wait", "move", "hover", "click", "type", "press", "scroll", "drag", "expect"];
1079
+ /** The most peers a take may have: each is a whole browser context. */
1080
+ declare const MAX_PEERS = 4;
1081
+ /** The most steps one peer may take. */
1082
+ declare const MAX_PEER_STEPS = 200;
1083
+ /**
1084
+ * Structural validation of `peers`, given the take's own steps (a trigger
1085
+ * names one of their ids). `validate` is the take's step validator, run on
1086
+ * each peer's list so a peer step is checked in exactly the take's words.
1087
+ */
1088
+ declare function validatePeers(value: unknown, takeSteps: unknown, validate: (steps: unknown[]) => string[]): string[];
1089
+ /** What a hosted recorder says of a script with peers, or null. */
1090
+ declare function peersHostedRefusal(peers: unknown): string | null;
1091
+ /**
1092
+ * The take's step boundaries, as promises a peer step waits on. The take
1093
+ * calls `begin` as the camera rolls, `start`/`end` around each step and
1094
+ * `close` once its steps are over; a trigger still pending then resolves
1095
+ * false and its step never runs.
1096
+ */
1097
+ declare class StepSignals {
1098
+ private readonly started;
1099
+ private readonly ended;
1100
+ private readonly began;
1101
+ private closed;
1102
+ constructor(ids: Iterable<string>);
1103
+ begin(): void;
1104
+ start(id: string | undefined): void;
1105
+ end(id: string | undefined): void;
1106
+ close(): void;
1107
+ get isClosed(): boolean;
1108
+ /** When a step with this trigger may start: true, or false for never. */
1109
+ when(trigger: {
1110
+ with?: string;
1111
+ after?: string;
1112
+ }): {
1113
+ ready: Promise<boolean>;
1114
+ already: boolean;
1115
+ };
1116
+ }
1117
+
1118
+ /** The most a peer step's kept value holds, in characters of JSON (as a take step's). */
1119
+ declare const PEER_VALUE_MAX = 2048;
1120
+ /**
1121
+ * What a peer's check last answered, kept on its step the way the take's
1122
+ * own steps keep theirs (`meta.steps[].value`): the value itself while its
1123
+ * JSON fits, else that JSON cut short as a string; absent when the read
1124
+ * answered nothing JSON can hold, or no value at all (a check that only
1125
+ * holds or not).
1126
+ */
1127
+ declare function peerValue(last: ConditionState): unknown;
1128
+ /** What a peer did: its steps as they ran, and the ones that never did. */
1129
+ interface PeerRun {
1130
+ id: string;
1131
+ steps: PeerStepSpan[];
1132
+ /** Steps whose trigger never fired before the take's steps were over. */
1133
+ notRun: number[];
1134
+ }
1135
+ /**
1136
+ * Drive one peer's steps on its page, each when its trigger fires. Never
1137
+ * throws for a page's reasons: a miss is a skip on the run.
1138
+ */
1139
+ declare function runPeer(peer: Peer, rawPage: RecorderPage, signals: StepSignals, opts: {
1140
+ clock: RecorderClock;
1141
+ /** The take's footage clock, ms: what a span is placed by. */
1142
+ footageMs: () => number;
1143
+ log: (msg: string) => void;
1144
+ /** A rehearsal: no travel, every pause cut short. */
1145
+ dry?: boolean;
1146
+ }): Promise<PeerRun>;
1147
+
583
1148
  /**
584
1149
  * The action script — the declarative recipe an agent (or human) writes to
585
1150
  * drive a take. Small on purpose: selectors + a handful of verbs. The recorder
@@ -611,21 +1176,60 @@ interface ActionsFile {
611
1176
  * or stored.
612
1177
  */
613
1178
  setup?: SetupStep[];
1179
+ /**
1180
+ * When the first page counts as ready for step 1: `dom` (default),
1181
+ * once its DOM has made no change for a beat after `load`, at most 8 s;
1182
+ * `networkidle`, once the network has gone quiet, at most 45 s, which a
1183
+ * page holding a socket open never does. `--ready` overrides.
1184
+ */
1185
+ ready?: ReadyMode;
1186
+ /**
1187
+ * Every gesture waits on the app after it (`settle` on a step overrides):
1188
+ * a canvas that falls behind under load finishes the last stroke before
1189
+ * the next one starts.
1190
+ */
1191
+ settle?: boolean;
614
1192
  steps: ActionStep[];
1193
+ /**
1194
+ * Other people on the page, OFF CAMERA: each a browser context of its
1195
+ * own (its own storage state, so its own user) on the same `url`, driven
1196
+ * by its own steps, each started `with` a take step or `after` one (by
1197
+ * id). Only the take's page is filmed, and only its own steps make the
1198
+ * cursor track; a peer's work reaches the footage through the app's own
1199
+ * sync. A hosted take refuses peers.
1200
+ */
1201
+ peers?: Peer[];
615
1202
  }
616
1203
  type ActionStep = ({
617
1204
  do: 'wait';
618
1205
  ms: number;
619
- } | {
1206
+ }
1207
+ /**
1208
+ * Rest the pointer on `selector`'s centre, or on the viewport point
1209
+ * (`x`, `y`) when no selector names the spot (a place on a canvas).
1210
+ */
1211
+ | {
620
1212
  do: 'hover';
621
- selector: string;
1213
+ selector?: string;
1214
+ x?: number;
1215
+ y?: number;
622
1216
  ms?: number;
623
1217
  }
624
- /** `ms` is the settle after the press (default 150). */
1218
+ /**
1219
+ * Press `selector`'s centre, or the viewport point (`x`, `y`). `ms` is
1220
+ * the settle after the press (default 150). `count` 2 or 3 is a real
1221
+ * double or triple click; `button` presses the right or middle one;
1222
+ * `modifiers` are held for the press (a Shift-click) and released after.
1223
+ */
625
1224
  | {
626
1225
  do: 'click';
627
- selector: string;
1226
+ selector?: string;
1227
+ x?: number;
1228
+ y?: number;
628
1229
  ms?: number;
1230
+ count?: 1 | 2 | 3;
1231
+ button?: ClickButton;
1232
+ modifiers?: DragModifier[];
629
1233
  }
630
1234
  /**
631
1235
  * Type into `selector`. The recorder clicks the field first, which is what
@@ -655,14 +1259,18 @@ type ActionStep = ({
655
1259
  ms?: number;
656
1260
  }
657
1261
  /**
658
- * One named key, recorded as a typing ping where the pointer is (or on
659
- * `selector` when given, with no click): Escape closes what a click
660
- * opened, Enter submits what was typed, Tab moves on, an arrow nudges.
661
- * Never a character: words are typed with `type`.
1262
+ * A key, a chord or a sequence, each recorded as a typing ping where the
1263
+ * pointer is (or on `selector` when given, with no click), never as the
1264
+ * key itself. `key` names one control key (Escape closes what a click
1265
+ * opened, Enter submits what was typed); `keys` names a chord
1266
+ * (`"Shift+D"`, `"Mod+Z"`, with `Mod` Meta on macOS and Control
1267
+ * elsewhere), a plain key (`"r"`, a tool's shortcut) or a sequence
1268
+ * (`["v", "Escape"]`). Words are typed with `type`.
662
1269
  */
663
1270
  | {
664
1271
  do: 'press';
665
- key: PressKey;
1272
+ key?: PressKey;
1273
+ keys?: string | string[];
666
1274
  selector?: string;
667
1275
  ms?: number;
668
1276
  }
@@ -680,22 +1288,43 @@ type ActionStep = ({
680
1288
  * Press-move-release — real edits (drag an element on the stage canvas,
681
1289
  * slide a range input, move a timeline clip). Start = the selector's center
682
1290
  * when given, else (x, y); end = (tx, ty); eased over ms (default 700).
1291
+ * `path` draws instead: viewport points (inline, or a file beside the
1292
+ * actions file), the press at the first and the release at the last,
1293
+ * replayed by the points' own times or evenly over ms. `modifiers` are
1294
+ * held for the whole drag.
683
1295
  */
684
1296
  | {
685
1297
  do: 'drag';
686
1298
  selector?: string;
687
1299
  x?: number;
688
1300
  y?: number;
689
- tx: number;
690
- ty: number;
1301
+ tx?: number;
1302
+ ty?: number;
1303
+ path?: PathPoint[] | string;
1304
+ modifiers?: DragModifier[];
1305
+ ms?: number;
1306
+ }
1307
+ /**
1308
+ * Put words or a file (an image, an SVG) on the clipboard and paste it
1309
+ * the way a person's ⌘V does, into `selector` (clicked first) or where
1310
+ * the focus is. A file is read beside the actions file, by a plain name.
1311
+ */
1312
+ | {
1313
+ do: 'paste';
1314
+ selector?: string;
1315
+ text?: string;
1316
+ file?: string;
691
1317
  ms?: number;
692
1318
  }
693
1319
  /**
694
1320
  * Check that the page is as the flow expects, with no cursor move: an
695
1321
  * element visible (`selector`; with `enabled`, enabled or disabled), some
696
- * visible text containing `text`, or `gone` no longer visible. Exactly
697
- * one of the three. Waits up to `timeout` seconds (default 5); a
698
- * condition that never holds is a skip with its reason.
1322
+ * visible text containing `text`, `gone` no longer visible, or `js`, an
1323
+ * expression evaluated in the page that holds when its value is truthy
1324
+ * (with `equals`, when its value equals that one): the state a canvas
1325
+ * app keeps in script, never in the DOM. Exactly one of the four. Waits
1326
+ * up to `timeout` seconds (default 5); a condition that never holds is a
1327
+ * skip with its reason, which names the value a `js` last answered.
699
1328
  */
700
1329
  | {
701
1330
  do: 'expect';
@@ -703,6 +1332,8 @@ type ActionStep = ({
703
1332
  text?: string;
704
1333
  gone?: string;
705
1334
  enabled?: boolean;
1335
+ js?: string;
1336
+ equals?: unknown;
706
1337
  timeout?: number;
707
1338
  }
708
1339
  /**
@@ -755,6 +1386,15 @@ type ActionStep = ({
755
1386
  * `vos actions script` leads the beat with it.
756
1387
  */
757
1388
  caption?: string;
1389
+ /**
1390
+ * The cut's camera on this step: `true` marks the step as its beat's
1391
+ * payoff, the moment worth the zoom (`vos plan --zooms per-beat` keeps
1392
+ * the proposal on it, and only it); `false` means never zoom here (the
1393
+ * planner's proposal on it is rejected). The planner still decides the
1394
+ * level and the focus; a mark never invents a zoom. On a gesture only
1395
+ * (ZOOM_MARK_VERBS).
1396
+ */
1397
+ zoom?: boolean;
758
1398
  /**
759
1399
  * A selector whose words the recorder keeps once the step has settled
760
1400
  * (`meta.steps[].text`: trimmed, whitespace collapsed, at most 2 KB), so
@@ -764,16 +1404,27 @@ type ActionStep = ({
764
1404
  * what is read. Only what the flow names, never a secret it did not.
765
1405
  */
766
1406
  read?: string;
1407
+ /**
1408
+ * After the gesture, wait until the page has made no DOM change and
1409
+ * painted no new frame for a beat (at most a few seconds): an app that
1410
+ * falls behind catches up before the next step. The script's top-level
1411
+ * `settle` is the default.
1412
+ */
1413
+ settle?: boolean;
767
1414
  };
768
- /** What an `await` waits for: exactly one of the three. */
1415
+ /** What an `await` waits for: exactly one of the four (`equals` rides with `js`). */
769
1416
  interface AwaitUntil {
770
1417
  selector?: string;
771
1418
  text?: string;
772
1419
  gone?: string;
1420
+ js?: string;
1421
+ equals?: unknown;
773
1422
  }
774
1423
  /** The guards a step may carry: one today, a step that spends. */
775
1424
  declare const STEP_GUARDS: readonly ["spend"];
776
1425
  type StepGuard = (typeof STEP_GUARDS)[number];
1426
+ /** The verbs a `zoom` mark may sit on: the gestures a camera can be about. */
1427
+ declare const ZOOM_MARK_VERBS: readonly string[];
777
1428
  /** How long an `expect` waits by default, in seconds. */
778
1429
  declare const EXPECT_TIMEOUT_S = 5;
779
1430
  /** An `await`'s defaults, in seconds. */
@@ -817,10 +1468,20 @@ declare function guardVerdict(actions: Pick<ActionsFile, 'steps'>, confirm: read
817
1468
  };
818
1469
  /**
819
1470
  * What a hosted recorder cannot do with a script, in words, or []: a
820
- * guarded step has nobody to confirm it on a fleet, and a download has no
821
- * take directory to land in there.
1471
+ * guarded step has nobody to confirm it on a fleet, a download has no
1472
+ * take directory to land in there, and a `js` check is code a stranger's
1473
+ * script would run in the fleet's browser.
822
1474
  */
823
- declare function hostedRefusals(actions: Pick<ActionsFile, 'steps'>): string[];
1475
+ declare function hostedRefusals(actions: Pick<ActionsFile, 'steps' | 'peers'>): string[];
1476
+ /**
1477
+ * The files a script names beside itself, in order: a drag's path file
1478
+ * and a paste's file. A host reads them before anything is captured.
1479
+ */
1480
+ declare function scriptFiles(actions: Pick<ActionsFile, 'steps'>): {
1481
+ step: number;
1482
+ name: string;
1483
+ as: 'path' | 'paste';
1484
+ }[];
824
1485
 
825
1486
  /**
826
1487
  * The wall check: did the recorder land where it was asked, or in front of
@@ -933,6 +1594,8 @@ interface SkippedStep {
933
1594
  selector: string;
934
1595
  /** why, when it is not a selector that never appeared: an expect that never held, a download that never came */
935
1596
  reason?: string;
1597
+ /** A PEER's step (`peers[].id`): `step` is then an index into that peer's steps. */
1598
+ peer?: string;
936
1599
  }
937
1600
  /** A click whose target was disabled when it was pressed: the press happened, and likely did nothing. */
938
1601
  interface DisabledClick {
@@ -965,12 +1628,19 @@ interface RecordResult {
965
1628
  pace: PaceReport;
966
1629
  /** steps whose selector never became visible — the take continued without them. */
967
1630
  skipped: SkippedStep[];
968
- /** the initial goto never reached networkidle (recording proceeded anyway). */
1631
+ /** `ready: "networkidle"` was asked for and the first load never reached it (recording proceeded anyway). */
969
1632
  navTimeout: boolean;
1633
+ /** How the first load settled before step 1. */
1634
+ ready: ReadyReport;
970
1635
  /** A wall the caller let through; null when the recorder landed where it was asked. */
971
1636
  wall: WallVerdict | null;
972
1637
  /** Sensitive-looking text seen in the frame (kind + place, never the string). */
973
1638
  exposures: NonNullable<RecordingMeta['exposures']>;
1639
+ /**
1640
+ * INFORMATION, never a failure and never in `exposures`: the signed-in
1641
+ * account's own name and its recent-items lists (also `meta.notices`).
1642
+ */
1643
+ notices: NonNullable<RecordingMeta['notices']>;
974
1644
  /** The script's masks with how many elements each reached; hits 0 hid nothing. */
975
1645
  masks: NonNullable<RecordingMeta['masks']>;
976
1646
  /**
@@ -997,12 +1667,22 @@ interface RecordResult {
997
1667
  skips: RecordingSkip[];
998
1668
  /** The files `download` steps kept (also `meta.outputs`). */
999
1669
  outputs: RecordingOutput[];
1670
+ /** The sound the page played, each heard stretch in footage seconds (also `meta.media`). */
1671
+ media: NonNullable<RecordingMeta['media']>;
1000
1672
  /** An `await` timed out and the take stopped there; null when it ran to the end. */
1001
1673
  stopped: StoppedTake | null;
1002
1674
  /** The page's own console errors, each distinct one with its full text and how often it came. */
1003
1675
  pageErrors: PageError[];
1676
+ /**
1677
+ * Paced gestures that ran more than a fifth past their script (a drag's
1678
+ * travel, a path's timing, typing, a key sequence): the page could not
1679
+ * keep up with what it was sent. Also on `meta.steps[].overrunMs`.
1680
+ */
1681
+ overruns: StepOverrun[];
1004
1682
  /** The actions as recorded, `url` resolved: what a take directory writes back. */
1005
1683
  actions: ActionsFile;
1684
+ /** What each peer did, off camera (also `meta.peers`); [] with none. */
1685
+ peers: PeerRun[];
1006
1686
  }
1007
1687
  interface RecordOpts {
1008
1688
  /** Stop the capture at this many seconds (the hosted cap). */
@@ -1060,6 +1740,18 @@ interface RecordOpts {
1060
1740
  * A rehearsal fetches the file and keeps nothing.
1061
1741
  */
1062
1742
  outputs?: OutputSink;
1743
+ /**
1744
+ * Where the sound the page played is kept, fetched with the page's own
1745
+ * cookies at the end of the take. A host that hands in none still
1746
+ * records `meta.media`, with no file; a rehearsal fetches nothing.
1747
+ */
1748
+ sounds?: SoundSink;
1749
+ /**
1750
+ * The files the script names beside itself (a drag's path file, a
1751
+ * paste's file). A host that hands in none cannot run such a step: the
1752
+ * script is refused before anything is captured.
1753
+ */
1754
+ files?: ScriptFiles;
1063
1755
  /** Print every page error whole, each time it comes; by default one cut line per distinct error. */
1064
1756
  verbose?: boolean;
1065
1757
  /**
@@ -1067,8 +1759,22 @@ interface RecordOpts {
1067
1759
  * an `await`'s start, a heartbeat while it waits, and its end.
1068
1760
  */
1069
1761
  onEvent?: (event: Record<string, unknown>) => void;
1762
+ /**
1763
+ * A step the page stopped answering in (it ran into its bound) stops the
1764
+ * take there, as an `await` that timed out does (`--strict`). Without
1765
+ * it the step is a skip and the take goes on.
1766
+ */
1767
+ stopOnHang?: boolean;
1070
1768
  /** How often a long `await` reports it is still waiting (default 30 s). */
1071
1769
  awaitHeartbeatMs?: number;
1770
+ /**
1771
+ * Where a peer's `storageState` is read from: the CLI resolves the name
1772
+ * from the actions file's directory to a path the browser opens, and
1773
+ * throws in words when there is none. A host that hands in none cannot
1774
+ * sign a peer in, and such a script is refused before anything is
1775
+ * captured.
1776
+ */
1777
+ peerStorageState?: (name: string) => Promise<string> | string;
1072
1778
  }
1073
1779
  /**
1074
1780
  * The expression a scroll tick evaluates: move the first scrollable
@@ -1085,6 +1791,8 @@ declare function jpegDims(buf: Uint8Array): {
1085
1791
  /** Base64 to bytes without Node's Buffer, so the fleet's isolate runs it too. */
1086
1792
  declare function base64Bytes(data: string): Uint8Array;
1087
1793
  declare function recordTake(browser: RecorderBrowser, url: string, actions: ActionsFile, sink: FrameSink, log: (msg: string) => void, opts?: RecordOpts): Promise<RecordResult>;
1794
+ /** Why a step ended at its bound, with the real time it took. */
1795
+ declare function hangReason(verb: string, ms: number, boundMs: number): string;
1088
1796
  declare function extForType(type: string | undefined): string;
1089
1797
  /** A download's type from its name's extension, as `{ contentType }`, or `{}` when the extension says nothing. */
1090
1798
  declare function typeOfName(name: string): {
@@ -1141,6 +1849,25 @@ interface FreshPlanOptions {
1141
1849
  activity?: readonly number[] | null;
1142
1850
  }
1143
1851
  declare function planFreshTake(artifact: RecordingArtifact, recordingName: string, opts?: FreshPlanOptions): ProjectDoc;
1852
+ /**
1853
+ * The export a FRESH document opens on, sized to what the footage holds:
1854
+ * the ingest's default resolution, or the largest the recording supports
1855
+ * without upscaling when that is less (a 1280×720 take exports 720p, not a
1856
+ * 1080p that `vos validate` then warns upscales). Never raised. In place,
1857
+ * and returned.
1858
+ */
1859
+ declare function withFittingExport(doc: ProjectDoc): ProjectDoc;
1860
+ /**
1861
+ * The sound the page played (`meta.media`) as the document's audio: one
1862
+ * clip per heard stretch, keyed to the fetched file, placed at the OUTPUT
1863
+ * time of the footage moment it was heard (through the segments, the speed
1864
+ * spans and the freezes), stamped `source: 'page'`. A re-plan replaces only
1865
+ * those clips, so a clip someone added stays. An auto speed span over a
1866
+ * heard stretch is dropped first, or the footage would race ahead of what
1867
+ * the page was saying. A stretch with no fetched file (a hosted take, a
1868
+ * fetch that failed) places nothing. In place, and returned.
1869
+ */
1870
+ declare function withPageSound(doc: ProjectDoc, media: readonly RecordingPageMedia[] | undefined): ProjectDoc;
1144
1871
  /**
1145
1872
  * The footage's segments split at every cut an `await` made
1146
1873
  * (`meta.skips[].at`, source seconds), so each cut is a real segment
@@ -1153,97 +1880,6 @@ declare function splitSegmentsAtSkips(segments: ProjectDoc['segments'], skips: r
1153
1880
  at: number;
1154
1881
  }[] | undefined): ProjectDoc['segments'];
1155
1882
 
1156
- /**
1157
- * What an `expect` and an `await` wait for: a condition on the page, read
1158
- * without touching it (no cursor, no input), polled until it holds or the
1159
- * time runs out. `expect` polls on camera for a few seconds; `await` polls
1160
- * off camera for as long as a slow result takes. One reader for both, so
1161
- * "appeared" means the same thing in either.
1162
- */
1163
-
1164
- interface WaitCondition {
1165
- /** an element matching this selector is visible */
1166
- selector?: string;
1167
- /** some visible text on the page contains this */
1168
- text?: string;
1169
- /** no element matching this selector is visible */
1170
- gone?: string;
1171
- /** with `selector`: the element is enabled (true) or disabled (false) */
1172
- enabled?: boolean;
1173
- }
1174
- /** What the condition found the last time it looked: what a miss is said from. */
1175
- interface ConditionState {
1176
- holds: boolean;
1177
- /** with `selector`: whether a match was visible */
1178
- visible?: boolean;
1179
- }
1180
- /**
1181
- * The visible text of the page contains `text`: `innerText` is what the
1182
- * page renders, so a hidden element's words never count.
1183
- */
1184
- declare function visibleTextScript(text: string): string;
1185
- /** Read the condition once. A page mid-navigation reads as "not yet". */
1186
- declare function readCondition(page: RecorderPage, c: WaitCondition): Promise<ConditionState>;
1187
- /** What a poll found: whether the condition held, after how long, and the last read. */
1188
- interface PollResult {
1189
- ok: boolean;
1190
- ms: number;
1191
- last: ConditionState;
1192
- }
1193
- /**
1194
- * Poll the condition every `everyMs` until it holds or `timeoutMs` pass.
1195
- * `between` runs before each look but the first (an `await`'s reload).
1196
- * With `confirm`, a hold counts only once it has held on that many reads
1197
- * in a row, `confirm.gapMs` apart: a page that renders after network quiet
1198
- * reads half-built for a moment, and a `gone` condition holds on a list
1199
- * that has not been drawn yet. The clock is the wall's: a rehearsal waits
1200
- * as long as a take would, because how long a result takes is what a
1201
- * rehearsal is for.
1202
- */
1203
- declare function pollCondition(page: RecorderPage, c: WaitCondition, opts: {
1204
- timeoutMs: number;
1205
- everyMs: number;
1206
- sleep: (ms: number) => Promise<void>;
1207
- now?: () => number;
1208
- between?: () => Promise<void>;
1209
- confirm?: {
1210
- reads: number;
1211
- gapMs: number;
1212
- };
1213
- }): Promise<PollResult>;
1214
- /** The DOM counts as settled after this long with no change. */
1215
- declare const DOM_QUIET_MS = 800;
1216
- /** The longest a settle waits on a page that never stops changing. */
1217
- declare const DOM_SETTLE_CAP_MS = 5000;
1218
- /**
1219
- * The page's own count of DOM changes: an observer installed once per
1220
- * document, read back as a number. A string, the one evaluate shape every
1221
- * driver shares.
1222
- */
1223
- declare const DOM_CHANGE_PROBE = "(() => {\n if (!window.__vosDomChanges) {\n window.__vosDomChanges = { n: 0 };\n new MutationObserver((list) => { window.__vosDomChanges.n += list.length })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n }\n return window.__vosDomChanges.n;\n})()";
1224
- /**
1225
- * Wait until the DOM has not changed for `quietMs`, at most `capMs`: what
1226
- * a page does after its network went quiet (a list rendered from data, a
1227
- * hydration). The count is polled from here rather than timed in the
1228
- * page, because a background tab throttles its own timers.
1229
- */
1230
- declare function domSettled(page: RecorderPage, opts: {
1231
- sleep: (ms: number) => Promise<void>;
1232
- now?: () => number;
1233
- quietMs?: number;
1234
- capMs?: number;
1235
- tickMs?: number;
1236
- }): Promise<{
1237
- settled: boolean;
1238
- ms: number;
1239
- }>;
1240
- /** Seconds as the recorder says them: `5 s`, `2.5 s`, `3 min 20 s`. */
1241
- declare function sayDuration(ms: number): string;
1242
- /** What the condition waits for, in words: `'#result' to appear`. */
1243
- declare function sayCondition(c: WaitCondition): string;
1244
- /** Why the condition did not hold, in words: `'Generate' stayed disabled for 5 s`. */
1245
- declare function conditionMiss(c: WaitCondition, last: ConditionState, ms: number): string;
1246
-
1247
1883
  /** The most a download keeps: a file the page made, never a disk's worth. */
1248
1884
  declare const OUTPUT_MAX_BYTES: number;
1249
1885
  /**
@@ -1306,6 +1942,17 @@ declare class TakeClock {
1306
1942
  pause(wallMs: number): number;
1307
1943
  /** The camera rolls again; returns the wall ms the stretch lasted. */
1308
1944
  resume(wallMs: number): number;
1945
+ /**
1946
+ * The parts of a wall-clock stretch the camera saw: each sub-stretch
1947
+ * outside every off-camera wait, as its wall start (`wallMs`), its
1948
+ * footage start (`atMs`) and its length. A stretch an `await` cut in two
1949
+ * comes back as two; one wholly off camera as none.
1950
+ */
1951
+ onCamera(fromWall: number, toWall: number): {
1952
+ wallMs: number;
1953
+ atMs: number;
1954
+ ms: number;
1955
+ }[];
1309
1956
  /**
1310
1957
  * A captured frame's footage ms, or null when it is not later than the
1311
1958
  * last frame kept (the frame is dropped; see the module comment).
@@ -1313,4 +1960,311 @@ declare class TakeClock {
1313
1960
  placeFrame(captureWallMs: number): number | null;
1314
1961
  }
1315
1962
 
1316
- export { AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, type DeadReport, type DeadStep, type DisabledClick, EXPECT_TIMEOUT_S, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, type GuardedStep, MIN_SKIP_MS, MOTION_TICK_MS, type MaskReport, type MaskRule, OUTPUT_MAX_BYTES, type OutputSink, PAGE_ERROR_LINE_CHARS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, type PollResult, type PressKey, QUIET_MS, READ_BEAT_MS, RESPONSE_MS, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderDownload, type RecorderLocator, type RecorderLocatorRoot, type RecorderMediaElement, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, STEP_GUARDS, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type StepGuard, type StepPace, type StoppedTake, TRAILING_HOLD_MS, TakeClock, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, askedMs, base64Bytes, buildEncodePage, capReached, cappedLine, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, formatDurationCap, guardVerdict, guardedSteps, headersFor, holdLeftMs, hostedRefusals, isIdpHost, isSignInPath, jpegDims, maskInitScript, nameFromUrl, outputName, paceLine, paceReport, pageErrorLine, parseHeaders, plainName, planFreshTake, pointerTravelMs, pollCondition, publicUrl, readAllBytes, readCondition, readMediaSource, realSleep, recordTake, redirectedAway, resolveSetupText, runSetup, sayCondition, sayDuration, scrollByScript, scrollTravelMs, settleMs, settleVerdict, setupEnvNames, splitSegmentsAtSkips, typeOfName, urlSchemeProblem, validateActions, validateSetup, visibleTextScript, wallVerdict };
1963
+ interface SurfaceSize {
1964
+ width: number;
1965
+ height: number;
1966
+ }
1967
+ interface SurfaceFit {
1968
+ before: SurfaceSize;
1969
+ after: SurfaceSize;
1970
+ }
1971
+ /**
1972
+ * Make the surface the viewport. Returns what it measured, or null when
1973
+ * nothing could be measured; never throws, because a take made as before
1974
+ * is better than no take.
1975
+ */
1976
+ declare function fitSurfaceToViewport(cdp: RecorderCdp, width: number, height: number): Promise<SurfaceFit | null>;
1977
+
1978
+ /**
1979
+ * The sound a page plays during a take. The recorder captures frames, and
1980
+ * a product's sound (a voice preview, a video playing in the page) would
1981
+ * be lost with nothing to say it was there. Most of it is a media element,
1982
+ * so the recorder watches every `<video>` and `<audio>` the page plays, in
1983
+ * the page and in its same-origin frames, and keeps WHEN each was heard
1984
+ * and WHAT media time it played, so a clip of the same file can be placed
1985
+ * exactly under the footage (`meta.media`, then the plan's audio clips).
1986
+ *
1987
+ * Two pieces run IN the page, both plain JavaScript strings (a function
1988
+ * serialized out of a bundle picks up the bundler's helpers and dies in the
1989
+ * page): an init script that notes every play, pause, seek, end and volume
1990
+ * change into a log on the top window, and a drain that hands the log over
1991
+ * with the page's clock. The rest is here, in the recorder's clock: the
1992
+ * page's instants become wall instants, and the take clock turns those
1993
+ * into footage stretches, split wherever an `await` took the camera away.
1994
+ */
1995
+
1996
+ /**
1997
+ * The init script: runs at document start in every frame. A frame that
1998
+ * cannot reach the top window (another origin) notes nothing. A media
1999
+ * element out of the document (`new Audio(url).play()`) is reached by
2000
+ * the play it is started with.
2001
+ */
2002
+ declare const MEDIA_WATCH_SCRIPT = "(() => {\n let host\n try { host = window.top; void host.document.title } catch { return }\n if (window.__vosMediaWatch) return\n window.__vosMediaWatch = true\n if (!host.__vosMedia) {\n const M = { log: [], seq: 0, els: new Set(), doc: Math.random().toString(36).slice(2) }\n M.fields = (el, kind) => ({\n id: el.__vosMediaId,\n tag: el.tagName === 'VIDEO' ? 'video' : 'audio',\n src: el.currentSrc || el.src || '',\n kind,\n wall: Date.now(),\n t: el.currentTime,\n rate: el.playbackRate || 1,\n paused: el.paused || el.ended,\n audible: !el.muted && el.volume > 0,\n volume: el.volume,\n duration: isFinite(el.duration) ? el.duration : null,\n ab: typeof el.webkitAudioDecodedByteCount === 'number' ? el.webkitAudioDecodedByteCount : null,\n })\n host.__vosMedia = M\n }\n const M = host.__vosMedia\n const EVENTS = ['playing', 'pause', 'ended', 'seeking', 'seeked', 'volumechange', 'ratechange', 'emptied']\n const note = (e) => {\n const el = e.target\n if (!el || typeof el.currentTime !== 'number' || !/^(VIDEO|AUDIO)$/.test(el.tagName || '')) return\n if (e.__vosNoted) return\n e.__vosNoted = true\n if (!el.__vosMediaId) {\n if (M.els.size >= 50) return\n el.__vosMediaId = ++M.seq\n M.els.add(el)\n }\n if (M.log.length < 2000) M.log.push(M.fields(el, e.type))\n }\n for (const type of EVENTS) document.addEventListener(type, note, true)\n const proto = window.HTMLMediaElement && window.HTMLMediaElement.prototype\n if (proto && proto.play) {\n const play = proto.play\n proto.play = function () {\n if (!this.__vosListening && !this.isConnected) {\n this.__vosListening = true\n for (const type of EVENTS) this.addEventListener(type, note)\n }\n return play.apply(this, arguments)\n }\n }\n})()";
2003
+ /**
2004
+ * The drain: the notes since the last drain, a `tick` for every element
2005
+ * followed (what it is doing now, which catches a note that never came),
2006
+ * the page's clock, and which document the log belongs to (a navigation
2007
+ * starts a new one).
2008
+ */
2009
+ declare const MEDIA_DRAIN_PROBE = "(() => {\n const M = window.__vosMedia\n if (!M) return { now: Date.now(), doc: null, notes: [] }\n const notes = M.log.splice(0)\n M.els.forEach((el) => notes.push(M.fields(el, 'tick')))\n return { now: Date.now(), doc: M.doc, notes }\n})()";
2010
+ /** One note from the page. */
2011
+ interface MediaNote {
2012
+ id: number;
2013
+ tag: 'video' | 'audio';
2014
+ src: string;
2015
+ kind: string;
2016
+ wall: number;
2017
+ t: number;
2018
+ rate: number;
2019
+ paused: boolean;
2020
+ audible: boolean;
2021
+ volume: number;
2022
+ duration: number | null;
2023
+ ab: number | null;
2024
+ }
2025
+ interface MediaDrain {
2026
+ now: number;
2027
+ doc: string | null;
2028
+ notes: MediaNote[];
2029
+ }
2030
+ /** A page sound with its full source, for the fetch; `meta` is what the take keeps. */
2031
+ interface HeardSound {
2032
+ meta: RecordingPageMedia;
2033
+ /** the address as the page played it, query and all: fetched, never stored */
2034
+ src: string;
2035
+ }
2036
+ /** What `meta.media[].src` keeps of an address: origin and path; a `blob:` or `data:` by its scheme. */
2037
+ declare function soundSrcForMeta(src: string): string;
2038
+ /**
2039
+ * Accumulates the page's notes across a take, in the recorder's clock.
2040
+ * `add` takes each drain with the recorder's own `Date.now()` at the
2041
+ * moment it came back; `heard` closes what is still playing at the end
2042
+ * and maps every stretch to footage time.
2043
+ */
2044
+ declare class PageSoundLog {
2045
+ private byKey;
2046
+ private openKeyOf;
2047
+ private doc;
2048
+ private lastWall;
2049
+ add(drain: MediaDrain | null, nodeNow: number, step?: number): void;
2050
+ private note;
2051
+ private close;
2052
+ private closeAll;
2053
+ /**
2054
+ * Every element the page played, each heard stretch as footage seconds
2055
+ * `at` with media seconds `from`..`to`, none past `endMs` of footage.
2056
+ * A video whose decoder never decoded a byte of sound played none.
2057
+ */
2058
+ heard(clock: TakeClock, endWall: number, endMs: number): HeardSound[];
2059
+ }
2060
+
2061
+ /**
2062
+ * A step never runs past its own bound, however the page behaves. A page
2063
+ * that stops answering (a busy loop, a renderer stuck after a reload the
2064
+ * page made itself) leaves every driver call pending for good, and a poll
2065
+ * that reads it waits on the read, not on its clock: an `expect` that said
2066
+ * "for 5 s" ran for fifteen minutes. So the page the recorder drives is
2067
+ * BOUNDED: while a step runs, every call into it is raced against the
2068
+ * step's deadline, and once the deadline has passed a pending call answers
2069
+ * `undefined` (what an absent element answers) instead of waiting. The
2070
+ * step then ends as a skip with the real time it took; the call it left
2071
+ * pending is abandoned, never awaited.
2072
+ *
2073
+ * Between steps the same race holds with a looser bound, so the take's
2074
+ * own finish (its last reads, the page's title) cannot hang either.
2075
+ */
2076
+ /** A gesture's bound past what it planned to take. */
2077
+ declare const STEP_MARGIN_MS = 30000;
2078
+ /** A check's (`expect`, `await`) bound past its own timeout. */
2079
+ declare const WAIT_MARGIN_MS = 5000;
2080
+ /** One call's bound while no step runs (a navigation has its own 45 s timeout). */
2081
+ declare const IDLE_CALL_MS = 60000;
2082
+ /** The bound of one step, in ms: its own timeout for a check, its plan plus a margin for a gesture. */
2083
+ declare function stepBoundMs(step: {
2084
+ do: string;
2085
+ timeout?: number;
2086
+ until?: unknown;
2087
+ reload?: boolean;
2088
+ }, plannedMs: number, defaults: {
2089
+ expectS: number;
2090
+ awaitS: number;
2091
+ downloadS: number;
2092
+ }): number;
2093
+ declare class StepBound {
2094
+ private readonly now;
2095
+ private readonly idleMs;
2096
+ private deadline;
2097
+ private started;
2098
+ private hung;
2099
+ constructor(now?: () => number, idleMs?: number);
2100
+ /** A step begins: calls into the page answer within `boundMs` from now. */
2101
+ start(boundMs: number): void;
2102
+ /** The step ends: whether it ran into its bound, and how long it really took. */
2103
+ end(): {
2104
+ hung: boolean;
2105
+ ms: number;
2106
+ boundMs: number;
2107
+ };
2108
+ /** The pending call, or `undefined` once the bound has passed. */
2109
+ race<T>(p: Promise<T>): Promise<T>;
2110
+ }
2111
+ /**
2112
+ * `target` with every method that answers a promise raced through the
2113
+ * guard, and every object it hands back (`page.mouse`, `locator()`,
2114
+ * `.first()`) guarded the same way. A call is made on the real object, so
2115
+ * nothing private to the driver is touched through the proxy.
2116
+ */
2117
+ declare function bounded<T extends object>(target: T, bound: StepBound): T;
2118
+
2119
+ /**
2120
+ * The `paste` step, the pure parts: what a file is pasted as, and the two
2121
+ * page scripts that put an item where a person's ⌘V finds it.
2122
+ *
2123
+ * The first rung is the browser's own clipboard: the item is written with
2124
+ * `navigator.clipboard.write` (the context is granted clipboard access
2125
+ * first), and the recorder then presses the platform's paste chord, so the
2126
+ * page receives a TRUSTED keydown and a trusted `paste` event carrying the
2127
+ * item, and a page that reads `navigator.clipboard.read()` itself sees it
2128
+ * there too. A headless browser keeps its own clipboard, so nothing on the
2129
+ * machine's clipboard is touched.
2130
+ *
2131
+ * When the browser refuses the write (no clipboard access on this driver,
2132
+ * a type it will not hold), the second rung dispatches the paste in the
2133
+ * page: a `paste` event with a `DataTransfer` holding the item on the
2134
+ * focused element, and `navigator.clipboard.read`/`readText` answering the
2135
+ * item for the rest of the take. Those events are untrusted, which the
2136
+ * recorder says.
2137
+ *
2138
+ * Both scripts are strings, the one evaluate shape every driver shares,
2139
+ * and neither carries anything the cursor track keeps: a paste is a typing
2140
+ * ping, never its contents.
2141
+ */
2142
+ /** How long after a dispatched paste is first read its item stays on the async reads, ms. */
2143
+ declare const PASTE_READ_WINDOW_MS = 300;
2144
+ /** How long a dispatched paste's item stays on the async reads when nothing reads it, ms. */
2145
+ declare const PASTE_UNREAD_MS = 3000;
2146
+ /** The most bytes a pasted file may hold. */
2147
+ declare const PASTE_MAX_BYTES: number;
2148
+ /** The file types a paste takes, by extension. */
2149
+ declare const PASTE_TYPES: Record<string, string>;
2150
+ /** A pasted file's type from its name, or null when a paste does not take it. */
2151
+ declare function pasteTypeOf(name: string): string | null;
2152
+ /** What a paste puts on the clipboard: words, or a file's bytes (base64) with its type and name. */
2153
+ interface PasteItem {
2154
+ text?: string;
2155
+ b64?: string;
2156
+ type?: string;
2157
+ name?: string;
2158
+ }
2159
+ /**
2160
+ * Rung one: write the item to the browser's clipboard. Answers 'ok', or
2161
+ * 'refused: <why>'. An SVG the clipboard will not hold as an image is
2162
+ * written as its markup alone.
2163
+ */
2164
+ declare function stageClipboardScript(item: PasteItem): string;
2165
+ /**
2166
+ * Rung two: the paste dispatched in the page, on the focused element (or
2167
+ * the body), with the item in its `clipboardData`; and the async clipboard
2168
+ * reads answer the item from here on. A plain field the page did not take
2169
+ * the paste for gets the words inserted, as the browser's own paste does.
2170
+ * Answers 'ok', or 'failed: <why>'.
2171
+ */
2172
+ declare function syntheticPasteScript(item: PasteItem, opts?: {
2173
+ original?: boolean;
2174
+ }): string;
2175
+
2176
+ /**
2177
+ * A gesture that waits on the app (`settle`): after the gesture, the
2178
+ * recorder waits until the page has made no DOM change and painted no new
2179
+ * frame for a beat, at most a few seconds. A heavy canvas falls behind
2180
+ * under load, and a script paced by the wall clock then outruns it: the
2181
+ * next stroke is drawn before the app has processed the last one.
2182
+ *
2183
+ * The DOM half is the recorder's one DOM-settle reader (`domSettled`, the
2184
+ * same an `await`'s reload lets the page finish with). The paint half is
2185
+ * the screencast: it emits a frame only when the compositor painted
2186
+ * something new, a canvas redrawn by `requestAnimationFrame` included, and
2187
+ * it is read from here rather than by an animation frame in the page,
2188
+ * which a background tab never fires. The two are one count: the page's
2189
+ * DOM changes plus the frames captured so far. A rehearsal captures no
2190
+ * frames, so there it waits on the DOM alone.
2191
+ */
2192
+
2193
+ /** The quiet a settle waits for: no DOM change and no new frame, ms. */
2194
+ declare const APP_QUIET_MS = 300;
2195
+ /** The longest a settle waits on a page that never stops changing, ms. */
2196
+ declare const APP_SETTLE_CAP_MS = 4000;
2197
+ /** The verbs a settle follows: the ones that send the page input. */
2198
+ declare const SETTLE_VERBS: Set<string>;
2199
+ /**
2200
+ * How long ago the page last changed its DOM, in ms by the page's own clock,
2201
+ * or -1 when this is the first look at this document (the reader is
2202
+ * installed now and knows no history). A string, the one evaluate shape
2203
+ * every driver shares.
2204
+ */
2205
+ declare const CHANGE_AGE_PROBE = "(() => {\n const w = window;\n if (!w.__vosChangeAt) {\n w.__vosChangeAt = { at: Date.now() };\n new MutationObserver(() => { w.__vosChangeAt.at = Date.now() })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n return -1;\n }\n return Date.now() - w.__vosChangeAt.at;\n})()";
2206
+ /** Whether a step waits on the app after its gesture: its own `settle`, else the script's. */
2207
+ declare function settleOn(step: {
2208
+ do: string;
2209
+ settle?: boolean;
2210
+ }, scriptDefault: boolean | undefined): boolean;
2211
+ /**
2212
+ * Wait for the app: no DOM change and no new captured frame for `quietMs`,
2213
+ * at most `capMs`. `frames` reads how many frames the capture has kept.
2214
+ */
2215
+ declare function appSettled(page: Pick<RecorderPage, 'evaluate'>, frames: () => number, opts: {
2216
+ sleep: (ms: number) => Promise<void>;
2217
+ now?: () => number;
2218
+ quietMs?: number;
2219
+ capMs?: number;
2220
+ /** ms since the last captured frame, or null when none was captured */
2221
+ sinceFrame?: () => number | null;
2222
+ }): Promise<{
2223
+ settled: boolean;
2224
+ ms: number;
2225
+ }>;
2226
+
2227
+ /**
2228
+ * The files a script names beside itself, read before the take: a drag's
2229
+ * path file (points as JSON) and a paste's file (its bytes). The host
2230
+ * hands in where they come from (`ScriptFiles`); the names are plain file
2231
+ * names, checked by `validateActions`, so a script can only name what sits
2232
+ * next to it.
2233
+ */
2234
+
2235
+ interface LoadedFiles {
2236
+ /** a drag's path, read from its file, by step index */
2237
+ paths: Map<number, {
2238
+ points: PathSample[];
2239
+ timed: boolean;
2240
+ }>;
2241
+ /** a paste's file, by step index */
2242
+ pastes: Map<number, {
2243
+ bytes: Uint8Array;
2244
+ type: string;
2245
+ name: string;
2246
+ }>;
2247
+ }
2248
+ /**
2249
+ * Read every file the script names. Answers the files, or the problem in
2250
+ * words (the step, the file, what is wrong) for the caller to refuse the
2251
+ * take with.
2252
+ */
2253
+ declare function loadScriptFiles(actions: Pick<ActionsFile, 'steps'>, source: ScriptFiles | undefined): Promise<LoadedFiles | {
2254
+ problem: string;
2255
+ }>;
2256
+ /** A read path as inline points: its own times when it had them, its pressure when it carried one. */
2257
+ declare function inlinePoints(path: {
2258
+ points: PathSample[];
2259
+ timed: boolean;
2260
+ }): PathPoint[];
2261
+ /**
2262
+ * The script as it ran, self-contained: every path a file held is written
2263
+ * inline, so the script a take keeps (and the one a push embeds, a fetch
2264
+ * writes back and a hosted recorder runs) re-records the same strokes with
2265
+ * the file gone. A paste's file stays a name: it is bytes, kept beside the
2266
+ * script by the host.
2267
+ */
2268
+ declare function inlineScript<T extends Pick<ActionsFile, 'steps'>>(actions: T, files: LoadedFiles): T;
2269
+
2270
+ export { APP_QUIET_MS, APP_SETTLE_CAP_MS, AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, CHANGE_AGE_PROBE, CLICK_BUTTONS, type ClickButton, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DEFAULT_READY, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, DRAG_HOLD_MS, DRAG_MODIFIERS, type DeadReport, type DeadStep, type DisabledClick, type DragModifier, EXPECT_TIMEOUT_S, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, type GuardedStep, type HeardSound, IDLE_CALL_MS, JS_SAID_MAX, JS_VALUE_MAX, KEY_ROUNDTRIP_MS, KEY_SEQUENCE_GAP_MS, type LoadedFiles, MAX_KEYS, MAX_PATH_POINTS, MAX_PEERS, MAX_PEER_STEPS, MEDIA_DRAIN_PROBE, MEDIA_WATCH_SCRIPT, MIN_SKIP_MS, MODIFIERS, MOTION_TICK_MS, MULTI_CLICK_GAP_MS, type MaskReport, type MaskRule, type MediaDrain, type MediaNote, type Modifier, NAV_TIMEOUT_MS, NETWORKIDLE_HYDRATION_MS, NOTICE_SRC, type Notice, NoticeLog, OUTPUT_MAX_BYTES, OVERRUN_MIN_MS, OVERRUN_MIN_PLANNED_MS, OVERRUN_SHARE, type OutputSink, PAGE_ERROR_LINE_CHARS, PASTE_MAX_BYTES, PASTE_READ_WINDOW_MS, PASTE_TYPES, PASTE_UNREAD_MS, PATH_DEFAULT_MS, PEER_VALUE_MAX, PEER_VERBS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, PageSoundLog, type PasteItem, type PathPoint, type PathSample, type Peer, type PeerRun, type PeerStep, type PollResult, type PressKey, QUIET_MS, READY_MODES, READY_SETTLE_CAP_MS, READ_BEAT_MS, RESPONSE_MS, type ReadyMode, type ReadyOpts, type ReadyReport, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, type RecorderButtonOpts, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderDownload, type RecorderLocator, type RecorderLocatorRoot, type RecorderMediaElement, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, SETTLE_VERBS, STEP_GUARDS, STEP_MARGIN_MS, type ScannedNotice, type ScriptFiles, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type SoundSink, StepBound, type StepGuard, type StepOverrun, type StepPace, StepSignals, type StoppedTake, type SurfaceFit, type SurfaceSize, TRAILING_HOLD_MS, TakeClock, WAIT_MARGIN_MS, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, ZOOM_MARK_VERBS, appSettled, askedMs, base64Bytes, bounded, buildEncodePage, buttonNumber, capReached, cappedLine, chordProblem, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, fitSurfaceToViewport, formatDurationCap, guardVerdict, guardedSteps, hangReason, headersFor, holdLeftMs, hostedRefusals, inlinePoints, inlineScript, isIdpHost, isKeyName, isSignInPath, jpegDims, jsConditionScript, keptValue, keySequencePlanMs, loadScriptFiles, maskInitScript, modifierBits, nameFromUrl, noticeAdvice, noticeLine, outputName, overrunLine, overrunOf, paceLine, paceReport, pageErrorLine, parseHeaders, pasteTypeOf, pathDurationMs, peerValue, peersHostedRefusal, plainName, planFreshTake, pointerTravelMs, pollCondition, pressSequence, publicUrl, readAllBytes, readCondition, readMediaSource, readPath, readyLine, readyProblem, realSleep, recordTake, redirectedAway, reloadUntilReady, resolveChord, resolveModifier, resolveSetupText, runPeer, runSetup, sameJson, sayCondition, sayDuration, sayJs, sayValue, schedulePath, scriptFiles, scrollByScript, scrollTravelMs, settleMs, settleOn, settleVerdict, setupEnvNames, soundSrcForMeta, splitChord, splitSegmentsAtSkips, stageClipboardScript, stepBoundMs, syntheticPasteScript, typeOfName, untilReady, urlSchemeProblem, validateActions, validatePeers, validateSetup, visibleTextScript, wallVerdict, withFittingExport, withPageSound };