autokap 1.5.3 → 1.5.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.
@@ -113,6 +113,14 @@ export function buildCursorOverlayScript(theme = 'minimal') {
113
113
  triggerPulse();
114
114
  };
115
115
 
116
+ // AUT-80 — Browser-side mousedown timestamp buffer. Real CDP-dispatched
117
+ // \`mousedown\` events fire here at the exact moment the click happens in
118
+ // the recorded video (the cursor pulse is purely decorative and fires
119
+ // slightly later). The runner reads this buffer after each click action
120
+ // and uses the timestamps for the mouse SFX track so audio = visual
121
+ // click, even across cursor animation latency and frame quantisation.
122
+ window.__akClickAt = [];
123
+
116
124
  // Keep DOM event listeners as fallback for real mouse events (headed mode)
117
125
  document.addEventListener('mousemove', function(e) {
118
126
  setCursorPosition(e.clientX, e.clientY);
@@ -122,6 +130,7 @@ export function buildCursorOverlayScript(theme = 'minimal') {
122
130
  setCursorPosition(e.clientX, e.clientY);
123
131
  cursor.classList.add('__ak_pressed');
124
132
  triggerPulse();
133
+ window.__akClickAt.push(Date.now());
125
134
  }, true);
126
135
  window.addEventListener('mouseup', function(e) {
127
136
  setCursorPosition(e.clientX, e.clientY);
@@ -130,6 +139,14 @@ export function buildCursorOverlayScript(theme = 'minimal') {
130
139
  window.addEventListener('click', function(e) {
131
140
  setCursorPosition(e.clientX, e.clientY);
132
141
  triggerPulse();
142
+ // Capture synthetic click() dispatches that bypass mousedown (e.g.
143
+ // dispatchEvent('click') from JS-dispatch opcode paths). Skip if a
144
+ // mousedown landed in the last 80 ms so we don't double-count a
145
+ // regular mouse-driven click.
146
+ var last = window.__akClickAt[window.__akClickAt.length - 1];
147
+ if (last == null || (Date.now() - last) > 80) {
148
+ window.__akClickAt.push(Date.now());
149
+ }
133
150
  }, true);
134
151
  }
135
152
 
@@ -165,4 +165,18 @@ export declare class WebPlaywrightLocal implements RuntimeAdapter {
165
165
  private relativeClickPosition;
166
166
  private moveClipCursorToPoint;
167
167
  private emitClipClickPulse;
168
+ /**
169
+ * Drain the browser-side `__akClickAt` buffer for timestamps newer than
170
+ * `sinceMs`, replay them through `onClick`, and reset the buffer so the
171
+ * next click action starts fresh. This is what makes mouse SFX line up
172
+ * exactly with the visual click in the recorded video — the mousedown
173
+ * listener inside the cursor overlay timestamps each click at the same
174
+ * instant the browser dispatches it, which is also the frame the CDP
175
+ * screencast captures.
176
+ *
177
+ * Falls back to `sinceMs` (Node wall-clock at action dispatch) when the
178
+ * buffer is empty (e.g. `useKeyboard` Enter-press path, or transient
179
+ * page.evaluate failure).
180
+ */
181
+ private reportClickSfxTimestamps;
168
182
  }
@@ -77,12 +77,12 @@ export class WebPlaywrightLocal {
77
77
  const page = await this.browser.currentPage;
78
78
  const t0 = Date.now();
79
79
  logger.debug(`[click] start selector="${selector}"${options?.useKeyboard ? ' mode=keyboard' : ''}${options?.useJsDispatch ? ' mode=js_dispatch' : ''}${options?.coordinates ? ` mode=coords(${options.coordinates.x},${options.coordinates.y})` : ''}`);
80
- const fireClickSfx = () => options?.onClick?.(Date.now());
81
80
  try {
82
81
  if (options?.coordinates) {
83
82
  await this.moveClipCursorToPoint(options.coordinates);
84
- fireClickSfx();
83
+ const dispatchedAt = Date.now();
85
84
  await this.browser.clickByCoordinates(options.coordinates.x, options.coordinates.y);
85
+ await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
86
86
  logger.debug(`[click] done coords took ${Date.now() - t0}ms`);
87
87
  return;
88
88
  }
@@ -90,14 +90,17 @@ export class WebPlaywrightLocal {
90
90
  const animatedTarget = await this.moveClipCursorToLocator(locator);
91
91
  if (options?.useKeyboard) {
92
92
  await locator.focus();
93
- fireClickSfx();
93
+ const dispatchedAt = Date.now();
94
94
  await page.keyboard.press('Enter');
95
+ // No real mousedown fires on Enter — fall back to Node timing.
96
+ options?.onClick?.(dispatchedAt);
95
97
  logger.debug(`[click] done keyboard took ${Date.now() - t0}ms`);
96
98
  return;
97
99
  }
98
100
  if (options?.useJsDispatch) {
99
- fireClickSfx();
101
+ const dispatchedAt = Date.now();
100
102
  await locator.dispatchEvent('click');
103
+ await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
101
104
  logger.debug(`[click] done js_dispatch took ${Date.now() - t0}ms`);
102
105
  return;
103
106
  }
@@ -107,18 +110,19 @@ export class WebPlaywrightLocal {
107
110
  ? await this.relativeClickPosition(locator, animatedTarget)
108
111
  : null;
109
112
  if (options?.button && options.button !== 'left') {
110
- fireClickSfx();
113
+ const dispatchedAt = Date.now();
111
114
  await locator.click({
112
115
  button: options.button,
113
116
  timeout: 5000,
114
117
  force: options?.force,
115
118
  ...(clickPosition ? { position: clickPosition } : {}),
116
119
  });
120
+ await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
117
121
  logger.debug(`[click] done button=${options.button} took ${Date.now() - t0}ms`);
118
122
  return;
119
123
  }
124
+ const dispatchedAt = Date.now();
120
125
  if (clickPosition) {
121
- fireClickSfx();
122
126
  await locator.click({
123
127
  timeout: 5000,
124
128
  force: options?.force,
@@ -126,9 +130,9 @@ export class WebPlaywrightLocal {
126
130
  });
127
131
  }
128
132
  else {
129
- fireClickSfx();
130
133
  await this.browser.clickBySelector(selector, { force: options?.force });
131
134
  }
135
+ await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
132
136
  await this.emitClipClickPulse();
133
137
  logger.debug(`[click] done normal took ${Date.now() - t0}ms`);
134
138
  }
@@ -151,11 +155,12 @@ export class WebPlaywrightLocal {
151
155
  const position = target
152
156
  ? await this.relativeClickPosition(resolved.locator, target)
153
157
  : null;
154
- opts.onClick?.(Date.now());
158
+ const dispatchedAt = Date.now();
155
159
  await resolved.locator.click({
156
160
  timeout: 5000,
157
161
  ...(position ? { position } : {}),
158
162
  });
163
+ await this.reportClickSfxTimestamps(page, dispatchedAt, opts.onClick);
159
164
  }
160
165
  /**
161
166
  * Type into an element using semantic target resolution.
@@ -649,13 +654,14 @@ export class WebPlaywrightLocal {
649
654
  ? await this.relativeClickPosition(locator, target)
650
655
  : null;
651
656
  const opts = { timeout: 5000, ...(position ? { position } : {}) };
652
- actionOpts?.onClick?.(Date.now());
657
+ const dispatchedAt = Date.now();
653
658
  if (checked) {
654
659
  await locator.check(opts);
655
660
  }
656
661
  else {
657
662
  await locator.uncheck(opts);
658
663
  }
664
+ await this.reportClickSfxTimestamps(page, dispatchedAt, actionOpts?.onClick);
659
665
  }
660
666
  async doubleClick(selector, actionOpts) {
661
667
  const page = await this.browser.currentPage;
@@ -664,11 +670,12 @@ export class WebPlaywrightLocal {
664
670
  const position = target
665
671
  ? await this.relativeClickPosition(locator, target)
666
672
  : null;
667
- actionOpts?.onClick?.(Date.now());
673
+ const dispatchedAt = Date.now();
668
674
  await locator.dblclick({
669
675
  timeout: 5000,
670
676
  ...(position ? { position } : {}),
671
677
  });
678
+ await this.reportClickSfxTimestamps(page, dispatchedAt, actionOpts?.onClick);
672
679
  }
673
680
  async drag(opts) {
674
681
  const page = await this.browser.currentPage;
@@ -1021,6 +1028,47 @@ export class WebPlaywrightLocal {
1021
1028
  window.__akClickPulse(px, py);
1022
1029
  }, { px: Math.round(x), py: Math.round(y) }).catch(() => { });
1023
1030
  }
1031
+ /**
1032
+ * Drain the browser-side `__akClickAt` buffer for timestamps newer than
1033
+ * `sinceMs`, replay them through `onClick`, and reset the buffer so the
1034
+ * next click action starts fresh. This is what makes mouse SFX line up
1035
+ * exactly with the visual click in the recorded video — the mousedown
1036
+ * listener inside the cursor overlay timestamps each click at the same
1037
+ * instant the browser dispatches it, which is also the frame the CDP
1038
+ * screencast captures.
1039
+ *
1040
+ * Falls back to `sinceMs` (Node wall-clock at action dispatch) when the
1041
+ * buffer is empty (e.g. `useKeyboard` Enter-press path, or transient
1042
+ * page.evaluate failure).
1043
+ */
1044
+ async reportClickSfxTimestamps(page, sinceMs, onClick) {
1045
+ if (!onClick)
1046
+ return;
1047
+ let captured = [];
1048
+ try {
1049
+ captured = (await page.evaluate((cutoff) => {
1050
+ const buf = window.__akClickAt;
1051
+ if (!Array.isArray(buf))
1052
+ return [];
1053
+ const fresh = buf.filter((t) => typeof t === 'number' && t >= cutoff);
1054
+ // Reset the buffer so successive click actions don't see each
1055
+ // others' timestamps. We mutate the array in place to keep the
1056
+ // reference stable for any other consumers (none today).
1057
+ buf.length = 0;
1058
+ return fresh;
1059
+ }, sinceMs));
1060
+ }
1061
+ catch {
1062
+ captured = [];
1063
+ }
1064
+ if (captured.length > 0) {
1065
+ for (const t of captured)
1066
+ onClick(t);
1067
+ }
1068
+ else {
1069
+ onClick(sinceMs);
1070
+ }
1071
+ }
1024
1072
  }
1025
1073
  function describeResolveOptions(opts) {
1026
1074
  const parts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",