Skip to content

feat: new baseui popover + site controls#272

Open
iamEvanYT wants to merge 15 commits intomainfrom
evan/sidebar-info-popover-2
Open

feat: new baseui popover + site controls#272
iamEvanYT wants to merge 15 commits intomainfrom
evan/sidebar-info-popover-2

Conversation

@iamEvanYT
Copy link
Copy Markdown
Member

@iamEvanYT iamEvanYT commented May 1, 2026

  • new popover powered by baseui
  • better arrow, no disconnected border
  • cleaner design + translucency style
  • half-finished SiteControls for later

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Site Controls menu to the browser sidebar for managing extensions.
    • Extensions now display with icons and action badges in the sidebar.
    • Added ability to pin or unpin extensions directly from the sidebar.
    • Included quick access buttons to extension settings and the Chrome Web Store.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Walkthrough

This pull request refactors the popover infrastructure and sidebar UI components from a PortalPopover/PopoverListbox pattern to a new Popover/Command model using Base UI. It introduces new site controls UI for displaying extension actions and adds supporting utility components.

Changes

Cohort / File(s) Summary
Dependencies & Documentation
docs/contributing/dependencies.md, package.json
Added @base-ui/react as a dev dependency and documented it in the frontend dependencies list.
Sidebar UI Address Bar
src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx
Reordered imports and added commented-out scaffolding for future SiteControls integration without enabling new behavior.
Bottom Extras Menu Refactor
src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx
Migrated from popover-listbox-driven index-based activation to Popover/Command model with URL-based item selection; added new local BottomExtrasMenuItem component.
Browser Action List & Navigation Controls Refactor
src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx, src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx
Updated popover implementations from PortalPopover/PopoverListbox to Popover/Command pattern with adjusted trigger rendering and keyboard event handling via BubbleEvent.
Site Controls Components
src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx, src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx
Added new site controls UI system with ExtensionsList displaying extension actions (icons, badges, pin toggles) and SiteControls wrapper component managing popover state and theme-aware styling.
Popover Infrastructure
src/renderer/src/components/portal/popover.tsx, src/renderer/src/components/ui/popover.tsx
Refactored popover exports from PortalPopover object to named Popover/PopoverContent functions; converted PopoverContent to use Base UI primitives with delayed-unmount animation and additional layout props; added PopoverHeader/PopoverTitle/PopoverDescription exports.
Removed Utilities & Extensions
src/renderer/src/components/ui/popover-listbox.tsx
Deleted entire popover-listbox module (hook + components) as functionality migrates to Command-based patterns.
Portal & Utility Updates
src/renderer/src/components/portal/portal.tsx, src/renderer/src/components/logic/bubble-event.tsx, src/renderer/src/routes/extensions/page.tsx
Added portalBodyRef prop to sync portal body element; introduced new BubbleEvent logic component for re-dispatching document events within target subtree; exported CHROME_WEB_STORE_URL constant.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Popovers now dance in Base UI's light,
Commands replace lists, all aligned just right,
Extensions peek through their sidebar throne,
While BubbleEvents make keyboard events roam,
The refactor hops on! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: new baseui popover + site controls' directly reflects the main changes: introducing a Base UI popover implementation and adding a SiteControls component for browser sidebar functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch evan/sidebar-info-popover-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Build artifacts for all platforms are ready! 🚀

Download the artifacts for:

One-line installer (Unstable):
bunx flow-debug-build@1.2.1 --open 25229045599

(execution 25229045599 / attempt 1)

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR replaces the Radix UI-based popover with a @base-ui/react popover, introduces a translucent variant with a new custom arrow, migrates all popover call sites from the custom usePopoverListbox to cmdk Command/CommandItem, and adds a half-finished SiteControls panel (commented out at its call site) for future use.

  • P1ExtensionGridTile renders a <button> (pin overlay) inside ExtensionButtonContainer which is also a <button>. Nested interactive elements are invalid HTML and lead to unpredictable click-event routing across browsers.
  • P2portalContainerRef in portal/popover.tsx is null on every initial mount; the base-ui portal container is only set later via a useEffect ref mutation, which doesn't trigger a re-render, so the popover may initially render into the main window rather than the overlay window.
  • P2BubbleEvent accepts a generic EventType but always constructs a KeyboardEvent, making the type signature misleading and the component broken for non-keyboard event types.

