jev-cdp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/dist/cli.js +249 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,8 +27,8 @@ This is an early experimental port. See [NOTICE.md](NOTICE.md) for source attrib
27
27
  Run the published CLI without adding it to a project. Bun must be installed for either command:
28
28
 
29
29
  ```bash
30
- bunx jev-cdp@0.1.2 help run
31
- npx -y jev-cdp@0.1.2 help run
30
+ bunx jev-cdp@0.1.3 help run
31
+ npx -y jev-cdp@0.1.3 help run
32
32
  ```
33
33
 
34
34
  Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg is required for `--recording`.
@@ -96,10 +96,12 @@ bun run run -- run \
96
96
  --final-state
97
97
  ```
98
98
 
99
- `--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay in milliseconds after moving to a click target and before pressing the mouse; Jev does not choose or observe this delay. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
99
+ `--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay after opening or loading a page, switching to an attached tab, or changing the URL within a page. Jev chooses during that delay, and the adapter waits only for any time left before acting. The same flag also pauses after moving to a click target and before pressing the mouse. Jev does not choose or observe these delays. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
100
100
 
101
101
  `--final-state` adds an AI-oriented semantic snapshot to the final JSON on standard output. It includes the final URL, title, visible text, viewport, scroll state, actionable elements, accessible labels, and control state such as `pressed`, `checked`, `selected`, and `expanded`. Progress remains on standard error, so a coding agent can parse standard output as one JSON object and choose the next bounded goal without another browser observation.
102
102
 
103
+ The snapshot includes visible controls in direct child iframes, including cross-origin frames. Clicking a link there uses its frame coordinates and checks that the observed control is still current. If that click opens a new tab, Jev switches to it, returns its target ID for the next goal, and keeps the recording and 1120×780 viewport consistent across the switch.
104
+
103
105
  ## Supply known field values without another LLM
104
106
 
105
107
  For portable QA scenarios, let the planner provide exact test data instead of invoking the Luna fallback. Match a field by its observed accessible label:
@@ -181,7 +183,7 @@ The context is disposed after evidence capture by default. Combine it with `--ke
181
183
  | `--tab TARGET_ID` | — | create a new tab | Attach to one exact existing Chrome page target |
182
184
  | `tabs` | — | — | Print target IDs, titles, and URLs for open page tabs |
183
185
  | `--recording PATH.mp4` | — | off | Record the complete goal with an animated cursor |
184
- | `--interaction-pauses MS` | — | `0` | Wait after moving to a click target, before mousedown |
186
+ | `--interaction-pauses MS` | — | `0` | Pause after page loads and before clicks, overlapping page pauses with Jev decisions |
185
187
  | `--screenshot PATH.jpg` | — | off | Save the final browser viewport |
186
188
  | `--final-state` | — | off | Include the final semantic page state in stdout JSON |
187
189
  | `--field-value LABEL=VALUE` | — | Luna fallback | Type caller-provided test data into the exactly labeled field |
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "jev-cdp",
6
- version: "0.1.2",
6
+ version: "0.1.4",
7
7
  description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -257,6 +257,24 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
257
257
 
258
258
  // src/browser.ts
259
259
  var MARKER = `(() => { const state=${snapshot_default}; return state?.marker ?? null; })()`;
260
+ var RECORDING_CURSOR_INIT = `(() => {
261
+ if (window !== window.top) return;
262
+ const mount = () => {
263
+ if (!document.documentElement) return false;
264
+ if (document.getElementById('__jev-recording-cursor')) return true;
265
+ const cursor = document.createElement('div');
266
+ cursor.id = '__jev-recording-cursor';
267
+ cursor.setAttribute('aria-hidden', 'true');
268
+ cursor.innerHTML = '<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
269
+ Object.assign(cursor.style, {position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
270
+ document.documentElement.append(cursor);
271
+ return true;
272
+ };
273
+ if (!mount()) {
274
+ const observer = new MutationObserver(() => { if (mount()) observer.disconnect(); });
275
+ observer.observe(document, { childList: true });
276
+ }
277
+ })()`;
260
278
 
261
279
  class StalePageError extends Error {
262
280
  }
