jev-cdp 0.1.6 → 0.1.7

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 -6
  2. package/dist/cli.js +226 -23
  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.6 help run
31
- npx -y jev-cdp@0.1.6 help run
30
+ bunx jev-cdp@0.1.7 help run
31
+ npx -y jev-cdp@0.1.7 help run
32
32
  ```
33
33
 
34
34
  Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg with H.264 encoding (`libx264`) is required for `--recording`. Jev selects `-fps_mode vfr` when FFmpeg supports it and falls back to `-vsync vfr` for older builds. Run `jev-cdp doctor` to check the installed encoder with a short MP4 encode and decode.
@@ -96,13 +96,13 @@ 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 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.
99
+ `--recording` captures Chrome's compositor screencast stream for the full goal and renders a 1280×720 H.264 MP4. A 40-pixel browser bar shows the active tab's URL above the page. Chrome uses a 1506×800 viewport, scaled to fit the video beneath the bar, so controls remain at their observed coordinates while more of the page fits on screen. Because Chrome's native pointer is not part of the 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, changing the URL within a page, or moving to a click target before pressing the mouse. Jev chooses during page pauses, and the adapter waits only for any time left before acting. Jev does not choose or observe those delays. When an action opens a new tab, the recording shows a "New tab opened" notice over the previous tab for the full configured pause, then switches to the new tab. With zero pause, the notice appears for one frame without delaying Jev. `--screenshot` saves a 1280×720 image with the same browser bar, even when recording is off. If a new tab opened, the final screenshot also includes the notice.
100
100
 
101
101
  `jev-cdp run` writes JSON Lines to standard output: one `type:"action"` object per executed action, followed by one `type:"result"` object with the final status and action budget. Action objects include elapsed execution time, the operation, page and tab URLs, the viewport, and a CSS selector, role, name, frame, and coordinates for the element. Fill actions include the entered text; password fields and `--field-value-env` values are redacted and must be supplied separately for replay. Select and scroll actions include their option value or wheel delta. The CSS selector and page URL can be used as Playwright replay targets, with the coordinates as a fallback at the recorded viewport size. `--final-state` adds the final semantic page snapshot: a frame tree with URLs and loading state, actionable elements with frame identity, screen bounds, nearby text, region, clickability, and any element covering the click point, plus new-tab and redirect transitions. Automatic navigation and embedded-content waits use `--wait-budget-ms` (15 seconds by default) and do not consume `--max-steps`; a timeout returns `wait_timeout`, its pending condition and elapsed wait time, and the latest semantic state. Runtime failures emit a `type:"result"` object with `status:"error"`; diagnostic text uses standard error.
102
102
 
103
103
  Each action object includes `consoleErrors` observed during that step. The result object includes `initialConsoleErrors` already present when attaching to the tab and `consoleErrors` for the full run. These fields include browser console errors, uncaught exceptions, and error-level DevTools log entries from the page and its frames. Review them alongside the semantic state; a console error does not by itself establish that the goal failed.
104
104
 
105
- The snapshot includes visible controls in nested 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.
105
+ The snapshot includes visible controls in nested 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 1506×800 viewport consistent across the switch.
106
106
 
107
107
  ## Supply known field values without another LLM
108
108
 
@@ -184,9 +184,9 @@ The context is disposed after evidence capture by default. Combine it with `--ke
184
184
  | `--cdp URL` | `CHROME_CDP_URL` | `http://127.0.0.1:9222` | Chrome DevTools HTTP endpoint |
185
185
  | `--tab TARGET_ID` | — | create a new tab | Attach to one exact existing Chrome page target |
186
186
  | `tabs` | — | — | Print target IDs, titles, and URLs for open page tabs |
187
- | `--recording PATH.mp4` | — | off | Record the complete goal with an animated cursor |
187
+ | `--recording PATH.mp4` | — | off | Record a 1280×720 video with a URL bar, animated cursor, and tab notices |
188
188
  | `--interaction-pauses MS` | — | `0` | Pause after page loads and before clicks, overlapping page pauses with Jev decisions |
