From cf58df008cc5493b4ab28b026667ef16dff25faa Mon Sep 17 00:00:00 2001 From: Mule Date: Sat, 28 Mar 2026 15:06:06 +0000 Subject: [PATCH 1/2] Fix click cursor animation detaching when page scrolls after click Track the active target element during click/type operations and reposition the cursor overlay on scroll events. When the target remains in the viewport, the cursor follows it; when the target scrolls out of view, the cursor is hidden. Closes #1 --- extension/src/content/content-script.ts | 31 ++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/extension/src/content/content-script.ts b/extension/src/content/content-script.ts index 96e7956..615ca77 100644 --- a/extension/src/content/content-script.ts +++ b/extension/src/content/content-script.ts @@ -74,6 +74,7 @@ class CursorAgent { private hoveredElement: Element | null = null; private lastCliActivityAt = Date.now(); private lastIdleMoveFinishedAt = 0; + private activeTarget: Element | null = null; constructor() { document.addEventListener('visibilitychange', () => { @@ -97,6 +98,27 @@ class CursorAgent { const point = this.clampPoint({ x: this.currentX, y: this.currentY }); this.moveInstant(point.x, point.y); }); + + window.addEventListener('scroll', () => { + if (!this.enabled) { + return; + } + if (this.activeTarget) { + const rect = this.activeTarget.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + // If the target is still within the viewport, track it + if ( + centerX >= 0 && centerX <= window.innerWidth && + centerY >= 0 && centerY <= window.innerHeight + ) { + this.moveInstant(centerX, centerY); + } else { + // Target scrolled out of view — hide the cursor overlay + this.moveInstant(-100, -100); + } + } + }, { passive: true }); } start(sessionId: string): void { @@ -121,6 +143,7 @@ class CursorAgent { this.taskMode = false; this.cancelMotion(); this.hoveredElement = null; + this.activeTarget = null; if (cursorOverlay && document.documentElement.contains(cursorOverlay)) { cursorOverlay.remove(); @@ -128,11 +151,12 @@ class CursorAgent { cursorOverlay = null; } - beginTask(): void { + beginTask(target?: Element): void { if (!this.enabled) { this.start('implicit'); } this.cancelMotion(); + this.activeTarget = target ?? null; this.setTaskMode(true); } @@ -140,6 +164,7 @@ class CursorAgent { if (!this.enabled) { return; } + this.activeTarget = null; this.setTaskMode(false); this.lastIdleMoveFinishedAt = Date.now(); this.startIdleLoop(); @@ -499,7 +524,7 @@ async function handleClick(req: ContentRequest): Promise { const beforeUrl = location.href; const beforeTitle = document.title; - cursorAgent.beginTask(); + cursorAgent.beginTask(target); try { const point = await prepareInteractionPoint(target); @@ -542,7 +567,7 @@ async function handleType(req: ContentRequest): Promise { const beforeUrl = location.href; const beforeTitle = document.title; - cursorAgent.beginTask(); + cursorAgent.beginTask(target); try { const point = await prepareInteractionPoint(target); From 3e02d071f59ba5be2840162b854ef4090fc40805 Mon Sep 17 00:00:00 2001 From: Mule Date: Sat, 28 Mar 2026 16:06:16 +0000 Subject: [PATCH 2/2] Address review: guard detached targets and use rect intersection for visibility --- extension/src/content/content-script.ts | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/extension/src/content/content-script.ts b/extension/src/content/content-script.ts index 615ca77..9f2cd8f 100644 --- a/extension/src/content/content-script.ts +++ b/extension/src/content/content-script.ts @@ -104,17 +104,25 @@ class CursorAgent { return; } if (this.activeTarget) { + // Target may have been unmounted by SPA rerender + if (!this.activeTarget.isConnected) { + this.activeTarget = null; + return; + } const rect = this.activeTarget.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - // If the target is still within the viewport, track it - if ( - centerX >= 0 && centerX <= window.innerWidth && - centerY >= 0 && centerY <= window.innerHeight - ) { - this.moveInstant(centerX, centerY); + // Check if rect intersects viewport at all + const inViewport = + rect.bottom > 0 && + rect.top < window.innerHeight && + rect.right > 0 && + rect.left < window.innerWidth; + + if (inViewport) { + // Clamp to viewport for cursor position + const x = Math.max(0, Math.min(rect.left + rect.width / 2, window.innerWidth)); + const y = Math.max(0, Math.min(rect.top + rect.height / 2, window.innerHeight)); + this.moveInstant(x, y); } else { - // Target scrolled out of view — hide the cursor overlay this.moveInstant(-100, -100); } }