agex 0.2.8 → 0.2.10

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.
package/README.md CHANGED
@@ -75,6 +75,36 @@ agex prove "youtube homepage shows at least 6 video thumbnails with titles and v
75
75
 
76
76
  ---
77
77
 
78
+ ### `agex demo` - Record demo videos
79
+
80
+ Record a narrated browser walkthrough as a video. The agent opens a URL, performs the task, and captures the session.
81
+
82
+ ```bash
83
+ # Record a demo of selecting a brand design from a menu
84
+ agex demo "how to select a brand design from the menu" --url https://example.com
85
+
86
+ # With subtitles and custom colors (via task text)
87
+ agex demo "how to create a new project. use subtitles with white text on red background" \
88
+ --url https://app.example.com --agent cursor
89
+
90
+ # Non-headless for debugging
91
+ agex demo "sign up for a free trial" --url https://example.com --no-headless
92
+ ```
93
+
94
+ | Flag | Description | Default |
95
+ |------|-------------|---------|
96
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | auto-detected |
97
+ | `--url, -u` | URL to navigate to | - |
98
+ | `--output, -o` | Output directory | `./demo-results` |
99
+ | `--video` | Enable video recording | `true` |
100
+ | `--screenshots` | Enable screenshots | `true` |
101
+ | `--model, -m` | Model to use | - |
102
+ | `--timeout, -t` | Timeout in ms | `300000` |
103
+ | `--viewport` | Viewport size (WxH) | `1920x1080` |
104
+ | `--headless` | Run headless | `true` |
105
+
106
+ ---
107
+
78
108
  ### `agex prove-pr` - PR verification in CI
79
109
 
80
110
  Run it in your CI. It reads the diff, thinks, acts, and reports autonomously.
@@ -3,6 +3,19 @@
3
3
  let clickRippleEnabled = true;
4
4
  let trailEnabled = false;
5
5
  let trailPoints = [];
6
+ let currentMode = 'default'; // 'default' or 'pointer'
7
+ let animationLock = false; // when true, mousemove won't update demo cursor position
8
+
9
+ const ARROW_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
10
+ <path d="M8.5 3L8.5 24.2l5-5.6 3.6 7.7 3.5-1.5-3.7-7.6 7.1-.5L8.5 3z" fill="#000" stroke="#fff" stroke-width="1.8" stroke-linejoin="round"></path>
11
+ </svg>`;
12
+
13
+ const POINTER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
14
+ <g transform="rotate(-20, 16, 16)">
15
+ <path d="M14 2 C14 2 11.5 2 11.5 4.5 L11.5 15 L10 13.5 C10 13.5 8.5 12 7 12.5 C5.5 13 5.8 14.5 6.5 15.8 L10.5 22.5 C12 25.5 14.5 27.5 18 27.5 L19.5 27.5 C23.5 27.5 26.5 24 26.5 20 L26.5 15.5 C26.5 14 25 13 23.5 13.5 C23 13.7 22.5 14.2 22.5 15 L22.5 14 C22.5 12.5 21 11.5 19.5 12 C19 12.2 18.5 12.7 18.5 13.5 L18.5 12.5 C18.5 11 17 10 15.5 10.5 C15 10.7 14.5 11.3 14.5 12 L14.5 4.5 C14.5 4.5 14.5 2 14 2 Z" fill="#000" stroke="#fff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
16
+ <line x1="18.5" y1="13" x2="18.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/><line x1="22.5" y1="14.5" x2="22.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/>
17
+ </g>
18
+ </svg>`;
6
19
 