189
- | `--screenshot PATH.jpg` | — | off | Save the final browser viewport |
189
+ | `--screenshot PATH.jpg` | — | off | Save a 1280×720 image with the URL bar and any new-tab notice |
190
190
  | `--final-state` | — | off | Include the final semantic page state in stdout JSON |
191
191
  | `--field-value LABEL=VALUE` | — | Luna fallback | Type caller-provided test data into the exactly labeled field |
192
192
  | `--field-value-env LABEL=NAME` | — | off | Read a sensitive field value from an environment variable |
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.6",
6
+ version: "0.1.7",
7
7
  description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -225,6 +225,82 @@ file '${quoted}'
225
225
  }
226
226
  }
227
227
 
228
+ // src/recording-visual.ts
229
+ var VIDEO_WIDTH = 1280;
230
+ var VIDEO_HEIGHT = 720;
231
+ var BAR_HEIGHT = 40;
232
+ var PAGE_WIDTH = 1506;
233
+ var PAGE_HEIGHT = 800;
234
+ function composeFrameExpression(jpeg, url, newTabUrl) {
235
+ return `(async () => {
236
+ const bytes = Uint8Array.from(atob(${JSON.stringify(jpeg)}), character => character.charCodeAt(0));
237
+ const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/jpeg' }));
238
+ const canvas = document.createElement('canvas');
239
+ canvas.width = ${VIDEO_WIDTH}; canvas.height = ${VIDEO_HEIGHT};
240
+ const context = canvas.getContext('2d');
241
+ if (!context) throw new Error('Could not create recording canvas');
242
+ context.fillStyle = '#f5f7fa';
243
+ context.fillRect(0, 0, canvas.width, ${BAR_HEIGHT});
244
+ context.fillStyle = '#ffffff';
245
+ context.beginPath();
246
+ context.roundRect(12, 6, canvas.width - 24, 28, 7);
247
+ context.fill();
248
+ context.strokeStyle = '#d7dce4';
249
+ context.lineWidth = 1;
250
+ context.stroke();
251
+ context.fillStyle = '#617185';
252
+ context.beginPath(); context.arc(28, 20, 4, 0, Math.PI * 2); context.fill();
253
+ const fit = (value, maxWidth) => {
254
+ if (context.measureText(value).width <= maxWidth) return value;
255
+ while (value.length && context.measureText(value + '\u2026').width > maxWidth) value = value.slice(0, -1);
256
+ return value + '\u2026';
257
+ };
258
+ const url = ${JSON.stringify(url)};
259
+ context.font = '14px system-ui, sans-serif';
260
+ context.fillStyle = '#263343';
261
+ context.textBaseline = 'middle';
262
+ context.fillText(fit(url, canvas.width - 72), 42, 20);
263
+ const chromeBorder = '#61738a';
264
+ context.fillStyle = chromeBorder;
265
+ context.fillRect(0, ${BAR_HEIGHT - 2}, canvas.width, 2);
266
+ context.drawImage(bitmap, 0, ${BAR_HEIGHT}, canvas.width, canvas.height - ${BAR_HEIGHT});
267
+ bitmap.close();
268
+ const newTabUrl = ${JSON.stringify(newTabUrl)};
269
+ if (newTabUrl) {
270
+ const cardWidth = 620, cardHeight = 104, radius = 12, x = (canvas.width - cardWidth) / 2;
271
+ const cardPath = () => {
272
+ context.beginPath();
273
+ context.moveTo(x, 0);
274
+ context.lineTo(x + cardWidth, 0);
275
+ context.lineTo(x + cardWidth, cardHeight - radius);
276
+ context.quadraticCurveTo(x + cardWidth, cardHeight, x + cardWidth - radius, cardHeight);
277
+ context.lineTo(x + radius, cardHeight);
278
+ context.quadraticCurveTo(x, cardHeight, x, cardHeight - radius);
279
+ context.closePath();
280
+ };
281
+ context.save();
282
+ context.shadowColor = 'rgba(15, 23, 42, .24)';
283
+ context.shadowBlur = 14;
284
+ context.shadowOffsetY = 5;
285
+ context.fillStyle = '#ffffff';
286
+ cardPath(); context.fill();
287
+ context.restore();
288
+ context.strokeStyle = chromeBorder;
289
+ context.lineWidth = 1.5;
290
+ cardPath(); context.stroke();
291
+ context.fillStyle = '#2563eb';
292
+ context.beginPath(); context.arc(x + 28, 35, 7, 0, Math.PI * 2); context.fill();
293
+ context.fillStyle = '#172033';
294
+ context.font = '600 19px system-ui, sans-serif';
295
+ context.fillText('New tab opened', x + 48, 35);
296
+ context.fillStyle = '#536277';
297
+ context.font = '14px system-ui, sans-serif';
298
+ context.fillText(fit(newTabUrl, cardWidth - 52), x + 24, 72);
299
+ }
300
+ return canvas.toDataURL('image/jpeg', .88).split(',')[1];
301
+ })()`;
302
+ }
303
+
228
304
  // src/snapshot.js
