@vosjs/render-core 0.7.0 → 0.8.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.
@@ -29,6 +29,7 @@ var SETTLE_MS = {
29
29
  click: 150,
30
30
  type: 150,
31
31
  press: 150,
32
+ paste: 150,
32
33
  scroll: 200,
33
34
  drag: 80
34
35
  };
@@ -112,6 +113,8 @@ function askedMs(step) {
112
113
  return (step.text?.length ?? 0) * (step.delayMs ?? 40);
113
114
  case "press":
114
115
  return step.ms ?? SETTLE_MS.press;
116
+ case "paste":
117
+ return step.ms ?? SETTLE_MS.paste;
115
118
  default:
116
119
  return 0;
117
120
  }
@@ -372,6 +375,113 @@ var EXPOSURE_MATCHERS_SRC = `(() => {
372
375
  }
373
376
  return { kindsIn }
374
377
  })()`;
378
+ var NOTICE_SRC = String.raw`const notices = () => {
379
+ // A label made only of these words is the control's, never a person's:
380
+ // "Account menu", "Open user menu", "Profile settings".
381
+ const UI_WORDS = new Set(['account', 'accounts', 'profile', 'profiles', 'menu', 'user', 'users', 'settings', 'setting', 'avatar', 'open', 'close', 'toggle', 'show', 'hide', 'the', 'my', 'your', 'me', 'you', 'home', 'guest', 'admin', 'team', 'workspace', 'dashboard', 'sign', 'signed', 'in', 'out', 'log', 'logged', 'logout', 'login', 'help', 'search', 'notification', 'notifications', 'more', 'options', 'option', 'button', 'dropdown', 'picture', 'image', 'photo', 'main', 'navigation', 'nav', 'preferences', 'switch', 'manage'])
382
+ const nameLike = (v) => {
383
+ if (!v) return null
384
+ const t = v.replace(/\s+/g, ' ').trim().replace(/[!.,:]+$/, '')
385
+ if (t.length < 2 || t.length > 40 || /[@\d<>/|]/.test(t)) return null
386
+ if (!/^\p{Lu}[\p{L}'’.-]*(?: \p{L}[\p{L}'’.-]*){0,3}$/u.test(t)) return null
387
+ if (t.toLowerCase().split(' ').every((w) => UI_WORDS.has(w))) return null
388
+ return t
389
+ }
390
+ const GREETING = /^(?:signed in as|logged in as|welcome back,?|welcome,|hi,|hello,|hey,)\s+(.+)$/i
391
+ const LABELS = [
392
+ /^(?:account|user|profile)(?: menu)?(?: for|:|,| -)\s*(.+)$/i,
393
+ /^open (?:the )?(?:user|account|profile) menu(?: for|,|:)?\s*(.+)$/i,
394
+ /^(.+?)[’']s? (?:account|profile|avatar)$/i,
395
+ ]
396
+ const avatarish = (el) => el.tagName === 'IMG' || !!el.querySelector('img, svg') || /avatar/i.test(String(el.className || '') + ' ' + (el.getAttribute('data-testid') || ''))
397
+ let sure = false
398
+ const texts = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
399
+ for (let n; !acct && (n = texts.nextNode()); ) {
400
+ const el = n.parentElement
401
+ if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
402
+ const line = (el.textContent || '').replace(/\s+/g, ' ').trim()
403
+ if (line.length > 80) continue
404
+ const m = GREETING.exec(line)
405
+ const who = m && nameLike(m[1])
406
+ if (who && seen(el)) { acct = who; sure = true }
407
+ }
408
+ let maybe = null
409
+ if (!acct) {
410
+ for (const el of document.querySelectorAll('[aria-label], img[alt], [title]')) {
411
+ const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || ''
412
+ const box = el.closest('button, a, [role="button"], [aria-haspopup]') || el
413
+ if (!seen(el) && !seen(box)) continue
414
+ let hit = null
415
+ for (const rx of LABELS) { const m = rx.exec(label.trim()); if (m && (hit = nameLike(m[1]))) break }
416
+ if (hit) { acct = hit; sure = true; break }
417
+ if (!maybe && (avatarish(el) || avatarish(box)) && box !== el.ownerDocument.body) maybe = nameLike(label)
418
+ }
419
+ }
420
+ // The account menu's TRIGGER, whose own words are the name: a control
421
+ // that opens a menu and holds the avatar, or one labelled as the
422
+ // account's. The name-like line among its words (an address there is
423
+ // an exposure already, and never a name).
424
+ if (!acct) {
425
+ const TRIGGER_LABEL = /(account|user|profile|avatar)/i
426
+ for (const el of document.querySelectorAll('button, a, [role="button"], [aria-haspopup]')) {
427
+ const label = (el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('title') || '')
428
+ const trigger = (el.hasAttribute('aria-haspopup') && avatarish(el)) || TRIGGER_LABEL.test(label)
429
+ if (!trigger || !seen(el)) continue
430
+ const words = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
431
+ for (let n; !acct && (n = words.nextNode()); ) {
432
+ const who = nameLike(n.nodeValue)
433
+ if (who && n.parentElement && seen(n.parentElement)) { acct = who; sure = true }
434
+ }
435
+ if (acct) break
436
+ }
437
+ }
438
+ if (!acct && maybe) acct = maybe
439
+ const found = []
440
+ if (acct) {
441
+ const places = [], at = new Set()
442
+ const walk = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
443
+ for (let n; (n = walk.nextNode()) && places.length < 20; ) {
444
+ const el = n.parentElement
445
+ if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
446
+ if (!(n.nodeValue || '').includes(acct)) continue
447
+ const r = seen(el)
448
+ if (!r) continue
449
+ const selector = name(el)
450
+ if (at.has(selector)) continue
451
+ at.add(selector)
452
+ places.push({ selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
453
+ }
454
+ if (places.length) found.push({ kind: 'account-name', sure, places })
455
+ }
456
+ const RECENT = /^(recents?|recently (?:viewed|opened|visited|edited)|history|recent (?:files|projects|documents|items|pages|boards|chats|conversations|searches))$/i
457
+ const ITEMS = 'a[href], [role="link"], [role="menuitem"], [role="treeitem"], [role="option"]'
458
+ const listed = new Set()
459
+ for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"], summary, div, span, p, li, label, strong, button, a')) {
460
+ if (el.children.length > 2) continue
461
+ const word = (el.textContent || '').replace(/\s+/g, ' ').trim()
462
+ if (word.length > 40 || !RECENT.test(word)) continue
463
+ // A nav link that happens to say History is a page, not a list.
464
+ if (/^(A|BUTTON)$/.test(el.tagName) && !el.hasAttribute('aria-expanded')) continue
465
+ if (!seen(el)) continue
466
+ for (const c of [el.nextElementSibling, el.parentElement && el.parentElement.nextElementSibling, el.parentElement]) {
467
+ if (!c || c === document.body) continue
468
+ const items = [...c.querySelectorAll(ITEMS)].filter((x) => x !== el && !x.contains(el) && seen(x))
469
+ if (items.length < 3) continue
470
+ const r = seen(c)
471
+ if (!r) continue
472
+ const selector = name(c)
473
+ if (!listed.has(selector)) {
474
+ listed.add(selector)
475
+ found.push({ kind: 'personal-list', heading: word, selector, items: items.length, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
476
+ }
477
+ break
478
+ }
479
+ if (listed.size >= 5) break
480
+ }
481
+ return found
482
+ }
483
+ // Read before any selector is made, so none of them quotes the name.
484
+ const noticeList = notices()`;
375
485
  var EXPOSURE_PROBE = `(() => {
376
486
  const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
377
487
  const vw = innerWidth, vh = innerHeight
@@ -383,7 +493,10 @@ var EXPOSURE_PROBE = `(() => {
383
493
  // layout's, and they come last. A value that itself holds what the scan
384
494
  // looks for is never used: the selector would quote it.
385
495
  const attr = (v) => '"' + v.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"') + '"'
386
- const quiet = (v) => !!v && v.length <= 80 && !kindsIn(v).length
496
+ // The signed-in account's own name, once the notices have read it: a
497
+ // selector never quotes it either.
498
+ let acct = null
499
+ const quiet = (v) => !!v && v.length <= 80 && !kindsIn(v).length && !(acct && v.toLowerCase().includes(acct.toLowerCase()))
387
500
  const segment = (el) => {
388
501
  const tag = el.tagName.toLowerCase()
389
502
  if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id) && !/\\d{2,}/.test(el.id) && quiet(el.id)) return tag + '#' + el.id
@@ -425,6 +538,7 @@ var EXPOSURE_PROBE = `(() => {
425
538
  if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
426
539
  return r
427
540
  }
541
+ ${NOTICE_SRC}
428
542
  const out = [], keys = new Set()
429
543
  const add = (el, text) => {
430
544
  const kinds = kindsIn(text)
@@ -451,7 +565,7 @@ var EXPOSURE_PROBE = `(() => {
451
565
  }
452
566
  const hits = {}
453
567
  for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
454
- return { exposures: out, maskHits: hits }
568
+ return { exposures: out, maskHits: hits, notices: noticeList }
455
569
  })()`;
456
570
  function maskInitScript(masks) {
457
571
  const rules = masks.map((m) => ({
@@ -536,6 +650,43 @@ function exposureLine(e) {
536
650
  return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
537
651
  }
538
652
  var 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.';
653
+ var NoticeLog = class {
654
+ byKey = /* @__PURE__ */ new Map();
655
+ add(step, notices) {
656
+ for (const n of notices ?? []) {
657
+ const key = n.kind === "account-name" ? n.kind : `${n.kind}|${n.selector ?? ""}`;
658
+ const had = this.byKey.get(key);
659
+ if (!had) {
660
+ this.byKey.set(key, { step, ...n, seen: 1 });
661
+ continue;
662
+ }
663
+ had.seen += 1;
664
+ if (n.kind !== "account-name") continue;
665
+ if (n.sure) had.sure = true;
666
+ const places = had.places ?? [];
667
+ for (const p of n.places ?? [])
668
+ if (places.length < 20 && !places.some((q) => q.selector === p.selector))
669
+ places.push(p);
670
+ had.places = places;
671
+ }
672
+ }
673
+ notices() {
674
+ return [...this.byKey.values()];
675
+ }
676
+ };
677
+ function noticeLine(n) {
678
+ const when = n.step < 0 ? "as the page opened" : `from step #${n.step}`;
679
+ if (n.kind === "account-name") {
680
+ const places = n.places ?? [];
681
+ return `${n.sure ? "" : "what may be "}the account's name (${places.length} ${places.length === 1 ? "place" : "places"}, ${when}): ${places.map((p) => p.selector).join("; ")}`;
682
+ }
683
+ const mask = JSON.stringify({ selector: n.selector, as: "blur" });
684
+ return `a "${n.heading}" list of the account's own items (${n.items} items, ${when}) in ${n.selector}; to hide it, add to "mask": ${mask}`;
685
+ }
686
+ function noticeAdvice(notices) {
687
+ const name = notices.some((n) => n.kind === "account-name");
688
+ return `These are the signed-in account's own: fine for a demo account, worth a mask on a real one.${name ? ' The name reads better swapped than blurred: { "selector": "\u2026", "as": "text", "text": "Jane Doe" }.' : ""}`;
689
+ }
539
690
 
540
691
  // src/record/setup.ts
541
692
  var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