Confidence Score: 3/5

Not safe to merge as-is due to the nested interactive element bug in ExtensionGridTile, though SiteControls is currently commented out.

One clear P1 (nested button in button), one P2 with potential real rendering consequence (portal container timing), and a misleading type in BubbleEvent. The P1 is in new code that isn't yet wired up (SiteControls is commented out), which slightly limits immediate user impact but doesn't change the validity of the bug.

site-controls/extensions.tsx (nested button), portal/popover.tsx (portalContainerRef timing), bubble-event.tsx (KeyboardEvent hardcoding)

Important Files Changed

Filename Overview
src/renderer/src/components/ui/popover.tsx Full rewrite migrating from Radix UI to @base-ui/react Popover; adds custom arrow SVG, translucent variant, positioner, and portal container wiring. Solid overall.
src/renderer/src/components/portal/popover.tsx Refactored portal popover to use BasePopover + useDelayedOpenValue; portalContainerRef starts null on every open and a ref mutation alone won't cause base-ui to re-portal into the overlay window.
src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx New extension grid for SiteControls; has invalid nested button inside ExtensionButtonContainer and duplicate SVG mask IDs across instances.
src/renderer/src/components/logic/bubble-event.tsx New utility that forwards document-level events into a portalled subtree for cmdk keyboard nav; hardcodes KeyboardEvent constructor despite a generic EventType signature.
src/renderer/src/components/portal/portal.tsx Minor addition of portalBodyRef prop to PortalComponent, synced to portal window's document.body via useEffect.
src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx Migrated from PortalPopover + custom listbox to new Popover + Command/CommandItem; cleaner item model with URL-based dispatch.
src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx Navigation history popover migrated to new Popover + Command; callback simplified to accept entry directly instead of index.
src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx Migrated to new portal Popover API; trigger button inlined directly into PopoverTrigger. No issues.
src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx Half-finished SiteControls component (commented out at call site); hardcodes Secure label — acknowledged as work-in-progress per PR description.
src/renderer/src/components/ui/popover-listbox.tsx Deleted — custom listbox implementation replaced by @base-ui/react + cmdk Command throughout the codebase.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Popover open = true] --> B[portal/popover.tsx\nPopoverContent renders]
    B --> C[useDelayedOpenValue\ndelayedOpen = true]
    C --> D{delayedOpen?}
    D -- false --> E[return null\nunmount tree]
    D -- true --> F[portalContainerRef = useRef null]
    F --> G[BasePopoverContent\nportalContainer = ref]
    F --> H[PortalComponent\nportalBodyRef = ref]
    H --> I[useEffect\nref.current = portal.window.body]
    G --> J{ref.current?}
    J -- null on first render --> K[Portals to main\ndocument.body]
    J -- set after effect --> L[Portals to overlay\nwindow body]
    I -.->|ref mutation no re-render| J

    M[User triggers popover] --> N[Popover / PopoverTrigger\nui/popover.tsx]
    N --> O[portal/popover.tsx\nPopover sets context open]
    O --> A

    P[BubbleEvent] --> Q[document keydown listener]
    Q --> R[dispatches KeyboardEvent\nto Command ref]
    R --> S[cmdk handles\narrow key navigation]
Loading

Comments Outside Diff (1)

  1. src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx, line 464-488 (link)

    P2 Duplicate SVG mask ID across multiple instances

    RotatedPinInCircle uses the hardcoded mask ID "pin-mask-rotated". When multiple extensions are shown in the grid, every <mask id="pin-mask-rotated"> is added to the DOM. Browsers resolve mask="url(#pin-mask-rotated)" using the first match in document order, so all subsequent instances reference the same mask definition — which is fine as long as the SVG dimensions never vary, but creates a fragile implicit dependency.

    Consider using useId() (or a module-level counter) to generate a unique ID per instance.

Reviews (1): Last reviewed commit: "chore: revert 2" | Re-trigger Greptile

Comment on lines +106 to +120
<button
className={cn(
"relative flex items-center justify-center rounded-md group/button",
"w-11.5 h-8 shrink-0",
"bg-black/10 dark:bg-white/10",
"hover:bg-black/20 dark:hover:bg-white/20",
"transition-colors duration-150",
className
)}
{...props}
>
{children}
</button>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Nested <button> inside <button> — invalid HTML

