Skip to content

feat: recent#166

Open
aidenybai wants to merge 29 commits intomainfrom
recent
Open

feat: recent#166
aidenybai wants to merge 29 commits intomainfrom
recent

Conversation

@aidenybai
Copy link
Owner

@aidenybai aidenybai commented Feb 8, 2026

Note

Medium Risk
Large UI/interaction refactor across the overlay toolbar and keyboard handlers; risk is mainly regressions in dragging/snap behavior and shortcut/event handling rather than data/security concerns.

Overview
Adds richer History (Recent) interactions: the dropdown UI is redesigned with icon actions + tooltips, empty-state messaging, and full keyboard navigation (ArrowUp/Down highlight + Enter select), plus a new R shortcut to open it while the overlay is active. Clearing is renamed/rewired to clearRecentItems/onRecentClearAll, the dropdown is dismissed on overlay deactivation, and overlay keyboard handling now defers Enter/Escape/arrow-navigation when history is open.

Refactors the toolbar drag/dock system to support bottom snapping, side docking with vertical layout, dock previews, and release/transition animations; exposes data-react-grab-toolbar-orientation for tests and reads persisted edge state for snap assertions. Updates icons (new IconCopy/IconTrash, revised IconSelect/IconInbox, active-capable IconComment), tweaks unread indicator detection in e2e, and switches overlay/site typography to Inter with improved font smoothing and a new animate-toolbar-dock-shift animation.

Written by Cursor Bugbot for commit 730303a. This will update automatically on new commits. Configure here.


Summary by cubic

Adds a “Recent” history with a toolbar inbox for up to 20 past copies, plus a refined draggable toolbar with dock previews, release animations, side‑vertical layouts, bottom snap, and a new active accent color. Updates dark‑mode icon styling and switches to Inter with improved font smoothing across the overlay and site.

  • New Features

    • Toolbar “Recent” button with unread state; shows only when items exist. Dropdown clamps to viewport; closes on Escape/outside/context menu/overlay off; keyboard navigation and tooltips.
    • Items show tag/component and relative time; hover highlight with comment preview; Enter to select; click to re‑copy or reopen comments. “Copy all” and “Clear”; persists across activations.
    • Draggable toolbar: dock preview and release animation with more prominent transitions; side‑vertical layout; bottom snap. Updated active accent and new copy/trash/comment icons for dark UI.
  • Bug Fixes

    • Prevent opening when empty; refine hover styles/radius; fix dropdown radius and reliable “Clear”. Fallback copy for disconnected comment items.
    • Unread indicator uses hasUnreadRecentItems; freeze state checks isHistoryOpen; unread icon visible on dark toolbar. Dismiss recents on overlay deactivation.
    • Clear snap timeout on drag start and reset isSnapping to avoid stuck snaps. Restore button animations with transition‑all; fix toggle drift and missed clicks.

Written for commit 730303a. Summary will update on new commits.

@vercel
Copy link

vercel bot commented Feb 8, 2026

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

Project Deployment Actions Updated (UTC)
react-grab-website Ready Ready Preview, Comment Feb 9, 2026 7:32pm

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Solid feature implementation with good UX patterns. Found 3 critical issues: memory leak in recentElementMap, potential ID collision in rapid operations, and missing sync between storage and state on item removal. Review comments below detail each concern and suggest fixes.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

import { MAX_RECENT_ITEMS } from "../constants.js";
import type { RecentItem } from "../types.js";

