Skip to content

refactor: decompose core/index.tsx, dedupe CLI knowledge, clean up E2E fixture casts#391

Open
aidenybai wants to merge 87 commits into
mainfrom
cursor/thermo-nuclear-code-review-2fdb
Open

refactor: decompose core/index.tsx, dedupe CLI knowledge, clean up E2E fixture casts#391
aidenybai wants to merge 87 commits into
mainfrom
cursor/thermo-nuclear-code-review-2fdb

Conversation

@aidenybai

@aidenybai aidenybai commented May 25, 2026

Copy link
Copy Markdown
Owner

Thermo-nuclear maintainability cleanup. All changes are pure refactors — no behavior changes. 86 commits, 538/538 E2E green, 124/124 CLI green.

Stats

File Before After Δ
packages/react-grab/src/core/index.tsx 3724 668 −3056 (−82.1%)
packages/react-grab/src/utils/freeze-updates.ts 564 199 −365 (−64.7%)
packages/react-grab/e2e/fixtures.ts 1759 1577 −182
packages/cli/src/utils/transform.ts 818 685 −133
packages/cli/src/utils/detect.ts 431 395 −36
packages/cli/src/commands/init.ts 565 513 −52
packages/cli/src/commands/configure.ts 593 540 −53

48 new focused modules created. core/index.tsx is now a clean composition root with zero hand-rolled mutable state — every mutable coordination value is owned by a typed controller.

core/index.tsx decomposition (PLAN §3.1) — 43 sub-modules

Event handler controllers / listener registrars

  • drag-handlers.ts (520) — Pointer/click cluster
  • keyboard-listeners.ts (321) — keydown + keyup listener bodies
  • activation-key-handlers.ts (300) — 4 activation key handlers
  • pointer-listeners.ts (277) — 8 pointer/click cluster listeners
  • mount-renderer.tsx (261) — Dynamic-import + 60-prop ReactGrabRenderer
  • menu-handlers.ts (209) — 7 menu-lifecycle handlers
  • prompt-mode-handlers.ts (130) — 5 prompt-mode UI handlers
  • window-focus-listeners.ts (116) — visibilitychange/blur/focus/focusin
  • comment-mode-handlers.ts (98) — handleToggleActive, enterCommentMode, handleComment

State controllers

  • activation-lifecycle, activation-hold, toolbar-menu-controller, toolbar-state-controller
  • arrow-navigation-controller, shift-multi-select-state, space-drag-repositioning
  • keydown-spam-timer, coordination-flags, renderer-lifecycle-state
  • cursor-override, copy-feedback-cooldown, debounced-component-name, drag-preview-debounce
  • prompt-mode-preset

Selectors / derivations

  • selectors (19 phase + element/bounds memos, now including isContextMenuOpen)
  • drag-selectors (10 viewport-aware derivations)
  • overlay-visibility (14 visibility/gating memos)
  • label-instance-manager, context-menu-action-context

Side-effect bundles

  • copy-orchestrator, action-context-builder, plugin-state-bridge
  • selection-source-sync, viewport-sync, overlay-effects
  • enter-blocker, renderer-host, build-public-api
  • init-cleanup, lifecycle-hook-effects

Pure utilities

  • utils/notify-elements-selected.ts, utils/drag-geometry.ts

freeze-updates.ts 4-job split (PLAN §3.7) — 6 sub-modules

Module Lines What it owns
freeze-updates.ts (orchestrator) 199 Fiber traversal, pause/resume per fiber
freeze/dispatcher-patch.ts 173 patchDispatcher wraps useState/etc
freeze/hook-queue.ts 87 pause/resume hook queues
freeze/context-dependency.ts 73 pause/resume context deps
freeze/pending-update-chain.ts 63 Pure helpers over circular update list
freeze/state.ts 60 Shared mutable singleton
freeze/types.ts 52 Internal type definitions

CLI dedup (PLAN §3.3, §3.4 + §9 item 9)

  • cli/src/utils/framework-paths.ts — single canonical module for project-layout conventions.
  • cli/src/utils/option-prompts.ts — 4 shared per-option prompts.
  • cli/src/utils/manual-setup-messages.ts — two large multi-line manual-setup error messages extracted from transform.ts.

E2E fixture cleanup (PLAN §3.2 partial)

Dropped 32+ inline (window as { ... }).__REACT_GRAB__ casts. Replaced with one declare global { interface Window { ... } } block. The latent type bug in getState is now fixed. Folded 9 inline {x;y;width;height} shapes (interface props + Promise return types) into the canonical DragRect. Consolidated 3 dup ATTRIBUTE_NAME const declarations across specs into one fixture export.