229
305
  var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1ad2 under the MIT License.
230
306
  (() => {
@@ -417,6 +493,12 @@ class Browser {
417
493
  #recordingStartedAt = 0;
418
494
  #recordingSequence = 0;
419
495
  #recordingWrites = Promise.resolve();
496
+ #activeUrl = "about:blank";
497
+ #recordingUrlEvents = [];
498
+ #newTabNoticeUntil = 0;
499
+ #newTabNoticeUrl = null;
500
+ #newTabNoticeWindows = [];
501
+ #hadNewTab = false;
420
502
  #stopRecordingEvents;
421
503
  #stopPageEvents = [];
422
504
  #mainFrameId;
@@ -512,6 +594,8 @@ class Browser {
512
594
  }
513
595
  if (!info?.targetId || !info.url || info.url === "about:blank")
514
596
  return;
597
+ if (info.targetId === browser.#targetId)
598
+ browser.setActiveUrl(info.url);
515
599
  const urls = browser.#targetUrls.get(info.targetId) ?? [];
516
600
  if (urls.at(-1) !== info.url)
517
601
  urls.push(info.url);
@@ -537,8 +621,8 @@ class Browser {
537
621
  await browser.watchConsole();
538
622
  await browser.call("Page.enable");
539
623
  await browser.call("Emulation.setDeviceMetricsOverride", {
540
- width: 1120,
541
- height: 780,
624
+ width: PAGE_WIDTH,
625
+ height: PAGE_HEIGHT,
542
626
  deviceScaleFactor: 1,
543
627
  mobile: false
544
628
  });
@@ -547,6 +631,7 @@ class Browser {
547
631
  if (options.url)
548
632
  await browser.call("Page.navigate", { url: options.url });
549
633
  await browser.waitForReady();
634
+ browser.#activeUrl = await browser.evaluate("location.href") ?? browser.#activeUrl;
550
635
  browser.#pauseUntil = performance.now() + browser.#interactionPauses;
551
636
  await browser.startRecording();
552
637
  return browser;
@@ -563,6 +648,18 @@ class Browser {
563
648
  get targetId() {
564
649
  return this.#targetId;
565
650
  }
651
+ setActiveUrl(url) {
652
+ if (!url || url === this.#activeUrl)
653
+ return;
654
+ this.#activeUrl = url;
655
+ if (this.#recordingDirectory && this.#recordingFrames.length) {
656
+ this.#recordingUrlEvents.push({
657
+ elapsedMs: performance.now() - this.#recordingStartedAt,
658
+ targetId: this.#targetId,
659
+ url
660
+ });
661
+ }
662
+ }
566
663
  takeConsoleErrors() {
567
664
  return this.#consoleErrors.splice(0);
568
665
  }
@@ -606,6 +703,8 @@ class Browser {
606
703
  await this.call("Page.enable");
607
704
  const tree = await this.call("Page.getFrameTree");
608
705
  this.#mainFrameId = tree.frameTree.frame.id;
706
+ if (tree.frameTree.frame.url)
707
+ this.setActiveUrl(tree.frameTree.frame.url);
609
708
  await this.call("Runtime.addBinding", { name: "__jevDomChanged" });
610
709
  const observeDom = `(() => {
611
710
  if (window.__jevDomWatching) return;
@@ -642,8 +741,11 @@ class Browser {
642
741
  this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
643
742
  const frame = params.frame;
644
743
  this.signalChange();
645
- if (frame && frame.id === this.#mainFrameId && !frame.parentId)
744
+ if (frame && frame.id === this.#mainFrameId && !frame.parentId) {
745
+ if (frame.url)
746
+ this.setActiveUrl(frame.url);
646
747
  loading({ frameId: frame.id });
748
+ }
647
749
  }), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
648
750
  if (typeof params.frameId === "string")
649
751
  this.#loadingFrames.delete(params.frameId);
@@ -653,6 +755,8 @@ class Browser {
653
755
  }), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
654
756
  this.signalChange();
655
757
  if (params.frameId === this.#mainFrameId) {
758
+ if (typeof params.url === "string")
759
+ this.setActiveUrl(params.url);
656
760
  this.#pauseUntil = performance.now() + this.#interactionPauses;
657
761
  this.resetRecordingCursor().catch(() => {
658
762
  return;
@@ -728,7 +832,14 @@ class Browser {
728
832
  await Bun.write(initialPath, Buffer.from(initial.data, "base64"));
729
833
  if (firstSegment)
730
834
  this.#recordingStartedAt = performance.now();
731
- this.#recordingFrames.push({ path: initialPath, elapsedMs: firstSegment ? 0 : performance.now() - this.#recordingStartedAt });
835
+ const initialElapsedMs = firstSegment ? 0 : performance.now() - this.#recordingStartedAt;
836
+ this.#recordingFrames.push({
837
+ path: initialPath,
838
+ elapsedMs: initialElapsedMs,
839
+ targetId: this.#targetId,
840
+ url: this.#activeUrl,
841
+ newTab: false
842
+ });
732
843
  this.#stopRecordingEvents = this.#cdp.on("Page.screencastFrame", this.#sessionId, (params) => {
733
844
  const frame = params;
734
845
  this.call("Page.screencastFrameAck", { sessionId: frame.sessionId }).catch(() => {
@@ -736,20 +847,49 @@ class Browser {
736
847
  });
737
848
  const sequence = this.#recordingSequence++;
738
849
  const path = join2(this.#recordingDirectory, `${String(sequence).padStart(6, "0")}.jpg`);
739
- const elapsedMs = sequence ? performance.now() - this.#recordingStartedAt : 0;
850
+ const capturedAt = performance.now();
851
+ const elapsedMs = sequence ? capturedAt - this.#recordingStartedAt : 0;
852
+ const url = this.#activeUrl;
853
+ const targetId = this.#targetId;
854
+ const notice = capturedAt < this.#newTabNoticeUntil;
855
+ const noticeUrl = notice ? this.#newTabNoticeUrl ?? undefined : undefined;
740
856
  this.#recordingWrites = this.#recordingWrites.then(async () => {
741
857
  await Bun.write(path, Buffer.from(frame.data, "base64"));
742
- this.#recordingFrames.push({ path, elapsedMs });
858
+ this.#recordingFrames.push({
859
+ path,
860
+ elapsedMs,
861
+ targetId,
862
+ url,
863
+ newTab: notice,
864
+ ...noticeUrl ? { noticeUrl } : {}
865
+ });
743
866
  });
744
867
  });
745
868
  await this.call("Page.startScreencast", {
746
869
  format: "jpeg",
747
870
  quality: 70,
748
- maxWidth: 1120,
749
- maxHeight: 780,
871
+ maxWidth: PAGE_WIDTH,
872
+ maxHeight: PAGE_HEIGHT,
750
873
  everyNthFrame: 3
751
874
  });
752
875
  }
876
+ async recordNewTabNotice(url) {
877
+ if (!this.#recordingPath || !this.#recordingDirectory)
878
+ return;
879
+ const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 70 });
880
+ const elapsedMs = performance.now() - this.#recordingStartedAt;
881
+ const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
882
+ await Bun.write(path, Buffer.from(capture.data, "base64"));
883
+ this.#recordingFrames.push({
884
+ path,
885
+ elapsedMs,
886
+ targetId: this.#targetId,
887
+ url: this.#activeUrl,
888
+ newTab: true,
889
+ noticeUrl: url
890
+ });
891
+ return elapsedMs;
892
+ }
753
893
  async finishRecording() {
754
894
  if (!this.#recordingPath || !this.#recordingDirectory)
755
895
  return;
@@ -762,8 +902,53 @@ class Browser {
762
902
  await this.#recordingWrites;
763
903
  if (!this.#recordingFrames.length)
764
904
  throw new Error("Recording produced no browser frames");
905
+ const stoppedAt = performance.now() - this.#recordingStartedAt;
906
+ this.#recordingFrames.sort((a, b) => a.elapsedMs - b.elapsedMs);
907
+ for (const event of this.#recordingUrlEvents) {
908
+ let index = this.#recordingFrames.length - 1;
909
+ while (index >= 0 && (this.#recordingFrames[index].elapsedMs >= event.elapsedMs || this.#recordingFrames[index].targetId !== event.targetId))
910
+ index--;
911
+ const before = this.#recordingFrames[index];
912
+ if (!before || before.url === event.url)
913
+ continue;
914
+ const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
915
+ await Bun.write(path, Bun.file(before.path));
916
+ const frame = {
917
+ path,
918
+ elapsedMs: event.elapsedMs,
919
+ targetId: event.targetId,
920
+ url: event.url,
921
+ newTab: this.#newTabNoticeWindows.some((window) => event.elapsedMs >= window.startMs && event.elapsedMs < window.endMs),
922
+ noticeUrl: before.noticeUrl
923
+ };
924
+ this.#recordingFrames.push(frame);
925
+ this.#recordingFrames.sort((a, b) => a.elapsedMs - b.elapsedMs);
926
+ }
927
+ for (const { endMs } of this.#newTabNoticeWindows) {
928
+ if (this.#interactionPauses > 0)
929
+ continue;
930
+ let index = this.#recordingFrames.length - 1;
931
+ while (index >= 0 && this.#recordingFrames[index].elapsedMs >= endMs)
932
+ index--;
933
+ const before = this.#recordingFrames[index];
934
+ if (!before?.newTab || this.#recordingFrames[index + 1]?.elapsedMs === endMs)
935
+ continue;
936
+ const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
937
+ await Bun.write(path, Bun.file(before.path));
938
+ this.#recordingFrames.splice(index + 1, 0, {
939
+ path,
940
+ elapsedMs: endMs,
941
+ targetId: before.targetId,
942
+ url: before.url,
943
+ newTab: false
944
+ });
945
+ }
946
+ for (const frame of this.#recordingFrames) {
947
+ const jpeg = Buffer.from(await Bun.file(frame.path).arrayBuffer()).toString("base64");
948
+ await Bun.write(frame.path, Buffer.from(await this.composeFrame(jpeg, frame.url, frame.newTab ? frame.noticeUrl ?? frame.url : null), "base64"));
949
+ }
765
950
  await mkdir(dirname(this.#recordingPath), { recursive: true });
766
- const finishedAt = performance.now() - this.#recordingStartedAt;
951
+ const finishedAt = Math.max(stoppedAt, this.#recordingFrames.at(-1).elapsedMs + 40);
767
952
  const quoted = (path) => path.replaceAll("'", "'\\''");
768
953
  const lines = ["ffconcat version 1.0"];
769
954
  for (let index = 0;index < this.#recordingFrames.length; index++) {
@@ -785,16 +970,15 @@ class Browser {
785
970
  if (!this.#screenshotPath)
786
971
  return;
787
972
  await mkdir(dirname(this.#screenshotPath), { recursive: true });
788
- if (this.#recordingDirectory) {
789
- await this.#recordingWrites;
790
- const finalFrame = this.#recordingFrames.at(-1);
791
- if (finalFrame) {
792
- await Bun.write(this.#screenshotPath, Bun.file(finalFrame.path));
793
- return;
794
- }
795
- }
796
973
  const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 90 });
797
- await Bun.write(this.#screenshotPath, Buffer.from(capture.data, "base64"));
974
+ const url = await this.evaluate("location.href") ?? this.#activeUrl;
975
+ await Bun.write(this.#screenshotPath, Buffer.from(await this.composeFrame(capture.data, url, this.#hadNewTab ? url : null), "base64"));
976
+ }
977
+ async composeFrame(jpeg, url, newTabUrl) {
978
+ const composed = await this.evaluate(composeFrameExpression(jpeg, url, newTabUrl), true);
979
+ if (!composed)
980
+ throw new Error("Could not compose browser recording frame");
981
+ return composed;
798
982
  }
799
983
  async animateCursor(x, y, click = false) {
800
984
  if (!this.#recordingPath)
@@ -941,13 +1125,25 @@ class Browser {
941
1125
  const attached = await this.#cdp.command("Target.attachToTarget", { targetId: target.id, flatten: true });
942
1126
  try {
943
1127
  await this.#cdp.command("Emulation.setDeviceMetricsOverride", {
944
- width: 1120,
945
- height: 780,
1128
+ width: PAGE_WIDTH,
1129
+ height: PAGE_HEIGHT,
946
1130
  deviceScaleFactor: 1,
947
1131
  mobile: false
948
1132
  }, attached.sessionId);
949
1133
  } finally {
950
1134
  if (this.#switchOnPopup) {
1135
+ const noticeStart = await this.recordNewTabNotice(target.url);
1136
+ const noticeStartAt = noticeStart === undefined ? performance.now() : this.#recordingStartedAt + noticeStart;
1137
+ this.#newTabNoticeUrl = target.url;
1138
+ this.#newTabNoticeUntil = this.#interactionPauses > 0 ? Infinity : 0;
1139
+ const noticeWindow = noticeStart === undefined ? undefined : {
1140
+ startMs: noticeStart,
1141
+ endMs: noticeStart + Math.max(40, this.#interactionPauses)
1142
+ };
1143
+ if (noticeWindow)
1144
+ this.#newTabNoticeWindows.push(noticeWindow);
1145
+ if (this.#interactionPauses > 0)
1146
+ await Bun.sleep(Math.max(0, noticeStartAt + this.#interactionPauses - performance.now()));
951
1147
  if (this.#recordingPath) {
952
1148
  await this.call("Page.stopScreencast").catch(() => {
953
1149
  return;
@@ -956,13 +1152,20 @@ class Browser {
956
1152
  this.#stopRecordingEvents = undefined;
957
1153
  await this.#recordingWrites;
958
1154
  }
1155
+ if (noticeWindow && this.#interactionPauses > 0)
1156
+ noticeWindow.endMs = performance.now() - this.#recordingStartedAt;
1157
+ this.#newTabNoticeUrl = null;
1158
+ this.#newTabNoticeUntil = 0;
959
1159
  this.#sessionId = attached.sessionId;
960
1160
  this.#targetId = target.id;
1161
+ this.#activeUrl = target.url;
961
1162
  this.#loadingFrames.clear();
962
1163
  await this.watchConsole();
963
1164
  await this.watchPageLoads();
964
1165
  this.#switchOnPopup = false;
1166
+ this.#hadNewTab = true;
965
1167
  await this.startRecording();
1168
+ this.#pauseUntil = performance.now();
966
1169
  } else {
967
1170
  await this.#cdp.command("Target.detachFromTarget", { sessionId: attached.sessionId });
968
1171
  }
@@ -1140,7 +1343,7 @@ ${child.text}`.slice(0, 6000);
1140
1343
  const page = { ...info, frames, transitions: [...this.#transitions], fingerprint: fingerprint(info) };
1141
1344
  if (screenshot) {
1142
1345
  const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 72 });
1143
- page.screenshot = capture.data;
1346
+ page.screenshot = await this.composeFrame(capture.data, info.url, performance.now() < this.#newTabNoticeUntil ? this.#newTabNoticeUrl : null);
1144
1347
  }
1145
1348
  return page;
1146
1349
  }
@@ -2045,7 +2248,7 @@ Known field values:
2045
2248
  Evidence and output:
2046
2249
  --recording <file.mp4> Record the complete goal with an animated cursor.
2047
2250
  Requires FFmpeg.
2048
- --screenshot <file.jpg> Save the final browser viewport.
2251
+ --screenshot <file.jpg> Save a 1280x720 image with the URL bar.
2049
2252
  --final-state Add the final semantic page state to stdout JSON.
2050
2253
  -h, --help Show this help and exit.
2051
2254
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-cdp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
5
5
  "type": "module",
6
6
  "license": "MIT",