ExtensionButtonContainer renders a <button> element, and ExtensionGridTile places another <button> (the pin overlay) as a direct child. The HTML spec forbids interactive content inside <button>, so browsers implicitly close the outer button before the inner one is reached. This means clicks on the pin icon may not reach the outer onClick handler as expected, and event behaviour between the two buttons becomes browser-dependent.

Consider rendering the outer element as a <div> with role="button" / tabIndex={0} / onKeyDown, or using a CSS-only overlay (e.g., <span> with pointer-events:auto) for the pin affordance instead of a nested <button>.

Comment on lines +37 to +40
const handler = (event: DocumentEventMap[EventType]) => {
if (bubbleTarget.contains(event.target as Node)) return;
const cloned = new KeyboardEvent(event.type, event);
bubbleTarget.dispatchEvent(cloned);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 KeyboardEvent hardcoded for generic event type

The component is typed generically as EventType extends keyof DocumentEventMap, accepting any event name, but the handler always constructs a new KeyboardEvent(event.type, event). If eventType is ever anything other than "keydown" / "keyup" / "keypress", a KeyboardEvent is dispatched instead of the correct event subtype (e.g., MouseEvent, WheelEvent), which will silently deliver the wrong event to listeners and break any handler inspecting mouse-specific properties.

Either narrow the type to "keydown" | "keyup" | "keypress" to match the implementation, or make the event constructor dynamic (new (event.constructor as typeof Event)(event.type, event)).

Comment on lines +70 to 88
export function PopoverContent({ ...props }: Omit<React.ComponentProps<typeof BasePopoverContent>, "portalContainer">) {
const { open } = usePopover();
const delayedOpen = useDelayedOpenValue(open, 200);
const portalContainerRef = useRef<HTMLDivElement>(null);
if (!delayedOpen) return null;

return (
<AnimatePresence mode="wait">
{open && (
<motion.div initial={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
<PortalComponent autoFocus className="w-screen h-screen absolute top-0 left-0" zIndex={ViewLayer.POPOVER}>
<PopoverContent {...props} portal={false}>
<PopoverArrow className="fill-popover h-2 w-4 outline-hidden stroke-border" />
{children}
</PopoverContent>
</PortalComponent>
</motion.div>
)}
</AnimatePresence>
<>
<BasePopoverContent {...props} portalContainer={portalContainerRef} />
<PortalComponent
visible={delayedOpen}
autoFocus
className="w-screen h-screen absolute top-0 left-0"
zIndex={ViewLayer.POPOVER}
portalBodyRef={portalContainerRef}
/>
</>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 portalContainerRef is null on first render

portalContainerRef starts as null. BasePopoverContent receives it immediately as portalContainer, but PortalComponent only sets portalContainerRef.current = portal.window.document.body inside a useEffect — which runs after the first paint. Since a ref mutation does not trigger a re-render, base-ui's portal never learns about the updated container, and the popover content will render into the main window's document.body rather than the overlay window.

One approach: gate the BasePopoverContent render on portalContainerRef.current being non-null by tracking it in a state variable updated after the portal body is ready.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/components/portal/popover.tsx (1)

90-97: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale error message references removed API.

The error message says "usePopover must be used within a PortalPopover.Root" but PortalPopover.Root no longer exists in the codebase. The component is now just Popover.

🐛 Proposed fix
 export const usePopover = () => {
   const context = useContext(PopoverContext);
   if (!context) {
-    throw new Error("usePopover must be used within a PortalPopover.Root");
+    throw new Error("usePopover must be used within a Popover");
   }
   return context;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/portal/popover.tsx` around lines 90 - 97, The
error thrown in usePopover references a non-existent component name; update the
message in the usePopover hook (which checks PopoverContext) to reference the
current component name (Popover) instead of "PortalPopover.Root". Locate the
usePopover function and replace the stale error text so it reads something like
"usePopover must be used within a Popover" (or similar) to match the Popover
component API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/contributing/dependencies.md`:
- Line 84: The renderer dependency list is out of alphabetical order because
`@base-ui/react` was appended after `@chenglou/pretext`; reorder the entries so the
list is A–Z sorted—move the `@base-ui/react` line to the correct alphabetical
position before/after the appropriate entries (e.g., place `@base-ui/react` in
proper order relative to `@chenglou/pretext`) so the renderer dependency section
complies with the documented sorting rule.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx`:
- Around line 171-183: The PopoverTrigger used for the icon-only button (the
PopoverTrigger wrapping PuzzleIcon) lacks an accessible name; update the
PopoverTrigger to provide one (e.g., add aria-label="Open extensions menu" or
aria-labelledby pointing to a visually-hidden text) so screen readers can
identify the control; ensure the label is meaningful (e.g., "Open extensions
menu" or "Extensions") and attach it to the PopoverTrigger that contains
PuzzleIcon to make the trigger discoverable and operable by assistive tech.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx`:
- Around line 154-167: The popover for history (PopoverContent / CommandList)
currently grows indefinitely; limit its height and make the list scrollable so
older entries remain reachable. Update the PopoverContent (or the CommandList
wrapper) to include a max-height (e.g., max-h-64 / max-h-[calc(...)] or
equivalent token) and overflow-y-auto while preserving existing padding and
truncate behavior on each CommandItem; ensure the scrollable container wraps the
mapped entries (entries.map) so onSelect/onActivateHistory still works as
before.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx`:
- Around line 151-160: The onContextMenu handler currently returns the result of
onActivated("contextmenu") even though onActivated returns void and React
ignores handler return values; remove the misleading return and simply call
onActivated("contextmenu") after stopping propagation. Update the onContextMenu
useCallback (and keep onClick as-is) so it invokes onActivated("contextmenu")
without returning its value.
- Around line 63-79: The isError flag set by setIsError is not cleared when the
iconUrl changes (e.g., when action.id or activeTabId updates), causing a
persistent PuzzleIcon fallback; fix this by importing and using React's
useEffect in the component and add an effect that watches the computed iconUrl
(or its constituents like action.id and activeTabId) and calls setIsError(false)
whenever it changes so the image reload can attempt again; ensure you reference
the existing isError/setIsError state and the iconUrl variable in the effect
dependency list.
- Around line 8-22: Export the existing ExtensionAction and Action type
definitions from browser-action-provider (add export to their declarations in
browser-action-provider.tsx), remove the duplicate local definitions in
extensions.tsx and browser-action-list.tsx, and import the types from the
provider into those components (import { ExtensionAction, Action } from
'browser-action-provider' or the correct module export). Ensure the exported
names exactly match the identifiers used in extensions.tsx and
browser-action-list.tsx so existing usages (tabs, id, popup, iconModified, etc.)
compile without changes.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx`:
- Around line 54-60: The Button instances using className with the hardcoded
"text-white" (see the two Button elements containing LockIcon and EllipsisIcon
and the cn(...) calls) should use theme-aware tokens instead (e.g.,
"text-foreground" or a popover-specific token like "text-popover-foreground") so
text/icons remain visible if the surrounding variant or theme changes; update
both Button className strings to replace "text-white" with the chosen token and
ensure the icons inherit color (use currentColor-compatible icon classes) so
they follow the new foreground color.

In `@src/renderer/src/components/portal/popover.tsx`:
- Around line 52-65: The useEffect in popover.tsx currently returns an
unnecessary empty cleanup when value === true; remove the `return () => {};` so
the effect simply sets setDelayedValue(value) and returns nothing in that
branch, leaving the existing timeout/clearTimeout cleanup in the else branch
intact; locate the useEffect that references value, delay, and setDelayedValue
to apply this change.

In `@src/renderer/src/components/portal/portal.tsx`:
- Around line 67-71: The effect in portal.tsx sets portalBodyRef.current to
portal.window.document.body but never clears it, leaving a stale reference when
the portal/window unmounts or closes; update the useEffect that reads portal and
portalBodyRef so that when portal or portal.window is falsy you set
portalBodyRef.current = null (guarding that portalBodyRef exists) and also
return a cleanup function that clears portalBodyRef.current = null on unmount,
ensuring callers don't keep a detached document body; refer to the existing
useEffect handling portal/window and the portalBodyRef symbol to implement these
changes.

---

Outside diff comments:
In `@src/renderer/src/components/portal/popover.tsx`:
- Around line 90-97: The error thrown in usePopover references a non-existent
component name; update the message in the usePopover hook (which checks
PopoverContext) to reference the current component name (Popover) instead of
"PortalPopover.Root". Locate the usePopover function and replace the stale error
text so it reads something like "usePopover must be used within a Popover" (or
similar) to match the Popover component API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e68541db-98f8-4127-bbdf-e9f8719982cd

📥 Commits

Reviewing files that changed from the base of the PR and between ed559ed and d401b91.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • docs/contributing/dependencies.md
  • package.json
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx
  • src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx
  • src/renderer/src/components/logic/bubble-event.tsx
  • src/renderer/src/components/portal/popover.tsx
  • src/renderer/src/components/portal/portal.tsx
  • src/renderer/src/components/ui/popover-listbox.tsx
  • src/renderer/src/components/ui/popover.tsx
  • src/renderer/src/routes/extensions/page.tsx
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/ui/popover-listbox.tsx

- nuqs - Use for managing query parameters in the URL.
- @tanstack/react-query - React Query for the frontend.
- @chenglou/pretext - Calculate text width for the frontend.
- @base-ui/react - UI Components for the frontend.
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep this list A–Z sorted.

@base-ui/react was appended after @chenglou/pretext, so the renderer dependency section no longer matches the ordering rule documented at the top of this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/contributing/dependencies.md` at line 84, The renderer dependency list
is out of alphabetical order because `@base-ui/react` was appended after
`@chenglou/pretext`; reorder the entries so the list is A–Z sorted—move the
`@base-ui/react` line to the correct alphabetical position before/after the
appropriate entries (e.g., place `@base-ui/react` in proper order relative to
`@chenglou/pretext`) so the renderer dependency section complies with the
documented sorting rule.

Comment on lines +171 to 183
<PopoverTrigger
className={cn(
"size-6 flex items-center justify-center rounded-md",
"hover:bg-black/15 dark:hover:bg-white/20",
"transition-colors duration-150",
"relative shrink-0"
)}
onClick={(event) => {
event.stopPropagation();
}}
>
<PuzzleIcon strokeWidth={2} className="size-4 text-black/80 dark:text-white/80" />
</PopoverTrigger>
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an accessible name to this icon-only trigger.

Right now this will be exposed as an unlabeled button, which makes the extensions menu hard to discover and operate with assistive tech.

Proposed fix
       <PopoverTrigger
+        aria-label="Open extensions menu"
+        title="Open extensions menu"
         className={cn(
           "size-6 flex items-center justify-center rounded-md",
           "hover:bg-black/15 dark:hover:bg-white/20",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PopoverTrigger
className={cn(
"size-6 flex items-center justify-center rounded-md",
"hover:bg-black/15 dark:hover:bg-white/20",
"transition-colors duration-150",
"relative shrink-0"
)}
onClick={(event) => {
event.stopPropagation();
}}
>
<PuzzleIcon strokeWidth={2} className="size-4 text-black/80 dark:text-white/80" />
</PopoverTrigger>
<PopoverTrigger
aria-label="Open extensions menu"
title="Open extensions menu"
className={cn(
"size-6 flex items-center justify-center rounded-md",
"hover:bg-black/15 dark:hover:bg-white/20",
"transition-colors duration-150",
"relative shrink-0"
)}
onClick={(event) => {
event.stopPropagation();
}}
>
<PuzzleIcon strokeWidth={2} className="size-4 text-black/80 dark:text-white/80" />
</PopoverTrigger>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx`
around lines 171 - 183, The PopoverTrigger used for the icon-only button (the
PopoverTrigger wrapping PuzzleIcon) lacks an accessible name; update the
PopoverTrigger to provide one (e.g., add aria-label="Open extensions menu" or
aria-labelledby pointing to a visually-hidden text) so screen readers can
identify the control; ensure the label is meaningful (e.g., "Open extensions
menu" or "Extensions") and attach it to the PopoverTrigger that contains
PuzzleIcon to make the trigger discoverable and operable by assistive tech.

Comment on lines +154 to +167
<PopoverContent className={cn("w-56 p-2 select-none")} positionerClassName={spaceInjectedClasses}>
<Command ref={commandRef} loop>
<BubbleEvent targetRef={commandRef} eventType="keydown" />
<CommandList>
{entries.map((entry) => (
<CommandItem
key={entry.index}
value={entry.index.toString()}
onSelect={() => onActivateHistory(entry)}
>
<span className="truncate">{entry.title || entry.url}</span>
</CommandItem>
))}
</CommandList>
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap and scroll the history list.

A tab can easily have more history entries than fit on screen. In the current shape the popover grows indefinitely, so older entries can end up off-screen and unreachable.

Proposed fix
           <PopoverContent className={cn("w-56 p-2 select-none")} positionerClassName={spaceInjectedClasses}>
             <Command ref={commandRef} loop>
               <BubbleEvent targetRef={commandRef} eventType="keydown" />
-              <CommandList>
-                {entries.map((entry) => (
-                  <CommandItem
-                    key={entry.index}
-                    value={entry.index.toString()}
-                    onSelect={() => onActivateHistory(entry)}
-                  >
-                    <span className="truncate">{entry.title || entry.url}</span>
-                  </CommandItem>
-                ))}
-              </CommandList>
+              <div className="max-h-80 overflow-y-auto">
+                <CommandList>
+                  {entries.map((entry) => (
+                    <CommandItem
+                      key={entry.index}
+                      value={entry.index.toString()}
+                      onSelect={() => onActivateHistory(entry)}
+                    >
+                      <span className="truncate">{entry.title || entry.url}</span>
+                    </CommandItem>
+                  ))}
+                </CommandList>
+              </div>
             </Command>
           </PopoverContent>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PopoverContent className={cn("w-56 p-2 select-none")} positionerClassName={spaceInjectedClasses}>
<Command ref={commandRef} loop>
<BubbleEvent targetRef={commandRef} eventType="keydown" />
<CommandList>
{entries.map((entry) => (
<CommandItem
key={entry.index}
value={entry.index.toString()}
onSelect={() => onActivateHistory(entry)}
>
<span className="truncate">{entry.title || entry.url}</span>
</CommandItem>
))}
</CommandList>
<PopoverContent className={cn("w-56 p-2 select-none")} positionerClassName={spaceInjectedClasses}>
<Command ref={commandRef} loop>
<BubbleEvent targetRef={commandRef} eventType="keydown" />
<div className="max-h-80 overflow-y-auto">
<CommandList>
{entries.map((entry) => (
<CommandItem
key={entry.index}
value={entry.index.toString()}
onSelect={() => onActivateHistory(entry)}
>
<span className="truncate">{entry.title || entry.url}</span>
</CommandItem>
))}
</CommandList>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx`
around lines 154 - 167, The popover for history (PopoverContent / CommandList)
currently grows indefinitely; limit its height and make the list scrollable so
older entries remain reachable. Update the PopoverContent (or the CommandList
wrapper) to include a max-height (e.g., max-h-64 / max-h-[calc(...)] or
equivalent token) and overflow-y-auto while preserving existing padding and
truncate behavior on each CommandItem; ensure the scrollable container wraps the
mapped entries (entries.map) so onSelect/onActivateHistory still works as
before.

Comment on lines +8 to +22
interface ExtensionAction {
color?: string;
text?: string;
title?: string;
icon?: chrome.browserAction.TabIconDetails;
popup?: string;
iconModified?: number;
}

interface Action {
id: string;
title: string;
popup: string;
tabs: Record<string, ExtensionAction>;
}
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.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Action/ExtensionAction types are exported from the provider
rg -n "export (type|interface) (Action|ExtensionAction)" src/renderer/src/components/providers/browser-action-provider.tsx

Repository: MultiboxLabs/flow-browser

Length of output: 51


🏁 Script executed:

# First, check if the extensions.tsx file exists and its imports
head -30 "src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx"

Repository: MultiboxLabs/flow-browser

Length of output: 1052


🏁 Script executed:

# Search for where Action and ExtensionAction types are defined in the codebase
rg -n "(export )?(type|interface) (Action|ExtensionAction)" --type ts --type tsx

Repository: MultiboxLabs/flow-browser

Length of output: 97


🏁 Script executed:

# Check if browser-action-provider.tsx exists and what it contains
find . -name "*browser-action-provider*" -type f

Repository: MultiboxLabs/flow-browser

Length of output: 137


🏁 Script executed:

# Search for useBrowserAction hook to understand where it's defined
rg -n "useBrowserAction" --type ts --type tsx -A 2

Repository: MultiboxLabs/flow-browser

Length of output: 97


🏁 Script executed:

# Read the browser-action-provider.tsx to see what types it exports
cat "src/renderer/src/components/providers/browser-action-provider.tsx"

Repository: MultiboxLabs/flow-browser

Length of output: 5236


🏁 Script executed:

# Search for Action and ExtensionAction definitions without type filter
rg -n "(export )?(type|interface) (Action|ExtensionAction)"

Repository: MultiboxLabs/flow-browser

Length of output: 920


Export the Action and ExtensionAction types from the browser-action-provider and import them instead of redefining locally.

These types are currently defined in the provider but not exported. They're being redefined identically in both extensions.tsx and browser-action-list.tsx, which violates DRY and risks drift. Export them from browser-action-provider.tsx and import them in the components that use them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx`
around lines 8 - 22, Export the existing ExtensionAction and Action type
definitions from browser-action-provider (add export to their declarations in
browser-action-provider.tsx), remove the duplicate local definitions in
extensions.tsx and browser-action-list.tsx, and import the types from the
provider into those components (import { ExtensionAction, Action } from
'browser-action-provider' or the correct module export). Ensure the exported
names exactly match the identifiers used in extensions.tsx and
browser-action-list.tsx so existing usages (tabs, id, popup, iconModified, etc.)
compile without changes.

Comment on lines +63 to +79
const { iconModified } = { ...action, ...tabInfo };
const [isError, setIsError] = useState(false);
const iconSize = 32;
const resizeType = 2;
const timeParam = iconModified ? `&t=${iconModified}` : "";
const iconUrl = `crx://extension-icon/${action.id}/${iconSize}/${resizeType}?tabId=${activeTabId}${timeParam}&partition=${encodeURIComponent(partitionId)}`;

if (isError) {
return <PuzzleIcon className="size-4 shrink-0" />;
}

return (
<svg className="size-4 shrink-0">
{/* eslint-disable-next-line react/no-unknown-property */}
<image href={iconUrl} className="size-4 object-contain" onError={() => setIsError(true)} />
</svg>
);
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

isError state persists incorrectly when icon URL changes.

The isError state is never reset when action.id or activeTabId changes. If an icon fails to load, then the user switches tabs or extensions, the error state persists and shows the PuzzleIcon fallback for a different (potentially valid) extension icon.

🐛 Proposed fix: Reset error state when icon URL changes
 function BrowserActionIcon({
   action,
   activeTabId,
   tabInfo,
   partitionId
 }: {
   action: Action;
   activeTabId: number;
   tabInfo: ExtensionAction | null;
   partitionId: string;
 }) {
   const { iconModified } = { ...action, ...tabInfo };
   const [isError, setIsError] = useState(false);
   const iconSize = 32;
   const resizeType = 2;
   const timeParam = iconModified ? `&t=${iconModified}` : "";
   const iconUrl = `crx://extension-icon/${action.id}/${iconSize}/${resizeType}?tabId=${activeTabId}${timeParam}&partition=${encodeURIComponent(partitionId)}`;
 
+  // Reset error state when iconUrl changes
+  useEffect(() => {
+    setIsError(false);
+  }, [iconUrl]);
+
   if (isError) {
     return <PuzzleIcon className="size-4 shrink-0" />;
   }

This requires adding useEffect to the imports on line 6.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx`
around lines 63 - 79, The isError flag set by setIsError is not cleared when the
iconUrl changes (e.g., when action.id or activeTabId updates), causing a
persistent PuzzleIcon fallback; fix this by importing and using React's
useEffect in the component and add an effect that watches the computed iconUrl
(or its constituents like action.id and activeTabId) and calls setIsError(false)
whenever it changes so the image reload can attempt again; ensure you reference
the existing isError/setIsError state and the iconUrl variable in the effect
dependency list.

Comment on lines +151 to +160
const onClick = useCallback(() => onActivated("click"), [onActivated]);

const onContextMenu = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
return onActivated("contextmenu");
},
[onActivated]
);
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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Return value from context menu handler is unused.

Line 157 returns the result of onActivated("contextmenu"), but onActivated returns void and React's onContextMenu handler ignores the return value. The return statement is harmless but misleading.

♻️ Suggested cleanup
   const onContextMenu = useCallback(
     (event: MouseEvent<HTMLButtonElement>) => {
       event.stopPropagation();
       event.nativeEvent.stopImmediatePropagation();
-      return onActivated("contextmenu");
+      onActivated("contextmenu");
     },
     [onActivated]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx`
around lines 151 - 160, The onContextMenu handler currently returns the result
of onActivated("contextmenu") even though onActivated returns void and React
ignores handler return values; remove the misleading return and simply call
onActivated("contextmenu") after stopping propagation. Update the onContextMenu
useCallback (and keep onClick as-is) so it invokes onActivated("contextmenu")
without returning its value.

Comment on lines +54 to +60
<Button className={cn("justify-start rounded-md", "text-white h-8", "bg-white/5 hover:bg-white/10")}>
<LockIcon className="size-4" />
<span className="font-medium truncate">Secure</span>
</Button>
<Button className={cn("justify-center rounded-full", "text-white size-8", "bg-white/5 hover:bg-white/10")}>
<EllipsisIcon className="size-4" />
</Button>
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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Hardcoded text-white may break in light mode context.

The utility buttons use text-white unconditionally, but the popover uses variant="translucent" which forces dark mode. If the variant logic changes or these buttons are reused elsewhere, the white text won't be visible on light backgrounds.

Consider using theme-aware colors like text-foreground or text-popover-foreground for better maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx`
around lines 54 - 60, The Button instances using className with the hardcoded
"text-white" (see the two Button elements containing LockIcon and EllipsisIcon
and the cn(...) calls) should use theme-aware tokens instead (e.g.,
"text-foreground" or a popover-specific token like "text-popover-foreground") so
text/icons remain visible if the surrounding variant or theme changes; update
both Button className strings to replace "text-white" with the chosen token and
ensure the icons inherit color (use currentColor-compatible icon classes) so
they follow the new foreground color.

Comment on lines +52 to +65
useEffect(() => {
if (value === true) {
setDelayedValue(value);
return () => {};
} else {
const timer = setTimeout(() => {
setDelayedValue(value);
}, delay);

return () => {
clearTimeout(timer);
};
}
}, [value, delay]);
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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Simplify cleanup function in effect.

The empty cleanup function return () => {}; on line 55 is unnecessary. When no cleanup is needed, the effect can simply not return anything.

♻️ Suggested simplification
   useEffect(() => {
     if (value === true) {
       setDelayedValue(value);
-      return () => {};
+      return;
     } else {
       const timer = setTimeout(() => {
         setDelayedValue(value);
       }, delay);

       return () => {
         clearTimeout(timer);
       };
     }
   }, [value, delay]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (value === true) {
setDelayedValue(value);
return () => {};
} else {
const timer = setTimeout(() => {
setDelayedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [value, delay]);
useEffect(() => {
if (value === true) {
setDelayedValue(value);
return;
} else {
const timer = setTimeout(() => {
setDelayedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [value, delay]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/portal/popover.tsx` around lines 52 - 65, The
useEffect in popover.tsx currently returns an unnecessary empty cleanup when
value === true; remove the `return () => {};` so the effect simply sets
setDelayedValue(value) and returns nothing in that branch, leaving the existing
timeout/clearTimeout cleanup in the else branch intact; locate the useEffect
that references value, delay, and setDelayedValue to apply this change.

Comment on lines +67 to +71
useEffect(() => {
if (!portal?.window) return;
if (!portalBodyRef) return;
portalBodyRef.current = portal.window.document.body;
}, [portal, portalBodyRef]);
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear portalBodyRef when the portal disappears.

This effect only writes the portal body once. If the portal window closes or this component unmounts, callers keep a stale body reference and can mount follow-up overlays into a detached window.

Proposed fix
   useEffect(() => {
-    if (!portal?.window) return;
-    if (!portalBodyRef) return;
-    portalBodyRef.current = portal.window.document.body;
+    if (!portalBodyRef) return;
+    portalBodyRef.current = portal?.window?.document.body ?? null;
+
+    return () => {
+      portalBodyRef.current = null;
+    };
   }, [portal, portalBodyRef]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/portal/portal.tsx` around lines 67 - 71, The
effect in portal.tsx sets portalBodyRef.current to portal.window.document.body
but never clears it, leaving a stale reference when the portal/window unmounts
or closes; update the useEffect that reads portal and portalBodyRef so that when
portal or portal.window is falsy you set portalBodyRef.current = null (guarding
that portalBodyRef exists) and also return a cleanup function that clears
portalBodyRef.current = null on unmount, ensuring callers don't keep a detached
document body; refer to the existing useEffect handling portal/window and the
portalBodyRef symbol to implement these changes.

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.

1 participant