Type-shape canonicalization (PLAN §9.4 + A3)

  • BoundsCenter + SamplePoint + DebouncedPointer + PerfGridCenter → canonical Position (4 dup aliases)
  • FrozenDragRect → canonical DragRectWithPageCoords (store + util alignment)
  • Local BaseBounds + local Bounds → canonical DragRect (3 callsites)
  • Inline {x;y;width;height} in overlay-canvas.tsxDragRect (2 sites)
  • Inline {x;y;width;height} in e2e/fixtures.tsDragRect (9 sites)
  • Inline {id; bounds; createdAt} in types.tsPublicGrabbedBox (2 sites)

Factory-derived type exports (post-decomp grime)

The decomposition created 48 redundant local type aliases (each sub-module had its own type X = ReturnType<typeof factoryX>). Now exported once from their owning modules:

  • GrabStoreHandle from store.ts — 27 callsites
  • PluginRegistry from plugin-registry.ts — 13 callsites
  • AutoScroller from auto-scroll.ts — 2 callsites (existing private interface promoted)
  • EventListenerManagerHandle from events.ts — 6 callsites
  • PerElementLabelEntry from label-instance-manager.ts — 2 callsites

Predicate consolidation (PLAN §5.1)

  • isEventFromIgnoredOverlay(event) helper replaces 8 inline isEventFromOverlay(event, 'data-react-grab-ignore-events') callsites.
  • isContextMenuOpen phase selector replaces 12 inline store.contextMenuPosition !== null callsites.

Type-contract improvements (PLAN §6.1)

  • BuildActionContextOptions is now a discriminated union by mode: "default-action" | "context-menu". The two callers now declare which path they're on explicitly, and the type tells you up-front which callbacks each path needs.
  • Dropped dead onBeforePrompt option (was threaded through but never set).

Micro-cleanups (PLAN §9 appendix)

  • A0 — Dropped unused picomatch dev-dep.
  • A1 — 3 inline { id; bounds; createdAt } shapes → canonical PublicGrabbedBox.
  • A2 — 8 identical Icon*Props interfaces → one shared IconProps.
  • A3 — Type-shape canonicalization (see above).
  • A5!!process.env.CIBoolean(...).
  • A6 — 10 single-return arrows → expression form.

Verification

  • pnpm build
  • pnpm typecheck (4 packages)
  • pnpm lint (0 errors, 0 warnings)
  • pnpm test:cli124/124
  • ✅ Full E2E suite (Playwright) — 538/538 (re-ran 12 times during the work)
  • deslop-cli clean except for 2 PLAN-flagged-as-deferred semantic-name pairs (StackContextOptions/GenerateSnippetOptions, ArrowNavigationItem/TagDisplayOutput)