7
20
  function createCursor() {
8
21
  if (document.getElementById('agex-cursor')) return;
@@ -10,21 +23,36 @@
10
23
  const cursor = document.createElement('div');
11
24
  cursor.id = 'agex-cursor';
12
25
  cursor.style.cssText = `
13
- width: 24px;
14
- height: 24px;
15
- background: rgba(255, 50, 50, 0.85);
16
- border: 2px solid white;
17
- border-radius: 50%;
18
26
  position: fixed;
19
27
  pointer-events: none;
20
28
  z-index: 2147483647;
21
- transform: translate(-50%, -50%);
22
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
23
29
  left: -100px;
24
30
  top: -100px;
25
31
  opacity: 0;
26
- transition: left 0.08s ease-out, top 0.08s ease-out, opacity 0.15s, transform 0.1s, background 0.1s;
32
+ transition: opacity 0.15s, transform 0.1s ease-out;
33
+ transform-origin: 2px 2px;
34
+ will-change: left, top;
35
+ `;
36
+
37
+ const dot = document.createElement('div');
38
+ dot.className = 'agex-cursor-dot';
39
+ dot.style.cssText = `
40
+ position: absolute;
41
+ width: 38px;
42
+ height: 38px;
43
+ border-radius: 50%;
44
+ background: rgb(251, 255, 0, 0.45);
45
+ top: -16px;
46
+ left: -10px;
47
+ pointer-events: none;
27
48
  `;
49
+
50
+ const svg = document.createElement('div');
51
+ svg.className = 'agex-cursor-svg';
52
+ svg.innerHTML = ARROW_SVG;
53
+
54
+ cursor.appendChild(dot);
55
+ cursor.appendChild(svg);
28
56
  document.body.appendChild(cursor);
29
57
 
30
58
  const style = document.createElement('style');
@@ -33,20 +61,28 @@
33
61
  .agex-cursor-ripple {
34
62
  position: fixed;
35
63
  border-radius: 50%;
36
- background: rgba(255, 50, 50, 0.4);
37
64
  pointer-events: none;
38
65
  z-index: 2147483646;
39
66
  transform: translate(-50%, -50%) scale(0);
40
- animation: agex-ripple 0.4s ease-out forwards;
67
+ animation: agex-ripple 0.5s ease-out forwards;
68
+ }
69
+ .agex-cursor-ripple-ring {
70
+ position: fixed;
71
+ border-radius: 50%;
72
+ border: 3px solid rgba(0, 0, 0, 0.4);
73
+ pointer-events: none;
74
+ z-index: 2147483646;
75
+ transform: translate(-50%, -50%) scale(0);
76
+ animation: agex-ripple 0.6s ease-out forwards;
41
77
  }
42
78
  @keyframes agex-ripple {
43
79
  to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
44
80
  }
45
81
  .agex-cursor-trail {
46
82
  position: fixed;
47
- width: 8px;
48
- height: 8px;
49
- background: rgba(255, 50, 50, 0.3);
83
+ width: 6px;
84
+ height: 6px;
85
+ background: rgba(0, 0, 0, 0.12);
50
86
  border-radius: 50%;
51
87
  pointer-events: none;
52
88
  z-index: 2147483645;
@@ -55,16 +91,29 @@
55
91
  `;
56
92
  document.head.appendChild(style);
57
93
 
94
+ const CLICKABLE = 'a, button, [role="button"], input[type="submit"], input[type="button"], select, label[for], [onclick], [data-action], summary, [tabindex]';
95
+
58
96
  document.addEventListener('mousemove', (e) => {
59
97
  const c = document.getElementById('agex-cursor');
60
98
  if (c) {
61
- c.style.left = `${e.clientX}px`;
62
- c.style.top = `${e.clientY}px`;
63
- if (!hasMoved) {
64
- hasMoved = true;
65
- c.style.opacity = '1';
99
+ if (!animationLock) {
100
+ c.style.left = `${e.clientX}px`;
101
+ c.style.top = `${e.clientY}px`;
102
+ if (!hasMoved) {
103
+ hasMoved = true;
104
+ c.style.opacity = '1';
105
+ }
106
+ }
107
+
108
+ const hovered = document.elementFromPoint(e.clientX, e.clientY);
109
+ const isClickable = hovered && hovered.closest(CLICKABLE);
110
+ const newMode = isClickable ? 'pointer' : 'default';
111
+ if (newMode !== currentMode) {
112
+ currentMode = newMode;
113
+ const svgEl = c.querySelector('.agex-cursor-svg');
114
+ if (svgEl) svgEl.innerHTML = currentMode === 'pointer' ? POINTER_SVG : ARROW_SVG;
66
115
  }
67
-
116
+
68
117
  if (trailEnabled) {
69
118
  createTrailPoint(e.clientX, e.clientY);
70
119
  }
@@ -74,9 +123,8 @@
74
123
  document.addEventListener('mousedown', (e) => {
75
124
  const c = document.getElementById('agex-cursor');
76
125
  if (c) {
77
- c.style.transform = 'translate(-50%, -50%) scale(0.8)';
78
- c.style.background = 'rgba(255, 200, 50, 1)';
79
-
126
+ c.style.transform = 'scale(0.85)';
127
+
80
128
  if (clickRippleEnabled) {
81
129
  createClickRipple(e.clientX, e.clientY);
82
130
  }
@@ -86,21 +134,30 @@
86
134
  document.addEventListener('mouseup', () => {
87
135
  const c = document.getElementById('agex-cursor');
88
136
  if (c) {
89
- c.style.transform = 'translate(-50%, -50%) scale(1)';
90
- c.style.background = 'rgba(255, 50, 50, 0.85)';
137
+ c.style.transform = 'scale(1)';
91
138
  }
92
139
  }, true);
93
140
  }
94
141
 
95
142
  function createClickRipple(x, y) {
96
- const ripple = document.createElement('div');
97
- ripple.className = 'agex-cursor-ripple';
98
- ripple.style.left = `${x}px`;
99
- ripple.style.top = `${y}px`;
100
- ripple.style.width = '60px';
101
- ripple.style.height = '60px';
102
- document.body.appendChild(ripple);
103
- setTimeout(() => ripple.remove(), 400);
143
+ const fill = document.createElement('div');
144
+ fill.className = 'agex-cursor-ripple';
145
+ fill.style.left = `${x}px`;
146
+ fill.style.top = `${y}px`;
147
+ fill.style.width = '50px';
148
+ fill.style.height = '50px';
149
+ fill.style.background = 'rgba(251, 255, 0, 0.35)';
150
+ document.body.appendChild(fill);
151
+ setTimeout(() => fill.remove(), 500);
152
+
153
+ const ring = document.createElement('div');
154
+ ring.className = 'agex-cursor-ripple-ring';
155
+ ring.style.left = `${x}px`;
156
+ ring.style.top = `${y}px`;
157
+ ring.style.width = '70px';
158
+ ring.style.height = '70px';
159
+ document.body.appendChild(ring);
160
+ setTimeout(() => ring.remove(), 600);
104
161
  }
105
162
 
106
163
  function createTrailPoint(x, y) {
@@ -122,15 +179,16 @@
122
179
  }, 400);
123
180
  }
124
181
 
125
- // Expose cursor configuration API
126
182
  window.__agexCursor = {
183
+ setAnimationLock(locked) {
184
+ animationLock = locked;
185
+ },
127
186
  setSize(size) {
128
187
  const cursor = document.getElementById('agex-cursor');
129
188
  if (!cursor) return;
130
- const sizes = { normal: 16, large: 24, 'extra-large': 32 };
131
- const s = sizes[size] || sizes.large;
132
- cursor.style.width = `${s}px`;
133
- cursor.style.height = `${s}px`;
189
+ const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
190
+ const s = scales[size] || scales.large;
191
+ cursor.style.transform = `scale(${s})`;
134
192
  },
135
193
  setClickRipple(enabled) {
136
194
  clickRippleEnabled = enabled;
@@ -143,6 +201,26 @@
143
201
  if (options.clickRipple !== undefined) this.setClickRipple(options.clickRipple);
144
202
  if (options.trail !== undefined) this.setTrail(options.trail);
145
203
  },
204
+ setMode(mode) {
205
+ const cursor = document.getElementById('agex-cursor');
206
+ if (!cursor) return;
207
+ const svgEl = cursor.querySelector('.agex-cursor-svg');
208
+ if (!svgEl) return;
209
+ if (mode === 'pointer') {
210
+ currentMode = 'pointer';
211
+ svgEl.innerHTML = POINTER_SVG;
212
+ } else {
213
+ currentMode = 'default';
214
+ svgEl.innerHTML = ARROW_SVG;
215
+ }
216
+ },
217
+ showClickRipple() {
218
+ const cursor = document.getElementById('agex-cursor');
219
+ if (!cursor) return;
220
+ const x = parseFloat(cursor.style.left) || 0;
221
+ const y = parseFloat(cursor.style.top) || 0;
222
+ createClickRipple(x, y);
223
+ },
146
224
  sync() {
147
225
  const cursor = document.getElementById('agex-cursor');
148
226
  if (!cursor) return Promise.resolve('no cursor');
@@ -95,6 +95,18 @@
95
95
  0%, 100% { box-shadow: 0 0 20px var(--color, #ff4444), inset 0 0 20px var(--color, #ff4444); }
96
96
  50% { box-shadow: 0 0 35px var(--color, #ff4444), inset 0 0 35px var(--color, #ff4444); }
97
97
  }
98
+ .${EFFECTS_ID}-input-focus {
99
+ position: absolute;
100
+ pointer-events: none;
101
+ z-index: 2147483639;
102
+ border-radius: 6px;
103
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3);
104
+ animation: ${EFFECTS_ID}-input-focus-pulse 1.5s ease-in-out infinite;
105
+ }
106
+ @keyframes ${EFFECTS_ID}-input-focus-pulse {
107
+ 0%, 100% { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3); }
108
+ 50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.7), 0 0 24px rgba(59, 130, 246, 0.5); }
109
+ }
98
110
  .${EFFECTS_ID}-highlight.pulse {
99
111
  animation: ${EFFECTS_ID}-pulse 1s ease-in-out infinite;
100
112
  border: 3px solid var(--color, #ff4444);
@@ -150,17 +162,22 @@
150
162
  font-family: system-ui, sans-serif;
151
163
  transition: all 0.3s ease;
152
164
  }
165
+ @keyframes ${EFFECTS_ID}-subtitle-slide-down {
166
+ from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
167
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
168
+ }
153
169
  .${EFFECTS_ID}-subtitle {
154
170
  position: fixed;
155
171
  left: 50%;
156
172
  transform: translateX(-50%);
157
- background: rgba(0, 0, 0, 0.8);
158
- color: white;
173
+ background: var(--subtitle-bg, rgba(0, 0, 0, 0.8));
174
+ color: var(--subtitle-fg, white);
159
175
  padding: 12px 24px;
160
176
  border-radius: 8px;
161
177
  font-size: 18px;
162
178
  max-width: 80%;
163
179
  text-align: center;
180
+ animation: ${EFFECTS_ID}-subtitle-slide-down 0.35s ease-out;
164
181
  }
165
182
  .${EFFECTS_ID}-drawing-canvas {
166
183
  position: fixed;
@@ -182,6 +199,7 @@
182
199
  // HIGHLIGHT
183
200
  highlight: {
184
201
  add(selector, { color = '#ff4444', style = 'border', duration } = {}) {
202
+ injectStyles();
185
203
  const el = utils.getElement(selector);
186
204
  if (!el) return false;
187
205
 
@@ -213,6 +231,7 @@
213
231
  // ANNOTATION
214
232
  annotation: {
215
233
  add(selector, { text, position = 'top', color = '#333', arrow = true } = {}) {
234
+ injectStyles();
216
235
  const el = utils.getElement(selector);
217
236
  if (!el) return false;
218
237
 
@@ -247,6 +266,7 @@
247
266
  // SPOTLIGHT
248
267
  spotlight: {
249
268
  show(selector, { opacity = 0.7, label = '' } = {}) {
269
+ injectStyles();
250
270
  const el = utils.getElement(selector);
251
271
  if (!el) return false;
252
272
 
@@ -355,10 +375,9 @@
355
375
  const cursor = document.getElementById('agex-cursor');
356
376
  if (!cursor) return;
357
377
 
358
- const sizes = { normal: 16, large: 24, 'extra-large': 32 };
359
- const s = sizes[size] || 24;
360
- cursor.style.width = `${s}px`;
361
- cursor.style.height = `${s}px`;
378
+ const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
379
+ const s = scales[size] || 1;
380
+ cursor.style.transform = `scale(${s})`;
362
381
 
363
382
  cursor.dataset.clickRipple = clickRipple;
364
383
  cursor.dataset.trail = trail;
@@ -390,7 +409,7 @@
390
409
  return true;
391
410
  },
392
411
 
393
- showClick(x, y, color = '#ff4444') {
412
+ showClick(x, y, color = 'rgba(0, 0, 0, 0.15)') {
394
413
  const ripple = utils.createElement('div', {
395
414
  position: 'fixed',
396
415
  left: `${x}px`,
@@ -457,6 +476,117 @@
457
476
  await this.moveTo(points[i + 1].x, points[i + 1].y, segmentDuration);
458
477
  }
459
478
  return true;
479
+ },
480
+
481
+ _lastPos: null,
482
+
483
+ animateToPosition(targetX, targetY) {
484
+ const c = document.getElementById('agex-cursor');
485
+ if (!c) return 0;
486
+
487
+ // Lock demo cursor so native mousemove won't fight the animation
488
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(true);
489
+
490
+ const vh = window.innerHeight;
491
+ const wasHidden = c.style.opacity === '0' || c.style.opacity === '' || c.style.left === '-100px';
492
+ const startX = wasHidden ? (this._lastPos ? this._lastPos.x : 0) : parseFloat(c.style.left) || 0;
493
+ const startY = wasHidden ? (this._lastPos ? this._lastPos.y : vh / 2) : parseFloat(c.style.top) || vh / 2;
494
+
495
+ c.style.transition = 'none';
496
+ c.style.left = startX + 'px';
497
+ c.style.top = startY + 'px';
498
+ c.style.opacity = '1';
499
+ c.offsetHeight;
500
+
501
+ if (isNaN(targetX) || isNaN(targetY)) {
502
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
503
+ return 0;
504
+ }
505
+
506
+ this._lastPos = { x: targetX, y: targetY };
507
+
508
+ const dist = Math.sqrt((targetX - startX) ** 2 + (targetY - startY) ** 2);
509
+ const duration = Math.max(1200, Math.round(dist / 0.3));
510
+ const start = performance.now();
511
+ const self = this;
512
+
513
+ const step = (now) => {
514
+ const elapsed = now - start;
515
+ const t = Math.min(elapsed / duration, 1);
516
+ const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
517
+ c.style.left = (startX + (targetX - startX) * ease) + 'px';
518
+ c.style.top = (startY + (targetY - startY) * ease) + 'px';
519
+ if (t < 1) {
520
+ requestAnimationFrame(step);
521
+ } else {
522
+ // Animation done — release lock
523
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
524
+ }
525
+ };
526
+ requestAnimationFrame(step);
527
+ return duration;
528
+ },
529
+
530
+ showInputFocus(selector) {
531
+ this.clearInputFocus();
532
+ var el = utils.getElement(selector);
533
+ if (!el) return;
534
+ var rect = el.getBoundingClientRect();
535
+ var overlay = document.createElement('div');
536
+ overlay.id = EFFECTS_ID + '-input-focus';
537
+ overlay.className = EFFECTS_ID + '-input-focus';
538
+ overlay.style.left = (rect.left + window.scrollX - 2) + 'px';
539
+ overlay.style.top = (rect.top + window.scrollY - 2) + 'px';
540
+ overlay.style.width = (rect.width + 4) + 'px';
541
+ overlay.style.height = (rect.height + 4) + 'px';
542
+ document.body.appendChild(overlay);
543
+ },
544
+
545
+ clearInputFocus() {
546
+ var el = document.getElementById(EFFECTS_ID + '-input-focus');
547
+ if (el) el.remove();
548
+ },
549
+
550
+ typeText(selector, text, charDelay) {
551
+ charDelay = charDelay || 80;
552
+ var el = utils.getElement(selector);
553
+ if (!el) return 0;
554
+ el.focus();
555
+ var i = 0;
556
+ var len = text.length;
557
+ function typeNext() {
558
+ if (i >= len) return;
559
+ var ch = text[i];
560
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true }));
561
+ el.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true }));
562
+ // For contenteditable elements, insert text differently
563
+ if (el.isContentEditable) {
564
+ document.execCommand('insertText', false, ch);
565
+ } else {
566
+ // Use InputEvent for proper React/framework compatibility
567
+ var nativeInputValueSetter = Object.getOwnPropertyDescriptor(
568
+ Object.getPrototypeOf(el), 'value'
569
+ );
570
+ if (nativeInputValueSetter && nativeInputValueSetter.set) {
571
+ nativeInputValueSetter.set.call(el, el.value + ch);
572
+ } else {
573
+ el.value = el.value + ch;
574
+ }
575
+ el.dispatchEvent(new Event('input', { bubbles: true }));
576
+ }
577
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true }));
578
+ i++;
579
+ if (i < len) setTimeout(typeNext, charDelay);
580
+ }
581
+ typeNext();
582
+ return len * charDelay;
583
+ },
584
+
585
+ fadeOut() {
586
+ const c = document.getElementById('agex-cursor');
587
+ if (!c) return;
588
+ c.style.transition = 'opacity 0.4s ease-out';
589
+ c.style.opacity = '0';
460
590
  }
461
591
  },
462
592
 
@@ -1289,12 +1419,15 @@
1289
1419
  });
1290
1420
  },
1291
1421
 
1292
- showSubtitle(text, duration = 3000, position = 'bottom') {
1422
+ showSubtitle(text, duration = 3000, position = 'bottom', fg = '', bg = '') {
1423
+ injectStyles();
1293
1424
  this.clear();
1294
1425
  const subtitle = utils.createElement('div', {
1295
1426
  [position]: '40px'
1296
1427
  }, `${EFFECTS_ID}-subtitle`);
1297
1428
  subtitle.classList.add(`${EFFECTS_ID}-subtitle`);
1429
+ if (fg) subtitle.style.setProperty('--subtitle-fg', fg);
1430
+ if (bg) subtitle.style.setProperty('--subtitle-bg', bg);
1298
1431
  subtitle.textContent = text;
1299
1432
  document.body.appendChild(subtitle);
1300
1433
 
@@ -1305,6 +1438,7 @@
1305
1438
 
1306
1439
  clear() {
1307
1440
  utils.removeElements(`#${EFFECTS_ID}-subtitle`);
1441
+ utils.removeElements(`.${EFFECTS_ID}-subtitle`);
1308
1442
  if ('speechSynthesis' in window) {
1309
1443
  speechSynthesis.cancel();
1310
1444
  }
@@ -1499,6 +1633,14 @@
1499
1633
  if (el) el.scrollIntoView({ block: 'center', behavior: 'instant' });
1500
1634
  },
1501
1635
 
1636
+ scrollElementToCenter(selector) {
1637
+ var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
1638
+ if (!el) return false;
1639
+ // scrollIntoView with block:'center' handles nested scrollable containers
1640
+ el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
1641
+ return true;
1642
+ },
1643
+
1502
1644
  scrollToCoords(y, viewportHeight, elHeight) {
1503
1645
  window.scrollTo({
1504
1646
  top: window.scrollY + y - (viewportHeight / 2) + (elHeight / 2),
@@ -0,0 +1,85 @@
1
+ #!/bin/bash
2
+ # Smooth click: animates visual cursor to element, clicks, then fades cursor out
3
+ # 1. If cursor not visible, starts from left-center of viewport
4
+ # 2. Smoothly moves visual cursor to element's center
5
+ # 3. Performs the actual click via agent-browser
6
+ # 4. Fades the cursor out
7
+ # Usage: ab-click <selector|@ref>
8
+ SELECTOR="$1"
9
+ if [ -z "$SELECTOR" ]; then
10
+ echo "Usage: ab-click <selector|@ref>"
11
+ exit 1
12
+ fi
13
+
14
+ # Scroll element into center of its container before measuring position
15
+ if [[ "$SELECTOR" == @* ]]; then
16
+ agent-browser scrollintoview "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
17
+ else
18
+ ESCAPED_SELECTOR=$(printf '%s' "$SELECTOR" | sed "s/'/\\\\'/g")
19
+ agent-browser eval "window.__agexEffects.proof.scrollElementToCenter('${ESCAPED_SELECTOR}')" {{SESSION_ARG}} >/dev/null 2>&1
20
+ fi
21
+ agent-browser wait 300 {{SESSION_ARG}} >/dev/null 2>&1
22
+
23
+ # Get element bounding box without interacting with it
24
+ if [[ "$SELECTOR" == @* ]]; then
25
+ # For @ref: use agent-browser get styles --json which returns bounding box
26
+ STYLES=$(agent-browser get styles "$SELECTOR" --json {{SESSION_ARG}} 2>&1) || true
27
+ EL_X=$(echo "$STYLES" | grep -o '"x":[0-9.-]*' | head -1 | cut -d: -f2)
28
+ EL_Y=$(echo "$STYLES" | grep -o '"y":[0-9.-]*' | head -1 | cut -d: -f2)
29
+ EL_WIDTH=$(echo "$STYLES" | grep -o '"width":[0-9.-]*' | head -1 | cut -d: -f2)
30
+ EL_HEIGHT=$(echo "$STYLES" | grep -o '"height":[0-9.-]*' | head -1 | cut -d: -f2)
31
+
32
+ if [ -z "$EL_X" ] || [ -z "$EL_Y" ] || [ -z "$EL_WIDTH" ] || [ -z "$EL_HEIGHT" ]; then
33
+ # Last resort: just click without animation
34
+ agent-browser click "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
35
+ echo "clicked $SELECTOR (no animation - could not get position)"
36
+ exit 0
37
+ fi
38
+ else
39
+ BOX=$(agent-browser eval "JSON.stringify(window.__agexEffects.proof.getBoundingBox('${ESCAPED_SELECTOR}'))" {{SESSION_ARG}} 2>&1) || true
40
+ if [ -z "$BOX" ] || [[ "$BOX" == "null" ]] || [[ "$BOX" == *"error"* ]]; then
41
+ echo "element not found: $SELECTOR"
42
+ exit 1
43
+ fi
44
+ EL_X=$(echo "$BOX" | grep -o '"x":[0-9.-]*' | cut -d: -f2)
45
+ EL_Y=$(echo "$BOX" | grep -o '"y":[0-9.-]*' | cut -d: -f2)
46
+ EL_WIDTH=$(echo "$BOX" | grep -o '"width":[0-9.-]*' | cut -d: -f2)
47
+ EL_HEIGHT=$(echo "$BOX" | grep -o '"height":[0-9.-]*' | cut -d: -f2)
48
+ fi
49
+
50
+ # Click target: element center
51
+ CLICK_X=$(echo "$EL_X + $EL_WIDTH / 2" | bc -l)
52
+ CLICK_Y=$(echo "$EL_Y + $EL_HEIGHT / 2" | bc -l)
53
+
54
+ # Cursor target: center of element
55
+ TARGET_X=$CLICK_X
56
+ TARGET_Y=$CLICK_Y
57
+
58
+ # Start animation (fire-and-forget via requestAnimationFrame) and get duration synchronously
59
+ ANIM_DURATION=$(agent-browser eval "'' + window.__agexEffects.cursor.animateToPosition(${TARGET_X}, ${TARGET_Y})" {{SESSION_ARG}} 2>/dev/null)
60
+
61
+ # Parse duration
62
+ ANIM_DURATION=$(echo "$ANIM_DURATION" | tr -d '[:space:]"')
63
+ if [ -z "$ANIM_DURATION" ] || [ "$ANIM_DURATION" = "0" ]; then
64
+ ANIM_DURATION=2000
65
+ fi
66
+
67
+ # Wait for animation to complete + pause so viewer can see the target
68
+ WAIT_MS=$((ANIM_DURATION + 300))
69
+ agent-browser wait "$WAIT_MS" {{SESSION_ARG}} >/dev/null 2>&1
70
+ agent-browser wait 1000 {{SESSION_ARG}} >/dev/null 2>&1
71
+
72
+ # Show click ripple and perform the actual click
73
+ agent-browser eval "window.__agexEffects.cursor.showClick(${CLICK_X}, ${CLICK_Y})" {{SESSION_ARG}} >/dev/null 2>&1
74
+ agent-browser click "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
75
+
76
+ # Sync _lastPos to the actual native click position (element center)
77
+ # so the next animateToPosition starts from where the native mouse really is
78
+ agent-browser eval "window.__agexEffects.cursor._lastPos = { x: ${CLICK_X}, y: ${CLICK_Y} }" {{SESSION_ARG}} >/dev/null 2>&1
79
+
80
+ # Fade cursor out
81
+ agent-browser eval "window.__agexEffects.cursor.fadeOut()" {{SESSION_ARG}} >/dev/null 2>&1
82
+
83
+ agent-browser wait 450 {{SESSION_ARG}} >/dev/null 2>&1
84
+
85
+ echo "clicked $SELECTOR"
@@ -13,11 +13,11 @@ deduplicate_video() {
13
13
 
14
14
  echo "Removing duplicate frames (output fps: $fps)..."
15
15
  # Suppress ffmpeg/ffprobe progress output (extremely verbose, confuses agent)
16
- if ffmpeg -y -i "$input" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -r "$fps" -c:v libx264 "$output" 2>/dev/null; then
16
+ if ffmpeg -y -i "$input" -vf "mpdecimate=hi=200:lo=100:frac=0.1:max=8,setpts=N/FRAME_RATE/TB" -r "$fps" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -minrate 500k -maxrate 5M -bufsize 2M "$output" 2>/dev/null; then
17
17
  local orig_frames=$(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=noprint_wrappers=1:nokey=1 "$input" 2>/dev/null)
18
18
  local new_frames=$(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=noprint_wrappers=1:nokey=1 "$output" 2>/dev/null)
19
19
  echo "Deduplicated: $orig_frames -> $new_frames frames"
20
- rm -f "$input"
20
+ # Keep raw video for debugging (don't delete $input)
21
21
  return 0
22
22
  else
23
23
  echo "Warning: deduplication failed, keeping original"
@@ -97,7 +97,7 @@ else
97
97
  fi
98
98
 
99
99
  # Run ffmpeg with crossfade
100
- FFMPEG_CMD="ffmpeg $INPUTS -filter_complex \"$FILTER\" $OUTPUT_MAP -y \"{{RAW_VIDEO_PATH}}\""
100
+ FFMPEG_CMD="ffmpeg $INPUTS -filter_complex \"$FILTER\" $OUTPUT_MAP -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -minrate 500k -maxrate 5M -bufsize 2M -y \"{{RAW_VIDEO_PATH}}\""
101
101
 
102
102
  # Suppress ffmpeg progress output (extremely verbose, confuses agent)
103
103
  if eval "$FFMPEG_CMD" 2>/dev/null; then
@@ -8,8 +8,8 @@ SEGMENT_PATH="{{SEGMENTS_DIR}}/$SEGMENT_NAME"
8
8
 
9
9
  agent-browser record start "$SEGMENT_PATH" {{SESSION_ARG}}
10
10
 
11
- # RE-INJECT init script after recording starts (recording causes page context change)
12
- # stdout suppressed to avoid echoing JS source back to agent
11
+ # Recording creates a fresh browser context restore viewport + zoom + effects
12
+ agent-browser set viewport {{VIEWPORT_WIDTH}} {{VIEWPORT_HEIGHT}} {{SESSION_ARG}}
13
13
  agent-browser eval --stdin {{SESSION_ARG}} < "{{INIT_SCRIPT_PATH}}" >/dev/null
14
14
 
15
15
  # Wait to capture initial frame
@@ -19,8 +19,8 @@ SEGMENT_PATH="{{SEGMENTS_DIR}}/$SEGMENT_NAME"
19
19
  agent-browser record start "$SEGMENT_PATH" {{SESSION_ARG}}
20
20
  echo "Recording segment $SEGMENT_NAME: $TITLE"
21
21
 
22
- # RE-INJECT init script after recording starts (recording causes page context change)
23
- # stdout suppressed to avoid echoing JS source back to agent
22
+ # Recording creates a fresh browser context restore viewport + zoom + effects
23
+ agent-browser set viewport {{VIEWPORT_WIDTH}} {{VIEWPORT_HEIGHT}} {{SESSION_ARG}}
24
24
  agent-browser eval --stdin {{SESSION_ARG}} < "{{INIT_SCRIPT_PATH}}" >/dev/null
25
25
 
26
26
  # Inject title overlay