diff --git a/.gitignore b/.gitignore index 9410ad399..0a4a02732 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ test-results playwright-report .vercel .cursor/debug.log -.cursor \ No newline at end of file +.cursor +meta.json \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 5773c4a4d..d9fb39056 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ pnpm typecheck # runs type checking pnpm format ``` -## Cursor Cloud specific instructions +## Development instructions This is a pnpm + Turborepo monorepo (19 packages under `packages/`). No external services (databases, Docker, etc.) are required. @@ -117,10 +117,6 @@ The root `package.json` has `pnpm.onlyBuiltDependencies` configured for `@parcel E2E tests (`pnpm test` at root) run Playwright against the `e2e-playground` Vite dev server on port 5175 (auto-started by the Playwright config). Chromium must be installed: `npx --prefix packages/react-grab playwright install chromium --with-deps`. -### Known flaky test - -`e2e/history-items.spec.ts` > "should reposition when toolbar is dragged to top edge" intermittently times out in headless CI environments. This is a pre-existing issue. - ### Key commands reference See root `package.json` scripts and `CONTRIBUTING.md` for the full list. Quick reference: diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 72f684281..40fbd0c16 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -24,13 +24,14 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@solidjs/web": "2.0.0-beta.3", "react-grab": "workspace:*", - "solid-js": "^1.9.10" + "solid-js": "2.0.0-beta.3" }, "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-typescript": "^7.28.5", - "babel-preset-solid": "^1.9.10", + "babel-preset-solid": "2.0.0-beta.3", "esbuild-plugin-babel": "^0.2.3", "tsup": "^8.2.4" } diff --git a/packages/design-system/src/index.tsx b/packages/design-system/src/index.tsx index 6594f068b..9564a1a81 100644 --- a/packages/design-system/src/index.tsx +++ b/packages/design-system/src/index.tsx @@ -1,7 +1,7 @@ // @ts-expect-error - CSS imported as text via tsup loader import cssText from "react-grab/dist/styles.css"; -import { render } from "solid-js/web"; -import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; +import { render } from "@solidjs/web"; +import { createSignal, For, onCleanup, onSettled, Show } from "solid-js"; import { SelectionLabel } from "react-grab/src/components/selection-label/index.js"; import { ContextMenu } from "react-grab/src/components/context-menu.js"; import { ToolbarContent } from "react-grab/src/components/toolbar/toolbar-content.js"; @@ -2721,14 +2721,13 @@ const FpsMeter = (props: FpsMeterProps) => { animationFrameId = requestAnimationFrame(measureFps); }; - onMount(() => { + onSettled(() => { animationFrameId = requestAnimationFrame(measureFps); - }); - - onCleanup(() => { - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - } + return () => { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + } + }; }); return ( @@ -3110,19 +3109,22 @@ const DesignSystemGrid = () => {
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3134,19 +3136,22 @@ const DesignSystemGrid = () => { Flows
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3158,19 +3163,22 @@ const DesignSystemGrid = () => { Selection Label
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3182,19 +3190,22 @@ const DesignSystemGrid = () => { Context Menu (Right-Click)
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3206,19 +3217,22 @@ const DesignSystemGrid = () => { Toolbar
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3230,19 +3244,22 @@ const DesignSystemGrid = () => { History Dropdown
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
@@ -3254,19 +3271,22 @@ const DesignSystemGrid = () => { Agent States
- {(state) => ( - getBoundsForCell(state.id)} - registerCell={(element) => registerCell(state.id, element)} - onRefresh={createRefreshHandler(state.id)} - getTargetDisplayText={() => getTargetDisplayText(state)} - isStarred={isStarred(state.id)} - onToggleStar={() => handleToggleStar(state.id)} - isScrambled={isScrambled()} - /> - )} + {(stateAccessor) => { + const state = stateAccessor(); + return ( + getBoundsForCell(state.id)} + registerCell={(element) => registerCell(state.id, element)} + onRefresh={createRefreshHandler(state.id)} + getTargetDisplayText={() => getTargetDisplayText(state)} + isStarred={isStarred(state.id)} + onToggleStar={() => handleToggleStar(state.id)} + isScrambled={isScrambled()} + /> + ); + }}
diff --git a/packages/design-system/tsup.config.ts b/packages/design-system/tsup.config.ts index 61f01b870..97fd2b1fd 100644 --- a/packages/design-system/tsup.config.ts +++ b/packages/design-system/tsup.config.ts @@ -13,7 +13,7 @@ const options: Options = { ".css": "text", }, minify: process.env.NODE_ENV === "production", - noExternal: ["solid-js", /^react-grab\/src/, "react-grab/dist/styles.css"], + noExternal: ["solid-js", "@solidjs/web", /^react-grab\/src/, "react-grab/dist/styles.css"], outDir: "./dist", platform: "browser", sourcemap: false, diff --git a/packages/react-grab/package.json b/packages/react-grab/package.json index bb2c0ab89..f0fe555f2 100644 --- a/packages/react-grab/package.json +++ b/packages/react-grab/package.json @@ -100,9 +100,10 @@ "dependencies": { "@medv/finder": "^4.0.2", "@react-grab/cli": "workspace:*", + "@solidjs/web": "2.0.0-beta.3", "bippy": "^0.5.32", "element-source": "^0.0.3", - "solid-js": "^1.9.10" + "solid-js": "2.0.0-beta.3" }, "devDependencies": { "@babel/core": "^7.28.5", @@ -111,13 +112,12 @@ "@tailwindcss/cli": "^4.1.17", "@types/node": "^20.19.23", "@types/react": "^19.2.11", - "babel-preset-solid": "^1.9.10", + "babel-preset-solid": "2.0.0-beta.3", "clsx": "^2.1.1", "concurrently": "^9.1.2", "esbuild-plugin-babel": "^0.2.3", "oxlint": "^1.42.0", "publint": "^0.2.12", - "tailwind-merge": "^2.5.5", "tailwindcss": "^4.1.0", "tsup": "^8.2.4" }, diff --git a/packages/react-grab/src/components/clear-history-prompt.tsx b/packages/react-grab/src/components/clear-history-prompt.tsx index 127adda43..659a3aa62 100644 --- a/packages/react-grab/src/components/clear-history-prompt.tsx +++ b/packages/react-grab/src/components/clear-history-prompt.tsx @@ -1,4 +1,4 @@ -import { Show, onMount, onCleanup } from "solid-js"; +import { Show, onSettled } from "solid-js"; import type { Component } from "solid-js"; import type { DropdownAnchor } from "../types.js"; import { DROPDOWN_EDGE_TRANSFORM_ORIGIN, Z_INDEX_LABEL } from "../constants.js"; @@ -24,7 +24,7 @@ export const ClearHistoryPrompt: Component = ( () => props.position, ); - onMount(() => { + onSettled(() => { dropdown.measure(); const unregisterOverlayDismiss = registerOverlayDismiss({ isOpen: () => Boolean(props.position), @@ -33,10 +33,10 @@ export const ClearHistoryPrompt: Component = ( shouldIgnoreInputEvents: true, }); - onCleanup(() => { + return () => { dropdown.clearAnimationHandles(); unregisterOverlayDismiss(); - }); + }; }); return ( diff --git a/packages/react-grab/src/components/context-menu.tsx b/packages/react-grab/src/components/context-menu.tsx index c10a72af6..1a257d72c 100644 --- a/packages/react-grab/src/components/context-menu.tsx +++ b/packages/react-grab/src/components/context-menu.tsx @@ -1,8 +1,7 @@ import { Show, For, - onMount, - onCleanup, + onSettled, createSignal, createEffect, createMemo, @@ -80,11 +79,14 @@ export const ContextMenu: Component = (props) => { } }; - createEffect(() => { - if (isVisible()) { - nativeRequestAnimationFrame(measureContainer); - } - }); + createEffect( + () => isVisible(), + (visible) => { + if (visible) { + nativeRequestAnimationFrame(measureContainer); + } + }, + ); const computedPosition = createMemo(() => { const bounds = props.selectionBounds; @@ -165,7 +167,7 @@ export const ContextMenu: Component = (props) => { } }; - onMount(() => { + onSettled(() => { measureContainer(); const handleKeyDown = (event: KeyboardEvent) => { @@ -219,10 +221,10 @@ export const ContextMenu: Component = (props) => { }); window.addEventListener("keydown", handleKeyDown, { capture: true }); - onCleanup(() => { + return () => { unregisterOverlayDismiss(); window.removeEventListener("keydown", handleKeyDown, { capture: true }); - }); + }; }); return ( @@ -283,33 +285,36 @@ export const ContextMenu: Component = (props) => { class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out" /> - {(item) => ( - - )} + {(itemAccessor) => { + const menuItem = () => itemAccessor(); + return ( + + ); + }} diff --git a/packages/react-grab/src/components/history-dropdown.tsx b/packages/react-grab/src/components/history-dropdown.tsx index 5c7763997..6172b2327 100644 --- a/packages/react-grab/src/components/history-dropdown.tsx +++ b/packages/react-grab/src/components/history-dropdown.tsx @@ -1,12 +1,4 @@ -import { - Show, - For, - onMount, - onCleanup, - createSignal, - createEffect, - on, -} from "solid-js"; +import { Show, For, onSettled, createSignal, createEffect } from "solid-js"; import type { Component } from "solid-js"; import type { HistoryItem, DropdownAnchor } from "../types.js"; import { @@ -102,15 +94,14 @@ export const HistoryDropdown: Component = (props) => { // HACK: mouseenter doesn't fire when an element appears under the cursor, so we check :hover after the enter animation commits createEffect( - on( - () => dropdown.isAnimatedIn(), - (animatedIn) => { - if (animatedIn && containerRef?.matches(":hover")) { - props.onDropdownHover?.(true); - } - }, - { defer: true }, - ), + () => dropdown.isAnimatedIn(), + (animatedIn) => { + if (animatedIn && containerRef?.matches(":hover")) { + props.onDropdownHover?.(true); + } + }, + undefined, + { defer: true }, ); const clampedMaxWidth = () => @@ -129,7 +120,7 @@ export const HistoryDropdown: Component = (props) => { const panelMinWidth = () => Math.max(DROPDOWN_MIN_WIDTH_PX, props.position?.toolbarWidth ?? 0); - onMount(() => { + onSettled(() => { dropdown.measure(); const handleKeyDown = (event: KeyboardEvent) => { @@ -143,13 +134,13 @@ export const HistoryDropdown: Component = (props) => { window.addEventListener("keydown", handleKeyDown, { capture: true }); - onCleanup(() => { + return () => { clearTimeout(copyAllFeedbackTimeout); clearTimeout(copyItemFeedbackTimeout); dropdown.clearAnimationHandles(); window.removeEventListener("keydown", handleKeyDown, { capture: true }); safePolygonTracker.stop(); - }); + }; }); return ( @@ -284,104 +275,114 @@ export const HistoryDropdown: Component = (props) => { class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out" /> - {(item) => ( -
event.stopPropagation()} - onClick={(event) => { - event.stopPropagation(); - props.onSelectItem?.(item); - setConfirmedCopyItemId(item.id); - clearTimeout(copyItemFeedbackTimeout); - copyItemFeedbackTimeout = setTimeout(() => { - setConfirmedCopyItemId(null); - }, FEEDBACK_DURATION_MS); - }} - onKeyDown={(event) => { - if ( - event.code === "Space" && - event.currentTarget === event.target - ) { - event.preventDefault(); + {(itemAccessor) => { + const historyItem = () => itemAccessor(); + return ( +
event.stopPropagation()} + onClick={(event) => { event.stopPropagation(); - props.onSelectItem?.(item); - } - }} - onMouseEnter={(event) => { - if (!props.disconnectedItemIds?.has(item.id)) { - props.onItemHover?.(item.id); - } - updateHighlight(event.currentTarget); - }} - onMouseLeave={() => { - props.onItemHover?.(null); - clearHighlight(); - }} - onFocus={(event) => updateHighlight(event.currentTarget)} - onBlur={clearHighlight} - > - - - {getHistoryItemDisplayName(item)} - - - - {item.commentText} + props.onSelectItem?.(historyItem()); + setConfirmedCopyItemId(historyItem().id); + clearTimeout(copyItemFeedbackTimeout); + copyItemFeedbackTimeout = setTimeout(() => { + setConfirmedCopyItemId(null); + }, FEEDBACK_DURATION_MS); + }} + onKeyDown={(event) => { + if ( + event.code === "Space" && + event.currentTarget === event.target + ) { + event.preventDefault(); + event.stopPropagation(); + props.onSelectItem?.(historyItem()); + } + }} + onMouseEnter={(event) => { + if (!props.disconnectedItemIds?.has(historyItem().id)) { + props.onItemHover?.(historyItem().id); + } + updateHighlight(event.currentTarget); + }} + onMouseLeave={() => { + props.onItemHover?.(null); + clearHighlight(); + }} + onFocus={(event) => updateHighlight(event.currentTarget)} + onBlur={clearHighlight} + > + + + {getHistoryItemDisplayName(historyItem())} - - - - - {formatRelativeTime(item.timestamp)} + + + {historyItem().commentText} + + - - -
- )} +
+ ); + }}
diff --git a/packages/react-grab/src/components/overlay-canvas.tsx b/packages/react-grab/src/components/overlay-canvas.tsx index 462a8f726..c63ab4bd5 100644 --- a/packages/react-grab/src/components/overlay-canvas.tsx +++ b/packages/react-grab/src/components/overlay-canvas.tsx @@ -1,4 +1,4 @@ -import { createEffect, onCleanup, onMount, on } from "solid-js"; +import { createEffect, onSettled } from "solid-js"; import type { Component } from "solid-js"; import type { OverlayBounds, @@ -492,202 +492,192 @@ export const OverlayCanvas: Component = (props) => { }; createEffect( - on( - () => - [ - props.selectionVisible, - props.selectionBounds, - props.selectionBoundsMultiple, - props.selectionIsFading, - props.selectionShouldSnap, - ] as const, - ([isVisible, singleBounds, multipleBounds, , shouldSnap]) => { - if ( - !isVisible || - (!singleBounds && (!multipleBounds || multipleBounds.length === 0)) - ) { - selectionAnimations = []; - scheduleAnimationFrame(); - return; - } - - const boundsToRender = - multipleBounds && multipleBounds.length > 0 - ? multipleBounds - : singleBounds - ? [singleBounds] - : []; + () => + [ + props.selectionVisible, + props.selectionBounds, + props.selectionBoundsMultiple, + props.selectionIsFading, + props.selectionShouldSnap, + ] as const, + ([isVisible, singleBounds, multipleBounds, , shouldSnap]) => { + if ( + !isVisible || + (!singleBounds && (!multipleBounds || multipleBounds.length === 0)) + ) { + selectionAnimations = []; + scheduleAnimationFrame(); + return; + } - selectionAnimations = boundsToRender.map((bounds, index) => { - const animationId = `selection-${index}`; - const existingAnimation = selectionAnimations.find( - (animation) => animation.id === animationId, - ); + const boundsToRender = + multipleBounds && multipleBounds.length > 0 + ? multipleBounds + : singleBounds + ? [singleBounds] + : []; + + selectionAnimations = boundsToRender.map((bounds, index) => { + const animationId = `selection-${index}`; + const existingAnimation = selectionAnimations.find( + (animation) => animation.id === animationId, + ); - if (existingAnimation) { - updateAnimationTarget(existingAnimation, bounds); - if (shouldSnap) { - existingAnimation.current = { ...existingAnimation.target }; - } - return existingAnimation; + if (existingAnimation) { + updateAnimationTarget(existingAnimation, bounds); + if (shouldSnap) { + existingAnimation.current = { ...existingAnimation.target }; } + return existingAnimation; + } - return createAnimatedBounds(animationId, bounds); - }); + return createAnimatedBounds(animationId, bounds); + }); - scheduleAnimationFrame(); - }, - ), + scheduleAnimationFrame(); + }, ); createEffect( - on( - () => [props.dragVisible, props.dragBounds] as const, - ([isVisible, bounds]) => { - if (!isVisible || !bounds) { - dragAnimation = null; - scheduleAnimationFrame(); - return; - } + () => [props.dragVisible, props.dragBounds] as const, + ([isVisible, bounds]) => { + if (!isVisible || !bounds) { + dragAnimation = null; + scheduleAnimationFrame(); + return; + } - if (dragAnimation) { - updateAnimationTarget(dragAnimation, bounds); - } else { - dragAnimation = createAnimatedBounds("drag", bounds); - } + if (dragAnimation) { + updateAnimationTarget(dragAnimation, bounds); + } else { + dragAnimation = createAnimatedBounds("drag", bounds); + } - scheduleAnimationFrame(); - }, - ), + scheduleAnimationFrame(); + }, ); createEffect( - on( - () => props.grabbedBoxes, - (grabbedBoxes) => { - const boxesToProcess = grabbedBoxes ?? []; - const activeBoxIds = new Set(boxesToProcess.map((box) => box.id)); - const existingAnimationIds = new Set( - grabbedAnimations.map((animation) => animation.id), - ); + () => props.grabbedBoxes, + (grabbedBoxes) => { + const boxesToProcess = grabbedBoxes ?? []; + const activeBoxIds = new Set(boxesToProcess.map((box) => box.id)); + const existingAnimationIds = new Set( + grabbedAnimations.map((animation) => animation.id), + ); - for (const box of boxesToProcess) { - if (!existingAnimationIds.has(box.id)) { - grabbedAnimations.push( - createAnimatedBounds(box.id, box.bounds, { - createdAt: box.createdAt, - }), - ); - } + for (const box of boxesToProcess) { + if (!existingAnimationIds.has(box.id)) { + grabbedAnimations.push( + createAnimatedBounds(box.id, box.bounds, { + createdAt: box.createdAt, + }), + ); } + } - for (const animation of grabbedAnimations) { - const matchingBox = boxesToProcess.find( - (box) => box.id === animation.id, - ); - if (matchingBox) { - updateAnimationTarget(animation, matchingBox.bounds); - } + for (const animation of grabbedAnimations) { + const matchingBox = boxesToProcess.find( + (box) => box.id === animation.id, + ); + if (matchingBox) { + updateAnimationTarget(animation, matchingBox.bounds); } + } - grabbedAnimations = grabbedAnimations.filter((animation) => { - if (animation.id.startsWith("label-")) { - return true; - } - return activeBoxIds.has(animation.id); - }); + grabbedAnimations = grabbedAnimations.filter((animation) => { + if (animation.id.startsWith("label-")) { + return true; + } + return activeBoxIds.has(animation.id); + }); - scheduleAnimationFrame(); - }, - ), + scheduleAnimationFrame(); + }, ); createEffect( - on( - () => props.agentSessions, - (agentSessions) => { - if (!agentSessions || agentSessions.size === 0) { - processingAnimations = []; - scheduleAnimationFrame(); - return; - } + () => props.agentSessions, + (agentSessions) => { + if (!agentSessions || agentSessions.size === 0) { + processingAnimations = []; + scheduleAnimationFrame(); + return; + } - const updatedAnimations: AnimatedBounds[] = []; + const updatedAnimations: AnimatedBounds[] = []; - for (const [sessionId, session] of agentSessions) { - for (let index = 0; index < session.selectionBounds.length; index++) { - const bounds = session.selectionBounds[index]; - const animationId = `processing-${sessionId}-${index}`; - const existingAnimation = processingAnimations.find( - (animation) => animation.id === animationId, - ); + for (const [sessionId, session] of agentSessions) { + for (let index = 0; index < session.selectionBounds.length; index++) { + const bounds = session.selectionBounds[index]; + const animationId = `processing-${sessionId}-${index}`; + const existingAnimation = processingAnimations.find( + (animation) => animation.id === animationId, + ); - if (existingAnimation) { - updateAnimationTarget(existingAnimation, bounds); - updatedAnimations.push(existingAnimation); - } else { - updatedAnimations.push(createAnimatedBounds(animationId, bounds)); - } + if (existingAnimation) { + updateAnimationTarget(existingAnimation, bounds); + updatedAnimations.push(existingAnimation); + } else { + updatedAnimations.push(createAnimatedBounds(animationId, bounds)); } } + } - processingAnimations = updatedAnimations; - scheduleAnimationFrame(); - }, - ), + processingAnimations = updatedAnimations; + scheduleAnimationFrame(); + }, ); createEffect( - on( - () => props.labelInstances, - (labelInstances) => { - const instancesToProcess = labelInstances ?? []; - - for (const instance of instancesToProcess) { - const boundsToRender = resolveBoundsArray(instance); - const targetOpacity = instance.status === "fading" ? 0 : 1; - - for (let index = 0; index < boundsToRender.length; index++) { - const bounds = boundsToRender[index]; - const animationId = `label-${instance.id}-${index}`; - const existingAnimation = grabbedAnimations.find( - (animation) => animation.id === animationId, - ); + () => props.labelInstances, + (labelInstances) => { + const instancesToProcess = labelInstances ?? []; + + for (const instance of instancesToProcess) { + const boundsToRender = resolveBoundsArray(instance); + const targetOpacity = instance.status === "fading" ? 0 : 1; + + for (let index = 0; index < boundsToRender.length; index++) { + const bounds = boundsToRender[index]; + const animationId = `label-${instance.id}-${index}`; + const existingAnimation = grabbedAnimations.find( + (animation) => animation.id === animationId, + ); - if (existingAnimation) { - updateAnimationTarget(existingAnimation, bounds, targetOpacity); - } else { - grabbedAnimations.push( - createAnimatedBounds(animationId, bounds, { - opacity: 1, - targetOpacity, - }), - ); - } + if (existingAnimation) { + updateAnimationTarget(existingAnimation, bounds, targetOpacity); + } else { + grabbedAnimations.push( + createAnimatedBounds(animationId, bounds, { + opacity: 1, + targetOpacity, + }), + ); } } + } - const activeLabelIds = new Set(); - for (const instance of instancesToProcess) { - const boundsToRender = resolveBoundsArray(instance); - for (let index = 0; index < boundsToRender.length; index++) { - activeLabelIds.add(`label-${instance.id}-${index}`); - } + const activeLabelIds = new Set(); + for (const instance of instancesToProcess) { + const boundsToRender = resolveBoundsArray(instance); + for (let index = 0; index < boundsToRender.length; index++) { + activeLabelIds.add(`label-${instance.id}-${index}`); } + } - grabbedAnimations = grabbedAnimations.filter((animation) => { - if (animation.id.startsWith("label-")) { - return activeLabelIds.has(animation.id); - } - return true; - }); + grabbedAnimations = grabbedAnimations.filter((animation) => { + if (animation.id.startsWith("label-")) { + return activeLabelIds.has(animation.id); + } + return true; + }); - scheduleAnimationFrame(); - }, - ), + scheduleAnimationFrame(); + }, ); - onMount(() => { + onSettled(() => { initializeCanvas(); scheduleAnimationFrame(); @@ -724,7 +714,7 @@ export const OverlayCanvas: Component = (props) => { setupDprMediaQuery(); - onCleanup(() => { + return () => { window.removeEventListener("resize", handleWindowResize); if (currentDprMediaQuery) { currentDprMediaQuery.removeEventListener( @@ -735,7 +725,7 @@ export const OverlayCanvas: Component = (props) => { if (animationFrameId !== null) { nativeCancelAnimationFrame(animationFrameId); } - }); + }; }); return ( diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index 781e3889d..017c7fa49 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -1,4 +1,4 @@ -import { Show, Index } from "solid-js"; +import { Show, For } from "solid-js"; import type { Component } from "solid-js"; import type { AgentSession, ReactGrabRendererProps } from "../types.js"; import { @@ -64,10 +64,11 @@ export const ReactGrabRenderer: Component = (props) => { }} /> - {(session) => ( 0}> @@ -116,7 +117,7 @@ export const ReactGrabRenderer: Component = (props) => { /> )} - + = (props) => { /> - + {(instance) => ( = (props) => { } /> )} - + = ( return activeMenuButton ?? undefined; }; - createEffect(() => { - void props.items; - didPointerMove = false; - }); + createEffect( + () => props.items, + () => { + didPointerMove = false; + }, + ); - createEffect(() => { - const activeMenuItem = getMenuItemByIndex(props.activeIndex); - if (activeMenuItem) { - updateHighlight(activeMenuItem); - } - }); + createEffect( + () => props.activeIndex, + (activeIndex) => { + const activeMenuItem = getMenuItemByIndex(activeIndex); + if (activeMenuItem) { + updateHighlight(activeMenuItem); + } + }, + ); return ( @@ -62,47 +67,52 @@ export const ArrowNavigationMenu: Component = ( class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out" /> - {(item, itemIndex) => ( - - )} + + + {navItem().componentName} + . + + {navItem().tagName} + + + ); + }} diff --git a/packages/react-grab/src/components/selection-label/completion-view.tsx b/packages/react-grab/src/components/selection-label/completion-view.tsx index a1f30a5cb..c2b2e9f55 100644 --- a/packages/react-grab/src/components/selection-label/completion-view.tsx +++ b/packages/react-grab/src/components/selection-label/completion-view.tsx @@ -1,4 +1,4 @@ -import { Show, createSignal, onMount, onCleanup } from "solid-js"; +import { Show, createSignal, onSettled } from "solid-js"; import type { Component } from "solid-js"; import type { CompletionViewProps } from "../../types.js"; import { @@ -142,20 +142,20 @@ export const CompletionView: Component = (props) => { confirmationFocusManager.claim(instanceId); }; - onMount(() => { + onSettled(() => { confirmationFocusManager.claim(instanceId); window.addEventListener("keydown", handleKeyDown, { capture: true }); if (props.supportsFollowUp && props.onFollowUpSubmit && inputRef) { inputRef.focus(); } - }); - onCleanup(() => { - confirmationFocusManager.release(instanceId); - window.removeEventListener("keydown", handleKeyDown, { capture: true }); - if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId); - if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId); + return () => { + confirmationFocusManager.release(instanceId); + window.removeEventListener("keydown", handleKeyDown, { capture: true }); + if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId); + if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId); + }; }); return ( diff --git a/packages/react-grab/src/components/selection-label/discard-prompt.tsx b/packages/react-grab/src/components/selection-label/discard-prompt.tsx index 43db71203..e94848203 100644 --- a/packages/react-grab/src/components/selection-label/discard-prompt.tsx +++ b/packages/react-grab/src/components/selection-label/discard-prompt.tsx @@ -1,4 +1,4 @@ -import { onMount, onCleanup } from "solid-js"; +import { onSettled } from "solid-js"; import type { Component } from "solid-js"; import type { DiscardPromptProps } from "../../types.js"; import { confirmationFocusManager } from "../../utils/confirmation-focus-manager.js"; @@ -30,14 +30,13 @@ export const DiscardPrompt: Component = (props) => { confirmationFocusManager.claim(instanceId); }; - onMount(() => { + onSettled(() => { confirmationFocusManager.claim(instanceId); window.addEventListener("keydown", handleKeyDown, { capture: true }); - }); - - onCleanup(() => { - confirmationFocusManager.release(instanceId); - window.removeEventListener("keydown", handleKeyDown, { capture: true }); + return () => { + confirmationFocusManager.release(instanceId); + window.removeEventListener("keydown", handleKeyDown, { capture: true }); + }; }); return ( diff --git a/packages/react-grab/src/components/selection-label/error-view.tsx b/packages/react-grab/src/components/selection-label/error-view.tsx index 3dea9d749..1e56467fd 100644 --- a/packages/react-grab/src/components/selection-label/error-view.tsx +++ b/packages/react-grab/src/components/selection-label/error-view.tsx @@ -1,4 +1,4 @@ -import { onMount, onCleanup, Show } from "solid-js"; +import { onSettled, Show } from "solid-js"; import type { Component } from "solid-js"; import type { ErrorViewProps } from "../../types.js"; import { confirmationFocusManager } from "../../utils/confirmation-focus-manager.js"; @@ -31,14 +31,13 @@ export const ErrorView: Component = (props) => { confirmationFocusManager.claim(instanceId); }; - onMount(() => { + onSettled(() => { confirmationFocusManager.claim(instanceId); window.addEventListener("keydown", handleKeyDown, { capture: true }); - }); - - onCleanup(() => { - confirmationFocusManager.release(instanceId); - window.removeEventListener("keydown", handleKeyDown, { capture: true }); + return () => { + confirmationFocusManager.release(instanceId); + window.removeEventListener("keydown", handleKeyDown, { capture: true }); + }; }); const hasActions = () => Boolean(props.onRetry || props.onAcknowledge); @@ -51,8 +50,10 @@ export const ErrorView: Component = (props) => { onClick={handleFocus} >
= (props) => { } }; - onMount(() => { + onSettled(() => { resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const rect = entry.target.getBoundingClientRect(); @@ -157,36 +155,42 @@ export const SelectionLabel: Component = (props) => { window.visualViewport?.addEventListener("resize", handleViewportChange); window.visualViewport?.addEventListener("scroll", handleViewportChange); window.addEventListener("keydown", handleGlobalKeyDown, { capture: true }); - }); - - onCleanup(() => { - resizeObserver?.disconnect(); - window.removeEventListener("scroll", handleViewportChange, true); - window.removeEventListener("resize", handleViewportChange); - window.visualViewport?.removeEventListener("resize", handleViewportChange); - window.visualViewport?.removeEventListener("scroll", handleViewportChange); - window.removeEventListener("keydown", handleGlobalKeyDown, { - capture: true, - }); + return () => { + resizeObserver?.disconnect(); + window.removeEventListener("scroll", handleViewportChange, true); + window.removeEventListener("resize", handleViewportChange); + window.visualViewport?.removeEventListener( + "resize", + handleViewportChange, + ); + window.visualViewport?.removeEventListener( + "scroll", + handleViewportChange, + ); + window.removeEventListener("keydown", handleGlobalKeyDown, { + capture: true, + }); + }; }); const elementIdentity = () => `${props.tagName ?? ""}:${props.componentName ?? ""}`; - createEffect(() => { - if (props.isPromptMode && inputRef && props.onSubmit) { - // HACK: Defer focus one tick so the textarea is fully mounted. - const focusTimeout = setTimeout(() => { - if (inputRef) { - inputRef.focus(); - autoResizeTextarea(inputRef, TEXTAREA_MAX_HEIGHT_PX); - } - }, 0); - onCleanup(() => { - clearTimeout(focusTimeout); - }); - } - }); + createEffect( + () => [props.isPromptMode, Boolean(props.onSubmit)] as const, + ([isPromptMode, hasSubmit]) => { + if (isPromptMode && inputRef && hasSubmit) { + // HACK: Defer focus one tick so the textarea is fully mounted. + const focusTimeout = setTimeout(() => { + if (inputRef) { + inputRef.focus(); + autoResizeTextarea(inputRef, TEXTAREA_MAX_HEIGHT_PX); + } + }, 0); + return () => clearTimeout(focusTimeout); + } + }, + ); const positionComputation = createMemo( (previousResult: PositionResult): PositionResult => { @@ -324,11 +328,12 @@ export const SelectionLabel: Component = (props) => { const hadValidBounds = () => positionComputation().hadValidBounds; createEffect( - on( - () => props.selectionLabelShakeCount, - () => setIsShaking(true), - { defer: true }, - ), + () => props.selectionLabelShakeCount, + () => { + setIsShaking(true); + }, + undefined, + { defer: true }, ); const handleKeyDown = (event: KeyboardEvent) => { @@ -460,10 +465,12 @@ export const SelectionLabel: Component = (props) => { >
@@ -518,15 +525,19 @@ export const SelectionLabel: Component = (props) => {
= (props) => {
- {(item, itemIndex) => ( -
- - {item.label} - - - - {formatShortcut(item.shortcut!)} + {(itemAccessor, itemIndex) => { + const cycleItem = () => itemAccessor(); + return ( +
+ + {cycleItem().label} - -
- )} + + + {formatShortcut(cycleItem().shortcut!)} + + +
+ ); + }}
@@ -633,7 +650,7 @@ export const SelectionLabel: Component = (props) => { onKeyDown={handleKeyDown} placeholder="Add context" rows={1} - readOnly={!props.onSubmit} + readonly={!props.onSubmit} />