From fd0e48aa7c3eb27136b3b33c2e815413e6458537 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Mon, 22 Dec 2025 13:46:40 -0800 Subject: [PATCH 1/5] fix: drag logic --- packages/cli/src/utils/logger.ts | 1 + packages/provider-amp/CHANGELOG.md | 1 + packages/provider-amp/README.md | 1 + packages/provider-amp/tsconfig.json | 1 + .../src/client/code-validation.ts | 1 + .../provider-visual-edit/src/client/index.ts | 8 ++- packages/react-grab/src/agent.ts | 62 ++++++++++------- .../react-grab/src/components/icon-retry.tsx | 1 + .../react-grab/src/components/renderer.tsx | 29 ++++++-- .../src/components/selection-label/index.tsx | 4 ++ packages/react-grab/src/core.tsx | 67 +++++++++++++------ packages/react-grab/src/core/copy.ts | 1 + packages/react-grab/src/core/events.ts | 1 + packages/react-grab/src/core/noop-api.ts | 1 + .../src/hooks/use-animated-position.ts | 1 + packages/react-grab/src/state/machine.ts | 20 +++++- packages/react-grab/src/types.ts | 13 ++-- packages/react-grab/src/utils/get-tag-name.ts | 1 + .../src/utils/is-selection-backward.ts | 1 + .../src/utils/is-target-key-combination.ts | 1 + .../react-grab/src/utils/key-matches-code.ts | 1 + packages/utils/package.json | 1 + packages/utils/tsconfig.json | 1 + packages/utils/tsup.config.ts | 1 + .../website/app/blog/visual-edit/layout.tsx | 1 + .../website/app/blog/visual-edit/page.tsx | 1 + 26 files changed, 161 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 219dac072..59cddd0e7 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -20,3 +20,4 @@ export const logger = { console.log(""); }, }; + diff --git a/packages/provider-amp/CHANGELOG.md b/packages/provider-amp/CHANGELOG.md index 0630483e6..8f01cfc9f 100644 --- a/packages/provider-amp/CHANGELOG.md +++ b/packages/provider-amp/CHANGELOG.md @@ -87,3 +87,4 @@ - feat: add Amp SDK provider with undo and follow-up support - Updated dependencies - react-grab@0.0.81 + diff --git a/packages/provider-amp/README.md b/packages/provider-amp/README.md index ddea0e4bf..2f0df7da8 100644 --- a/packages/provider-amp/README.md +++ b/packages/provider-amp/README.md @@ -44,3 +44,4 @@ import "@react-grab/amp/client"; - **Undo**: Undo the last change made by Amp - **Streaming**: Real-time status updates during execution - **Tool calls**: See tool usage as it happens + diff --git a/packages/provider-amp/tsconfig.json b/packages/provider-amp/tsconfig.json index 361888679..c7ef11b46 100644 --- a/packages/provider-amp/tsconfig.json +++ b/packages/provider-amp/tsconfig.json @@ -13,3 +13,4 @@ }, "include": ["src/**/*"] } + diff --git a/packages/provider-visual-edit/src/client/code-validation.ts b/packages/provider-visual-edit/src/client/code-validation.ts index eaa7eabd8..55e113b03 100644 --- a/packages/provider-visual-edit/src/client/code-validation.ts +++ b/packages/provider-visual-edit/src/client/code-validation.ts @@ -45,3 +45,4 @@ export const validateCode = (code: string): ValidationResult => { return { isValid: true, sanitizedCode }; }; + diff --git a/packages/provider-visual-edit/src/client/index.ts b/packages/provider-visual-edit/src/client/index.ts index 782cea2e0..18d43d7a2 100644 --- a/packages/provider-visual-edit/src/client/index.ts +++ b/packages/provider-visual-edit/src/client/index.ts @@ -133,9 +133,10 @@ export const createVisualEditAgentProvider = ( return { requestId }; }; - const onStart = (session: AgentSession, element: Element | undefined) => { + const onStart = (session: AgentSession, elements: Element[]) => { const requestId = (session.context.options as RequestContext | undefined) ?.requestId; + const element = elements[0]; if (!requestId || !element) return; const html = buildAncestorContext(element); @@ -445,7 +446,7 @@ export const createVisualEditAgentProvider = ( const onComplete = async ( session: AgentSession, - element: Element | undefined, + elements: Element[], ): Promise => { const requestId = (session.context.options as RequestContext | undefined) ?.requestId; @@ -455,6 +456,7 @@ export const createVisualEditAgentProvider = ( if (!rawCode) return; const code = rawCode.trim(); + const element = elements[0]; if (!element) { cleanup(requestId); return { error: "Failed to edit: element not found" }; @@ -515,7 +517,7 @@ export const createVisualEditAgentProvider = ( cleanup(requestId); }; - const onUndo = () => { + const onUndo = (_session: AgentSession, _elements: Element[]) => { // HACK: Undo logic is handled by provider.undo, this callback is for session restoration in core.tsx }; diff --git a/packages/react-grab/src/agent.ts b/packages/react-grab/src/agent.ts index 4440c671a..fb5a45115 100644 --- a/packages/react-grab/src/agent.ts +++ b/packages/react-grab/src/agent.ts @@ -21,7 +21,7 @@ import { getNearestComponentName } from "./context.js"; import { RECENT_THRESHOLD_MS } from "./constants.js"; interface StartSessionParams { - element: Element; + elements: Element[]; prompt: string; position: { x: number; y: number }; selectionBounds?: OverlayBounds; @@ -35,6 +35,7 @@ interface SessionOperations { retry: (sessionId: string) => void; undo: (sessionId: string) => void; getElement: (sessionId: string) => Element | undefined; + getElements: (sessionId: string) => Element[]; tryResume: () => void; acknowledgeError: (sessionId: string) => string | undefined; } @@ -69,7 +70,7 @@ export const createAgentManager = ( const [canUndo, setCanUndo] = createSignal(false); const [canRedo, setCanRedo] = createSignal(false); const abortControllers = new Map(); - const sessionElements = new Map(); + const sessionElements = new Map(); let agentOptions = initialAgentOptions; @@ -128,10 +129,10 @@ export const createAgentManager = ( storage, ); setSessions((prev) => new Map(prev).set(session.id, completedSession)); - const element = sessionElements.get(session.id); + const elements = sessionElements.get(session.id) ?? []; const result = await agentOptions?.onComplete?.( completedSession, - element, + elements, ); updateUndoRedoState(); if (result?.error) { @@ -149,8 +150,8 @@ export const createAgentManager = ( if (error instanceof Error && error.name === "AbortError") { wasAborted = true; if (currentSession) { - const element = sessionElements.get(session.id); - agentOptions?.onAbort?.(currentSession, element); + const elements = sessionElements.get(session.id) ?? []; + agentOptions?.onAbort?.(currentSession, elements); } } else { const errorMessage = @@ -247,7 +248,7 @@ export const createAgentManager = ( for (const existingSession of resumableSessions) { const reacquiredElement = tryReacquireElement(existingSession); if (reacquiredElement) { - sessionElements.set(existingSession.id, reacquiredElement); + sessionElements.set(existingSession.id, [reacquiredElement]); } const sessionWithResumeStatus = { @@ -278,19 +279,20 @@ export const createAgentManager = ( }; const startSession = async (params: StartSessionParams) => { - const { element, prompt, position, selectionBounds, sessionId } = params; + const { elements, prompt, position, selectionBounds, sessionId } = params; const storage = agentOptions?.storage; - if (!agentOptions?.provider) { + if (!agentOptions?.provider || elements.length === 0) { return; } + const firstElement = elements[0]; const existingSession = sessionId ? sessions().get(sessionId) : undefined; const isFollowUp = Boolean(sessionId); const content = existingSession ? existingSession.context.content - : await generateSnippet([element], { maxLines: Infinity }); + : await generateSnippet(elements, { maxLines: Infinity }); const context: AgentContext = { content, @@ -311,9 +313,14 @@ export const createAgentManager = ( storage, ); } else { - const tagName = (element.tagName || "").toLowerCase() || undefined; + const tagName = + elements.length > 1 + ? `${elements.length} elements` + : (firstElement.tagName || "").toLowerCase() || undefined; const componentName = - (await getNearestComponentName(element)) || undefined; + elements.length > 1 + ? undefined + : (await getNearestComponentName(firstElement)) || undefined; session = createSession( context, @@ -323,12 +330,12 @@ export const createAgentManager = ( componentName, ); session.lastStatus = "Thinking…"; - sessionElements.set(session.id, element); + sessionElements.set(session.id, elements); } setSessions((prev) => new Map(prev).set(session.id, session)); saveSessionById(session, storage); - agentOptions.onStart?.(session, element); + agentOptions.onStart?.(session, elements); const abortController = new AbortController(); abortControllers.set(session.id, abortController); @@ -362,9 +369,9 @@ export const createAgentManager = ( const dismissSession = (sessionId: string) => { const currentSessions = sessions(); const session = currentSessions.get(sessionId); - const element = sessionElements.get(sessionId); - if (session && element) { - agentOptions?.onDismiss?.(session, element); + const elements = sessionElements.get(sessionId) ?? []; + if (session && elements.length > 0) { + agentOptions?.onDismiss?.(session, elements); } const storage = agentOptions?.storage; sessionElements.delete(sessionId); @@ -380,8 +387,8 @@ export const createAgentManager = ( const currentSessions = sessions(); const session = currentSessions.get(sessionId); if (session) { - const element = sessionElements.get(sessionId); - agentOptions?.onUndo?.(session, element); + const elements = sessionElements.get(sessionId) ?? []; + agentOptions?.onUndo?.(session, elements); void agentOptions?.provider?.undo?.(); } dismissSession(sessionId); @@ -412,7 +419,7 @@ export const createAgentManager = ( if (!session || !agentOptions?.provider) return; const storage = agentOptions.storage; - const element = sessionElements.get(sessionId); + const elements = sessionElements.get(sessionId) ?? []; const retriedSession = updateSession( session, @@ -427,8 +434,8 @@ export const createAgentManager = ( setSessions((prev) => new Map(prev).set(sessionId, retriedSession)); saveSessionById(retriedSession, storage); - if (element) { - agentOptions.onStart?.(retriedSession, element); + if (elements.length > 0) { + agentOptions.onStart?.(retriedSession, elements); } const abortController = new AbortController(); @@ -454,7 +461,8 @@ export const createAgentManager = ( let didUpdate = false; for (const [sessionId, session] of currentSessions) { - const element = sessionElements.get(sessionId); + const elements = sessionElements.get(sessionId) ?? []; + const element = elements[0]; if (element && document.contains(element)) { const newBounds = createElementBounds(element); @@ -485,7 +493,12 @@ export const createAgentManager = ( }; const getSessionElement = (sessionId: string): Element | undefined => { - return sessionElements.get(sessionId); + const elements = sessionElements.get(sessionId); + return elements?.[0]; + }; + + const getSessionElements = (sessionId: string): Element[] => { + return sessionElements.get(sessionId) ?? []; }; return { @@ -500,6 +513,7 @@ export const createAgentManager = ( retry: retrySession, undo: undoSession, getElement: getSessionElement, + getElements: getSessionElements, tryResume: tryResumeSessions, acknowledgeError: acknowledgeSessionError, }, diff --git a/packages/react-grab/src/components/icon-retry.tsx b/packages/react-grab/src/components/icon-retry.tsx index bed92a661..8a2cbe0f2 100644 --- a/packages/react-grab/src/components/icon-retry.tsx +++ b/packages/react-grab/src/components/icon-retry.tsx @@ -24,3 +24,4 @@ export const IconRetry: Component = (props) => { ); }; + diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index c07f8eae0..6164ed2bd 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -13,15 +13,29 @@ export const ReactGrabRenderer: Component = (props) => { props.agentSessions ? Array.from(props.agentSessions.values()) : [], ); + const selectionBoundsList = createMemo(() => { + if ( + props.selectionBoundsMultiple && + props.selectionBoundsMultiple.length > 0 + ) { + return props.selectionBoundsMultiple; + } + return props.selectionBounds ? [props.selectionBounds] : []; + }); + return ( <> - - + + + {(bounds) => ( + + )} + = (props) => { = (props) => { // old cached width, causing the label to be offset incorrectly. void props.tagName; void props.componentName; + void props.elementsCount; void props.statusText; void props.inputValue; void props.hasAgent; @@ -242,6 +243,9 @@ export const SelectionLabel: Component = (props) => { }; const tagDisplay = () => { + if (props.elementsCount && props.elementsCount > 1) { + return `${props.elementsCount} elements`; + } if (props.componentName && props.tagName) { return `${props.componentName}.${props.tagName}`; } diff --git a/packages/react-grab/src/core.tsx b/packages/react-grab/src/core.tsx index f84fb42a4..ce4dcd659 100644 --- a/packages/react-grab/src/core.tsx +++ b/packages/react-grab/src/core.tsx @@ -517,6 +517,10 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { if (isToggleFrozen()) { return snapshot().context.frozenElement; } + const frozen = snapshot().context.frozenElement; + if (frozen) { + return frozen; + } return targetElement(); }); @@ -540,6 +544,17 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { return createElementBounds(element); }); + const frozenElementsBounds = createMemo((): OverlayBounds[] => { + void snapshot().context.viewportVersion; + const elements = snapshot().context.frozenElements; + if (elements.length === 0) return []; + return elements.map((element) => createElementBounds(element)); + }); + + const frozenElementsCount = createMemo( + () => snapshot().context.frozenElements.length, + ); + const calculateDragDistance = (endX: number, endY: number) => { const endPageX = endX + window.scrollX; const endPageY = endY + window.scrollY; @@ -936,8 +951,9 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const restoreInputFromSession = ( session: AgentSession, - element: Element | undefined, + elements: Element[], ) => { + const element = elements[0]; if (element && document.contains(element)) { const rect = element.getBoundingClientRect(); const centerY = rect.top + rect.height / 2; @@ -946,7 +962,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { type: "MOUSE_MOVE", position: { x: session.position.x, y: centerY }, }); - send({ type: "FREEZE_ELEMENT", element }); + send({ type: "FREEZE_ELEMENTS", elements }); send({ type: "INPUT_CHANGE", value: session.context.prompt }); send({ type: "SET_TOGGLE_MODE", value: true }); @@ -959,13 +975,13 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const agentOptions = options.agent ? { ...options.agent, - onAbort: (session: AgentSession, element: Element | undefined) => { - options.agent?.onAbort?.(session, element); - restoreInputFromSession(session, element); + onAbort: (session: AgentSession, elements: Element[]) => { + options.agent?.onAbort?.(session, elements); + restoreInputFromSession(session, elements); }, - onUndo: (session: AgentSession, element: Element | undefined) => { - options.agent?.onUndo?.(session, element); - restoreInputFromSession(session, element); + onUndo: (session: AgentSession, elements: Element[]) => { + options.agent?.onUndo?.(session, elements); + restoreInputFromSession(session, elements); }, } : undefined; @@ -978,6 +994,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const handleInputSubmit = () => { send({ type: "SET_LAST_COPIED", element: null }); + const frozenElements = snapshot().context.frozenElements; const element = snapshot().context.frozenElement || targetElement(); const prompt = isInputMode() ? snapshot().context.inputText.trim() : ""; @@ -986,6 +1003,9 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { return; } + const elements = + frozenElements.length > 0 ? frozenElements : element ? [element] : []; + const bounds = createElementBounds(element); const labelPositionX = snapshot().context.mousePosition.x; const currentX = bounds.x + bounds.width / 2; @@ -999,7 +1019,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { send({ type: "SET_REPLY_SESSION", sessionId: null }); void agentManager.session.start({ - element, + elements, prompt, position: { x: labelPositionX, y: currentY }, selectionBounds: bounds, @@ -1023,7 +1043,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { void executeCopyOperation( currentX, currentY, - () => copyElementsToClipboard([element], prompt || undefined), + () => copyElementsToClipboard(elements, prompt || undefined), bounds, tagName, componentName ?? undefined, @@ -1185,6 +1205,11 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX; + // HACK: Calculate drag rectangle BEFORE sending DRAG_END, because DRAG_END resets dragStart + const dragRect = wasDragGesture + ? calculateDragRectangle(clientX, clientY) + : null; + if (wasDragGesture) { send({ type: "DRAG_END", position: { x: clientX, y: clientY } }); } else { @@ -1193,9 +1218,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { stopAutoScroll(); document.body.style.userSelect = ""; - if (wasDragGesture) { - const dragRect = calculateDragRectangle(clientX, clientY); - + if (dragRect) { const elements = getElementsInDrag(dragRect, isValidGrabbableElement); const selectedElements = elements.length > 0 @@ -1221,7 +1244,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { if (snapshot().context.hasAgentProvider) { send({ type: "MOUSE_MOVE", position: { x: centerX, y: centerY } }); - send({ type: "FREEZE_ELEMENT", element: firstElement }); + send({ type: "FREEZE_ELEMENTS", elements: selectedElements }); activateInputMode(); if (!isActivated()) { activateRenderer(); @@ -2265,6 +2288,8 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { { agentManager.session.dismiss(sessionId); void agentManager.session.start({ - element, + elements: [element], prompt, position: { x: positionX, @@ -2434,13 +2459,13 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { ...existingOptions, ...newAgentOptions, provider: newAgentOptions.provider ?? existingOptions?.provider, - onAbort: (session: AgentSession, element: Element | undefined) => { - newAgentOptions?.onAbort?.(session, element); - restoreInputFromSession(session, element); + onAbort: (session: AgentSession, elements: Element[]) => { + newAgentOptions?.onAbort?.(session, elements); + restoreInputFromSession(session, elements); }, - onUndo: (session: AgentSession, element: Element | undefined) => { - newAgentOptions?.onUndo?.(session, element); - restoreInputFromSession(session, element); + onUndo: (session: AgentSession, elements: Element[]) => { + newAgentOptions?.onUndo?.(session, elements); + restoreInputFromSession(session, elements); }, onDismiss: newAgentOptions?.onDismiss, }; diff --git a/packages/react-grab/src/core/copy.ts b/packages/react-grab/src/core/copy.ts index 90378c37c..15ad359fb 100644 --- a/packages/react-grab/src/core/copy.ts +++ b/packages/react-grab/src/core/copy.ts @@ -89,3 +89,4 @@ export const tryCopyWithFallback = async ( return didCopy; }; + diff --git a/packages/react-grab/src/core/events.ts b/packages/react-grab/src/core/events.ts index 097873ffb..6e2d24421 100644 --- a/packages/react-grab/src/core/events.ts +++ b/packages/react-grab/src/core/events.ts @@ -45,3 +45,4 @@ export const createEventListenerManager = (): EventListenerManager => { addDocumentListener, }; }; + diff --git a/packages/react-grab/src/core/noop-api.ts b/packages/react-grab/src/core/noop-api.ts index 389c24b7e..87d50952f 100644 --- a/packages/react-grab/src/core/noop-api.ts +++ b/packages/react-grab/src/core/noop-api.ts @@ -26,3 +26,4 @@ export const createNoopApi = (theme: Required): ReactGrabAPI => { updateOptions: () => {}, }; }; + diff --git a/packages/react-grab/src/hooks/use-animated-position.ts b/packages/react-grab/src/hooks/use-animated-position.ts index c4276ae4c..6d4655a03 100644 --- a/packages/react-grab/src/hooks/use-animated-position.ts +++ b/packages/react-grab/src/hooks/use-animated-position.ts @@ -65,3 +65,4 @@ export const useAnimatedPosition = (options: UseAnimatedPositionOptions) => { return { x, y }; }; + diff --git a/packages/react-grab/src/state/machine.ts b/packages/react-grab/src/state/machine.ts index 3207110b6..585944d02 100644 --- a/packages/react-grab/src/state/machine.ts +++ b/packages/react-grab/src/state/machine.ts @@ -36,6 +36,7 @@ interface GrabMachineContext { detectedElement: Element | null; frozenElement: Element | null; + frozenElements: Element[]; lastGrabbedElement: Element | null; lastCopiedElement: Element | null; @@ -84,6 +85,7 @@ const createInitialContext = (theme: Required): GrabMachineContext => ({ detectedElement: null, frozenElement: null, + frozenElements: [], lastGrabbedElement: null, lastCopiedElement: null, @@ -152,6 +154,7 @@ type GrabMachineEvent = | { type: "MOUSE_MOVE"; position: Position } | { type: "ELEMENT_DETECTED"; element: Element | null } | { type: "FREEZE_ELEMENT"; element: Element } + | { type: "FREEZE_ELEMENTS"; elements: Element[] } | { type: "SET_TOGGLE_MODE"; value: boolean } | { type: "TEXT_SELECTED"; elements: Element[]; cursor: Position } | { type: "SELECTION_CLEARED" } @@ -283,8 +286,21 @@ const stateMachine = setup({ setFrozenElement: assign({ frozenElement: ({ event }) => event.type === "FREEZE_ELEMENT" ? event.element : null, + frozenElements: ({ event }) => + event.type === "FREEZE_ELEMENT" ? [event.element] : [], + }), + setFrozenElements: assign({ + frozenElements: ({ event }) => + event.type === "FREEZE_ELEMENTS" ? event.elements : [], + frozenElement: ({ event }) => + event.type === "FREEZE_ELEMENTS" && event.elements.length > 0 + ? event.elements[0] + : null, + }), + clearFrozenElement: assign({ + frozenElement: () => null, + frozenElements: () => [], }), - clearFrozenElement: assign({ frozenElement: () => null }), setDragStart: assign({ dragStart: ({ event }) => { if (event.type === "DRAG_START") { @@ -540,6 +556,7 @@ const stateMachine = setup({ isToggleMode: () => false, inputText: () => "", frozenElement: () => null, + frozenElements: () => [], pendingClickData: () => null, activationTimestamp: () => null, }), @@ -725,6 +742,7 @@ const stateMachine = setup({ SET_AGENT_CAPABILITIES: { actions: ["setAgentCapabilities"] }, SET_TOGGLE_MODE: { actions: ["setToggleMode"] }, FREEZE_ELEMENT: { actions: ["setFrozenElement"] }, + FREEZE_ELEMENTS: { actions: ["setFrozenElements"] }, }, }, diff --git a/packages/react-grab/src/types.ts b/packages/react-grab/src/types.ts index 7d571a6f2..d47b67b58 100644 --- a/packages/react-grab/src/types.ts +++ b/packages/react-grab/src/types.ts @@ -170,17 +170,17 @@ export interface AgentOptions { provider?: AgentProvider; storage?: AgentSessionStorage | null; getOptions?: () => T; - onStart?: (session: AgentSession, element: Element | undefined) => void; + onStart?: (session: AgentSession, elements: Element[]) => void; onStatus?: (status: string, session: AgentSession) => void; onComplete?: ( session: AgentSession, - element: Element | undefined, + elements: Element[], ) => AgentCompleteResult | void | Promise; onError?: (error: Error, session: AgentSession) => void; onResume?: (session: AgentSession) => void; - onAbort?: (session: AgentSession, element: Element | undefined) => void; - onUndo?: (session: AgentSession, element: Element | undefined) => void; - onDismiss?: (session: AgentSession, element: Element | undefined) => void; + onAbort?: (session: AgentSession, elements: Element[]) => void; + onUndo?: (session: AgentSession, elements: Element[]) => void; + onDismiss?: (session: AgentSession, elements: Element[]) => void; } export type ActivationMode = "toggle" | "hold"; @@ -285,6 +285,8 @@ export interface SelectionLabelInstance { export interface ReactGrabRendererProps { selectionVisible?: boolean; selectionBounds?: OverlayBounds; + selectionBoundsMultiple?: OverlayBounds[]; + selectionElementsCount?: number; selectionFilePath?: string; selectionLineNumber?: number; selectionTagName?: string; @@ -419,6 +421,7 @@ export interface CompletionViewProps { export interface SelectionLabelProps { tagName?: string; componentName?: string; + elementsCount?: number; selectionBounds?: OverlayBounds; mouseX?: number; visible?: boolean; diff --git a/packages/react-grab/src/utils/get-tag-name.ts b/packages/react-grab/src/utils/get-tag-name.ts index 0d557c8a9..8cdc2ced1 100644 --- a/packages/react-grab/src/utils/get-tag-name.ts +++ b/packages/react-grab/src/utils/get-tag-name.ts @@ -1,2 +1,3 @@ export const getTagName = (element: Element): string => (element.tagName || "").toLowerCase(); + diff --git a/packages/react-grab/src/utils/is-selection-backward.ts b/packages/react-grab/src/utils/is-selection-backward.ts index dac3333bf..c6c3d954a 100644 --- a/packages/react-grab/src/utils/is-selection-backward.ts +++ b/packages/react-grab/src/utils/is-selection-backward.ts @@ -7,3 +7,4 @@ export const isSelectionBackward = (selection: Selection): boolean => { if (position & Node.DOCUMENT_POSITION_PRECEDING) return true; return selection.anchorOffset > selection.focusOffset; }; + diff --git a/packages/react-grab/src/utils/is-target-key-combination.ts b/packages/react-grab/src/utils/is-target-key-combination.ts index 7c4034dca..6df039191 100644 --- a/packages/react-grab/src/utils/is-target-key-combination.ts +++ b/packages/react-grab/src/utils/is-target-key-combination.ts @@ -71,3 +71,4 @@ export const isTargetKeyCombination = ( event.key && hasOnlyMetaOrCtrl && isCLikeKey(event.key, event.code), ); }; + diff --git a/packages/react-grab/src/utils/key-matches-code.ts b/packages/react-grab/src/utils/key-matches-code.ts index 1fd17346a..0dc837c55 100644 --- a/packages/react-grab/src/utils/key-matches-code.ts +++ b/packages/react-grab/src/utils/key-matches-code.ts @@ -8,3 +8,4 @@ export const keyMatchesCode = (targetKey: string, code: string): boolean => { } return false; }; + diff --git a/packages/utils/package.json b/packages/utils/package.json index 508f6a1b3..63af10d74 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -26,3 +26,4 @@ "tsup": "^8.4.0" } } + diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 83a6d3b7f..44b36b44b 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -12,3 +12,4 @@ }, "include": ["src/**/*"] } + diff --git a/packages/utils/tsup.config.ts b/packages/utils/tsup.config.ts index bd2c54d95..64da8311f 100644 --- a/packages/utils/tsup.config.ts +++ b/packages/utils/tsup.config.ts @@ -28,3 +28,4 @@ export default defineConfig([ treeshake: true, }, ]); + diff --git a/packages/website/app/blog/visual-edit/layout.tsx b/packages/website/app/blog/visual-edit/layout.tsx index af5010771..8a050686a 100644 --- a/packages/website/app/blog/visual-edit/layout.tsx +++ b/packages/website/app/blog/visual-edit/layout.tsx @@ -49,3 +49,4 @@ const VisualEditLayout = ({ children }: VisualEditLayoutProps) => { VisualEditLayout.displayName = "VisualEditLayout"; export default VisualEditLayout; + diff --git a/packages/website/app/blog/visual-edit/page.tsx b/packages/website/app/blog/visual-edit/page.tsx index 621e737bb..8f18d3583 100644 --- a/packages/website/app/blog/visual-edit/page.tsx +++ b/packages/website/app/blog/visual-edit/page.tsx @@ -502,3 +502,4 @@ const VisualEditPage = () => { VisualEditPage.displayName = "VisualEditPage"; export default VisualEditPage; + From 52c6514c040d79a55272e0b1c97e22d31fa1c1ab Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Mon, 22 Dec 2025 14:09:41 -0800 Subject: [PATCH 2/5] chore: empty commit From d554a5770fa33e87c6820a2ee0609c5c8b696436 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Mon, 22 Dec 2025 15:00:29 -0800 Subject: [PATCH 3/5] chore: empty commit From ba51e3af3e89e1f99c955475609b2f7654ed5004 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Tue, 23 Dec 2025 00:15:40 -0800 Subject: [PATCH 4/5] fix: improve element validation and session handling - Enhanced element validation logic in agent.ts to ensure valid HTML tag names are checked before proceeding. - Updated session handling in core.tsx to support multiple elements, improving the follow-up session submission process. --- packages/react-grab/src/agent.ts | 3 ++- packages/react-grab/src/core.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react-grab/src/agent.ts b/packages/react-grab/src/agent.ts index fb5a45115..6f7a594e0 100644 --- a/packages/react-grab/src/agent.ts +++ b/packages/react-grab/src/agent.ts @@ -197,7 +197,8 @@ export const createAgentManager = ( const element = document.elementFromPoint(centerX, centerY); if (!element) return undefined; - if (tagName && element.tagName.toLowerCase() !== tagName) { + const isValidHtmlTagName = tagName && !tagName.includes(" "); + if (isValidHtmlTagName && element.tagName.toLowerCase() !== tagName) { return undefined; } diff --git a/packages/react-grab/src/core.tsx b/packages/react-grab/src/core.tsx index ce4dcd659..54b8cc06d 100644 --- a/packages/react-grab/src/core.tsx +++ b/packages/react-grab/src/core.tsx @@ -2327,9 +2327,9 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { onUndoSession={(sessionId) => agentManager.session.undo(sessionId)} onFollowUpSubmitSession={(sessionId, prompt) => { const session = agentManager.sessions().get(sessionId); - const element = agentManager.session.getElement(sessionId); + const elements = agentManager.session.getElements(sessionId); const sessionBounds = session?.selectionBounds; - if (session && element && sessionBounds) { + if (session && elements.length > 0 && sessionBounds) { const positionX = session.position.x; const followUpSessionId = session.context.sessionId ?? sessionId; @@ -2337,7 +2337,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { agentManager.session.dismiss(sessionId); void agentManager.session.start({ - elements: [element], + elements, prompt, position: { x: positionX, From 0128e026838813e5a266f34af2c367d5f2220b27 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Tue, 23 Dec 2025 00:19:02 -0800 Subject: [PATCH 5/5] fix: update session handling in agent.ts - Adjusted session handling logic to ensure proper setting of session elements and status updates, enhancing overall session management. --- packages/react-grab/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-grab/src/agent.ts b/packages/react-grab/src/agent.ts index 6f7a594e0..41d73d976 100644 --- a/packages/react-grab/src/agent.ts +++ b/packages/react-grab/src/agent.ts @@ -331,9 +331,9 @@ export const createAgentManager = ( componentName, ); session.lastStatus = "Thinking…"; - sessionElements.set(session.id, elements); } + sessionElements.set(session.id, elements); setSessions((prev) => new Map(prev).set(session.id, session)); saveSessionById(session, storage); agentOptions.onStart?.(session, elements);