let recentItems: RecentItem[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Memory leak risk: This module-level variable persists for the entire application lifetime. The recentElementMap in core/index.tsx stores DOM element references keyed by these item IDs. When elements are removed from the DOM but still referenced in recentElementMap, they cannot be garbage collected.

Problem scenario:

  1. User copies element A (stored in recentElementMap)
  2. Element A is removed from DOM
  3. recentItems still contains the item (up to 20 items retained)
  4. recentElementMap still holds the detached element reference
  5. Element cannot be GC'd → memory leak

Suggested fix: Implement a cleanup mechanism that periodically removes disconnected elements from recentElementMap, or use WeakMap if the reference pattern allows.

Comment on lines 6 to 7
const generateRecentItemId = (): string =>
`recent-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

ID collision risk: Date.now() returns milliseconds. If addRecentItem() is called multiple times within the same millisecond (possible with rapid copy operations or programmatic calls), the random suffix (7 chars) is the only uniqueness guarantee.

Probability: With base-36 encoding and 7 characters, collision probability is ~1 in 78 billion per millisecond window. However:

  • Multiple rapid copies in the same millisecond are plausible
  • The deduplication logic (lines 690-704 in core/index.tsx) relies on unique IDs to map elements
  • A collision would cause recentElementMap.set() to overwrite the wrong element mapping

Suggested fix: Add a monotonic counter as a tie-breaker: recent-${Date.now()}-${counter++}-${Math.random()...}

Comment on lines 690 to 704
Copy link
Contributor

Choose a reason for hiding this comment

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

State synchronization issue: This deduplication logic calls removeRecentItem() which updates the module-level recentItems array in recent-storage.ts, but doesn't call setRecentItems() to update the local reactive state.

Problem: The recentItems() signal used throughout this component (line 689, 716, 3308, 3349, 3363, 3390, 3527) won't reflect the removal. This causes:

  1. Dropdown shows stale items until next copy operation
  2. Hover handlers may reference wrong elements
  3. "Copy all" includes deduplicated items

Suggested fix:

if (shouldDedup) {
  const updated = removeRecentItem(existingItemId);
  setRecentItems(updated);
  recentElementMap.delete(existingItemId);
  break;
}

Comment on lines 125 to 133
// HACK: Delay mousedown/touchstart listener to avoid catching the triggering click
const frameId = requestAnimationFrame(() => {
window.addEventListener("mousedown", handleClickOutside, {
capture: true,
});
window.addEventListener("touchstart", handleClickOutside, {
capture: true,
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Timing edge case: The requestAnimationFrame delay avoids catching the triggering click, but creates a race condition:

  1. User clicks "Recent" button at T=0
  2. Dropdown becomes visible at T=0
  3. onMount runs at T=0
  4. Event listeners are added at T=16ms (next frame)
  5. If user clicks elsewhere between T=0 and T=16ms, no dismissal occurs

Impact: Low probability but possible on fast interactions. The dropdown would stay open until the next click.

Alternative approach: Check event timestamp and ignore events within a threshold of mount time, eliminating the rAF delay:

const mountTime = Date.now();
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
  if (Date.now() - mountTime < 50) return; // Ignore first 50ms
  // ... rest of handler
};
window.addEventListener('mousedown', handleClickOutside, { capture: true });

Comment on lines 3321 to 3393
const handleRecentItemSelect = (item: RecentItem) => {
dismissRecentDropdown();

const element = recentElementMap.get(item.id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing element lifecycle check: When selecting a recent item, the code retrieves the element from recentElementMap but doesn't verify it still exists in the map before use.

Scenario:

  1. Recent item exists in dropdown
  2. User calls handleRecentClear() (line 3398) which clears recentElementMap
  3. Simultaneously/immediately after, user's click on an item triggers handleRecentItemSelect()
  4. recentElementMap.get(item.id) returns undefined
  5. Code proceeds with element being undefined

Current protection: Line 3324 checks isElementConnected(element) which handles undefined gracefully (returns false). Line 3332 also checks. So this is defensive, not broken.

Recommendation: Consider making the element retrieval more explicit for clarity:

const element = recentElementMap.get(item.id);
if (!element || !isElementConnected(element)) return;

This makes the intentionality clear to future maintainers.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 8, 2026

Open in StackBlitz

@react-grab/cli

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

grab

npm i https://pkg.pr.new/aidenybai/react-grab/grab@166

@react-grab/ami

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/ami@166

@react-grab/amp

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/amp@166

@react-grab/claude-code

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/claude-code@166

@react-grab/codex

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/codex@166

@react-grab/cursor

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cursor@166

@react-grab/droid

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/droid@166

@react-grab/gemini

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/gemini@166

@react-grab/opencode

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/opencode@166

react-grab

npm i https://pkg.pr.new/aidenybai/react-grab@166

@react-grab/relay

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/relay@166

@react-grab/utils

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/utils@166

commit: 730303a

Copy link
Contributor

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

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

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/react-grab/src/core/index.tsx">

<violation number="1" location="packages/react-grab/src/core/index.tsx:716">
P2: Prune `recentElementMap` when recent items are truncated; otherwise dropped items still keep DOM element references alive, leading to a memory leak.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@cursor
Copy link

cursor bot commented Feb 8, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: recentElementMap leaks DOM elements when items overflow limit
    • Added cleanup logic to remove evicted items from recentElementMap when the recent list exceeds 20 items, preventing memory leaks from accumulated DOM element references.

@cursor
Copy link

cursor bot commented Feb 8, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Recent dropdown persists after overlay deactivation
    • Added dismissRecentDropdown() call in deactivateRenderer to reset the recentDropdownPosition signal when the overlay is deactivated.

@cursor
Copy link

cursor bot commented Feb 8, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Comment recent items silently fail when element disconnected
    • Added else clause to copy content as fallback when element is disconnected for comment items, matching non-comment item behavior.

ben-million and others added 23 commits February 8, 2026 18:43
…nstants

- Fix e2e test selector to use fill="currentColor" instead of fill="#404040" to match IconInboxUnread component
- Fix font family name from "Inter Variable" to "Inter" to match Google Fonts registration
- Remove unused TOOLBAR_DOCK_PREVIEW_SNAP_DURATION_MS and TOOLBAR_DOCK_PREVIEW_SIDE_ROTATION_DEG constants
…nsition-all

The class string 'grid transition-all transition-transform ...' caused twMerge
to keep only transition-transform, breaking grid-cols and opacity animations.
Removed transition-transform since transition-all is needed for the button
container animations and already includes transform transitions.
When handlePointerDown clears the snapAnimationTimeout to avoid unintended snaps,
it now also calls setIsSnapping(false) to prevent the snap state from getting
permanently stuck. This ensures getTransitionClass returns the correct transition
class for subsequent toolbar operations instead of always returning the 300ms
snap transition.
Copy link

@cursor cursor bot left a comment

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 2 potential issues.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issues.

if (toggleAnchorLockAnimationFrame === undefined) return;
cancelAnimationFrame(toggleAnchorLockAnimationFrame);
toggleAnchorLockAnimationFrame = undefined;
};
Copy link

Choose a reason for hiding this comment

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

Dead toggle anchor lock infrastructure never assigned

Low Severity

toggleAnchorLockAnimationFrame is declared and has a clearToggleAnchorLockAnimationFrame helper, which is actively called in handleToggleEnabled and onCleanup, but the variable is never actually assigned a requestAnimationFrame ID. All clear calls are no-ops. Similarly, toggleEnabledButtonRef is bound via ref but never read. This appears to be scaffolding for a toggle anchor-locking feature that was wired up but never completed.

Additional Locations (1)

Fix in Cursor Fix in Web

export const OVERLAY_FILL_COLOR_DEFAULT = `rgba(${GRAB_PURPLE_RGB}, 0.08)`;
export const FROZEN_GLOW_COLOR = `rgba(${GRAB_PURPLE_RGB}, 0.15)`;
export const FROZEN_GLOW_EDGE_PX = 50;
export const TOOLBAR_ACTIVE_ACCENT_COLOR = "#FF96EC";
Copy link

Choose a reason for hiding this comment

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

Unused exported constant TOOLBAR_ACTIVE_ACCENT_COLOR

Low Severity

TOOLBAR_ACTIVE_ACCENT_COLOR is exported from constants.ts but is never imported or referenced anywhere in the codebase. It appears to be dead code introduced in this commit.

Fix in Cursor Fix in Web

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.

3 participants

Comments