@@ -620,7 +771,10 @@ async function runSetup(page, steps, opts) {
620
771
  lines.push(`setup #${i} wait ${step.ms}ms`);
621
772
  break;
622
773
  case "goto":
623
- await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
774
+ await (opts.goto ? opts.goto(step.url) : page.goto(step.url, {
775
+ waitUntil: "networkidle",
776
+ timeout: 45e3
777
+ })).catch(() => null);
624
778
  lines.push(`setup #${i} goto ${step.url}`);
625
779
  break;
626
780
  case "press":
@@ -689,6 +843,291 @@ function parseHeaders(given) {
689
843
  return out;
690
844
  }
691
845
 
846
+ // src/record/stepBound.ts
847
+ var STEP_MARGIN_MS = 3e4;
848
+ var WAIT_MARGIN_MS = 5e3;
849
+ var IDLE_CALL_MS = 6e4;
850
+ function stepBoundMs(step, plannedMs, defaults) {
851
+ if (step.do === "expect")
852
+ return (step.timeout ?? defaults.expectS) * 1e3 + WAIT_MARGIN_MS;
853
+ if (step.do === "await")
854
+ return (step.timeout ?? defaults.awaitS) * 1e3 + STEP_MARGIN_MS;
855
+ if (step.do === "download") return defaults.downloadS * 1e3 + STEP_MARGIN_MS;
856
+ return plannedMs + STEP_MARGIN_MS;
857
+ }
858
+ var StepBound = class {
859
+ constructor(now = Date.now, idleMs = IDLE_CALL_MS) {
860
+ this.now = now;
861
+ this.idleMs = idleMs;
862
+ }
863
+ now;
864
+ idleMs;
865
+ deadline = null;
866
+ started = 0;
867
+ hung = false;
868
+ /** A step begins: calls into the page answer within `boundMs` from now. */
869
+ start(boundMs) {
870
+ this.started = this.now();
871
+ this.deadline = this.started + boundMs;
872
+ this.hung = false;
873
+ }
874
+ /** The step ends: whether it ran into its bound, and how long it really took. */
875
+ end() {
876
+ const out = {
877
+ hung: this.hung,
878
+ ms: this.now() - this.started,
879
+ boundMs: (this.deadline ?? this.started) - this.started
880
+ };
881
+ this.deadline = null;
882
+ this.hung = false;
883
+ return out;
884
+ }
885
+ /** The pending call, or `undefined` once the bound has passed. */
886
+ race(p) {
887
+ const left = this.deadline === null ? this.idleMs : this.deadline - this.now();
888
+ if (left <= 0) {
889
+ this.hung = true;
890
+ p.catch(() => {
891
+ });
892
+ return Promise.resolve(void 0);
893
+ }
894
+ return new Promise((resolve, reject) => {
895
+ const timer = setTimeout(() => {
896
+ this.hung = true;
897
+ resolve(void 0);
898
+ }, left);
899
+ p.then(
900
+ (v) => {
901
+ clearTimeout(timer);
902
+ resolve(v);
903
+ },
904
+ (e) => {
905
+ clearTimeout(timer);
906
+ reject(e);
907
+ }
908
+ );
909
+ });
910
+ }
911
+ };
912
+ function bounded(target, bound) {
913
+ const cache = /* @__PURE__ */ new WeakMap();
914
+ const wrap = (obj) => {
915
+ const had = cache.get(obj);
916
+ if (had) return had;
917
+ const proxy = new Proxy(obj, {
918
+ get(t, prop) {
919
+ const value = Reflect.get(t, prop, t);
920
+ if (typeof value === "function")
921
+ return (...args) => {
922
+ const out = value.apply(
923
+ t,
924
+ args
925
+ );
926
+ if (out && typeof out.then === "function")
927
+ return bound.race(out);
928
+ if (out && typeof out === "object" && out !== t) return wrap(out);
929
+ return out;
930
+ };
931
+ if (value && typeof value === "object") return wrap(value);
932
+ return value;
933
+ }
934
+ });
935
+ cache.set(obj, proxy);
936
+ return proxy;
937
+ };
938
+ return wrap(target);
939
+ }
940
+
941
+ // src/record/pageSound.ts
942
+ var MAX_ELEMENTS = 50;
943
+ var MAX_NOTES = 2e3;
944
+ var MIN_HEARD_S = 0.05;
945
+ var MEDIA_WATCH_SCRIPT = `(() => {
946
+ let host
947
+ try { host = window.top; void host.document.title } catch { return }
948
+ if (window.__vosMediaWatch) return
949
+ window.__vosMediaWatch = true
950
+ if (!host.__vosMedia) {
951
+ const M = { log: [], seq: 0, els: new Set(), doc: Math.random().toString(36).slice(2) }
952
+ M.fields = (el, kind) => ({
953
+ id: el.__vosMediaId,
954
+ tag: el.tagName === 'VIDEO' ? 'video' : 'audio',
955
+ src: el.currentSrc || el.src || '',
956
+ kind,
957
+ wall: Date.now(),
958
+ t: el.currentTime,
959
+ rate: el.playbackRate || 1,
960
+ paused: el.paused || el.ended,
961
+ audible: !el.muted && el.volume > 0,
962
+ volume: el.volume,
963
+ duration: isFinite(el.duration) ? el.duration : null,
964
+ ab: typeof el.webkitAudioDecodedByteCount === 'number' ? el.webkitAudioDecodedByteCount : null,
965
+ })
966
+ host.__vosMedia = M
967
+ }
968
+ const M = host.__vosMedia
969
+ const EVENTS = ['playing', 'pause', 'ended', 'seeking', 'seeked', 'volumechange', 'ratechange', 'emptied']
970
+ const note = (e) => {
971
+ const el = e.target
972
+ if (!el || typeof el.currentTime !== 'number' || !/^(VIDEO|AUDIO)$/.test(el.tagName || '')) return
973
+ if (e.__vosNoted) return
974
+ e.__vosNoted = true
975
+ if (!el.__vosMediaId) {
976
+ if (M.els.size >= ${MAX_ELEMENTS}) return
977
+ el.__vosMediaId = ++M.seq
978
+ M.els.add(el)
979
+ }
980
+ if (M.log.length < ${MAX_NOTES}) M.log.push(M.fields(el, e.type))
981
+ }
982
+ for (const type of EVENTS) document.addEventListener(type, note, true)
983
+ const proto = window.HTMLMediaElement && window.HTMLMediaElement.prototype
984
+ if (proto && proto.play) {
985
+ const play = proto.play
986
+ proto.play = function () {
987
+ if (!this.__vosListening && !this.isConnected) {
988
+ this.__vosListening = true
989
+ for (const type of EVENTS) this.addEventListener(type, note)
990
+ }
991
+ return play.apply(this, arguments)
992
+ }
993
+ }
994
+ })()`;
995
+ var MEDIA_DRAIN_PROBE = `(() => {
996
+ const M = window.__vosMedia
997
+ if (!M) return { now: Date.now(), doc: null, notes: [] }
998
+ const notes = M.log.splice(0)
999
+ M.els.forEach((el) => notes.push(M.fields(el, 'tick')))
1000
+ return { now: Date.now(), doc: M.doc, notes }
1001
+ })()`;
1002
+ function soundSrcForMeta(src) {
1003
+ if (/^blob:/i.test(src)) return "blob:";
1004
+ if (/^data:/i.test(src)) return src.slice(0, src.search(/[;,]/) + 1 || 5);
1005
+ try {
1006
+ const u = new URL(src);
1007
+ return `${u.origin}${u.pathname}`;
1008
+ } catch {
1009
+ return "";
1010
+ }
1011
+ }
1012
+ var PageSoundLog = class {
1013
+ byKey = /* @__PURE__ */ new Map();
1014
+ openKeyOf = /* @__PURE__ */ new Map();
1015
+ doc = null;
1016
+ lastWall = 0;
1017
+ add(drain, nodeNow, step) {
1018
+ if (!drain) return;
1019
+ const skew = nodeNow - drain.now;
1020
+ if (drain.doc !== this.doc) {
1021
+ if (this.doc !== null) this.closeAll(this.lastWall);
1022
+ this.openKeyOf.clear();
1023
+ this.doc = drain.doc;
1024
+ }
1025
+ const notes = [...drain.notes].sort((a, b) => a.wall - b.wall);
1026
+ for (const n of notes) this.note(n, n.wall + skew, step);
1027
+ this.lastWall = nodeNow;
1028
+ }
1029
+ note(n, wall, step) {
1030
+ const key = `${this.doc}|${n.id}|${n.src}`;
1031
+ const prevKey = this.openKeyOf.get(n.id);
1032
+ if (prevKey && prevKey !== key) {
1033
+ const prev = this.byKey.get(prevKey);
1034
+ if (prev) this.close(prev, wall);
1035
+ }
1036
+ let f = this.byKey.get(key);
1037
+ if (!f) {
1038
+ if (!n.src) return;
1039
+ f = {
1040
+ tag: n.tag,
1041
+ src: n.src,
1042
+ played: false,
1043
+ audioBytes: null,
1044
+ open: null,
1045
+ stretches: []
1046
+ };
1047
+ this.byKey.set(key, f);
1048
+ }
1049
+ this.openKeyOf.set(n.id, key);
1050
+ if (n.duration !== null) f.duration = n.duration;
1051
+ if (n.ab !== null) f.audioBytes = Math.max(f.audioBytes ?? 0, n.ab);
1052
+ if (!n.paused) f.played = true;
1053
+ const stops = ["pause", "ended", "seeking", "emptied"].includes(n.kind);
1054
+ const heard = !n.paused && n.audible && !stops;
1055
+ const resync = ["playing", "seeked", "ratechange"].includes(n.kind);
1056
+ if (f.open && (!heard || resync))
1057
+ this.close(
1058
+ f,
1059
+ wall,
1060
+ n.kind === "ended" ? n.duration ?? void 0 : void 0
1061
+ );
1062
+ if (!f.open && heard) {
1063
+ f.open = { wall, t: n.t, rate: n.rate > 0 ? n.rate : 1 };
1064
+ f.volume = n.volume;
1065
+ if (f.step === void 0 && step !== void 0 && step >= 0) f.step = step;
1066
+ }
1067
+ }
1068
+ close(f, wall, endT) {
1069
+ if (!f.open) return;
1070
+ const { wall: from, t, rate } = f.open;
1071
+ f.open = null;
1072
+ if (wall <= from) return;
1073
+ const to = endT ?? t + (wall - from) / 1e3 * rate;
1074
+ f.stretches.push({
1075
+ fromWall: from,
1076
+ toWall: from + (to - t) / rate * 1e3,
1077
+ t0: t,
1078
+ rate
1079
+ });
1080
+ }
1081
+ closeAll(wall) {
1082
+ for (const f of this.byKey.values()) this.close(f, wall);
1083
+ }
1084
+ /**
1085
+ * Every element the page played, each heard stretch as footage seconds
1086
+ * `at` with media seconds `from`..`to`, none past `endMs` of footage.
1087
+ * A video whose decoder never decoded a byte of sound played none.
1088
+ */
1089
+ heard(clock, endWall, endMs) {
1090
+ this.closeAll(endWall);
1091
+ const out = [];
1092
+ for (const f of this.byKey.values()) {
1093
+ if (!f.played) continue;
1094
+ const silentVideo = f.tag === "video" && f.audioBytes === 0;
1095
+ const intervals = [];
1096
+ if (!silentVideo)
1097
+ for (const s of f.stretches)
1098
+ for (const c of clock.onCamera(s.fromWall, s.toWall)) {
1099
+ const at = c.atMs / 1e3;
1100
+ let from = s.t0 + (c.wallMs - s.fromWall) / 1e3 * s.rate;
1101
+ let to = from + c.ms / 1e3 * s.rate;
1102
+ if (f.duration !== void 0) to = Math.min(to, f.duration);
1103
+ const room = (endMs - c.atMs) / 1e3;
1104
+ if (room <= 0) continue;
1105
+ to = Math.min(to, from + room * s.rate);
1106
+ from = Math.max(0, from);
1107
+ if (to - from < MIN_HEARD_S) continue;
1108
+ intervals.push({
1109
+ at: +at.toFixed(3),
1110
+ from: +from.toFixed(3),
1111
+ to: +to.toFixed(3)
1112
+ });
1113
+ }
1114
+ out.push({
1115
+ src: f.src,
1116
+ meta: {
1117
+ ...f.step !== void 0 ? { step: f.step } : {},
1118
+ src: soundSrcForMeta(f.src),
1119
+ tag: f.tag,
1120
+ ...f.duration !== void 0 ? { duration: +f.duration.toFixed(3) } : {},
1121
+ ...intervals.length && f.volume !== void 0 ? { volume: +f.volume.toFixed(3) } : {},
1122
+ intervals,
1123
+ ...intervals.length ? {} : { muted: true }
1124
+ }
1125
+ });
1126
+ }
1127
+ return out;
1128
+ }
1129
+ };
1130
+
692
1131
  // src/record/takeClock.ts
693
1132
  var TakeClock = class {
694
1133
  constructor(t0) {
@@ -719,6 +1158,32 @@ var TakeClock = class {
719
1158
  open.to = Math.max(open.from, wallMs);
720
1159
  return open.to - open.from;
721
1160
  }
1161
+ /**
1162
+ * The parts of a wall-clock stretch the camera saw: each sub-stretch
1163
+ * outside every off-camera wait, as its wall start (`wallMs`), its
1164
+ * footage start (`atMs`) and its length. A stretch an `await` cut in two
1165
+ * comes back as two; one wholly off camera as none.
1166
+ */
1167
+ onCamera(fromWall, toWall) {
1168
+ const out = [];
1169
+ let cur = Math.max(fromWall, this.t0);
1170
+ for (const p of this.pauses) {
1171
+ const pTo = p.to ?? Infinity;
1172
+ if (pTo <= cur) continue;
1173
+ if (p.from >= toWall) break;
1174
+ if (p.from > cur)
1175
+ out.push({
1176
+ wallMs: cur,
1177
+ atMs: this.footageMs(cur),
1178
+ ms: p.from - cur
1179
+ });
1180
+ cur = Math.max(cur, pTo);
1181
+ if (cur >= toWall) return out;
1182
+ }
1183
+ if (toWall > cur)
1184
+ out.push({ wallMs: cur, atMs: this.footageMs(cur), ms: toWall - cur });
1185
+ return out;
1186
+ }
722
1187
  /**
723
1188
  * A captured frame's footage ms, or null when it is not later than the
724
1189
  * last frame kept (the frame is dropped; see the module comment).
@@ -764,6 +1229,591 @@ var PageErrorLog = class {
764
1229
  }
765
1230
  };
766
1231
 
1232
+ // src/record/surfaceFit.ts
1233
+ async function surfaceSize(cdp, dpr) {
1234
+ const shot = await cdp.send("Page.captureScreenshot", {
1235
+ format: "jpeg",
1236
+ quality: 1
1237
+ });
1238
+ const dims = shot.data ? jpegDims(base64Bytes(shot.data)) : null;
1239
+ if (!dims) return null;
1240
+ return { width: Math.round(dims.w / dpr), height: Math.round(dims.h / dpr) };
1241
+ }
1242
+ async function fitSurfaceToViewport(cdp, width, height) {
1243
+ try {
1244
+ const ratio = await cdp.send("Runtime.evaluate", {
1245
+ expression: "devicePixelRatio",
1246
+ returnByValue: true
1247
+ });
1248
+ const value = ratio.result?.value;
1249
+ const dpr = typeof value === "number" && value > 0 ? value : 1;
1250
+ const before = await surfaceSize(cdp, dpr);
1251
+ if (!before) return null;
1252
+ let after = before;
1253
+ for (let tries = 0; tries < 2; tries++) {
1254
+ if (after.width === width && after.height === height) break;
1255
+ const win = await cdp.send("Browser.getWindowForTarget");
1256
+ const { width: ww, height: wh } = win.bounds;
1257
+ if (ww === void 0 || wh === void 0) break;
1258
+ await cdp.send("Browser.setWindowBounds", {
1259
+ windowId: win.windowId,
1260
+ bounds: {
1261
+ width: ww + (width - after.width),
1262
+ height: wh + (height - after.height)
1263
+ }
1264
+ });
1265
+ after = await surfaceSize(cdp, dpr) ?? after;
1266
+ }
1267
+ return { before, after };
1268
+ } catch {
1269
+ return null;
1270
+ }
1271
+ }
1272
+
1273
+ // src/record/waits.ts
1274
+ function visibleTextScript(text) {
1275
+ return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
1276
+ }
1277
+ async function readCondition(page, c) {
1278
+ try {
1279
+ if (c.text !== void 0)
1280
+ return { holds: await page.evaluate(visibleTextScript(c.text)) };
1281
+ if (c.gone !== void 0)
1282
+ return { holds: !await page.locator(c.gone).first().isVisible() };
1283
+ if (c.selector !== void 0) {
1284
+ const loc = page.locator(c.selector).first();
1285
+ const visible = await loc.isVisible();
1286
+ if (!visible) return { holds: false, visible };
1287
+ if (c.enabled === void 0) return { holds: true, visible };
1288
+ const enabled = await loc.isEnabled({ timeout: 1e3 });
1289
+ return { holds: enabled === c.enabled, visible };
1290
+ }
1291
+ } catch {
1292
+ }
1293
+ return { holds: false };
1294
+ }
1295
+ async function pollCondition(page, c, opts) {
1296
+ const now = opts.now ?? Date.now;
1297
+ const start = now();
1298
+ const reads = Math.max(1, opts.confirm?.reads ?? 1);
1299
+ for (let first = true; ; first = false) {
1300
+ if (!first && opts.between) await opts.between();
1301
+ let last = await readCondition(page, c);
1302
+ for (let n = 1; last.holds && n < reads; n++) {
1303
+ await opts.sleep(opts.confirm?.gapMs ?? 0);
1304
+ last = await readCondition(page, c);
1305
+ }
1306
+ const ms = now() - start;
1307
+ if (last.holds) return { ok: true, ms, last };
1308
+ const left = opts.timeoutMs - ms;
1309
+ if (left <= 0) return { ok: false, ms, last };
1310
+ await opts.sleep(Math.min(opts.everyMs, left));
1311
+ }
1312
+ }
1313
+ var DOM_QUIET_MS = 800;
1314
+ var DOM_SETTLE_CAP_MS = 5e3;
1315
+ var DOM_CHANGE_PROBE = `(() => {
1316
+ if (!window.__vosDomChanges) {
1317
+ window.__vosDomChanges = { n: 0 };
1318
+ new MutationObserver((list) => { window.__vosDomChanges.n += list.length })
1319
+ .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
1320
+ }
1321
+ return window.__vosDomChanges.n;
1322
+ })()`;
1323
+ async function domSettled(page, opts) {
1324
+ const now = opts.now ?? Date.now;
1325
+ const quiet = opts.quietMs ?? DOM_QUIET_MS;
1326
+ const cap = opts.capMs ?? DOM_SETTLE_CAP_MS;
1327
+ const start = now();
1328
+ let seen = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
1329
+ let since = start;
1330
+ for (; ; ) {
1331
+ const t = now();
1332
+ if (t - since >= quiet) return { settled: true, ms: t - start };
1333
+ if (t - start >= cap) return { settled: false, ms: t - start };
1334
+ await opts.sleep(opts.tickMs ?? 100);
1335
+ const n = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
1336
+ if (n !== seen) {
1337
+ seen = n;
1338
+ since = now();
1339
+ }
1340
+ }
1341
+ }
1342
+ function sayDuration(ms) {
1343
+ const s = Math.round(ms / 100) / 10;
1344
+ if (s < 60) return `${s} s`;
1345
+ const m = Math.floor(s / 60);
1346
+ const rest = Math.round(s - m * 60);
1347
+ return rest ? `${m} min ${rest} s` : `${m} min`;
1348
+ }
1349
+ function sayCondition(c) {
1350
+ if (c.text !== void 0) return `text "${c.text}" to show`;
1351
+ if (c.gone !== void 0) return `'${c.gone}' to go`;
1352
+ if (c.enabled === true) return `'${c.selector}' to be enabled`;
1353
+ if (c.enabled === false) return `'${c.selector}' to be disabled`;
1354
+ return `'${c.selector}' to appear`;
1355
+ }
1356
+ function conditionMiss(c, last, ms) {
1357
+ const t = sayDuration(ms);
1358
+ if (c.text !== void 0)
1359
+ return `no visible text contained "${c.text}" for ${t}`;
1360
+ if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
1361
+ if (last.visible && c.enabled === true)
1362
+ return `'${c.selector}' stayed disabled for ${t}`;
1363
+ if (last.visible && c.enabled === false)
1364
+ return `'${c.selector}' stayed enabled for ${t}`;
1365
+ return `'${c.selector}' never appeared in ${t}`;
1366
+ }
1367
+
1368
+ // src/record/ready.ts
1369
+ var READY_MODES = ["dom", "networkidle"];
1370
+ var DEFAULT_READY = "dom";
1371
+ var NAV_TIMEOUT_MS = 45e3;
1372
+ var READY_SETTLE_CAP_MS = 8e3;
1373
+ var NETWORKIDLE_HYDRATION_MS = 800;
1374
+ function readyProblem(v) {
1375
+ if (v === void 0) return null;
1376
+ return typeof v === "string" && READY_MODES.includes(v) ? null : `ready must be ${READY_MODES.map((m) => `"${m}"`).join(" or ")}`;
1377
+ }
1378
+ async function untilReady(page, mode, navigate, opts) {
1379
+ const now = opts.now ?? Date.now;
1380
+ const start = now();
1381
+ const timeout = opts.timeoutMs ?? NAV_TIMEOUT_MS;
1382
+ if (mode === "networkidle") {
1383
+ let idle = true;
1384
+ const response2 = await navigate({
1385
+ waitUntil: "networkidle",
1386
+ timeout
1387
+ }).catch(() => {
1388
+ idle = false;
1389
+ return null;
1390
+ });
1391
+ await opts.sleep(NETWORKIDLE_HYDRATION_MS);
1392
+ return {
1393
+ response: response2,
1394
+ ready: { mode, ms: Math.round(now() - start), capped: !idle }
1395
+ };
1396
+ }
1397
+ const response = await navigate({ waitUntil: "load", timeout }).catch(
1398
+ () => null
1399
+ );
1400
+ const settled = await domSettled(page, {
1401
+ sleep: opts.sleep,
1402
+ now,
1403
+ quietMs: DOM_QUIET_MS,
1404
+ capMs: opts.capMs ?? READY_SETTLE_CAP_MS
1405
+ });
1406
+ return {
1407
+ response,
1408
+ ready: { mode, ms: Math.round(now() - start), capped: !settled.settled }
1409
+ };
1410
+ }
1411
+ async function reloadUntilReady(page, mode, timeoutMs, opts = {}) {
1412
+ const nav = {
1413
+ waitUntil: mode === "networkidle" ? "networkidle" : "load",
1414
+ timeout: Math.min(NAV_TIMEOUT_MS, timeoutMs)
1415
+ };
1416
+ await (page.reload ? page.reload(nav) : page.goto(page.url(), nav)).catch(
1417
+ () => null
1418
+ );
1419
+ await domSettled(page, { sleep: opts.sleep ?? realSleep });
1420
+ }
1421
+ function readyLine(r) {
1422
+ const s = (r.ms / 1e3).toFixed(1);
1423
+ if (r.mode === "networkidle")
1424
+ return r.capped ? `(networkidle timeout after ${s} s, continuing)` : `(network idle after ${s} s)`;
1425
+ return r.capped ? `(the page was still changing after ${s} s, starting anyway)` : `(the page settled after ${s} s)`;
1426
+ }
1427
+
1428
+ // src/record/gestures.ts
1429
+ var MODIFIERS = [
1430
+ "Shift",
1431
+ "Control",
1432
+ "Alt",
1433
+ "Meta",
1434
+ "Mod",
1435
+ "ControlOrMeta"
1436
+ ];
1437
+ var DRAG_MODIFIERS = [
1438
+ "Alt",
1439
+ "Shift",
1440
+ "Meta",
1441
+ "Control",
1442
+ "Mod"
1443
+ ];
1444
+ var NAMED_KEYS = /* @__PURE__ */ new Set([
1445
+ "Escape",
1446
+ "Enter",
1447
+ "Tab",
1448
+ "Backspace",
1449
+ "Delete",
1450
+ "Insert",
1451
+ "ArrowUp",
1452
+ "ArrowDown",
1453
+ "ArrowLeft",
1454
+ "ArrowRight",
1455
+ "Home",
1456
+ "End",
1457
+ "PageUp",
1458
+ "PageDown",
1459
+ "Space",
1460
+ "CapsLock",
1461
+ "ContextMenu",
1462
+ "Backquote",
1463
+ "Minus",
1464
+ "Equal",
1465
+ "BracketLeft",
1466
+ "BracketRight",
1467
+ "Backslash",
1468
+ "Semicolon",
1469
+ "Quote",
1470
+ "Comma",
1471
+ "Period",
1472
+ "Slash",
1473
+ "NumpadAdd",
1474
+ "NumpadSubtract",
1475
+ "NumpadMultiply",
1476
+ "NumpadDivide",
1477
+ "NumpadDecimal",
1478
+ "NumpadEnter"
1479
+ ]);
1480
+ var isModifier = (s) => MODIFIERS.includes(s);
1481
+ function isKeyName(k) {
1482
+ if ([...k].length === 1) return k.trim().length === 1 || k === " ";
1483
+ if (NAMED_KEYS.has(k)) return true;
1484
+ return /^Key[A-Z]$/.test(k) || /^Digit[0-9]$/.test(k) || /^Numpad[0-9]$/.test(k) || /^F([1-9]|1[0-9]|2[0-4])$/.test(k);
1485
+ }
1486
+ function splitChord(chord) {
1487
+ if (chord === "+") return { mods: [], key: "+" };
1488
+ const plusKey = chord.endsWith("++");
1489
+ const parts = (plusKey ? chord.slice(0, -2) : chord).split("+");
1490
+ if (plusKey) return { mods: parts, key: "+" };
1491
+ const key = parts.pop() ?? "";
1492
+ return { mods: parts, key };
1493
+ }
1494
+ function chordProblem(chord) {
1495
+ if (typeof chord !== "string" || !chord)
1496
+ return 'must be a key name or a chord like "Shift+D"';
1497
+ const { mods, key } = splitChord(chord);
1498
+ for (const m of mods) {
1499
+ if (!isModifier(m))
1500
+ return `"${chord}": "${m}" is not a modifier (one of ${MODIFIERS.join(", ")})`;
1501
+ }
1502
+ if (new Set(mods).size !== mods.length)
1503
+ return `"${chord}" names a modifier twice`;
1504
+ if (!key) return `"${chord}" ends without a key`;
1505
+ if (isModifier(key)) {
1506
+ if (mods.length) return `"${chord}" ends on a modifier, not a key`;
1507
+ return null;
1508
+ }
1509
+ if (!isKeyName(key))
1510
+ return `"${chord}": "${key}" is not a key (a single character, a named key like Escape or ArrowLeft, F1 to F24, or a code like KeyA or Digit1)`;
1511
+ return null;
1512
+ }
1513
+ function resolveChord(chord) {
1514
+ const { mods, key } = splitChord(chord);
1515
+ const k = key === "Mod" ? "ControlOrMeta" : key;
1516
+ return [...mods.map((m) => m === "Mod" ? "ControlOrMeta" : m), k].join("+");
1517
+ }
1518
+ function pressSequence(step) {
1519
+ if (step.keys !== void 0)
1520
+ return typeof step.keys === "string" ? [step.keys] : step.keys;
1521
+ return step.key ? [step.key] : [];
1522
+ }
1523
+ var KEY_SEQUENCE_GAP_MS = 150;
1524
+ var KEY_ROUNDTRIP_MS = 50;
1525
+ var keySequencePlanMs = (keys) => Math.max(0, keys - 1) * KEY_SEQUENCE_GAP_MS + keys * KEY_ROUNDTRIP_MS;
1526
+ var DRAG_HOLD_MS = 400;
1527
+ var MAX_KEYS = 50;
1528
+ function resolveModifier(m) {
1529
+ return m === "Mod" ? "ControlOrMeta" : m;
1530
+ }
1531
+ function modifierBits(mods, platform) {
1532
+ let bits = 0;
1533
+ for (const m of mods) {
1534
+ const k = m === "Mod" ? platform === "mac" ? "Meta" : "Control" : m;
1535
+ bits |= k === "Alt" ? 1 : k === "Control" ? 2 : k === "Meta" ? 4 : 8;
1536
+ }
1537
+ return bits;
1538
+ }
1539
+ var CLICK_BUTTONS = ["left", "right", "middle"];
1540
+ var buttonNumber = (b) => b === "right" ? 2 : b === "middle" ? 1 : 0;
1541
+ var MULTI_CLICK_GAP_MS = 90;
1542
+ var MAX_PATH_POINTS = 2e4;
1543
+ var PATH_DEFAULT_MS = 700;
1544
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
1545
+ function readPoint(raw) {
1546
+ if (Array.isArray(raw)) {
1547
+ if (raw.length < 2 || raw.length > 4)
1548
+ return "must be [x, y], [x, y, tMs] or [x, y, tMs, pressure]";
1549
+ const [x, y, t, pressure] = raw;
1550
+ if (!finite(x) || !finite(y)) return "x and y must be numbers (CSS px)";
1551
+ if (t !== void 0 && !finite(t)) return "its time must be a number of ms";
1552
+ if (pressure !== void 0 && !finite(pressure))
1553
+ return "its pressure must be a number from 0 to 1";
1554
+ return {
1555
+ x,
1556
+ y,
1557
+ ...t !== void 0 ? { t } : {},
1558
+ ...pressure !== void 0 ? { pressure } : {}
1559
+ };
1560
+ }
1561
+ if (typeof raw === "object" && raw !== null) {
1562
+ const o = raw;
1563
+ const extra = Object.keys(o).filter(
1564
+ (k) => !["x", "y", "t", "pressure"].includes(k)
1565
+ );
1566
+ if (extra.length)
1567
+ return `takes x, y, t and pressure, not ${extra.join(", ")}`;
1568
+ if (!finite(o.x) || !finite(o.y)) return "x and y must be numbers (CSS px)";
1569
+ if (o.t !== void 0 && !finite(o.t)) return "its t must be a number of ms";
1570
+ if (o.pressure !== void 0 && !finite(o.pressure))
1571
+ return "its pressure must be a number from 0 to 1";
1572
+ return {
1573
+ x: o.x,
1574
+ y: o.y,
1575
+ ...o.t !== void 0 ? { t: o.t } : {},
1576
+ ...o.pressure !== void 0 ? { pressure: o.pressure } : {}
1577
+ };
1578
+ }
1579
+ return "must be [x, y] or { x, y }";
1580
+ }
1581
+ function readPath(raw) {
1582
+ const list = typeof raw === "object" && raw !== null && !Array.isArray(raw) && Array.isArray(raw.points) ? raw.points : raw;
1583
+ if (!Array.isArray(list))
1584
+ return { problems: ['must be an array of points, or { "points": [...] }'] };
1585
+ if (list.length < 2) return { problems: ["needs at least two points"] };
1586
+ if (list.length > MAX_PATH_POINTS)
1587
+ return { problems: [`holds more than ${MAX_PATH_POINTS} points`] };
1588
+ const problems = [];
1589
+ const read = [];
1590
+ list.forEach((p, i) => {
1591
+ const r = readPoint(p);
1592
+ if (typeof r === "string") problems.push(`point ${i} ${r}`);
1593
+ else {
1594
+ if (r.x < 0 || r.y < 0)
1595
+ problems.push(`point ${i} is off the viewport (x and y are \u2265 0)`);
1596
+ if (r.pressure !== void 0 && (r.pressure < 0 || r.pressure > 1))
1597
+ problems.push(`point ${i} pressure must be from 0 to 1`);
1598
+ read.push(r);
1599
+ }
1600
+ });
1601
+ if (problems.length) return { problems: problems.slice(0, 5) };
1602
+ const timedCount = read.filter((p) => p.t !== void 0).length;
1603
+ if (timedCount !== 0 && timedCount !== read.length)
1604
+ return {
1605
+ problems: [
1606
+ "every point carries a time or none does: a path is replayed by its own timing, or evenly over ms"
1607
+ ]
1608
+ };
1609
+ const timed = timedCount > 0;
1610
+ if (timed) {
1611
+ for (let i = 1; i < read.length; i++)
1612
+ if (read[i].t < read[i - 1].t)
1613
+ return {
1614
+ problems: [`point ${i}'s time runs backwards (times never decrease)`]
1615
+ };
1616
+ }
1617
+ const t0 = timed ? read[0].t : 0;
1618
+ return {
1619
+ timed,
1620
+ points: read.map((p) => ({
1621
+ x: p.x,
1622
+ y: p.y,
1623
+ at: timed ? p.t - t0 : 0,
1624
+ ...p.pressure !== void 0 ? { pressure: p.pressure } : {}
1625
+ }))
1626
+ };
1627
+ }
1628
+ function schedulePath(path, ms) {
1629
+ if (path.timed) return path.points;
1630
+ const dur = ms ?? PATH_DEFAULT_MS;
1631
+ const n = path.points.length;
1632
+ return path.points.map((p, i) => ({
1633
+ ...p,
1634
+ at: n > 1 ? Math.round(dur * i / (n - 1)) : 0
1635
+ }));
1636
+ }
1637
+ var pathDurationMs = (s) => s.length ? s[s.length - 1].at : 0;
1638
+ var OVERRUN_SHARE = 0.2;
1639
+ var OVERRUN_MIN_PLANNED_MS = 100;
1640
+ var OVERRUN_MIN_MS = 100;
1641
+ function overrunOf(plannedMs, tookMs) {
1642
+ if (!(plannedMs >= OVERRUN_MIN_PLANNED_MS)) return null;
1643
+ if (tookMs <= plannedMs * (1 + OVERRUN_SHARE)) return null;
1644
+ if (tookMs - plannedMs < OVERRUN_MIN_MS) return null;
1645
+ return Math.round(tookMs - plannedMs);
1646
+ }
1647
+ function overrunLine(overruns) {
1648
+ if (!overruns.length) return null;
1649
+ const planned = overruns.reduce((a, o) => a + o.plannedMs, 0);
1650
+ const took = overruns.reduce((a, o) => a + o.tookMs, 0);
1651
+ const ratio = planned > 0 ? took / planned : 0;
1652
+ const n = overruns.length;
1653
+ const which = overruns.map((o) => `#${o.step}${o.id ? ` (${o.id})` : ""} ${o.do}`).join(", ");
1654
+ return `${n} step${n === 1 ? "" : "s"} ran ${ratio.toFixed(1)}\xD7 slower than scripted (${which}): the page could not keep up; add settle or raise ms`;
1655
+ }
1656
+
1657
+ // src/record/paste.ts
1658
+ var PASTE_READ_WINDOW_MS = 300;
1659
+ var PASTE_UNREAD_MS = 3e3;
1660
+ var PASTE_MAX_BYTES = 10 * 1024 * 1024;
1661
+ var PASTE_TYPES = {
1662
+ ".png": "image/png",
1663
+ ".jpg": "image/jpeg",
1664
+ ".jpeg": "image/jpeg",
1665
+ ".webp": "image/webp",
1666
+ ".gif": "image/gif",
1667
+ ".svg": "image/svg+xml",
1668
+ ".txt": "text/plain",
1669
+ ".md": "text/plain",
1670
+ ".html": "text/html",
1671
+ ".htm": "text/html",
1672
+ ".json": "text/plain",
1673
+ ".csv": "text/plain"
1674
+ };
1675
+ function pasteTypeOf(name) {
1676
+ const dot = name.lastIndexOf(".");
1677
+ if (dot < 0) return null;
1678
+ return PASTE_TYPES[name.slice(dot).toLowerCase()] ?? null;
1679
+ }
1680
+ var BUILD = (item) => `
1681
+ const item = ${JSON.stringify(item)};
1682
+ const bytes = item.b64 ? Uint8Array.from(atob(item.b64), (c) => c.charCodeAt(0)) : null;
1683
+ const asText = bytes ? new TextDecoder().decode(bytes) : null;
1684
+ async function blobs() {
1685
+ if (item.text !== undefined) return { 'text/plain': new Blob([item.text], { type: 'text/plain' }) };
1686
+ const type = item.type;
1687
+ if (type === 'text/plain') return { 'text/plain': new Blob([asText], { type }) };
1688
+ if (type === 'text/html') return { 'text/html': new Blob([asText], { type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) };
1689
+ if (type === 'image/svg+xml') return { 'image/svg+xml': new Blob([bytes], { type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) };
1690
+ if (type === 'image/png') return { 'image/png': new Blob([bytes], { type }) };
1691
+ const bmp = await createImageBitmap(new Blob([bytes], { type }));
1692
+ const c = document.createElement('canvas');
1693
+ c.width = bmp.width; c.height = bmp.height;
1694
+ c.getContext('2d').drawImage(bmp, 0, 0);
1695
+ const png = await new Promise((r) => c.toBlob(r, 'image/png'));
1696
+ return { 'image/png': png };
1697
+ }
1698
+ `;
1699
+ function stageClipboardScript(item) {
1700
+ return `(async () => {
1701
+ ${BUILD(item)}
1702
+ try {
1703
+ if (!navigator.clipboard || typeof ClipboardItem === 'undefined') return 'refused: this page has no clipboard';
1704
+ const b = await blobs();
1705
+ try {
1706
+ await navigator.clipboard.write([new ClipboardItem(b)]);
1707
+ } catch (e) {
1708
+ if (!b['text/plain'] || Object.keys(b).length === 1) throw e;
1709
+ await navigator.clipboard.write([new ClipboardItem({ 'text/plain': b['text/plain'] })]);
1710
+ }
1711
+ return 'ok';
1712
+ } catch (e) {
1713
+ return 'refused: ' + (e && e.message ? e.message : String(e));
1714
+ }
1715
+ })()`;
1716
+ }
1717
+ function syntheticPasteScript(item, opts = {}) {
1718
+ return `(async () => {
1719
+ ${BUILD(item)}
1720
+ try {
1721
+ // With original, an image file is handed over as its own bytes and type
1722
+ // (an SVG also as its markup), never redrawn.
1723
+ const b = ${opts.original ? `item.type === 'image/svg+xml' ? { 'image/svg+xml': new Blob([bytes], { type: item.type }), 'text/plain': new Blob([asText], { type: 'text/plain' }) } : { [item.type]: new Blob([bytes], { type: item.type }) }` : "await blobs()"};
1724
+ const dt = new DataTransfer();
1725
+ let file = null;
1726
+ for (const [type, blob] of Object.entries(b)) {
1727
+ if (type.startsWith('image/')) {
1728
+ file = new File([blob], item.name || 'image.png', { type: blob.type || type });
1729
+ dt.items.add(file);
1730
+ } else dt.setData(type, await blob.text());
1731
+ }
1732
+ try {
1733
+ // The async reads answer the item for THIS paste only: shortly after
1734
+ // the first read (an app may read text and items in turn), or after a
1735
+ // few seconds when nothing reads, the browser's own reads come back,
1736
+ // so a later paste through the clipboard is the clipboard's.
1737
+ const staged = [new ClipboardItem(b)];
1738
+ const clip = navigator.clipboard;
1739
+ const own = { read: Object.prototype.hasOwnProperty.call(clip, 'read'), readText: Object.prototype.hasOwnProperty.call(clip, 'readText') };
1740
+ const was = { read: clip.read, readText: clip.readText };
1741
+ let restored = false;
1742
+ const restore = () => {
1743
+ if (restored) return;
1744
+ restored = true;
1745
+ for (const k of ['read', 'readText']) {
1746
+ if (own[k]) clip[k] = was[k];
1747
+ else delete clip[k];
1748
+ }
1749
+ };
1750
+ let soon = null;
1751
+ const readOnce = () => { if (!soon) soon = setTimeout(restore, ${PASTE_READ_WINDOW_MS}) };
1752
+ clip.read = async () => { readOnce(); return staged };
1753
+ clip.readText = async () => { readOnce(); return b['text/plain'] ? b['text/plain'].text() : '' };
1754
+ setTimeout(restore, ${PASTE_UNREAD_MS});
1755
+ } catch {}
1756
+ const target = document.activeElement || document.body;
1757
+ const ev = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true, composed: true });
1758
+ const taken = !target.dispatchEvent(ev);
1759
+ const words = dt.getData('text/plain');
1760
+ if (!taken && words && (target.isContentEditable || /^(INPUT|TEXTAREA)$/.test(target.tagName)))
1761
+ document.execCommand('insertText', false, words);
1762
+ return 'ok';
1763
+ } catch (e) {
1764
+ return 'failed: ' + (e && e.message ? e.message : String(e));
1765
+ }
1766
+ })()`;
1767
+ }
1768
+
1769
+ // src/record/settle.ts
1770
+ var APP_QUIET_MS = 300;
1771
+ var APP_SETTLE_CAP_MS = 4e3;
1772
+ var SETTLE_VERBS = /* @__PURE__ */ new Set([
1773
+ "hover",
1774
+ "click",
1775
+ "type",
1776
+ "press",
1777
+ "scroll",
1778
+ "move",
1779
+ "drag",
1780
+ "paste"
1781
+ ]);
1782
+ var CHANGE_AGE_PROBE = `(() => {
1783
+ const w = window;
1784
+ if (!w.__vosChangeAt) {
1785
+ w.__vosChangeAt = { at: Date.now() };
1786
+ new MutationObserver(() => { w.__vosChangeAt.at = Date.now() })
1787
+ .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
1788
+ return -1;
1789
+ }
1790
+ return Date.now() - w.__vosChangeAt.at;
1791
+ })()`;
1792
+ function settleOn(step, scriptDefault) {
1793
+ if (!SETTLE_VERBS.has(step.do)) return false;
1794
+ return step.settle ?? scriptDefault ?? false;
1795
+ }
1796
+ async function appSettled(page, frames, opts) {
1797
+ const quiet = opts.quietMs ?? APP_QUIET_MS;
1798
+ const age = await page.evaluate(CHANGE_AGE_PROBE).catch(() => -1);
1799
+ const frameAge = opts.sinceFrame?.() ?? null;
1800
+ if (typeof age === "number" && age >= quiet && (frameAge === null || frameAge >= quiet))
1801
+ return { settled: true, ms: 0 };
1802
+ const counted = {
1803
+ evaluate: async (script) => {
1804
+ const n = await page.evaluate(script).catch(() => 0);
1805
+ return (typeof n === "number" && n > 0 ? n : 0) + frames();
1806
+ }
1807
+ };
1808
+ return domSettled(counted, {
1809
+ sleep: opts.sleep,
1810
+ ...opts.now ? { now: opts.now } : {},
1811
+ quietMs: quiet,
1812
+ capMs: opts.capMs ?? APP_SETTLE_CAP_MS,
1813
+ tickMs: 50
1814
+ });
1815
+ }
1816
+
767
1817
  // src/record/actions.ts
768
1818
  var STEP_GUARDS = ["spend"];
769
1819
  var GUARDABLE = /* @__PURE__ */ new Set(["click", "press", "type"]);
@@ -782,7 +1832,8 @@ var VERBS2 = /* @__PURE__ */ new Set([
782
1832
  "drag",
783
1833
  "expect",
784
1834
  "await",
785
- "download"
1835
+ "download",
1836
+ "paste"
786
1837
  ]);
787
1838
  var PRESS_KEYS = [
788
1839
  "Escape",
@@ -824,6 +1875,10 @@ function validateActions(value) {
824
1875
  }
825
1876
  }
826
1877
  if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
1878
+ const ready = readyProblem(obj.ready);
1879
+ if (ready) errors.push(ready);
1880
+ if (obj.settle !== void 0 && typeof obj.settle !== "boolean")
1881
+ errors.push("settle must be true or false");
827
1882
  if (obj.mask !== void 0) {
828
1883
  if (!Array.isArray(obj.mask)) {
829
1884
  errors.push("mask must be an array of { selector, as?, text? }");
@@ -871,9 +1926,24 @@ function validateActions(value) {
871
1926
  seenIds.add(s.id);
872
1927
  }
873
1928
  }
874
- const needSelector = s.do === "hover" || s.do === "click" || s.do === "type";
875
- if (needSelector && typeof s.selector !== "string")
876
- errors.push(`${at}: ${s.do} needs a selector`);
1929
+ if (s.do === "type" && typeof s.selector !== "string")
1930
+ errors.push(`${at}: type needs a selector`);
1931
+ if (s.do === "hover" || s.do === "click") {
1932
+ const point = s.x !== void 0 || s.y !== void 0;
1933
+ if (point && s.selector !== void 0)
1934
+ errors.push(
1935
+ `${at}: ${s.do} takes a selector or x and y (a viewport point), not both`
1936
+ );
1937
+ else if (point) {
1938
+ if (!isCoord(s.x) || !isCoord(s.y))
1939
+ errors.push(
1940
+ `${at}: ${s.do} x and y must both be numbers of CSS px, 0 or more`
1941
+ );
1942
+ } else if (typeof s.selector !== "string")
1943
+ errors.push(
1944
+ `${at}: ${s.do} needs a selector, or x and y (a viewport point)`
1945
+ );
1946
+ }
877
1947
  if (s.do === "wait" && typeof s.ms !== "number")
878
1948
  errors.push(`${at}: wait needs ms`);
879
1949
  if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
@@ -887,13 +1957,64 @@ function validateActions(value) {
887
1957
  if (s.do === "type" && s.at !== void 0 && s.at !== "end" && s.at !== "start")
888
1958
  errors.push(`${at}: type at must be "end" or "start"`);
889
1959
  if (s.do === "press") {
890
- if (typeof s.key !== "string" || !PRESS_KEYS.includes(s.key))
1960
+ if (s.key !== void 0 && s.keys !== void 0)
1961
+ errors.push(
1962
+ `${at}: press takes key or keys, not both (keys says everything key does)`
1963
+ );
1964
+ else if (s.keys !== void 0) {
1965
+ const list = Array.isArray(s.keys) ? s.keys : [s.keys];
1966
+ if (!list.length)
1967
+ errors.push(`${at}: press keys must name at least one key`);
1968
+ else if (list.length > MAX_KEYS)
1969
+ errors.push(`${at}: press keys names more than ${MAX_KEYS} keys`);
1970
+ list.forEach((k, j) => {
1971
+ const problem = chordProblem(k);
1972
+ if (problem)
1973
+ errors.push(
1974
+ `${at}: press keys${Array.isArray(s.keys) ? `[${j}]` : ""} ${problem}`
1975
+ );
1976
+ });
1977
+ } else if (typeof s.key !== "string" || !PRESS_KEYS.includes(s.key))
891
1978
  errors.push(
892
- `${at}: press needs a key, one of ${PRESS_KEYS.join(", ")}; words are typed with type`
1979
+ `${at}: press needs a key, one of ${PRESS_KEYS.join(", ")}, or keys (a chord like "Shift+D", a plain key like "r", or a sequence); words are typed with type`
893
1980
  );
894
1981
  if (s.selector !== void 0 && typeof s.selector !== "string")
895
1982
  errors.push(`${at}: press selector must be a string`);
896
1983
  }
1984
+ if (s.do === "click") {
1985
+ errors.push(...modifiersProblem(at, "click", s.modifiers));
1986
+ if (s.count !== void 0 && s.count !== 1 && s.count !== 2 && s.count !== 3)
1987
+ errors.push(
1988
+ `${at}: click count must be 1, 2 (a double click) or 3 (a triple click)`
1989
+ );
1990
+ if (s.button !== void 0 && !CLICK_BUTTONS.includes(s.button))
1991
+ errors.push(`${at}: click button must be "left", "right" or "middle"`);
1992
+ }
1993
+ if (s.settle !== void 0) {
1994
+ if (typeof s.settle !== "boolean")
1995
+ errors.push(`${at}: settle must be true or false`);
1996
+ else if (!SETTLE_VERBS.has(s.do))
1997
+ errors.push(
1998
+ `${at}: settle follows a gesture (hover, click, type, press, scroll, move, drag, paste); a ${s.do} sends the page nothing to catch up on`
1999
+ );
2000
+ }
2001
+ if (s.do === "paste") {
2002
+ const given = ["text", "file"].filter(
2003
+ (k) => s[k] !== void 0
2004
+ );
2005
+ if (given.length !== 1)
2006
+ errors.push(
2007
+ `${at}: paste needs exactly one of text (words) or file (an image, an SVG or a text file beside the actions file)`
2008
+ );
2009
+ if (s.text !== void 0 && typeof s.text !== "string")
2010
+ errors.push(`${at}: paste text must be a string`);
2011
+ if (s.file !== void 0) {
2012
+ const problem = typeof s.file === "string" ? fileNameProblem(s.file) ?? (pasteTypeOf(s.file) ? null : `"${s.file}" is not a type a paste takes (.png, .jpg, .webp, .gif, .svg, .txt, .md, .html, .json, .csv)`) : "is not a string";
2013
+ if (problem) errors.push(`${at}: paste file ${problem}`);
2014
+ }
2015
+ if (s.selector !== void 0 && !isFilled(s.selector))
2016
+ errors.push(`${at}: paste selector must be a non-empty string`);
2017
+ }
897
2018
  if (s.do === "scroll" && typeof s.dy !== "number")
898
2019
  errors.push(`${at}: scroll needs dy`);
899
2020
  if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
@@ -985,18 +2106,51 @@ function validateActions(value) {
985
2106
  }
986
2107
  }
987
2108
  if (s.do === "drag") {
988
- if (typeof s.tx !== "number" || typeof s.ty !== "number") {
989
- errors.push(`${at}: drag needs tx and ty (CSS px)`);
2109
+ if (s.path !== void 0) {
2110
+ const clash = ["selector", "x", "y", "tx", "ty"].filter(
2111
+ (k) => s[k] !== void 0
2112
+ );
2113
+ if (clash.length)
2114
+ errors.push(
2115
+ `${at}: a drag with a path starts at its first point and ends at its last, so it takes no ${clash.join(", ")}`
2116
+ );
2117
+ if (typeof s.path === "string") {
2118
+ const problem = fileNameProblem(s.path);
2119
+ if (problem)
2120
+ errors.push(
2121
+ `${at}: drag path ${problem} (a file beside the actions file)`
2122
+ );
2123
+ } else {
2124
+ const read = readPath(s.path);
2125
+ if ("problems" in read)
2126
+ for (const p of read.problems) errors.push(`${at}: drag path ${p}`);
2127
+ }
2128
+ } else {
2129
+ if (typeof s.tx !== "number" || typeof s.ty !== "number") {
2130
+ errors.push(`${at}: drag needs tx and ty (CSS px), or a path`);
2131
+ }
2132
+ const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
2133
+ if (!hasStart)
2134
+ errors.push(
2135
+ `${at}: drag needs a selector OR x and y as the start point`
2136
+ );
990
2137
  }
991
- const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
992
- if (!hasStart)
993
- errors.push(
994
- `${at}: drag needs a selector OR x and y as the start point`
995
- );
2138
+ errors.push(...modifiersProblem(at, "drag", s.modifiers));
996
2139
  }
997
2140
  });
998
2141
  return errors;
999
2142
  }
2143
+ var isCoord = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2144
+ function modifiersProblem(at, verb, v) {
2145
+ if (v === void 0) return [];
2146
+ if (!Array.isArray(v) || v.some((m) => !DRAG_MODIFIERS.includes(m)))
2147
+ return [
2148
+ `${at}: ${verb} modifiers must be a list of ${DRAG_MODIFIERS.map((m) => `"${m}"`).join(", ")} (Mod is Meta on macOS, Control elsewhere)`
2149
+ ];
2150
+ if (new Set(v).size !== v.length)
2151
+ return [`${at}: ${verb} modifiers names a modifier twice`];
2152
+ return [];
2153
+ }
1000
2154
  var isFilled = (v) => typeof v === "string" && v.trim().length > 0;
1001
2155
  function secondsProblem(at, what, v) {
1002
2156
  if (v === void 0) return [];
@@ -1058,103 +2212,137 @@ function hostedRefusals(actions) {
1058
2212
  out.push(
1059
2213
  `steps[${i}] is a download, which a hosted take does not keep: record it on this machine, where the file lands in the take's outputs/`
1060
2214
  );
2215
+ if (s.do === "paste")
2216
+ out.push(
2217
+ `steps[${i}] is a paste, and a hosted browser has no clipboard to paste from: record it on this machine`
2218
+ );
2219
+ if (s.do === "drag" && typeof s.path === "string")
2220
+ out.push(
2221
+ `steps[${i}] draws a path from the file "${s.path}", and a hosted take carries the script alone: write the points inline as path`
2222
+ );
1061
2223
  });
1062
2224
  return out;
1063
2225
  }
1064
-
1065
- // src/record/waits.ts
1066
- function visibleTextScript(text) {
1067
- return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
2226
+ function scriptFiles(actions) {
2227
+ const out = [];
2228
+ actions.steps.forEach((s, i) => {
2229
+ if (s.do === "drag" && typeof s.path === "string")
2230
+ out.push({ step: i, name: s.path, as: "path" });
2231
+ if (s.do === "paste" && typeof s.file === "string")
2232
+ out.push({ step: i, name: s.file, as: "paste" });
2233
+ });
2234
+ return out;
1068
2235
  }
1069
- async function readCondition(page, c) {
1070
- try {
1071
- if (c.text !== void 0)
1072
- return { holds: await page.evaluate(visibleTextScript(c.text)) };
1073
- if (c.gone !== void 0)
1074
- return { holds: !await page.locator(c.gone).first().isVisible() };
1075
- if (c.selector !== void 0) {
1076
- const loc = page.locator(c.selector).first();
1077
- const visible = await loc.isVisible();
1078
- if (!visible) return { holds: false, visible };
1079
- if (c.enabled === void 0) return { holds: true, visible };
1080
- const enabled = await loc.isEnabled({ timeout: 1e3 });
1081
- return { holds: enabled === c.enabled, visible };
1082
- }
1083
- } catch {
2236
+
2237
+ // src/record/scriptFiles.ts
2238
+ async function loadScriptFiles(actions, source) {
2239
+ const loaded = { paths: /* @__PURE__ */ new Map(), pastes: /* @__PURE__ */ new Map() };
2240
+ const named = scriptFiles(actions);
2241
+ if (!named.length) return loaded;
2242
+ if (!source) {
2243
+ const f = named[0];
2244
+ return {
2245
+ problem: `steps[${f.step}] names the file "${f.name}", and this recorder has no actions directory to read it from${f.as === "path" ? ": write the points inline as path" : ""}. Nothing was recorded.`
2246
+ };
1084
2247
  }
1085
- return { holds: false };
1086
- }
1087
- async function pollCondition(page, c, opts) {
1088
- const now = opts.now ?? Date.now;
1089
- const start = now();
1090
- const reads = Math.max(1, opts.confirm?.reads ?? 1);
1091
- for (let first = true; ; first = false) {
1092
- if (!first && opts.between) await opts.between();
1093
- let last = await readCondition(page, c);
1094
- for (let n = 1; last.holds && n < reads; n++) {
1095
- await opts.sleep(opts.confirm?.gapMs ?? 0);
1096
- last = await readCondition(page, c);
2248
+ for (const f of named) {
2249
+ const at = `steps[${f.step}]`;
2250
+ const bad = fileNameProblem(f.name);
2251
+ if (bad) return { problem: `${at}: ${bad}. Nothing was recorded.` };
2252
+ let bytes;
2253
+ try {
2254
+ bytes = await source.read(f.name);
2255
+ } catch (e) {
2256
+ const why = e instanceof Error ? e.message : String(e);
2257
+ return {
2258
+ problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched from vos.so carries its paths and its words, never a pasted file). Nothing was recorded.` : `${at} names "${f.name}", which could not be read beside the actions file (${why}). Nothing was recorded.`
2259
+ };
1097
2260
  }
1098
- const ms = now() - start;
1099
- if (last.holds) return { ok: true, ms, last };
1100
- const left = opts.timeoutMs - ms;
1101
- if (left <= 0) return { ok: false, ms, last };
1102
- await opts.sleep(Math.min(opts.everyMs, left));
1103
- }
1104
- }
1105
- var DOM_QUIET_MS = 800;
1106
- var DOM_SETTLE_CAP_MS = 5e3;
1107
- var DOM_CHANGE_PROBE = `(() => {
1108
- if (!window.__vosDomChanges) {
1109
- window.__vosDomChanges = { n: 0 };
1110
- new MutationObserver((list) => { window.__vosDomChanges.n += list.length })
1111
- .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
1112
- }
1113
- return window.__vosDomChanges.n;
1114
- })()`;
1115
- async function domSettled(page, opts) {
1116
- const now = opts.now ?? Date.now;
1117
- const quiet = opts.quietMs ?? DOM_QUIET_MS;
1118
- const cap = opts.capMs ?? DOM_SETTLE_CAP_MS;
1119
- const start = now();
1120
- let seen = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
1121
- let since = start;
1122
- for (; ; ) {
1123
- const t = now();
1124
- if (t - since >= quiet) return { settled: true, ms: t - start };
1125
- if (t - start >= cap) return { settled: false, ms: t - start };
1126
- await opts.sleep(opts.tickMs ?? 100);
1127
- const n = await page.evaluate(DOM_CHANGE_PROBE).catch(() => -1);
1128
- if (n !== seen) {
1129
- seen = n;
1130
- since = now();
2261
+ if (f.as === "path") {
2262
+ let raw;
2263
+ try {
2264
+ raw = JSON.parse(new TextDecoder().decode(bytes));
2265
+ } catch {
2266
+ return {
2267
+ problem: `${at}: the path file "${f.name}" is not JSON (an array of [x, y] or [x, y, tMs] points). Nothing was recorded.`
2268
+ };
2269
+ }
2270
+ const path = readPath(raw);
2271
+ if ("problems" in path)
2272
+ return {
2273
+ problem: `${at}: the path file "${f.name}" ${path.problems.join("; ")}. Nothing was recorded.`
2274
+ };
2275
+ loaded.paths.set(f.step, path);
2276
+ } else {
2277
+ if (bytes.length > PASTE_MAX_BYTES)
2278
+ return {
2279
+ problem: `${at}: "${f.name}" is larger than ${PASTE_MAX_BYTES / 1024 / 1024} MB, more than a paste carries. Nothing was recorded.`
2280
+ };
2281
+ loaded.pastes.set(f.step, {
2282
+ bytes,
2283
+ type: pasteTypeOf(f.name) ?? "application/octet-stream",
2284
+ name: f.name
2285
+ });
1131
2286
  }
1132
2287
  }
2288
+ return loaded;
1133
2289
  }
1134
- function sayDuration(ms) {
1135
- const s = Math.round(ms / 100) / 10;
1136
- if (s < 60) return `${s} s`;
1137
- const m = Math.floor(s / 60);
1138
- const rest = Math.round(s - m * 60);
1139
- return rest ? `${m} min ${rest} s` : `${m} min`;
2290
+ function inlinePoints(path) {
2291
+ return path.points.map((p) => {
2292
+ if (path.timed)
2293
+ return p.pressure !== void 0 ? [p.x, p.y, p.at, p.pressure] : [p.x, p.y, p.at];
2294
+ return p.pressure !== void 0 ? { x: p.x, y: p.y, pressure: p.pressure } : [p.x, p.y];
2295
+ });
1140
2296
  }
1141
- function sayCondition(c) {
1142
- if (c.text !== void 0) return `text "${c.text}" to show`;
1143
- if (c.gone !== void 0) return `'${c.gone}' to go`;
1144
- if (c.enabled === true) return `'${c.selector}' to be enabled`;
1145
- if (c.enabled === false) return `'${c.selector}' to be disabled`;
1146
- return `'${c.selector}' to appear`;
2297
+ function inlineScript(actions, files) {
2298
+ if (!files.paths.size) return actions;
2299
+ return {
2300
+ ...actions,
2301
+ steps: actions.steps.map((s, i) => {
2302
+ const read = files.paths.get(i);
2303
+ if (s.do !== "drag" || typeof s.path !== "string" || !read) return s;
2304
+ return { ...s, path: inlinePoints(read) };
2305
+ })
2306
+ };
1147
2307
  }
1148
- function conditionMiss(c, last, ms) {
1149
- const t = sayDuration(ms);
1150
- if (c.text !== void 0)
1151
- return `no visible text contained "${c.text}" for ${t}`;
1152
- if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
1153
- if (last.visible && c.enabled === true)
1154
- return `'${c.selector}' stayed disabled for ${t}`;
1155
- if (last.visible && c.enabled === false)
1156
- return `'${c.selector}' stayed enabled for ${t}`;
1157
- return `'${c.selector}' never appeared in ${t}`;
2308
+
2309
+ // src/record/pasteStep.ts
2310
+ function toBase64(bytes) {
2311
+ let bin = "";
2312
+ for (let i = 0; i < bytes.length; i += 32768)
2313
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
2314
+ return btoa(bin);
2315
+ }
2316
+ function pasteItemOf(step, stepIdx, files) {
2317
+ if (step.text !== void 0) return { text: step.text };
2318
+ const f = files.pastes.get(stepIdx);
2319
+ if (!f) return null;
2320
+ return { b64: toBase64(f.bytes), type: f.type, name: f.name };
2321
+ }
2322
+ async function runPaste(page, context, item, press) {
2323
+ if (item.b64 !== void 0 && item.type?.startsWith("image/")) {
2324
+ const done2 = await page.evaluate(syntheticPasteScript(item, { original: true })).catch((e) => `failed: ${e instanceof Error ? e.message : e}`);
2325
+ return done2 === "ok" ? { via: "file" } : { failed: `the page's paste failed (${done2.replace(/^failed: /, "")})` };
2326
+ }
2327
+ let origin;
2328
+ try {
2329
+ origin = new URL(page.url()).origin;
2330
+ } catch {
2331
+ origin = void 0;
2332
+ }
2333
+ if (context.grantPermissions && origin && origin !== "null")
2334
+ await context.grantPermissions(["clipboard-read", "clipboard-write"], { origin }).catch(() => void 0);
2335
+ const staged = await page.evaluate(stageClipboardScript(item)).catch((e) => `refused: ${e instanceof Error ? e.message : e}`);
2336
+ if (staged === "ok") {
2337
+ await press("ControlOrMeta+v");
2338
+ return { via: "clipboard" };
2339
+ }
2340
+ const why = staged.replace(/^refused: /, "");
2341
+ const done = await page.evaluate(syntheticPasteScript(item)).catch((e) => `failed: ${e instanceof Error ? e.message : e}`);
2342
+ if (done === "ok") return { via: "page", why };
2343
+ return {
2344
+ failed: `this browser has no clipboard to paste from (${why}), and the page's own paste failed too (${done.replace(/^failed: /, "")})`
2345
+ };
1158
2346
  }
1159
2347
 
1160
2348
  // src/record/outputs.ts
@@ -1318,6 +2506,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1318
2506
  `steps[${download}] is a download, and this recorder has nowhere to keep a file. Nothing was recorded.`
1319
2507
  );
1320
2508
  }
2509
+ const read = await loadScriptFiles(actions, opts.files);
2510
+ if ("problem" in read) throw new RecordRefusedError(read.problem);
2511
+ const files = read;
1321
2512
  const vw = actions.viewport?.width ?? 1280;
1322
2513
  const vh = actions.viewport?.height ?? 720;
1323
2514
  const context = opts.context ?? await browser.newContext({
@@ -1329,11 +2520,17 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1329
2520
  await applyHeaders(context, opts.headers, url, log);
1330
2521
  const masks = actions.mask ?? [];
1331
2522
  if (masks.length) await context.addInitScript(maskInitScript(masks));
2523
+ await context.addInitScript(MEDIA_WATCH_SCRIPT);
2524
+ const soundLog = new PageSoundLog();
1332
2525
  const exposureLog = new ExposureLog(masks);
1333
- const page = await context.newPage();
2526
+ const noticeLog = new NoticeLog();
2527
+ const rawPage = await context.newPage();
2528
+ const pageBound = new StepBound();
2529
+ const page = bounded(rawPage, pageBound);
1334
2530
  const scanExposures = async (step) => {
1335
2531
  const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
1336
2532
  exposureLog.add(step, scan);
2533
+ noticeLog.add(step, scan?.notices);
1337
2534
  };
1338
2535
  const pageErrors = new PageErrorLog(opts.verbose === true);
1339
2536
  page.on("console", (m) => {
@@ -1341,20 +2538,23 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1341
2538
  const line = pageErrors.add(m.text());
1342
2539
  if (line) log(line);
1343
2540
  });
1344
- log(`goto ${url}`);
1345
- let navTimeout = false;
1346
- let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
1347
- navTimeout = true;
1348
- log(" (networkidle timeout \u2014 continuing)");
1349
- return null;
2541
+ const readyMode = actions.ready ?? DEFAULT_READY;
2542
+ const open = (target) => untilReady(page, readyMode, (o) => page.goto(target, o), {
2543
+ sleep: realSleep
1350
2544
  });
1351
- await sleep(800);
2545
+ log(`goto ${url}`);
2546
+ const opened = await open(url);
2547
+ let response = opened.response;
2548
+ const ready = opened.ready;
2549
+ log(` ${readyLine(ready)}`);
2550
+ const navTimeout = ready.mode === "networkidle" && ready.capped;
1352
2551
  const setup = actions.setup ?? [];
1353
2552
  if (setup.length) {
1354
2553
  log(`setup: ${setup.length} step(s), off camera`);
1355
2554
  const result = await runSetup(page, setup, {
1356
2555
  sleep: realSleep,
1357
- env: opts.env ?? {}
2556
+ env: opts.env ?? {},
2557
+ goto: (target) => open(target)
1358
2558
  });
1359
2559
  for (const line of result.lines) log(` ${line}`);
1360
2560
  if (result.failed) {
@@ -1363,8 +2563,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1363
2563
  throw new SetupError(result.failed);
1364
2564
  }
1365
2565
  log(`goto ${url} (after setup)`);
1366
- response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
1367
- await sleep(800);
2566
+ response = (await open(url)).response;
1368
2567
  }
1369
2568
  let wall = null;
1370
2569
  if (opts.onArrival) {
@@ -1385,7 +2584,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1385
2584
  }
1386
2585
  }
1387
2586
  await scanExposures(-1);
1388
- const cdp = await context.newCDPSession(page);
2587
+ await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
2588
+ if (actions.steps.some((s) => settleOn(s, actions.settle)))
2589
+ await page.evaluate(CHANGE_AGE_PROBE).catch(() => void 0);
2590
+ const cdp = await context.newCDPSession(rawPage);
2591
+ if (!dry) await fitSurfaceToViewport(cdp, vw, vh);
1389
2592
  const frames = [];
1390
2593
  let frameIdx = 0;
1391
2594
  let t0 = 0;
@@ -1430,6 +2633,10 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1430
2633
  return { out, at, ms };
1431
2634
  };
1432
2635
  const emit = (e) => events.push({ t: now(), ...e });
2636
+ const drainSound = async (step) => {
2637
+ const drain = await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
2638
+ soundLog.add(drain, Date.now(), step);
2639
+ };
1433
2640
  await page.mouse.move(cur.x, cur.y);
1434
2641
  emit({ x: cur.x, y: cur.y, type: "move" });
1435
2642
  const clock = { now: () => Date.now(), sleep };
@@ -1521,27 +2728,58 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1521
2728
  ...type ? { contentType: type } : {}
1522
2729
  };
1523
2730
  };
1524
- const clickAt = async (rect) => {
1525
- await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
1526
- await sleep(PRESS_LEAD_MS);
1527
- emit({
1528
- x: Math.round(cur.x),
1529
- y: Math.round(cur.y),
1530
- type: "down",
1531
- button: 0,
1532
- rect
1533
- });
1534
- await page.mouse.down();
1535
- await sleep(PRESS_HOLD_MS);
1536
- await page.mouse.up();
1537
- emit({
1538
- x: Math.round(cur.x),
1539
- y: Math.round(cur.y),
1540
- type: "up",
1541
- button: 0,
1542
- rect
1543
- });
2731
+ const clickAt = async (at, how = {}) => {
2732
+ const rect = "w" in at ? at : void 0;
2733
+ await moveTo(
2734
+ rect ? rect.x + rect.w / 2 : at.x,
2735
+ rect ? rect.y + rect.h / 2 : at.y
2736
+ );
2737
+ const held = [];
2738
+ try {
2739
+ for (const m of how.modifiers ?? []) {
2740
+ const key = resolveModifier(m);
2741
+ await page.keyboard.down(key);
2742
+ held.push(key);
2743
+ }
2744
+ await sleep(PRESS_LEAD_MS);
2745
+ const count = how.count ?? 1;
2746
+ const button = buttonNumber(how.button);
2747
+ for (let n = 1; n <= count; n++) {
2748
+ if (n > 1) await sleep(MULTI_CLICK_GAP_MS);
2749
+ const press = {
2750
+ ...how.button && how.button !== "left" ? { button: how.button } : {},
2751
+ ...n > 1 ? { clickCount: n } : {}
2752
+ };
2753
+ const hasOpts = Object.keys(press).length > 0;
2754
+ emit({
2755
+ x: Math.round(cur.x),
2756
+ y: Math.round(cur.y),
2757
+ type: "down",
2758
+ button,
2759
+ ...rect ? { rect } : {}
2760
+ });
2761
+ await (hasOpts ? page.mouse.down(press) : page.mouse.down());
2762
+ await sleep(n < count ? PRESS_HOLD_MS / 2 : PRESS_HOLD_MS);
2763
+ await (hasOpts ? page.mouse.up(press) : page.mouse.up());
2764
+ emit({
2765
+ x: Math.round(cur.x),
2766
+ y: Math.round(cur.y),
2767
+ type: "up",
2768
+ button,
2769
+ ...rect ? { rect } : {}
2770
+ });
2771
+ }
2772
+ } finally {
2773
+ for (const key of held.reverse())
2774
+ await page.keyboard.up(key).catch(() => void 0);
2775
+ }
1544
2776
  };
2777
+ const keyPing = (rect) => emit({
2778
+ x: Math.round(rect ? rect.x + rect.w / 2 : cur.x),
2779
+ y: Math.round(rect ? rect.y + rect.h / 2 : cur.y),
2780
+ type: "key",
2781
+ ...rect ? { rect } : {}
2782
+ });
1545
2783
  const waitSettled = async () => {
1546
2784
  if (dry) return null;
1547
2785
  const pressAt = now();
@@ -1563,6 +2801,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1563
2801
  const skips = [];
1564
2802
  const outputs = [];
1565
2803
  const outputNames = /* @__PURE__ */ new Set();
2804
+ const overruns = [];
1566
2805
  let stopped = null;
1567
2806
  let unpressedGuard = null;
1568
2807
  let capped = false;
@@ -1580,6 +2819,16 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1580
2819
  return out;
1581
2820
  };
1582
2821
  const skippedBefore = skipped.length;
2822
+ const paced = {
2823
+ v: null
2824
+ };
2825
+ const timed = async (plannedMs, fn) => {
2826
+ const t = Date.now();
2827
+ const out = await fn();
2828
+ paced.v = { plannedMs, tookMs: Date.now() - t };
2829
+ return out;
2830
+ };
2831
+ let askedOverride = null;
1583
2832
  const urlBefore = page.url();
1584
2833
  let stepRect = null;
1585
2834
  const extra = {};
@@ -1595,7 +2844,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1595
2844
  if (reason) extra.reason = reason;
1596
2845
  };
1597
2846
  if (step.guard) extra.guard = step.guard;
1598
- const rehearseGuard = async (sel) => {
2847
+ const rehearseGuard = async (sel, point) => {
1599
2848
  if (!step.guard || !dry) return false;
1600
2849
  const rect = sel ? await boxOf(sel) : null;
1601
2850
  stepRect = rect;
@@ -1603,8 +2852,9 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1603
2852
  else {
1604
2853
  if (rect)
1605
2854
  await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
2855
+ else if (point) await gesture(() => moveTo(point.x, point.y));
1606
2856
  log(
1607
- `${tag}: guarded (${step.guard}), ${rect ? "hovered" : "left alone"}, not pressed`
2857
+ `${tag}: guarded (${step.guard}), ${rect || point ? "hovered" : "left alone"}, not pressed`
1608
2858
  );
1609
2859
  }
1610
2860
  unpressedGuard = stepIdx;
@@ -1618,14 +2868,40 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1618
2868
  );
1619
2869
  return true;
1620
2870
  };
2871
+ const boundMs = stepBoundMs(
2872
+ step,
2873
+ askedMs(step),
2874
+ {
2875
+ expectS: EXPECT_TIMEOUT_S,
2876
+ awaitS: AWAIT_TIMEOUT_S,
2877
+ downloadS: DOWNLOAD_TIMEOUT_S
2878
+ }
2879
+ );
2880
+ opts.onEvent?.({
2881
+ event: "step",
2882
+ phase: "start",
2883
+ i: stepIdx,
2884
+ ...step.id ? { id: step.id } : {},
2885
+ do: step.do
2886
+ });
2887
+ const stepWall = Date.now();
2888
+ pageBound.start(boundMs);
1621
2889
  switch (step.do) {
1622
2890
  case "wait":
1623
2891
  await sleep(clampWait(step.ms, now(), maxSeconds));
1624
2892
  break;
1625
2893
  case "move":
1626
- await moveTo(step.x, step.y);
2894
+ await gesture(() => moveTo(step.x, step.y));
1627
2895
  break;
1628
2896
  case "hover": {
2897
+ if (step.selector === void 0) {
2898
+ const px = step.x ?? cur.x;
2899
+ const py = step.y ?? cur.y;
2900
+ await gesture(() => moveTo(px, py));
2901
+ log(`hover ${Math.round(px)},${Math.round(py)}`);
2902
+ await sleep(step.ms ?? 700);
2903
+ break;
2904
+ }
1629
2905
  const rect = await boxOf(step.selector);
1630
2906
  stepRect = rect;
1631
2907
  if (rect) {
@@ -1637,6 +2913,21 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1637
2913
  break;
1638
2914
  }
1639
2915
  case "click": {
2916
+ const how = {
2917
+ count: step.count,
2918
+ button: step.button,
2919
+ modifiers: step.modifiers
2920
+ };
2921
+ const said = `${step.count === 3 ? "triple-click" : step.count === 2 ? "double-click" : "click"}${step.button && step.button !== "left" ? ` (${step.button})` : ""}${step.modifiers?.length ? ` holding ${step.modifiers.join("+")}` : ""}`;
2922
+ if (step.selector === void 0) {
2923
+ const point = { x: step.x ?? cur.x, y: step.y ?? cur.y };
2924
+ if (await rehearseGuard(void 0, point)) break;
2925
+ await gesture(() => clickAt(point, how));
2926
+ log(`${said} at ${Math.round(point.x)},${Math.round(point.y)}`);
2927
+ const sinceChange = await gesture(() => waitSettled());
2928
+ await sleep(holdLeftMs(settleMs(step), sinceChange));
2929
+ break;
2930
+ }
1640
2931
  if (await rehearseGuard(step.selector)) break;
1641
2932
  const rect = await boxOf(step.selector);
1642
2933
  stepRect = rect;
@@ -1652,8 +2943,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1652
2943
  ` (${tag} click: ${step.selector} is disabled; pressed anyway)`
1653
2944
  );
1654
2945
  }
1655
- await gesture(() => clickAt(rect));
1656
- log(`click ${step.selector}`);
2946
+ await gesture(() => clickAt(rect, how));
2947
+ log(`${said} ${step.selector}`);
1657
2948
  const sinceChange = await gesture(() => waitSettled());
1658
2949
  await sleep(holdLeftMs(settleMs(step), sinceChange));
1659
2950
  } else
@@ -1677,17 +2968,21 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1677
2968
  rect
1678
2969
  });
1679
2970
  let lastPing = -Infinity;
1680
- await clockTyping(
1681
- [...step.text],
1682
- delay,
1683
- async (ch) => {
1684
- if (now() - lastPing >= 350) {
1685
- lastPing = now();
1686
- ping();
1687
- }
1688
- await page.keyboard.type(ch);
1689
- },
1690
- clock
2971
+ const chars = [...step.text];
2972
+ await timed(
2973
+ Math.max(0, chars.length - 1) * delay,
2974
+ () => clockTyping(
2975
+ chars,
2976
+ delay,
2977
+ async (ch) => {
2978
+ if (now() - lastPing >= 350) {
2979
+ lastPing = now();
2980
+ ping();
2981
+ }
2982
+ await page.keyboard.type(ch);
2983
+ },
2984
+ clock
2985
+ )
1691
2986
  );
1692
2987
  ping();
1693
2988
  log(`type ${step.text.length} chars into ${step.selector}`);
@@ -1711,14 +3006,18 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1711
3006
  break;
1712
3007
  }
1713
3008
  }