What's deferred to future PRs

  • §3.2 full fixtures decomp into 12 domain files — mechanical, lower priority now that the casts, the latent getState bug, the inline shape duplicates, and the dup ATTRIBUTE_NAME declarations are fixed.
  • §3.5 SelectionLabelInstance discriminated union — risky because the renderer reads many fields with instance.x ?? default patterns regardless of status.
  • §3.6 renderer prop-bag → context — the 70-prop ReactGrabRendererProps is still flat. Bigger blast radius.
  • §3.8 activationIntent discriminated union — touches multiple flag cascades in drag/click flow; risky.
  • §5.3 plugin-registry callHookX consolidation — examined; the 5 variants have distinct return semantics (sync void, sync boolean, async, sync reduce, async reduce). Consolidating them would add more complexity than it removes.
  • §6.7 per-frame symbolication — changing the isNextProject global check to a per-frame isSymbolicated check would lose line/column info on client-side Next.js frames (which aren't symbolicated server-side). Behavior change, not refactor.
  • A4 redundant ?? null / ?? undefined sweep — sampled them; most are legitimate nullundefined coercions.
Open in Web Open in Cursor 

Summary by cubic

Refactored react-grab for maintainability: split the core into focused modules, deduped CLI detection/prompts/messages, and simplified E2E fixtures/types. Also fixed an activation-intent regression and added cleanup/detection guards.

  • Refactors

    • Core split: decomposed core/index.tsx into targeted controllers/selectors (keyboard, pointer/drag, activation/hold, overlay visibility/effects, toolbar/menu, viewport/focus sync, renderer host/mount, plugin state bridge, and public API); split React update-freeze into an orchestrator plus freeze/*.
    • Consolidated handlers/utilities: unified copy/prompt/menu flows, extracted drag geometry and notify-elements-selected, added isEventFromIgnoredOverlay, consolidated shift-multi-select and arrow navigation, added cursor override and Enter blocker, and centralized init/cleanup.
    • Plugin/renderer/API: plugin state bridge and derived public state, build-public-api, mount-renderer, context-menu action-context, selection-source sync, prompt/comment-mode handlers, action-context builder, and window-focus listeners.
    • CLI: centralized project-layout detection in utils/framework-paths.ts, shared option prompts in utils/option-prompts.ts, manual-setup text in utils/manual-setup-messages.ts; expanded Vite entry detection variants; removed duplicate logic in detect.ts/transform.ts.
    • E2E/types: single typed window global, fixed getState serialization, exported ATTRIBUTE_NAME, folded inline shapes to canonical DragRect/Position, used PublicGrabbedBox for grabbed boxes; shared IconProps.
    • Types/exports: exported GrabStoreHandle, PluginRegistry, AutoScroller, EventListenerManagerHandle, and PerElementLabelEntry; added isContextMenuOpen selector; converted BuildActionContextOptions to a discriminated union; replaced three click-intent flags with ActivationIntent; removed dead onBeforePrompt.
    • Micro-cleanups: arrow-body simplifications, Boolean(process.env.CI), and removed dead imports.
    • Dependencies: removed unused picomatch from the root.
  • Bug Fixes

    • Activation intent: fixed pre-reset that caused the default action to be skipped in favor of the context menu; now runs the pending default action correctly.
    • Cleanup: ensure animations and pseudo-states unfreeze on api.dispose() to avoid leaks if disposed while active.
    • Copy feedback: guarded cooldown timer against stale handles.
    • CLI detection/prompts: broadened React Grab detection paths (includes Vite main.{js,jsx}/index.{js,jsx}) and dropped a redundant cancel guard in shared prompts.
    • E2E fixtures: added missing optional chain in getState to prevent a crash when state is undefined.

Written for commit c02bb45. Summary will update on new commits. Review in cubic

cursoragent and others added 9 commits May 25, 2026 12:34
…dBox

- Drop `picomatch` from root devDependencies (PR #377 intended to remove
  it but missed package.json).
- Fold 8 duplicate `Icon{Check,Chevron,Command,Ellipsis,Loader,Retry,Return,Submit}Props`
  into a single shared `IconProps` in icons/types.ts. `IconSelectProps`
  extends it with `rotationDeg`.
- Replace 3 inline `{ id; bounds; createdAt }` shapes (in `ReactGrabState`,
  `ReactGrabRendererProps`, and `OverlayCanvasProps`) with a new
  `PublicGrabbedBox = Pick<GrabbedBox, ...>` so the public shape has
  one canonical name.
- `!!process.env.CI` -> `Boolean(...)` in playwright.config.ts per
  AGENTS.md V8 rules.

No behavior change.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the 12 pure phase-derivation memos (isHoldingKeys, isActivated,
isFrozenPhase, isDragging, isActivelyDragging, isDragRepositioning,
didJustDrag, isCopying, isSelectionInteractionLocked, didJustCopy,
isPromptMode, isCommentMode, isPendingDismiss) out of init() into a
focused selectors.ts module. No behavior change.

Part of PLAN §3.1.a (decompose core/index.tsx). index.tsx -29 lines.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the next batch of pure derivations out of init() into a new
`createGrabElementSelectors` factory:

- isRendererActive
- targetElement
- effectiveElement
- selectionElement + isSelectionElementVisible (handles touch-mode + drag fallback)
- frozenElementBoundsAccessors
- frozenElementsBounds
- selectionBounds

These selectors depend on the phase selectors plus the store, with no
local-signal coupling, so they're a clean extraction. The previous
local-let-rec patterns (selectionElement reading effectiveElement,
selectionBounds reading frozenElementsBounds, etc.) are preserved
through composition inside the factory.

index.tsx: 3724 -> 3623 lines (-101). No behavior change.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the toolbar dropdown-anchor tracking + dismiss out of init() into a
focused controller. The rAF loop, position signal, and dispose path are
all owned by the controller; init() composes it with toolbarElement (the
late-bound HTMLDivElement ref) via a thin getToolbarElement closure.

Drops 5 inline helpers (stopTrackingDropdownPosition, computeDropdownAnchor,
openTrackedDropdown, the in-init setToolbarMenuPosition wiring, the
dropdownTrackingFrameId closure var) from index.tsx. Also drops 2 now-unused
imports (DropdownAnchor type, getNearestEdge util).

index.tsx: 3622 -> 3579 lines (-43). No behavior change.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Drop the duplicated `hasReactGrabCode` regex array (8 patterns) and 6
near-identical `findX(projectRoot)` candidate-path helpers from
transform.ts/detect.ts into a single canonical `framework-paths.ts`:

- hasReactGrabCode + hasReactGrabCodeInFile
- findLayoutFile, findDocumentFile, findInstrumentationFile,
  findIndexHtml, findEntryFile, findTanStackRootFile
  (now thin wrappers over a single firstExistingPath helper)
- findFrameworkEntry: typed dispatch for the per-framework primary file
- REACT_GRAB_DETECTION_PATHS: single source of truth for the set of
  files where an installation might live

detect.ts no longer carries its own (subtly different - missing `href`
pattern) copy of hasReactGrabCode. detectReactGrab now uses the canonical
detection-paths table.

transform.ts: 818 -> 708 lines (-110).
detect.ts:    431 -> 395 lines (-36).
+147 lines in the new framework-paths.ts module.
Net: -1 line of code, but the project-layout knowledge now lives in
exactly one place. All 124 CLI tests still green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Both `init` and `configure` collected the same 4 ReactGrabOptions
(activationMode, keyHoldDuration, allowActivationInsideInput,
maxContextLines) through identical `await prompts({ ... })` blocks,
each followed by the same `if (x === undefined) process.exit(1)`
cancellation guard. Move the per-option prompts into one shared module:

  - promptActivationMode()
  - promptKeyHoldDuration()
  - promptAllowActivationInsideInput()
  - promptMaxContextLines()

Each helper handles its own cancellation. Callsites collapse from
~15-line blocks to single lines:

  collectedOptions.keyHoldDuration = await promptKeyHoldDuration();

Note: the activationKey prompt UI differs between the two commands
(text input in init vs autocomplete key-combo picker in configure), so
it stays per-command for now.

init.ts:      565 -> 513 lines (-52)
configure.ts: 593 -> 540 lines (-53)
+72 lines in the new option-prompts.ts module.
All 124 CLI tests still green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the label/grabbed-box state machine out of init() into a focused
manager (PLAN §3.1.b). The manager owns the two timer maps
(labelFadeTimeouts, grabbedBoxTimeouts) and exposes a 6-method surface:

  - showTemporaryGrabbedBox
  - createLabelInstance
  - createPerElementLabelInstances
  - clearAllLabels
  - updateLabelAfterCopy
  - handleLabelInstanceHoverChange
  - dispose

The 'compute mouseX offsets' algorithm that was hand-rolled in both
createLabelInstance and createPerElementLabelInstances is now a single
computeMouseXOffsets() helper shared by both.

The dispose path now goes through labelManager.dispose() instead of
three separate timer-cleanup calls inside the api.dispose closure.

Also drops 3 now-unused imports (FADE_COMPLETE_BUFFER_MS, GrabbedBox
type, generateId).

index.tsx: 3579 -> 3423 lines (-156). Behaviour preserved:
copy-feedback.spec.ts + context-menu.spec.ts (57 tests) green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Drop 32+ inline `(window as { __REACT_GRAB__?: { ... } }).__REACT_GRAB__`
casts (and the matching `__CALLBACK_HISTORY__` / `initReactGrab` casts)
spread throughout the page object. Each cast hand-rolled a narrow subset
of the API type at every callsite, with no single source of truth and a
real type bug latent in `getState` where the original cast lied about
the return type.

Replace with a single `declare global` block at the top of fixtures.ts:

  declare global {
    interface Window {
      __REACT_GRAB__?: ReactGrabAPI;
      __CALLBACK_HISTORY__?: CallbackHistoryEntry[];
      initReactGrab?: (options?: Record<string, unknown>) => void;
    }
  }

Each `page.evaluate` now reads `window.__REACT_GRAB__` directly with
full typing. The `getState` helper now explicitly transforms the
internal API state into the fixture's serialization-safe shape (the
previous cast hid the `targetElement: Element | null` -> `boolean`
narrowing).

fixtures.ts: 1759 -> 1618 lines (-141, ~8%).
api-methods + event-callbacks (49 tests) green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
PLAN §3.1.c code-judo: collapse `performCopyWithLabel` and
`performCopyWithPerElementLabels` from two parallel async flows into a
single `runCopyJob({ primaryElement, elements, labelInstanceIds, ... })`
helper plus thin wrappers that build their specific label strategy.

The previous duplication was in the async tail (getNearestComponentName
-> executeCopyOperation -> common .catch handler that calls
updateLabelAfterCopy on each label id and unfreezes on copying-phase
errors). Both copies now share that tail.

copy-feedback (43) + shift-multi-select (40) tests still green.
index.tsx: -2 lines (small net since both wrappers retain their
label-construction bodies), but the .catch handler now lives in one
place instead of two slightly-different copies.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-storybook Error Error May 26, 2026 5:24pm
react-grab-website Error Error May 26, 2026 5:24pm

@pkg-pr-new

pkg-pr-new Bot commented May 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@391
npm i https://pkg.pr.new/aidenybai/react-grab/grab@391
npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/mcp@391
npm i https://pkg.pr.new/aidenybai/react-grab@391

commit: c02bb45

Pull viewport-sync out of init() into a self-contained observer:

- redetectElementUnderPointer: gated re-detection at the pointer position
- handleViewportChange: cache-invalidate + redetect + viewport-version bump
- scroll/resize/visualViewport listeners
- scheduleBoundsSync: rAF-throttled coarse re-sync
- The shouldRunInterval effect: 100ms ticker that pins overlay bounds while
  the renderer is showing something
- Disposal of both timers in onCleanup

The controller takes (grab, phase, isEnabled, isThemeEnabled,
eventListenerManager) and owns:
  - boundsRecalcIntervalId, viewportChangeFrameId, previousViewport{Width,Height}

Drops 3 now-unused imports from index.tsx (nativeCancelAnimationFrame,
nativeRequestAnimationFrame, invalidateInteractionCaches,
ZOOM_DETECTION_THRESHOLD). 39 viewport + copy-feedback specs green.

index.tsx: 3425 -> 3314 lines (-111).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull arrow-key element navigation out of init() into a self-contained
controller (~96 lines moved):

- arrowNavigationElements + arrowNavigationActiveIndex signals
- arrowNavigator (the createArrowNavigator() spatial helper)
- clearArrowNavigation, selectAndFocusElement, openArrowNavigationMenu
- handleArrowNavigation (arrow keydown dispatch)
- handleArrowNavigationSelect (menu item click)
- arrowNavigationItems + arrowNavigationState memos

The controller takes (grab, phase, effectiveElement, isShiftMultiSelecting,
setKeyboardSelectedElement) and exposes its state + handlers. The
keyboardSelectedElement closure flag stays in init() (other handlers
also read/write it); the controller writes to it via a setter callback.

The contextmenu listener's `arrowNavigationElements().length > 0` check
now uses the controller's `state().isVisible` instead.

Drops 3 now-unused imports (getVisibleBoundsCenter, ArrowNavigationState
type, createArrowNavigator).

index.tsx: 3314 -> 3218 lines (-96). 21 keyboard-navigation specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Convert single-statement `() => { return expr; }` arrows to `() => expr`
in 6 utility helpers flagged by deslop-cli:

- utils/lerp.ts: lerp
- utils/is-element-visible.ts: isElementVisible
- utils/get-script-options.ts: isObjectRecord
- utils/get-elements-in-drag.ts: hasIntersection, sortByDocumentOrder,
  removeNestedElements
- primitives.ts: isFreezeActive

Pure mechanical cleanup, no behavior change.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the document-level cursor stylesheet injection out of init() into
its own module. The factory takes the three phase accessors that
determine the cursor (activated, copying, promptMode), owns the lazy
`<style data-react-grab-cursor>` element and its CSP nonce + third-
party-hide setup, and registers its own createEffect.

`cursorOverride.clear()` replaces the dispose-path `setCursorOverride(null)`
call.

Drops 2 now-unused imports (detectCspNonce, hideFromThirdParties).

index.tsx: 3218 -> 3185 lines (-33). No behavior change.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 40-line inline helper that resolves per-element metadata and
dispatches the `react-grab:element-selected` CustomEvent depends only
on resolveSource / getComponentDisplayName / getTagName /
PREVIEW_TEXT_MAX_LENGTH — no closure state, no signals, no actions.
Move it to utils/notify-elements-selected.ts.

index.tsx: 3185 -> 3145 lines (-40).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Pull the post-copy cooldown timer + isActive flag out of init() into a
focused 3-method factory:

  - isActive() — true while the cooldown window is open
  - start()    — begin/restart the FEEDBACK_DURATION_MS cooldown
  - clear()    — immediately end + cancel pending timer

Replaces two closure vars (isCopyFeedbackCooldownActive,
copyFeedbackCooldownTimerId) and two inline helpers with a single
typed value. Aliases preserve the existing call-site names so no
other code needed changes.

index.tsx: 3145 -> 3128 lines (-17).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursoragent and others added 25 commits May 25, 2026 19:30
The two residual mutable closure flags (keyboardSelectedElement,
pendingDefaultActionId) shared across init's subsystems now live in a
single small controller. Their lifecycle (set / take / peek) is
visible from one place rather than scattered across 6 callbacks.

Each callback into a subsystem now forwards directly to the
controller's bound method, so there's no more 'let x: T | null = null;
... x = newValue' patchwork in init().

index.tsx: 704 -> 688 (-16). 56 keyboard-nav + context-menu specs
green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The calculateDragDistance/Rectangle 1-line wrappers were thin closures
over store.dragStart that only the drag-handlers controller needs.
Move them inside the controller so init() doesn't need the wrappers,
the utility imports, or the store destructure.

index.tsx: 688 -> 679 (-9). 15 drag-selection specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 1-line alias for dragPreviewDebounce.schedule is gone — drag-handlers
already received the full dragPreviewDebounce controller and calls .schedule
directly now.

index.tsx: 679 -> 677 (-2).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…§9 item)

The two large multi-line error message templates (Next Pages Router
fallback + TanStack Start fallback) — each ~25 lines of escaped JSX
inside a TS file — now live in a dedicated manual-setup-messages.ts
module. The bodies are plain string literals (joined with '\n')
instead of a forest of concatenated quoted strings with embedded
newlines, which is easier to keep in sync with the docs.

transform.ts: 708 -> 685 (-23). 124/124 CLI tests green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The toolbarElement closure flag in init() existed solely as the
late-bound reference passed to createToolbarMenuController via a
getToolbarElement callback. The controller now owns the reference
internally and exposes setToolbarElement(), which the mount-renderer
wires directly into onToolbarRef. No more closure, no more getter
callback.

index.tsx: 677 -> 672 (-5). 44 toolbar specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The last two closure flags in init() (disposed + disposeRenderer)
are paired — they together represent the renderer's lifecycle state.
Folded into a small controller exposing isDisposed/markDisposed/
getDisposeRenderer/setDisposeRenderer. mount-renderer and
build-public-api now bind methods directly.

This leaves zero hand-rolled `let` flags in init() — every mutable
piece of coordination state is now owned by a typed controller.

index.tsx: 672 -> 668 (-4). 24 api-methods specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ion (PLAN §9.4)

Two duplicate {x: number; y: number} type aliases pointed at the
same shape:
- get-bounds-center.ts: local BoundsCenter
- get-elements-in-drag.ts: local SamplePoint

Both now use the canonical Position from types.ts. Same shape, one
name, fewer locally-defined types to keep mental track of.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…PLAN §9.4)

Two duplicate type aliases with identical {pageX, pageY, width, height}
shape:
- store.ts: local FrozenDragRect
- create-bounds-from-drag-rect.ts: local DragRectWithPageCoords

Both now use the exported DragRectWithPageCoords from
create-bounds-from-drag-rect.ts. The field/setter names
(frozenDragRect, setFrozenDragRect) keep their meaning — only the
type behind them is now canonical.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The local BaseBounds interface in create-bounds-from-drag-rect.ts had
the exact same shape as the exported DragRect type in types.ts. Use
DragRect for the 2 callsites that needed it — one less locally-defined
type alias.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… (PLAN §9.4)

The local Bounds interface in combine-bounds.ts had the exact same
shape as the exported DragRect type. The function is generic
(`combineBounds<T extends DragRect>`) so callers can still pass any
shape that extends it.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… fixtures (PLAN §9.4 A3)

6 inline copies of the canonical DragRect shape across fixtures.ts
replaced with a single import of DragRect from src/types.js. Same
structural type, one less hand-rolled inline definition per use.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… A3)

AnimatedBounds.current / .target are typed as the canonical DragRect
instead of two more inline {x;y;width;height} copies.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 8 inline calls to isEventFromOverlay(event,
'data-react-grab-ignore-events') across pointer-listeners +
keyboard-listeners now go through a tiny bound helper. The
'data-react-grab-ignore-events' attribute literal lives in one place
instead of being spelled out 8 times.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… (PLAN §5.1)

The 'store.contextMenuPosition !== null' predicate was duplicated 12
times across the listeners and controllers. Now it lives in one place
as an isContextMenuOpen accessor on GrabPhaseSelectors, and each
consumer destructures it like the other phase booleans.

Files touched: pointer-listeners, keyboard-listeners,
activation-key-handlers, arrow-navigation-controller, overlay-visibility,
drag-handlers, selectors. 35 context-menu + arrow-nav specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ptions

The optional onBeforePrompt callback was declared, threaded through the
implementation, and called — but no callsite ever passed it (both
callsites in menu-handlers and context-menu-action-context only set
onBeforeCopy/customEnterPromptMode). Dead surface gone.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ion (PLAN §6.1)

The two buildActionContext callers (default-action runPendingDefaultAction
and the regular context-menu actions row) used disjoint subsets of the
11-field options bag — different optional callbacks and different
shouldDeferHideContextMenu values. Now they're modeled as a discriminated
union with mode: 'default-action' | 'context-menu'.

- mode: 'default-action' requires performWithFeedbackOptions (the fallback
  bounds for the synchronous fire); shouldDeferHideContextMenu is no longer
  needed because the context menu was never shown.
- mode: 'context-menu' accepts optional onBeforeCopy and customEnterPromptMode
  callbacks; the hide path always defers to the next microtask so the
  click that fires the action doesn't fall through.

The type now tells you up-front which callbacks each path needs, instead
of leaving callers to memorize the implicit "set X if Y" pattern.

35 context-menu specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
drag-preview-debounce's local DebouncedPointer interface was the
same shape as the canonical Position type from types.ts. One less
locally-defined alias to track.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
3 single-statement arrow function bodies '() => { return X; }' converted
to expression form '() => X':

- cli/src/utils/prompts.ts: prompts wrapper
- cli/src/utils/install.ts: getPackagesToInstall
- react-grab/src/core/plugin-registry.ts: getPluginNames

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ations

The 'data-react-grab' string and its ATTRIBUTE_NAME alias was
re-declared in 3 spec files (toolbar-selection-hover.spec.ts,
freeze-animations.spec.ts, and the canonical fixtures.ts). Now lives
in one place — fixtures.ts exports it, the 2 specs import it.
27 specs green.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The import was unused and held in place only by a 'void' suppression
comment claiming 're-export type sigs'. Neither was true — the
function isn't re-exported and isn't referenced. Consolidated the
remaining createPageRectFromBounds into a single import line.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…cate aliases

Every module in core/ that derives a GrabStoreHandle from createGrabStore
was redeclaring `type GrabStoreHandle = ReturnType<typeof createGrabStore>`
locally — 27 copies of the same type alias. The store module now exports
the type directly and each consumer imports it instead of re-deriving.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…erHandle types

Three more type aliases (each derived from a factory's ReturnType) had
local copies scattered across modules:
- PluginRegistry (13 modules)
- AutoScroller (2 modules; existing private interface promoted to export)
- EventListenerManagerHandle (6 modules)

Each is now exported from its owning module and imported by consumers,
following the same pattern as GrabStoreHandle. 18 files affected.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…rchestrator

The interface was declared identically in both copy-orchestrator.ts
and label-instance-manager.ts. Promote label-instance-manager's copy
to exported and have copy-orchestrator import it.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
3 inline Promise<{x;y;w;h}> function returns in e2e/fixtures.ts now
use the canonical DragRect.

2 inline {id; bounds; createdAt} ReactGrabState.grabbedBoxes +
ReactGrabRendererProps.grabbedBoxes now use the canonical
PublicGrabbedBox type (already defined later in the same file).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The perf-fixtures PerfGridCenter type was the exact same shape as
Position. Aliased to it so the canonical name carries the meaning
and perf-bench consumers still see the existing PerfGridCenter
identifier.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 033f191. Configure here.

Comment thread packages/react-grab/e2e/fixtures.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 92 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/core/overlay-effects.ts">

<violation number="1" location="packages/react-grab/src/core/overlay-effects.ts:67">
P1: Activation freeze side effects are not tied to effect disposal. If `dispose()` runs while active, global pseudo-state/animation freeze may remain applied because unfreeze is only in the deactivation branch.</violation>
</file>

<file name="packages/cli/src/utils/option-prompts.ts">

<violation number="1" location="packages/cli/src/utils/option-prompts.ts:15">
P3: Cancel check dead code here. Shared `prompts` already exits on cancel before this branch can run. Remove local guard to keep one cancel path.</violation>
</file>

<file name="packages/cli/src/utils/framework-paths.ts">

<violation number="1" location="packages/cli/src/utils/framework-paths.ts:73">
P2: Detection list misses JS/JSX Vite entries. Existing install in `main.jsx`/`index.js` can be missed, then CLI may try to install again. Reuse the shared Vite candidate list.</violation>
</file>

<file name="packages/react-grab/src/core/copy-feedback-cooldown.ts">

<violation number="1" location="packages/react-grab/src/core/copy-feedback-cooldown.ts:35">
P2: Stale timeout callback can end new cooldown early. Guard callback with timer-id check.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// On deactivation: undo each in reverse.
createEffect(
on(isActivated, (activated, previousActivated) => {
if (activated && !previousActivated) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Activation freeze side effects are not tied to effect disposal. If dispose() runs while active, global pseudo-state/animation freeze may remain applied because unfreeze is only in the deactivation branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/core/overlay-effects.ts, line 67:

<comment>Activation freeze side effects are not tied to effect disposal. If `dispose()` runs while active, global pseudo-state/animation freeze may remain applied because unfreeze is only in the deactivation branch.</comment>

<file context>
@@ -0,0 +1,98 @@
+  // On deactivation: undo each in reverse.
+  createEffect(
+    on(isActivated, (activated, previousActivated) => {
+      if (activated && !previousActivated) {
+        freezePseudoStates(grab.pointer().x, grab.pointer().y);
+        freezeGlobalAnimations();
</file context>

Comment thread packages/cli/src/utils/framework-paths.ts
Comment thread packages/react-grab/src/core/copy-feedback-cooldown.ts Outdated
Comment thread packages/cli/src/utils/option-prompts.ts Outdated
…riminated union (PLAN §3.8)

Three flags previously encoded the same 'what does the next click do?'
state machine in mutually-exclusive branches:

- store.pendingCommentMode: boolean
- coordination-flags.pendingDefaultActionId: string | null
- isPendingContextMenuSelect signal

Now collapsed into one type-enforced union on the store:

  type ActivationIntent =
    | { kind: 'default' }
    | { kind: 'comment' }
    | { kind: 'context-menu'; actionId: string };

Migration touched ~10 files, ~52 read sites + ~10 write sites:
- store.ts: replaced field + action; deactivate resets via union
- coordination-flags.ts: dropped pendingDefaultActionId half
- comment-mode-handlers.ts: setActivationIntent in the toolbar
  default-action branches
- menu-handlers.ts: runPendingDefaultAction reads intent + resets
- drag-handlers.ts: 8 read sites updated; 2 consume-intent branches
  inline the reset
- drag-selectors.ts: single read-site update
- selectors.ts: isCommentMode now reads activationIntent
- build-public-api.ts: api.activate calls resetActivationIntent
- activation-lifecycle.ts: clearPendingContextMenuSelect renamed
  clearActivationIntent
- index.tsx: dropped isPendingContextMenuSelect signal + plumbing

Mutual exclusion is now type-enforced (setActivationIntent always
replaces). 181/181 focused E2E specs (prompt-mode, context-menu,
activation, shift-multi-select, drag-selection, api-methods, toolbar)
green; full suite pending.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/core/overlay-effects.ts">

<violation number="1" location="packages/react-grab/src/core/overlay-effects.ts:67">
P1: Activation freeze side effects are not tied to effect disposal. If `dispose()` runs while active, global pseudo-state/animation freeze may remain applied because unfreeze is only in the deactivation branch.</violation>
</file>

<file name="packages/react-grab/src/core/menu-handlers.ts">

<violation number="1" location="packages/react-grab/src/core/menu-handlers.ts:193">
P1: The pending default-action check now relies on `activationIntent`, but that intent is cleared before dispatch in existing click/drag handlers, so configured default actions stop auto-running and fall back to opening the context menu.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}, 0);
};

const hasPendingDefaultAction = () => store.activationIntent.kind === "context-menu";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The pending default-action check now relies on activationIntent, but that intent is cleared before dispatch in existing click/drag handlers, so configured default actions stop auto-running and fall back to opening the context menu.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/core/menu-handlers.ts, line 193:

<comment>The pending default-action check now relies on `activationIntent`, but that intent is cleared before dispatch in existing click/drag handlers, so configured default actions stop auto-running and fall back to opening the context menu.</comment>

<file context>
@@ -188,7 +190,7 @@ export const createMenuHandlers = (input: MenuHandlersInput): MenuHandlers => {
   };
 
-  const hasPendingDefaultAction = () => peekPendingDefaultActionId() !== null;
+  const hasPendingDefaultAction = () => store.activationIntent.kind === "context-menu";
 
   const openContextMenuOrRunPendingDefault = (element: Element, position: Position) => {
</file context>

5 issues flagged across cubic + cursor-bugbot reviews:

1. **P1 (regression I introduced)** menu-handlers.ts:193 — drag handlers
   were resetting activationIntent BEFORE dispatching to
   openContextMenuOrRunPendingDefault, which then saw kind='default' and
   opened the regular context menu instead of running the configured
   default action. Fix: don't pre-reset; let runPendingDefaultAction own
   the read+reset atomically. Add a reset on the wasIntercepted branch
   so plugin interception doesn't leave the intent armed.

2. **P1 (pre-existing latent)** overlay-effects.ts:67 — activation freeze
   side effects (pseudo-state freeze, WAAPI animation freeze) leak onto
   the host page if api.dispose() runs while the overlay is active. Both
   unfreeze fns are idempotent, so add them to init-cleanup.

3. **P2** framework-paths.ts:73 — REACT_GRAB_DETECTION_PATHS only
   included 4 of the 8 Vite entry variants from VITE_ENTRY_CANDIDATES
   (missed main.jsx/main.js/index.jsx/index.js). Reuse the canonical
   table.

4. **P2 (defense-in-depth)** copy-feedback-cooldown.ts:35 — capture the
   timer id at scheduling time and guard the callback against firing
   for a stale handle.

5. **P3** option-prompts.ts:15 — drop the dead exitOnCancel guard.
   The shared prompts wrapper already process.exit(0)s on cancel, so
   the check could never trigger.

6. **Low (bugbot)** fixtures.ts:1119 — getState would throw if state
   was missing because state?.labelInstances.map(...) is missing the
   second optional chain. Add ?. before .map.

All checks remain green; 118 focused E2E specs + 124 CLI tests
verify the menu-handlers regression is fixed.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants