Skip to content

Commit b5d3ac6

Browse files
committed
Refactor: Extract modules from core/index.tsx, eliminate silent error swallowing
- Extract label lifecycle management into `core/label-manager.ts` (create, fade, hover, timer cleanup) - Extract dropdown position tracking into `core/dropdown-controller.ts` (RAF-based tracking, anchor computation) - Create `utils/log-recoverable-error.ts` to replace 12 silent `catch {}` blocks across session.ts, history-storage.ts, freeze-updates.ts, copy-html/styles plugins, and core/index.tsx - Create `utils/generate-id.ts` to consolidate 4 duplicate ID generators (label, session, history, grabbed-box) - Create `utils/get-element-center.ts` to eliminate repeated getBoundsCenter(createElementBounds(element)) pattern - Rename detectionState fields for clarity (lastTime→lastDetectionTimestamp, latestX→latestPointerX, etc.) - Fix: labelManager.clearAll() now cancels orphaned fade timers that previously leaked when instances were cleared 583/584 e2e tests pass (1 pre-existing flaky). 0 lint errors, 0 type errors. Made-with: Cursor
1 parent c2f7ddb commit b5d3ac6

File tree

11 files changed

+358
-219
lines changed

11 files changed

+358
-219
lines changed

packages/react-grab/src/core/agent/session.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import type {
66
AgentSessionStorage,
77
OverlayBounds,
88
} from "../../types.js";
9+
import { generateId } from "../../utils/generate-id.js";
10+
import { logRecoverableError } from "../../utils/log-recoverable-error.js";
911

1012
const STORAGE_KEY = "react-grab:agent-sessions";
1113

12-
const generateSessionId = (): string =>
13-
`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
14-
1514
export const createSession = (
1615
context: AgentContext,
1716
position: Position,
@@ -21,7 +20,7 @@ export const createSession = (
2120
): AgentSession => {
2221
const now = Date.now();
2322
return {
24-
id: generateSessionId(),
23+
id: generateId("session"),
2524
context,
2625
lastStatus: "",
2726
isStreaming: true,
@@ -59,7 +58,11 @@ export const saveSessions = (
5958
try {
6059
const sessionsObject = Object.fromEntries(sessions);
6160
storage.setItem(STORAGE_KEY, JSON.stringify(sessionsObject));
62-
} catch {
61+
} catch (error) {
62+
logRecoverableError(
63+
"Failed to save sessions to storage, falling back to memory",
64+
error,
65+
);
6366
memorySessions.clear();
6467
sessions.forEach((session, id) => memorySessions.set(id, session));
6568
evictOldestMemorySessions();
@@ -87,7 +90,8 @@ export const loadSessions = (
8790
if (!data) return new Map();
8891
const sessionsObject = JSON.parse(data) as Record<string, AgentSession>;
8992
return new Map(Object.entries(sessionsObject));
90-
} catch {
93+
} catch (error) {
94+
logRecoverableError("Failed to load sessions from storage", error);
9195
return new Map();
9296
}
9397
};
@@ -100,7 +104,8 @@ export const clearSessions = (storage?: AgentSessionStorage | null): void => {
100104

101105
try {
102106
storage.removeItem(STORAGE_KEY);
103-
} catch {
107+
} catch (error) {
108+
logRecoverableError("Failed to clear sessions from storage", error);
104109
memorySessions.clear();
105110
}
106111
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { ToolbarState, DropdownAnchor } from "../types.js";
2+
import {
3+
nativeCancelAnimationFrame,
4+
nativeRequestAnimationFrame,
5+
} from "../utils/native-raf.js";
6+
7+
export interface DropdownController {
8+
startTracking: (computePosition: () => void) => void;
9+
stopTracking: () => void;
10+
computeAnchor: () => DropdownAnchor | null;
11+
dispose: () => void;
12+
}
13+
14+
const getNearestEdge = (rect: DOMRect): ToolbarState["edge"] => {
15+
const centerX = rect.left + rect.width / 2;
16+
const centerY = rect.top + rect.height / 2;
17+
const distanceToTop = centerY;
18+
const distanceToBottom = window.innerHeight - centerY;
19+
const distanceToLeft = centerX;
20+
const distanceToRight = window.innerWidth - centerX;
21+
const minimumDistance = Math.min(
22+
distanceToTop,
23+
distanceToBottom,
24+
distanceToLeft,
25+
distanceToRight,
26+
);
27+
if (minimumDistance === distanceToTop) return "top";
28+
if (minimumDistance === distanceToLeft) return "left";
29+
if (minimumDistance === distanceToRight) return "right";
30+
return "bottom";
31+
};
32+
33+
export const createDropdownController = (
34+
getToolbarElement: () => HTMLDivElement | undefined,
35+
): DropdownController => {
36+
let trackingFrameId: number | null = null;
37+
38+
const stopTracking = () => {
39+
if (trackingFrameId !== null) {
40+
nativeCancelAnimationFrame(trackingFrameId);
41+
trackingFrameId = null;
42+
}
43+
};
44+
45+
const startTracking = (computePosition: () => void) => {
46+
stopTracking();
47+
const updatePosition = () => {
48+
computePosition();
49+
trackingFrameId = nativeRequestAnimationFrame(updatePosition);
50+
};
51+
updatePosition();
52+
};
53+
54+
const computeAnchor = (): DropdownAnchor | null => {
55+
const toolbarElement = getToolbarElement();
56+
if (!toolbarElement) return null;
57+
const toolbarRect = toolbarElement.getBoundingClientRect();
58+
const edge = getNearestEdge(toolbarRect);
59+
60+
if (edge === "left" || edge === "right") {
61+
return {
62+
x: edge === "left" ? toolbarRect.right : toolbarRect.left,
63+
y: toolbarRect.top + toolbarRect.height / 2,
64+
edge,
65+
toolbarWidth: toolbarRect.width,
66+
};
67+
}
68+
69+
return {
70+
x: toolbarRect.left + toolbarRect.width / 2,
71+
y: edge === "top" ? toolbarRect.bottom : toolbarRect.top,
72+
edge,
73+
toolbarWidth: toolbarRect.width,
74+
};
75+
};
76+
77+
const dispose = () => {
78+
stopTracking();
79+
};
80+
81+
return {
82+
startTracking,
83+
stopTracking,
84+
computeAnchor,
85+
dispose,
86+
};
87+
};

0 commit comments

Comments
 (0)