Skip to content
This repository was archived by the owner on Apr 25, 2026. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions extension/src/content/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -97,6 +98,35 @@ 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) {
// Target may have been unmounted by SPA rerender
if (!this.activeTarget.isConnected) {
this.activeTarget = null;
return;
}
const rect = this.activeTarget.getBoundingClientRect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip scroll tracking for detached active targets

When a click/type action causes the target node to be unmounted before endTask() runs (common in SPA rerenders), this.activeTarget can remain set to a detached element and getBoundingClientRect() returns a zero rect. The current check then treats (0,0) as in-viewport and moves the cursor to the top-left corner on subsequent scroll events, which is a visible regression during post-action stability waits. Guarding with isConnected (or clearing activeTarget) avoids this misplacement.

Useful? React with 👍 / 👎.

// 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 {
this.moveInstant(-100, -100);
}
}
}, { passive: true });
}

start(sessionId: string): void {
Expand All @@ -121,25 +151,28 @@ class CursorAgent {
this.taskMode = false;
this.cancelMotion();
this.hoveredElement = null;
this.activeTarget = null;

if (cursorOverlay && document.documentElement.contains(cursorOverlay)) {
cursorOverlay.remove();
}
cursorOverlay = null;
}

beginTask(): void {
beginTask(target?: Element): void {
if (!this.enabled) {
this.start('implicit');
}
this.cancelMotion();
this.activeTarget = target ?? null;
this.setTaskMode(true);
}

endTask(): void {
if (!this.enabled) {
return;
}
this.activeTarget = null;
this.setTaskMode(false);
this.lastIdleMoveFinishedAt = Date.now();
this.startIdleLoop();
Expand Down Expand Up @@ -499,7 +532,7 @@ async function handleClick(req: ContentRequest): Promise<ContentResponse> {
const beforeUrl = location.href;
const beforeTitle = document.title;

cursorAgent.beginTask();
cursorAgent.beginTask(target);

try {
const point = await prepareInteractionPoint(target);
Expand Down Expand Up @@ -542,7 +575,7 @@ async function handleType(req: ContentRequest): Promise<ContentResponse> {
const beforeUrl = location.href;
const beforeTitle = document.title;

cursorAgent.beginTask();
cursorAgent.beginTask(target);

try {
const point = await prepareInteractionPoint(target);
Expand Down