@@ -294,8 +312,20 @@ class Browser {
294
312
  #recordingSequence = 0;
295
313
  #recordingWrites = Promise.resolve();
296
314
  #stopRecordingEvents;
315
+ #stopPageEvents = [];
316
+ #mainFrameId;
317
+ #pageLoadPromise = null;
318
+ #resolvePageLoad;
319
+ #pauseUntil = 0;
320
+ #frameSessions = new Map;
321
+ #frameContexts = new Map;
322
+ #knownTargets = new Set;
323
+ #openedTabs = [];
324
+ #cdpUrl;
325
+ #switchOnPopup = false;
297
326
  constructor(cdp, sessionId, targetId, ownsTarget, browserContextId, options) {
298
327
  this.#cdp = cdp;
328
+ this.#cdpUrl = options.cdpUrl;
299
329
  this.#sessionId = sessionId;
300
330
  this.#targetId = targetId;
301
331
  this.#ownsTarget = ownsTarget;
@@ -353,7 +383,20 @@ class Browser {
353
383
  flatten: true
354
384
  });
355
385
  const browser = new Browser(cdp, attached.sessionId, targetId, ownsTarget, browserContextId, options);
386
+ browser.#knownTargets = new Set((await listChromeTargets(options.cdpUrl)).map((target) => target.id));
356
387
  try {
388
+ browser.#stopPageEvents.push(cdp.on("Target.attachedToTarget", browser.#sessionId, (params) => {
389
+ const info = params.targetInfo;
390
+ if (info?.type === "iframe" && info.targetId && typeof params.sessionId === "string")
391
+ browser.#frameSessions.set(info.targetId, params.sessionId);
392
+ }));
393
+ browser.#stopPageEvents.push(cdp.on("Page.frameNavigated", browser.#sessionId, (params) => {
394
+ const frame = params.frame;
395
+ if (frame?.id)
396
+ browser.#frameContexts.delete(frame.id);
397
+ }));
398
+ await browser.call("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
399
+ await browser.call("Page.enable");
357
400
  await browser.call("Emulation.setDeviceMetricsOverride", {
358
401
  width: 1120,
359
402
  height: 780,
@@ -361,9 +404,11 @@ class Browser {
361
404
  mobile: false
362
405
  });
363
406
  await browser.call("Emulation.setFocusEmulationEnabled", { enabled: true });
407
+ await browser.watchPageLoads();
364
408
  if (options.url)
365
409
  await browser.call("Page.navigate", { url: options.url });
366
410
  await browser.waitForReady();
411
+ browser.#pauseUntil = performance.now() + browser.#interactionPauses;
367
412
  await browser.startRecording();
368
413
  return browser;
369
414
  } catch (error) {
@@ -377,23 +422,83 @@ class Browser {
377
422
  get targetId() {
378
423
  return this.#targetId;
379
424
  }
425
+ async watchPageLoads() {
426
+ if (!this.#interactionPauses)
427
+ return;
428
+ await this.call("Page.enable");
429
+ const tree = await this.call("Page.getFrameTree");
430
+ this.#mainFrameId = tree.frameTree.frame.id;
431
+ const loading = (params) => {
432
+ if (params.frameId !== this.#mainFrameId || this.#pageLoadPromise)
433
+ return;
434
+ this.#pageLoadPromise = new Promise((resolve2) => {
435
+ this.#resolvePageLoad = resolve2;
436
+ });
437
+ };
438
+ const loaded = () => {
439
+ if (!this.#pageLoadPromise)
440
+ return;
441
+ this.#pauseUntil = performance.now() + this.#interactionPauses;
442
+ this.#resolvePageLoad?.();
443
+ this.#resolvePageLoad = undefined;
444
+ this.#pageLoadPromise = null;
445
+ };
446
+ this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
447
+ const frame = params.frame;
448
+ if (frame && frame.id === this.#mainFrameId && !frame.parentId)
449
+ loading({ frameId: frame.id });
450
+ }), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
451
+ if (params.frameId === this.#mainFrameId)
452
+ loaded();
453
+ }), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
454
+ if (params.frameId === this.#mainFrameId) {
455
+ this.#pauseUntil = performance.now() + this.#interactionPauses;
456
+ this.resetRecordingCursor().catch(() => {
457
+ return;
458
+ });
459
+ }
460
+ }));
461
+ }
462
+ async waitForInteractionPause() {
463
+ if (!this.#interactionPauses)
464
+ return;
465
+ while (true) {
466
+ const pageLoad = this.#pageLoadPromise;
467
+ if (pageLoad) {
468
+ await Promise.race([
469
+ pageLoad,
470
+ Bun.sleep(15000).then(() => {
471
+ throw new Error("Page did not finish loading within 15 seconds");
472
+ })
473
+ ]);
474
+ continue;
475
+ }
476
+ const remaining = this.#pauseUntil - performance.now();
477
+ if (remaining <= 0)
478
+ return;
479
+ await Bun.sleep(remaining);
480
+ }
481
+ }
380
482
  async startRecording() {
381
483
  if (!this.#recordingPath)
382
484
  return;
383
485
  if (!Bun.which("ffmpeg"))
384
486
  throw new Error("--recording requires ffmpeg on PATH");
385
- this.#recordingDirectory = await mkdtemp(join(tmpdir(), "jev-cdp-recording-"));
487
+ const firstSegment = !this.#recordingDirectory;
488
+ if (firstSegment)
489
+ this.#recordingDirectory = await mkdtemp(join(tmpdir(), "jev-cdp-recording-"));
386
490
  await this.call("Page.enable");
491
+ await this.call("Page.addScriptToEvaluateOnNewDocument", { source: RECORDING_CURSOR_INIT });
387
492
  const viewport = await this.evaluate("({width: innerWidth, height: innerHeight})");
388
493
  if (!viewport)
389
494
  throw new Error("Could not read the recording viewport");
390
495
  await this.animateCursor(viewport.width / 2, viewport.height / 2);
391
496
  const initial = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 70 });
392
- const initialPath = join(this.#recordingDirectory, "000000.jpg");
497
+ const initialPath = join(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
393
498
  await Bun.write(initialPath, Buffer.from(initial.data, "base64"));
394
- this.#recordingFrames.push({ path: initialPath, elapsedMs: 0 });
395
- this.#recordingSequence = 1;
396
- this.#recordingStartedAt = performance.now();
499
+ if (firstSegment)
500
+ this.#recordingStartedAt = performance.now();
501
+ this.#recordingFrames.push({ path: initialPath, elapsedMs: firstSegment ? 0 : performance.now() - this.#recordingStartedAt });
397
502
  this.#stopRecordingEvents = this.#cdp.on("Page.screencastFrame", this.#sessionId, (params) => {
398
503
  const frame = params;
399
504
  this.call("Page.screencastFrameAck", { sessionId: frame.sessionId }).catch(() => {
@@ -485,25 +590,37 @@ class Browser {
485
590
  async animateCursor(x, y, click = false) {
486
591
  if (!this.#recordingPath)
487
592
  return;
593
+ await this.evaluate(RECORDING_CURSOR_INIT);
488
594
  await this.evaluate(`(point => {
489
- let cursor=document.getElementById('__jev-recording-cursor');
490
- if (!cursor) {
491
- cursor=document.createElement('div');
492
- cursor.id='__jev-recording-cursor'; cursor.setAttribute('aria-hidden','true');
493
- cursor.innerHTML='<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
494
- Object.assign(cursor.style,{position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
495
- document.documentElement.append(cursor);
496
- }
595
+ const cursor=document.getElementById('__jev-recording-cursor');
596
+ if (!cursor) return;
497
597
  cursor.style.left=point.x+'px'; cursor.style.top=point.y+'px';
498
598
  if (point.click) cursor.animate([{transform:'translate(-3px,-3px) scale(1)'},{transform:'translate(-3px,-3px) scale(.72)'},{transform:'translate(-3px,-3px) scale(1)'}],{duration:260,easing:'ease-out'});
499
599
  })(${JSON.stringify({ x, y, click })})`);
500
600
  await Bun.sleep(click ? 80 : 200);
501
601
  }
602
+ async resetRecordingCursor() {
603
+ if (!this.#recordingPath)
604
+ return;
605
+ await this.evaluate(`${RECORDING_CURSOR_INIT}; (() => {
606
+ const cursor = document.getElementById('__jev-recording-cursor');
607
+ if (!cursor) return;
608
+ cursor.style.transition = 'none';
609
+ cursor.style.left = '50vw';
610
+ cursor.style.top = '50vh';
611
+ requestAnimationFrame(() => { cursor.style.transition = 'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)'; });
612
+ })()`);
613
+ }
502
614
  async waitForReady() {
503
615
  const deadline = Date.now() + 15000;
504
616
  while (Date.now() < deadline) {
505
- if (await this.evaluate("document.readyState") === "complete")
506
- return;
617
+ try {
618
+ if (!this.#pageLoadPromise && await this.evaluate("document.readyState") === "complete")
619
+ return;
620
+ } catch (error) {
621
+ if (!(error instanceof StalePageError))
622
+ throw error;
623
+ }
507
624
  await Bun.sleep(20);
508
625
  }
509
626
  throw new Error("Page did not finish loading within 15 seconds");
@@ -514,6 +631,64 @@ class Browser {
514
631
  throw new StalePageError("Document changed during evaluation");
515
632
  return response.result?.value;
516
633
  }
634
+ async evaluateFrame(frameId, expression) {
635
+ const session = this.#frameSessions.get(frameId);
636
+ let contextId = this.#frameContexts.get(frameId);
637
+ if (!session && !contextId) {
638
+ const context = await this.call("Page.createIsolatedWorld", { frameId });
639
+ contextId = context.executionContextId;
640
+ this.#frameContexts.set(frameId, contextId);
641
+ }
642
+ const response = await this.#cdp.command("Runtime.evaluate", { expression, returnByValue: true, ...contextId ? { contextId } : {} }, session ?? this.#sessionId);
643
+ if (response.exceptionDetails)
644
+ throw new StalePageError("Frame changed during evaluation");
645
+ return response.result?.value;
646
+ }
647
+ async frameOffset(frameId) {
648
+ const owner = await this.call("DOM.getFrameOwner", { frameId });
649
+ const box = await this.call("DOM.getBoxModel", { backendNodeId: owner.backendNodeId });
650
+ return { x: box.model.content[0], y: box.model.content[1] };
651
+ }
652
+ async childFrames() {
653
+ const tree = await this.call("Page.getFrameTree");
654
+ return (tree.frameTree.childFrames ?? []).map((child) => child.frame.id);
655
+ }
656
+ async discoverTabs() {
657
+ if (!this.#switchOnPopup)
658
+ return;
659
+ for (const target of await listChromeTargets(this.#cdpUrl)) {
660
+ if (target.type !== "page" || this.#knownTargets.has(target.id))
661
+ continue;
662
+ this.#knownTargets.add(target.id);
663
+ const attached = await this.#cdp.command("Target.attachToTarget", { targetId: target.id, flatten: true });
664
+ try {
665
+ await this.#cdp.command("Emulation.setDeviceMetricsOverride", {
666
+ width: 1120,
667
+ height: 780,
668
+ deviceScaleFactor: 1,
669
+ mobile: false
670
+ }, attached.sessionId);
671
+ } finally {
672
+ if (this.#switchOnPopup) {
673
+ if (this.#recordingPath) {
674
+ await this.call("Page.stopScreencast").catch(() => {
675
+ return;
676
+ });
677
+ this.#stopRecordingEvents?.();
678
+ this.#stopRecordingEvents = undefined;
679
+ await this.#recordingWrites;
680
+ }
681
+ this.#sessionId = attached.sessionId;
682
+ this.#targetId = target.id;
683
+ this.#switchOnPopup = false;
684
+ await this.startRecording();
685
+ } else {
686
+ await this.#cdp.command("Target.detachFromTarget", { sessionId: attached.sessionId });
687
+ }
688
+ }
689
+ this.#openedTabs.push({ id: target.id, url: target.url, title: target.title });
690
+ }
691
+ }
517
692
  async settleAfterInput() {
518
693
  const action = this.#afterInput;
519
694
  this.#afterInput = null;
@@ -549,6 +724,7 @@ class Browser {
549
724
  }
550
725
  async observe(screenshot = this.#screenshots) {
551
726
  await this.settleAfterInput();
727
+ await this.discoverTabs();
552
728
  let info;
553
729
  for (let attempt = 0;attempt < 10; attempt++) {
554
730
  try {
@@ -563,6 +739,36 @@ class Browser {
563
739
  }
564
740
  if (!info)
565
741
  throw new StalePageError("Document is navigating");
742
+ if (this.#openedTabs.length) {
743
+ const targets = await listChromeTargets(this.#cdpUrl);
744
+ this.#openedTabs = this.#openedTabs.map((tab) => {
745
+ const current = targets.find((target) => target.id === tab.id);
746
+ return current ? { ...tab, url: current.url, title: current.title } : tab;
747
+ });
748
+ info.text = `${info.text}
749
+ ${this.#openedTabs.map((tab) => `Opened new tab: ${tab.title} ${tab.url} (target ${tab.id})`).join(`
750
+ `)}`;
751
+ }
752
+ for (const frameId of await this.childFrames()) {
753
+ try {
754
+ const offset = await this.frameOffset(frameId);
755
+ const child = await this.evaluateFrame(frameId, snapshot_default);
756
+ if (!child)
757
+ continue;
758
+ info.text = `${info.text}
759
+ ${child.text}`.slice(0, 6000);
760
+ for (const action of child.actions) {
761
+ if (!action.node || !action.rect)
762
+ continue;
763
+ const rect = { ...action.rect, x: action.rect.x + offset.x, y: action.rect.y + offset.y };
764
+ if (rect.x < 0 || rect.y < 0 || rect.x >= info.w || rect.y >= info.h)
765
+ continue;
766
+ info.actions.push({ ...action, frameId, rect, id: `e${info.actions.length + 1}` });
767
+ info.guards[`${frameId}:${action.node}`] = child.guards[String(action.node)];
768
+ }
769
+ info.marker = [info.marker, frameId, child.marker];
770
+ } catch {}
771
+ }
566
772
  const page = { ...info, fingerprint: fingerprint(info) };
567
773
  if (screenshot) {
568
774
  const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 72 });
@@ -571,6 +777,12 @@ class Browser {
571
777
  return page;
572
778
  }
573
779
  async fresh(page, action) {
780
+ if (action?.frameId && typeof action.node === "number") {
781
+ const current = await this.evaluateFrame(action.frameId, `(() => {
782
+ const c=window.__jevFast; return c ? c.guard(c.nodes.get(${action.node})) : null;
783
+ })()`);
784
+ return stableStringify(current) === stableStringify(page.guards[`${action.frameId}:${action.node}`]);
785
+ }
574
786
  if (action && (action.kind === "click" || action.kind === "select")) {
575
787
  if (typeof action.node !== "number")
576
788
  return false;
@@ -603,7 +815,11 @@ class Browser {
603
815
  }
604
816
  if (typeof action.node !== "number")
605
817
  throw new Error("Invalid observed node");
606
- const target = await this.evaluate(`(action => {
818
+ const target = await (action.frameId ? this.evaluateFrame(action.frameId, `(action => {
819
+ const e=window.__jevFast?.nodes.get(action.node);
820
+ if (!e?.isConnected || !e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return null;
821
+ const r=e.getBoundingClientRect(); return {x:r.x+r.width/2,y:r.y+r.height/2};
822
+ })(${JSON.stringify(action)})`) : this.evaluate(`(action => {
607
823
  const e=window.__jevFast?.nodes.get(action.node);
608
824
  if (!e?.isConnected || e.matches(':disabled') || e.closest('[aria-disabled="true"],[inert]') ||
609
825
  !e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return null;
@@ -619,12 +835,17 @@ class Browser {
619
835
  e.dispatchEvent(new Event('change',{bubbles:true}));
620
836
  }
621
837
  return {x,y};
622
- })(${JSON.stringify(action)})`);
838
+ })(${JSON.stringify(action)})`));
623
839
  if (!target) {
624
840
  if (action.kind === "select")
625
841
  throw new Error("Dropdown execution was not confirmed");
626
842
  throw new StalePageError("Target changed or is covered");
627
843
  }
844
+ if (action.frameId) {
845
+ const offset = await this.frameOffset(action.frameId);
846
+ target.x += offset.x;
847
+ target.y += offset.y;
848
+ }
628
849
  await this.animateCursor(target.x, target.y);
629
850
  if (action.kind !== "select") {
630
851
  if (action.kind === "click" && this.#interactionPauses > 0) {
@@ -665,11 +886,18 @@ class Browser {
665
886
  }
666
887
  }
667
888
  this.#afterInput = action;
889
+ if (action.frameId && action.kind === "click") {
890
+ this.#switchOnPopup = true;
891
+ await Bun.sleep(650);
892
+ }
668
893
  }
669
894
  async close() {
670
895
  if (this.#closed)
671
896
  return;
672
897
  this.#closed = true;
898
+ for (const stop of this.#stopPageEvents)
899
+ stop();
900
+ this.#stopPageEvents = [];
673
901
  let failure;
674
902
  try {
675
903
  if (this.#recordingPath)
@@ -1150,6 +1378,7 @@ class Agent {
1150
1378
  this.#textCalls.push({ ...helper, field: action.label, value: text });
1151
1379
  }
1152
1380
  }
1381
+ await this.#browser.waitForInteractionPause();
1153
1382
  await this.#browser.act(action, page, text ?? undefined);
1154
1383
  this.#pendingText = null;
1155
1384
  const entry = {
@@ -1279,7 +1508,8 @@ Browser behavior:
1279
1508
  [env: JEV_BROWSER_VISIBLE=1]
1280
1509
  --keep-open Leave a runner-created tab or context open.
1281
1510
  [env: JEV_BROWSER_KEEP_OPEN=1]
1282
- --interaction-pauses <ms> Pause after moving to a click target, before mousedown.
1511
+ --interaction-pauses <ms> Pause after page loads and before clicks. Jev decisions
1512
+ run during page pauses, so only remaining time is waited.
1283
1513
 
1284
1514
  Known field values:
1285
1515
  --field-value <label=value> Type an exact non-secret value when that accessible
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-cdp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
5
5
  "type": "module",
6
6
  "license": "MIT",