1714
- emit({
1715
- x: Math.round(rect ? rect.x + rect.w / 2 : cur.x),
1716
- y: Math.round(rect ? rect.y + rect.h / 2 : cur.y),
1717
- type: "key",
1718
- ...rect ? { rect } : {}
3009
+ const seq = pressSequence(step);
3010
+ const chords = step.keys !== void 0 ? seq.map(resolveChord) : seq;
3011
+ const plan = keySequencePlanMs(chords.length);
3012
+ if (chords.length > 1) askedOverride = plan + settleMs(step);
3013
+ await timed(plan, async () => {
3014
+ for (const [i, chord] of chords.entries()) {
3015
+ if (i > 0) await sleep(KEY_SEQUENCE_GAP_MS);
3016
+ keyPing(rect);
3017
+ await page.keyboard.press(chord);
3018
+ }
1719
3019
  });
1720
- await page.keyboard.press(step.key);
1721
- log(`press ${step.key}`);
3020
+ log(`press ${seq.join(", ")}`);
1722
3021
  const sinceChange = await gesture(() => waitSettled());
1723
3022
  await sleep(holdLeftMs(settleMs(step), sinceChange));
1724
3023
  break;
@@ -1749,9 +3048,19 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1749
3048
  break;
1750
3049
  }
1751
3050
  case "drag": {
3051
+ let path = null;
3052
+ if (typeof step.path === "string") {
3053
+ const read2 = files.paths.get(stepIdx);
3054
+ if (read2) path = schedulePath(read2, step.ms);
3055
+ } else if (step.path !== void 0) {
3056
+ const read2 = readPath(step.path);
3057
+ if (!("problems" in read2)) path = schedulePath(read2, step.ms);
3058
+ }
1752
3059
  let start = null;
1753
3060
  let rect;
1754
- if (step.selector) {
3061
+ if (path) {
3062
+ start = { x: path[0].x, y: path[0].y };
3063
+ } else if (step.selector) {
1755
3064
  const box = await boxOf(step.selector);
1756
3065
  stepRect = box;
1757
3066
  if (box) {
@@ -1769,42 +3078,148 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1769
3078
  start = { x: step.x, y: step.y };
1770
3079
  }
1771
3080
  if (!start) break;
1772
- await gesture(() => moveTo(start.x, start.y));
1773
- await gesture(() => sleep(PRESS_LEAD_MS));
1774
- emit({
1775
- x: Math.round(cur.x),
1776
- y: Math.round(cur.y),
1777
- type: "down",
1778
- button: 0,
1779
- rect
1780
- });
1781
- await page.mouse.down();
1782
- await gesture(() => sleep(PRESS_HOLD_MS));
1783
- const dur = step.ms ?? 700;
1784
- const from = { ...cur };
1785
- await clockMotion(
1786
- dur,
1787
- async (u) => {
1788
- cur.x = from.x + (step.tx - from.x) * u;
1789
- cur.y = from.y + (step.ty - from.y) * u;
1790
- await page.mouse.move(cur.x, cur.y);
1791
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
1792
- },
1793
- clock
3081
+ const from0 = start;
3082
+ await gesture(() => moveTo(from0.x, from0.y));
3083
+ const mods = step.modifiers ?? [];
3084
+ const held = [];
3085
+ try {
3086
+ for (const m of mods) {
3087
+ const key = resolveModifier(m);
3088
+ await page.keyboard.down(key);
3089
+ held.push(key);
3090
+ }
3091
+ await gesture(() => sleep(PRESS_LEAD_MS));
3092
+ const bits = modifierBits(mods, opts.platform);
3093
+ const penAt = (type, p) => cdp.send("Input.dispatchMouseEvent", {
3094
+ type,
3095
+ x: p.x,
3096
+ y: p.y,
3097
+ button: "left",
3098
+ buttons: type === "mouseReleased" ? 0 : 1,
3099
+ ...type === "mouseMoved" ? {} : { clickCount: 1 },
3100
+ pointerType: "pen",
3101
+ force: type === "mouseReleased" ? 0 : p.pressure ?? 0.5,
3102
+ modifiers: bits
3103
+ });
3104
+ let pen = path?.some((p) => p.pressure !== void 0) ?? false;
3105
+ if (pen && path) {
3106
+ try {
3107
+ await penAt("mousePressed", path[0]);
3108
+ } catch {
3109
+ pen = false;
3110
+ log(
3111
+ ` (${tag} drag: this browser takes no pen input; the path is drawn with the mouse and its pressure dropped)`
3112
+ );
3113
+ }
3114
+ }
3115
+ emit({
3116
+ x: Math.round(cur.x),
3117
+ y: Math.round(cur.y),
3118
+ type: "down",
3119
+ button: 0,
3120
+ rect
3121
+ });
3122
+ if (!pen) await page.mouse.down();
3123
+ await gesture(() => sleep(PRESS_HOLD_MS));
3124
+ if (path) {
3125
+ const pts = path;
3126
+ const total = pathDurationMs(pts);
3127
+ askedOverride = total;
3128
+ await timed(total, async () => {
3129
+ const from = Date.now();
3130
+ for (let i = 1; i < pts.length; i++) {
3131
+ const p = pts[i];
3132
+ if (!dry) {
3133
+ const wait = from + p.at - Date.now();
3134
+ if (wait > 0) await realSleep(wait);
3135
+ }
3136
+ if (pen) await penAt("mouseMoved", p);
3137
+ else await page.mouse.move(p.x, p.y);
3138
+ cur.x = p.x;
3139
+ cur.y = p.y;
3140
+ emit({ x: Math.round(p.x), y: Math.round(p.y), type: "move" });
3141
+ }
3142
+ });
3143
+ } else {
3144
+ const dur = step.ms ?? 700;
3145
+ const from = { ...cur };
3146
+ const tx = step.tx ?? from.x;
3147
+ const ty = step.ty ?? from.y;
3148
+ await timed(
3149
+ dur,
3150
+ () => clockMotion(
3151
+ dur,
3152
+ async (u) => {
3153
+ cur.x = from.x + (tx - from.x) * u;
3154
+ cur.y = from.y + (ty - from.y) * u;
3155
+ await page.mouse.move(cur.x, cur.y);
3156
+ emit({
3157
+ x: Math.round(cur.x),
3158
+ y: Math.round(cur.y),
3159
+ type: "move"
3160
+ });
3161
+ },
3162
+ clock
3163
+ )
3164
+ );
3165
+ }
3166
+ await gesture(() => sleep(settleMs({ do: "drag" })));
3167
+ if (pen) await penAt("mouseReleased", { x: cur.x, y: cur.y });
3168
+ else await page.mouse.up();
3169
+ emit({
3170
+ x: Math.round(cur.x),
3171
+ y: Math.round(cur.y),
3172
+ type: "up",
3173
+ button: 0,
3174
+ rect
3175
+ });
3176
+ } finally {
3177
+ for (const key of held.reverse())
3178
+ await page.keyboard.up(key).catch(() => void 0);
3179
+ }
3180
+ const holding = mods.length ? ` holding ${mods.join("+")}` : "";
3181
+ log(
3182
+ path ? `drag a path of ${path.length} points from ${Math.round(start.x)},${Math.round(start.y)}${holding}` : `drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}${holding}`
1794
3183
  );
1795
- await gesture(() => sleep(settleMs({ do: "drag" })));
1796
- await page.mouse.up();
1797
- emit({
1798
- x: Math.round(cur.x),
1799
- y: Math.round(cur.y),
1800
- type: "up",
1801
- button: 0,
1802
- rect
3184
+ await gesture(() => sleep(DRAG_HOLD_MS));
3185
+ break;
3186
+ }
3187
+ case "paste": {
3188
+ const item = pasteItemOf(step, stepIdx, files);
3189
+ if (!item) {
3190
+ skip(step.file ?? "", `"${step.file}" was not read`);
3191
+ break;
3192
+ }
3193
+ let rect = null;
3194
+ if (step.selector) {
3195
+ rect = await boxOf(step.selector);
3196
+ stepRect = rect;
3197
+ if (!rect) {
3198
+ skip(step.selector);
3199
+ break;
3200
+ }
3201
+ const at = rect;
3202
+ await gesture(() => clickAt(at));
3203
+ }
3204
+ const ping = { done: false };
3205
+ const outcome = await runPaste(page, context, item, async (chord) => {
3206
+ keyPing(rect);
3207
+ ping.done = true;
3208
+ await page.keyboard.press(chord);
1803
3209
  });
3210
+ if ("failed" in outcome) {
3211
+ skip(step.selector ?? step.file ?? "paste", outcome.failed);
3212
+ log(` (${tag} paste: ${outcome.failed})`);
3213
+ break;
3214
+ }
3215
+ if (!ping.done) keyPing(rect);
3216
+ const what = step.text !== void 0 ? `${[...step.text].length} chars` : `the file ${step.file}`;
3217
+ const into = step.selector ? ` into ${step.selector}` : "";
1804
3218
  log(
1805
- `drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}`
3219
+ outcome.via === "file" ? `paste ${what}${into}, dispatched in the page with the file's own bytes (the browser's clipboard re-encodes an image and drops what it carries, its density among it); its events are untrusted` : outcome.via === "page" ? `paste ${what}${into}, dispatched in the page (the browser's clipboard refused it: ${outcome.why}); its events are untrusted` : `paste ${what}${into}`
1806
3220
  );
1807
- await sleep(400);
3221
+ const sinceChange = await gesture(() => waitSettled());
3222
+ await sleep(holdLeftMs(settleMs(step), sinceChange));
1808
3223
  break;
1809
3224
  }
1810
3225
  case "expect": {
@@ -1828,14 +3243,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1828
3243
  if (notWaited()) break;
1829
3244
  const cond = step.until;
1830
3245
  const timeoutMs = (step.timeout ?? AWAIT_TIMEOUT_S) * 1e3;
1831
- const reload = step.reload ? async () => {
1832
- const opts2 = {
1833
- waitUntil: "networkidle",
1834
- timeout: Math.min(45e3, timeoutMs)
1835
- };
1836
- await (page.reload ? page.reload(opts2) : page.goto(page.url(), opts2)).catch(() => null);
1837
- await domSettled(page, { sleep: realSleep });
1838
- } : void 0;
3246
+ const reload = step.reload ? () => reloadUntilReady(page, readyMode, timeoutMs) : void 0;
1839
3247
  log(`await ${sayCondition(cond)} (off camera)\u2026`);
1840
3248
  const awaitEvent = {
1841
3249
  event: "await",
@@ -1965,6 +3373,50 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1965
3373
  break;
1966
3374
  }
1967
3375
  }
3376
+ if (settleOn(step, actions.settle) && skipped.length === skippedBefore && !stopped) {
3377
+ const res = await gesture(
3378
+ () => appSettled(page, () => frames.length, {
3379
+ sleep: realSleep,
3380
+ sinceFrame: () => frames.length ? now() - frames[frames.length - 1].tMs : null
3381
+ })
3382
+ );
3383
+ extra.settleMs = res.ms;
3384
+ if (!res.settled)
3385
+ log(
3386
+ ` (${tag} settle: the page was still changing after ${sayDuration(res.ms)}; going on)`
3387
+ );
3388
+ }
3389
+ if (paced.v && !dry) {
3390
+ const over = overrunOf(paced.v.plannedMs, paced.v.tookMs);
3391
+ if (over !== null) {
3392
+ overruns.push({
3393
+ step: stepIdx,
3394
+ ...step.id ? { id: step.id } : {},
3395
+ do: step.do,
3396
+ plannedMs: Math.round(paced.v.plannedMs),
3397
+ tookMs: Math.round(paced.v.tookMs),
3398
+ overrunMs: over
3399
+ });
3400
+ extra.overrunMs = over;
3401
+ }
3402
+ }
3403
+ const hang = pageBound.end();
3404
+ if (hang.hung) {
3405
+ const reason = hangReason(step.do, hang.ms, hang.boundMs);
3406
+ const said = skipped.length > skippedBefore ? skipped.at(-1) : void 0;
3407
+ if (said) {
3408
+ said.reason = reason;
3409
+ extra.reason = reason;
3410
+ } else skip(step.selector ?? "", reason);
3411
+ log(` (${tag} ${step.do}: ${reason})`);
3412
+ if (opts.stopOnHang && !stopped)
3413
+ stopped = {
3414
+ step: stepIdx,
3415
+ ...step.id ? { id: step.id } : {},
3416
+ reason: `${tag} ${step.do}: ${reason}, so the take stopped there`
3417
+ };
3418
+ }
3419
+ pageBound.start(POST_STEP_READ_MS);
1968
3420
  if (step.guard && !dry && skipped.length === skippedBefore) {
1969
3421
  confirmed.push(step.id ?? String(stepIdx));
1970
3422
  extra.confirmed = true;
@@ -1999,11 +3451,24 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
1999
3451
  paces.push({
2000
3452
  step: stepIdx,
2001
3453
  do: step.do,
2002
- askedMs: askedMs(step),
3454
+ askedMs: askedOverride ?? askedMs(step),
2003
3455
  gestureMs,
2004
3456
  wallMs: now() - stepStart
2005
3457
  });
2006
3458
  await scanExposures(stepIdx);
3459
+ await drainSound(stepIdx);
3460
+ pageBound.end();
3461
+ const stepSpan = stepSpans[stepSpans.length - 1];
3462
+ opts.onEvent?.({
3463
+ event: "step",
3464
+ phase: "end",
3465
+ i: stepIdx,
3466
+ ...step.id ? { id: step.id } : {},
3467
+ do: step.do,
3468
+ ms: Date.now() - stepWall,
3469
+ ...stepSpan?.skipped ? { skipped: true } : {},
3470
+ ...hang.hung ? { hung: true } : {}
3471
+ });
2007
3472
  if (stopped) break;
2008
3473
  }
2009
3474
  if (!capped && !stopped) await sleep(TRAILING_HOLD_MS);
@@ -2011,13 +3476,27 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2011
3476
  capped = true;
2012
3477
  log(` ${cappedLine(maxSeconds)}`);
2013
3478
  }
3479
+ pageBound.start(POST_STEP_READ_MS * 2);
3480
+ await drainSound();
3481
+ const endWall = Date.now();
2014
3482
  const durationMs = capped ? Math.min(now(), maxSeconds * 1e3) : now();
3483
+ const heard = soundLog.heard(takeClock, endWall, durationMs);
2015
3484
  await cdp.send("Page.stopScreencast").catch(() => {
2016
3485
  });
3486
+ const media = await keepPageSound(
3487
+ heard,
3488
+ dry ? void 0 : opts.sounds,
3489
+ fetchSource,
3490
+ log
3491
+ );
2017
3492
  await sleep(200);
2018
- const pageTitle = await page.title().catch(() => "");
3493
+ const pageTitle = await page.title().catch(() => "") ?? "";
2019
3494
  for (const line of pageErrors.repeats()) log(line);
2020
- await context.close();
3495
+ pageBound.end();
3496
+ pageBound.start(CLOSE_BOUND_MS);
3497
+ await pageBound.race(context.close().catch(() => {
3498
+ }));
3499
+ pageBound.end();
2021
3500
  await sinkChain;
2022
3501
  const meta = {
2023
3502
  dpr: 1,
@@ -2040,9 +3519,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2040
3519
  steps: stepSpans,
2041
3520
  ...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
2042
3521
  ...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
3522
+ ...noticeLog.notices().length ? { notices: noticeLog.notices() } : {},
2043
3523
  ...masks.length ? { masks: exposureLog.masks(masks) } : {},
2044
3524
  ...skips.length ? { skips } : {},
2045
- ...outputs.length ? { outputs } : {}
3525
+ ...outputs.length ? { outputs } : {},
3526
+ ...media.length ? { media } : {}
2046
3527
  };
2047
3528
  const freezes = [];
2048
3529
  for (let i = 1; i < frames.length; i++) {
@@ -2058,9 +3539,11 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2058
3539
  const frozenMs = freezes.reduce((sum, f) => sum + f.ms, 0);
2059
3540
  const freezePct = durationMs > 0 ? Math.round(frozenMs / durationMs * 100) : 0;
2060
3541
  const dead = deadTime(stepSpans, frames, events, durationMs);
2061
- if (deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
3542
+ if (!dry && deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
2062
3543
  const pace = paceReport(paces);
2063
3544
  if (!dry) log(` ${paceLine(pace)}`);
3545
+ const overrun = overrunLine(overruns);
3546
+ if (overrun) log(` WARNING: ${overrun}`);
2064
3547
  return {
2065
3548
  events,
2066
3549
  frames,
@@ -2068,8 +3551,10 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2068
3551
  pace,
2069
3552
  skipped,
2070
3553
  navTimeout,
3554
+ ready,
2071
3555
  wall,
2072
3556
  exposures: exposureLog.exposures(),
3557
+ notices: noticeLog.notices(),
2073
3558
  masks: exposureLog.masks(masks),
2074
3559
  freezes,
2075
3560
  freezePct,
@@ -2079,11 +3564,50 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2079
3564
  confirmed,
2080
3565
  skips,
2081
3566
  outputs,
3567
+ media,
2082
3568
  stopped,
2083
3569
  pageErrors: pageErrors.entries(),
2084
- actions: { ...actions, url }
3570
+ overruns,
3571
+ // Self-contained: a path a file held is inline, so the take re-records
3572
+ // from its own script with the file gone.
3573
+ actions: { ...inlineScript(actions, files), url }
2085
3574
  };
2086
3575
  }
3576
+ async function keepPageSound(heard, sink, fetchSource, log) {
3577
+ const kept = /* @__PURE__ */ new Map();
3578
+ const used = /* @__PURE__ */ new Set();
3579
+ for (const h of heard) {
3580
+ if (!h.meta.intervals.length) continue;
3581
+ const stretches = h.meta.intervals.length;
3582
+ log(
3583
+ ` page sound: ${h.meta.src || "a media element"} heard ${stretches === 1 ? "once" : `${stretches} times`}`
3584
+ );
3585
+ if (!sink || kept.has(h.src)) continue;
3586
+ try {
3587
+ const got = await fetchSource(h.src);
3588
+ const name = outputName(
3589
+ void 0,
3590
+ got.suggested,
3591
+ `sound-${kept.size + 1}${extForType(got.contentType)}`,
3592
+ used
3593
+ );
3594
+ used.add(name);
3595
+ kept.set(h.src, {
3596
+ file: await sink.save(name, got.bytes, got.contentType)
3597
+ });
3598
+ } catch (e) {
3599
+ const why = e instanceof Error ? e.message : String(e);
3600
+ kept.set(h.src, { fetchError: why });
3601
+ log(` (page sound not kept: ${why})`);
3602
+ }
3603
+ }
3604
+ return heard.map((h) => ({ ...h.meta, ...kept.get(h.src) ?? {} }));
3605
+ }
3606
+ var POST_STEP_READ_MS = 5e3;
3607
+ var CLOSE_BOUND_MS = 1e4;
3608
+ function hangReason(verb, ms, boundMs) {
3609
+ return `the page stopped answering: this ${verb} ran ${sayDuration(ms)}, past its bound of ${sayDuration(boundMs)}`;
3610
+ }
2087
3611
  var EXT_FOR_TYPE = {
2088
3612
  "video/mp4": ".mp4",
2089
3613
  "video/webm": ".webm",
@@ -2145,7 +3669,15 @@ try {
2145
3669
  fi = next
2146
3670
  const blob = await (await fetch(${JSON.stringify(o.framesBaseUrl)} + frames[fi].file + ${JSON.stringify(q)})).blob()
2147
3671
  const bmp = await createImageBitmap(blob)
2148
- c2.drawImage(bmp, 0, 0, W, H)
3672
+ // A frame is drawn at its own size, never stretched to the take's: a
3673
+ // frame of another size means the surface moved mid-take, and a
3674
+ // picture scaled to fit would be a distorted take nobody was told of.
3675
+ if (bmp.width !== W || bmp.height !== H) {
3676
+ const size = bmp.width + 'x' + bmp.height
3677
+ bmp.close()
3678
+ throw new Error('frame ' + frames[fi].file + ' is ' + size + ', the take is ' + W + 'x' + H + ' (meta.captureWidth/captureHeight): frames are never resized, so the recorded surface changed size mid-take. Re-record it')
3679
+ }
3680
+ c2.drawImage(bmp, 0, 0)
2149
3681
  bmp.close()
2150
3682
  }
2151
3683
  await src.add(t, 1 / FPS)
@@ -2169,9 +3701,12 @@ try {
2169
3701
  // src/record/plan.ts
2170
3702
  import {
2171
3703
  DEFAULT_FRAME_STYLE,
3704
+ EXPORT_RESOLUTION_OPTIONS,
2172
3705
  planAutoSpeed,
2173
3706
  planAutoZoom,
2174
3707
  projectFromArtifact,
3708
+ ratedSegments,
3709
+ recommendedExportResolution,
2175
3710
  withBackdrop
2176
3711
  } from "@vosjs/studio-core";
2177
3712
  function planFreshTake(artifact, recordingName, opts = {}) {
@@ -2190,6 +3725,66 @@ function planFreshTake(artifact, recordingName, opts = {}) {
2190
3725
  params: doc.speedParams,
2191
3726
  activity: opts.activity
2192
3727
  });
3728
+ return withFittingExport(withPageSound(doc, artifact.meta.media));
3729
+ }
3730
+ function withFittingExport(doc) {
3731
+ if (!doc.export) return doc;
3732
+ const fits = recommendedExportResolution(doc);
3733
+ const order = EXPORT_RESOLUTION_OPTIONS;
3734
+ if (order.indexOf(fits) < order.indexOf(doc.export.resolution))
3735
+ doc.export = { ...doc.export, resolution: fits };
3736
+ return doc;
3737
+ }
3738
+ function sourceToOutput(pieces, s) {
3739
+ let acc = 0;
3740
+ for (const p of pieces) {
3741
+ const rate = p.rate && p.rate > 0 ? p.rate : 1;
3742
+ if (p.media) {
3743
+ acc += Math.max(0, p.out - p.in) / rate;
3744
+ continue;
3745
+ }
3746
+ if (s < p.in) return acc;
3747
+ if (s <= p.out) return acc + (s - p.in) / rate;
3748
+ acc += Math.max(0, p.out - p.in) / rate;
3749
+ }
3750
+ return acc;
3751
+ }
3752
+ function withPageSound(doc, media) {
3753
+ const kept = (doc.audio ?? []).filter((a) => a.source !== "page");
3754
+ const heard = (media ?? []).filter((m) => m.file && m.intervals.length);
3755
+ if (!heard.length) {
3756
+ doc.audio = kept;
3757
+ return doc;
3758
+ }
3759
+ const spans = heard.flatMap(
3760
+ (m) => m.intervals.map((iv) => ({ in: iv.at, out: iv.at + (iv.to - iv.from) }))
3761
+ );
3762
+ doc.speed = (doc.speed ?? []).filter(
3763
+ (sp) => sp.source !== "auto" || !!sp.media || !spans.some((h) => sp.in < h.out && h.in < sp.out)
3764
+ );
3765
+ const pieces = ratedSegments(doc);
3766
+ const clips = [];
3767
+ heard.forEach((m, i) => {
3768
+ const file = m.file;
3769
+ const name = file.split("/").pop() || file;
3770
+ const length = Math.max(m.duration ?? 0, ...m.intervals.map((iv) => iv.to));
3771
+ m.intervals.forEach((iv, j) => {
3772
+ clips.push({
3773
+ id: `page-${i + 1}-${j + 1}`,
3774
+ key: file,
3775
+ name,
3776
+ start: +sourceToOutput(pieces, iv.at).toFixed(3),
3777
+ in: iv.from,
3778
+ out: iv.to,
3779
+ duration: +length.toFixed(3),
3780
+ gain: m.volume ?? 1,
3781
+ fadeIn: 0,
3782
+ fadeOut: 0,
3783
+ source: "page"
3784
+ });
3785
+ });
3786
+ });
3787
+ doc.audio = [...kept, ...clips];
2193
3788
  return doc;
2194
3789
  }
2195
3790
  function splitSegmentsAtSkips(segments, skips) {
@@ -2223,24 +3818,52 @@ function splitSegmentsAtSkips(segments, skips) {
2223
3818
  return out;
2224
3819
  }
2225
3820
  export {
3821
+ APP_QUIET_MS,
3822
+ APP_SETTLE_CAP_MS,
2226
3823
  AWAIT_HEARTBEAT_MS,
2227
3824
  AWAIT_POLL_S,
2228
3825
  AWAIT_TIMEOUT_S,
3826
+ CHANGE_AGE_PROBE,
3827
+ CLICK_BUTTONS,
2229
3828
  DEAD_SHARE_WARN_PCT,
2230
3829
  DEAD_STRETCH_WARN_MS,
3830
+ DEFAULT_READY,
2231
3831
  DOM_CHANGE_PROBE,
2232
3832
  DOM_QUIET_MS,
2233
3833
  DOM_SETTLE_CAP_MS,
2234
3834
  DOWNLOAD_TIMEOUT_S,
3835
+ DRAG_HOLD_MS,
3836
+ DRAG_MODIFIERS,
2235
3837
  EXPECT_TIMEOUT_S,
2236
3838
  EXPOSURE_ADVICE,
2237
3839
  EXPOSURE_MATCHERS_SRC,
2238
3840
  EXPOSURE_PROBE,
2239
3841
  ExposureLog,
3842
+ IDLE_CALL_MS,
3843
+ KEY_ROUNDTRIP_MS,
3844
+ KEY_SEQUENCE_GAP_MS,
3845
+ MAX_KEYS,
3846
+ MAX_PATH_POINTS,
3847
+ MEDIA_DRAIN_PROBE,
3848
+ MEDIA_WATCH_SCRIPT,
2240
3849
  MIN_SKIP_MS,
3850
+ MODIFIERS,
2241
3851
  MOTION_TICK_MS,
3852
+ MULTI_CLICK_GAP_MS,
3853
+ NAV_TIMEOUT_MS,
3854
+ NETWORKIDLE_HYDRATION_MS,
3855
+ NOTICE_SRC,
3856
+ NoticeLog,
2242
3857
  OUTPUT_MAX_BYTES,
3858
+ OVERRUN_MIN_MS,
3859
+ OVERRUN_MIN_PLANNED_MS,
3860
+ OVERRUN_SHARE,
2243
3861
  PAGE_ERROR_LINE_CHARS,
3862
+ PASTE_MAX_BYTES,
3863
+ PASTE_READ_WINDOW_MS,
3864
+ PASTE_TYPES,
3865
+ PASTE_UNREAD_MS,
3866
+ PATH_DEFAULT_MS,
2244
3867
  POINTER_MAX_MS,
2245
3868
  POINTER_MIN_MS,
2246
3869
  POINTER_MS_PER_PX,
@@ -2248,7 +3871,10 @@ export {
2248
3871
  PRESS_KEYS,
2249
3872
  PRESS_LEAD_MS,
2250
3873
  PageErrorLog,
3874
+ PageSoundLog,
2251
3875
  QUIET_MS,
3876
+ READY_MODES,
3877
+ READY_SETTLE_CAP_MS,
2252
3878
  READ_BEAT_MS,
2253
3879
  RESPONSE_MS,
2254
3880
  RecordRefusedError,
@@ -2258,18 +3884,26 @@ export {
2258
3884
  SCROLL_SETTLE_MS,
2259
3885
  SETTLE_CAP_MS,
2260
3886
  SETTLE_MS,
3887
+ SETTLE_VERBS,
2261
3888
  STEP_GUARDS,
3889
+ STEP_MARGIN_MS,
2262
3890
  SetupEnvError,
2263
3891
  SetupError,
3892
+ StepBound,
2264
3893
  TRAILING_HOLD_MS,
2265
3894
  TakeClock,
3895
+ WAIT_MARGIN_MS,
2266
3896
  WALL_PROBE,
2267
3897
  WallError,
3898
+ appSettled,
2268
3899
  askedMs,
2269
3900
  base64Bytes,
3901
+ bounded,
2270
3902
  buildEncodePage,
3903
+ buttonNumber,
2271
3904
  capReached,
2272
3905
  cappedLine,
3906
+ chordProblem,
2273
3907
  clampWait,
2274
3908
  clockMotion,
2275
3909
  clockTyping,
@@ -2283,48 +3917,80 @@ export {
2283
3917
  extForType,
2284
3918
  fetchInPageScript,
2285
3919
  fileNameProblem,
3920
+ fitSurfaceToViewport,
2286
3921
  formatDurationCap,
2287
3922
  guardVerdict,
2288
3923
  guardedSteps,
3924
+ hangReason,
2289
3925
  headersFor,
2290
3926
  holdLeftMs,
2291
3927
  hostedRefusals,
3928
+ inlinePoints,
3929
+ inlineScript,
2292
3930
  isIdpHost,
3931
+ isKeyName,
2293
3932
  isSignInPath,
2294
3933
  jpegDims,
3934
+ keySequencePlanMs,
3935
+ loadScriptFiles,
2295
3936
  maskInitScript,
3937
+ modifierBits,
2296
3938
  nameFromUrl,
3939
+ noticeAdvice,
3940
+ noticeLine,
2297
3941
  outputName,
3942
+ overrunLine,
3943
+ overrunOf,
2298
3944
  paceLine,
2299
3945
  paceReport,
2300
3946
  pageErrorLine,
2301
3947
  parseHeaders,
3948
+ pasteTypeOf,
3949
+ pathDurationMs,
2302
3950
  plainName,
2303
3951
  planFreshTake,
2304
3952
  pointerTravelMs,
2305
3953
  pollCondition,
3954
+ pressSequence,
2306
3955
  publicUrl,
2307
3956
  readAllBytes,
2308
3957
  readCondition,
2309
3958
  readMediaSource,
3959
+ readPath,
3960
+ readyLine,
3961
+ readyProblem,
2310
3962
  realSleep,
2311
3963
  recordTake,
2312
3964
  redirectedAway,
3965
+ reloadUntilReady,
3966
+ resolveChord,
3967
+ resolveModifier,
2313
3968
  resolveSetupText,
2314
3969
  runSetup,
2315
3970
  sayCondition,
2316
3971
  sayDuration,
3972
+ schedulePath,
3973
+ scriptFiles,
2317
3974
  scrollByScript,
2318
3975
  scrollTravelMs,
2319
3976
  settleMs,
3977
+ settleOn,
2320
3978
  settleVerdict,
2321
3979
  setupEnvNames,
3980
+ soundSrcForMeta,
3981
+ splitChord,
2322
3982
  splitSegmentsAtSkips,
3983
+ stageClipboardScript,
3984
+ stepBoundMs,
3985
+ syntheticPasteScript,
2323
3986
  typeOfName,
3987
+ untilReady,
2324
3988
  urlSchemeProblem,
2325
3989
  validateActions,
2326
3990
  validateSetup,
2327
3991
  visibleTextScript,
2328
- wallVerdict
3992
+ wallVerdict,
3993
+ withFittingExport,
3994
+ withPageSound
2329
3995
  };
2330
3996
  //# sourceMappingURL=index.js.map