Skip to content

feat(desktop): port browser pane to v2 workspaces with global persistence#3346

Merged
saddlepaddle merged 4 commits intomainfrom
saddlepaddle/honorable-burn
Apr 11, 2026
Merged

feat(desktop): port browser pane to v2 workspaces with global persistence#3346
saddlepaddle merged 4 commits intomainfrom
saddlepaddle/honorable-burn

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 10, 2026

Summary

  • Ports the v1 browser pane (webview, URL toolbar, history, autocomplete, error overlay, new-window handler) into v2 workspaces as a native implementation, using @superset/panes idioms (runtime registry + ctx.actions.updateData + renderToolbar slot). v1 is untouched.
  • Introduces a float-over webview architecture: every <webview> lives at document.body > #browser-runtime-root and is positioned via a ResizeObserver on a per-pane placeholder div. The element never reparents, so Electron's guest WebContents survives tab switches, workspace route remounts, and pane drags — no page reloads, no lost state, ever.
  • Adds a keep-alive tab rendering mode to @superset/panes: visited tabs stay mounted behind visibility: hidden so their React state survives tab switches. Benefits every stateful pane kind (terminal scrollback, chat subscriptions, editor state).
  • Single-pane browser tabs show the page favicon + live page title in the tab bar via a new optional renderTabLabel prop.

Why / Context

The v2 workspace previously registered a browser pane kind with a placeholder <iframe> stub. Porting v1's real implementation surfaced a fundamental Electron issue: reparenting a <webview> element via appendChild destroys its guest WebContents and reloads the page. v1 works around this with a "park in hidden container on unmount / reclaim on remount" pattern — but every reparent triggers the reload (v1 even has a // e.g. after DOM reparenting comment in its dom-ready handler re-registering the new webContentsId).

For v2, where tab switches, pane drags, and workspace switches all cause React subtrees to unmount and remount, the only correct answer is don't reparent at all. Float-over is that guarantee.

How It Works

browserRuntimeRegistry (module-level singleton, modeled on terminalRuntimeRegistry):

  • Lazily appends a zero-sized, pointer-events: none #browser-runtime-root div to document.body.
  • Each paneId maps to an entry holding the <webview> element (child of root container), per-pane runtime state, and a ResizeObserver.
  • attach(paneId, placeholder, initialUrl, onDidStopLoading): creates the webview on first call (appended once, forever), stores the placeholder ref, starts a ResizeObserver on it, and rAF-schedules an initial updateLayout.
  • updateLayout(entry): reads placeholder.getBoundingClientRect() and writes the result to the webview's fixed-position top/left/width/height.
  • detach: stops the observer and hides the webview. Does NOT reparent, does NOT remove.
  • setVisibility(paneId, visible): driven by BrowserPane's subscription to activeTabId === ctx.tab.id, toggles the webview's CSS visibility so inactive-tab webviews don't paint on top of the current tab.
  • destroy: the only path that removes the webview from the DOM. Called from a single diff effect in page.tsx that walks all tabs' browser pane ids each render — anything that disappeared since the previous render gets destroyed. Correctly handles tab closes, intra-tab split closes, and pane drags between tabs (moved panes stay in the current set under their new tab, so they're not destroyed).

Keep-alive in packages/panes: Workspace.tsx tracks mountedTabIds and renders every visited tab into an absolute-stacked container. The active tab is fully interactive; inactive visited tabs use invisible pointer-events-none. BrowserPane doesn't unmount on tab switch, so usePersistentWebview's cleanup never runs, so the webview never even gets a chance to move.

Registry subscription pattern: to avoid the @trpc/react-query single-shared-React-context gotcha where workspaceTrpc.Provider hijacks electronTrpc React hooks inside v2 routes, every Electron-router call goes through the imperative electronTrpcClient proxy (matching the v2 TerminalPane pattern). All registry mutations/queries/subscriptions use .mutate(...) / .query(...) / .subscribe(...) directly — no useMutation / useQuery / useSubscription for electronTrpc.browser.* or electronTrpc.browserHistory.*.

Manual QA Checklist

Browser pane basics

  • Create a browser tab → blank-page placeholder renders
  • Type a URL → navigates, tab title + favicon update live
  • Back/forward buttons work
  • URL autocomplete shows history suggestions
  • Error overlay appears for unreachable URLs
  • Overflow menu (screenshot, hard reload, copy URL, clear cookies/history)
  • DevTools button opens detached DevTools window

Global persistence (the main win)

  • Scroll into a page → switch to another tab → return → scroll position preserved, no network refetch
  • Type into a form → switch tab → return → input preserved
  • Open browser in workspace A → switch to workspace B → return to A → webview untouched, no reload

Tab bar

  • Single-pane browser tabs show favicon + live page title
  • Multi-pane tabs fall back to default title
  • Favicon updates across navigations without flicker
  • No rounded corners on favicons
  • Missing/broken favicons show no icon (no Globe fallback)

Split panes

  • Split a browser pane → two independent webviews, each tracks its own state
  • Resize the split handle → webview snaps smoothly via ResizeObserver
  • Close one half → its webview is destroyed, other unaffected

Other pane kinds (benefit from keep-alive)

  • Terminal scrollback survives tab switches
  • Chat pane subscriptions stay open across tab switches

Destroy paths

  • Close a browser tab → webview removed from #browser-runtime-root
  • Close one half of a browser split → only that webview destroyed
  • Drag a pane to another tab → moved pane survives (not destroyed by the diff effect)

Known Limitations

  • Dropping panes onto another browser pane's body does not register. The Electron <webview> compositor layer intercepts drag events at its painted position regardless of pointer-events: none, visibility: hidden, or moving the element off-screen. Drops on pane headers still work. Deferred for a follow-up that likely involves patching @superset/panes to portal the drop target overlay above the webview, or moving the pane's drop target to a dedicated element.
  • Cross-workspace dev server preview is broken. For remote/relay-tunneled workspaces, http://localhost:3000 resolves to the renderer machine, not the host. No port-proxy route exists on the host-service or relay yet. Out of scope; browser panes work correctly for local workspaces. A future /proxy/:port/* route + a last-mile URL rewrite in usePersistentWebview is the planned path.
  • Modal z-index with webviews. Float-over webviews paint on their own compositor layer and may bleed through modal backdrops depending on stacking context. Not observed in the tested flows but worth watching during review.

Testing

  • bun run typecheck (25 packages) ✓
  • bun run lint
  • bunx sherif
  • bun test — 2389 pass / 3 pre-existing failures unrelated to this work (teardown.test.ts, setup.test.ts — git worktree fixture issues; useOrderedSections.test.tsx — pre-existing electronTrpcClient init-order bug in test env; none touch browser pane or panes library files)
  • Manual testing in bun dev

Design Decisions

  • Float-over instead of park-and-reclaim. Electron <webview> reparenting is fundamentally broken for state preservation. Float-over guarantees no reparenting and is the only reliable solution.
  • Keep-alive tab rendering in the library, not just for browsers. Even with float-over, BrowserPane needs to stay mounted on tab switch so its subscription to activeTabId keeps the webview visible at the right moments. Keep-alive also benefits every other stateful pane for free.
  • electronTrpcClient imperative calls inside v2. @trpc/react-query stores its context as a module singleton, so workspaceTrpc.Provider inside v2 routes hijacks every electronTrpc.* React hook. Using the imperative proxy sidesteps the issue. Canonical precedent: v2 TerminalPane does the same.
  • Diff effect as single source of truth for destroy. An earlier version of the destroy logic used an onTabRemoved callback that received the removed tab's old snapshot — which broke pane drags between tabs, because the moved pane was still in the old snapshot's panes record and got destroyed right as the target tab tried to attach it. The current pane-id diff correctly handles moves.
  • No faviconUrl fallback icon. Tabs without a favicon show just the title, no Globe icon.

Follow-ups

  • Fix drop-onto-browser-pane-body (library-level change to portal the drop target above the webview, or restructure to keep the drop target element on top of the float-over webview).
  • Host-service /proxy/:port/* + renderer URL rewrite for dev server previews on remote workspaces.
  • Consider wiring up the v2 devtools pane kind (currently still a stub).

Summary by cubic

Ports the browser pane to v2 workspaces with a float‑over Electron webview and global persistence. Removes keep‑alive tabs; persistence now comes from a registry that keeps each webview alive across tab switches, drags, and workspace remounts.

  • New Features

    • Native v2 browser pane: webview lives under #browser-runtime-root and is positioned via ResizeObserver (no reparenting, no reloads).
    • URL bar with back/forward/reload, history autocomplete, error overlay, overflow menu, and DevTools.
    • Live tab titles and favicons for single‑pane browser tabs via getTabTitle + renderTabIcon; favicons persist across restarts.
    • Navigation improvements: back/forward use native webview history (keeps scroll/form/BFCache) with live canGoBack/Forward; stricter URL sanitization (auto scheme, localhost, search fallback); clears stale favicons on load; avoids redundant history writes on back/forward.
    • New browser panes start at about:blank.
  • Migration

    • @superset/panes Workspace adds optional renderTabIcon, getTabTitle, and a new onRemoved(pane) hook. Workspace renders only the active tab; browser cleanup uses onRemoved to call the runtime registry’s destroy.
    • BrowserPaneData: remove mode; add optional pageTitle and faviconUrl; default url is about:blank.

Written for commit 84a8f63. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Full browser pane with navigation (Back/Forward/Reload), URL bar with autocomplete, error overlay with retry, DevTools access, and overflow menu (copy URL, screenshot, hard reload, clear data).
    • Browser tabs now show page titles and favicons; URL suggestions dropdown included.
  • Changes

    • New browser tabs now open to a blank page by default.

…ence

- Port v1 browser pane (webview, URL toolbar, history, error overlay, new-window
  handler) into v2 workspace tree as a native implementation. Uses the v2
  Workspace component's renderToolbar slot for a Chrome-style URL bar in the
  pane header.
- Float-over webview: each <webview> lives at document.body under a fixed
  #browser-runtime-root, positioned via ResizeObserver on a per-pane placeholder
  div. Elements never reparent, so Electron's guest WebContents survives tab
  switches, pane drags, and workspace route remounts — no page reloads.
- Keep-alive tab rendering in @superset/panes Workspace.tsx: tabs that have
  been visited stay mounted behind visibility:hidden so their React state
  (and persistent DOM like webviews) survives tab switches. Also benefits
  terminal scrollback, chat subscriptions, editor state.
- BrowserRuntimeRegistry: module-level singleton keyed by paneId, modeled on
  terminalRuntimeRegistry. Owns webview lifecycle, state subscriptions via
  useSyncExternalStore, drag-passthrough listeners, equality-guarded setState
  to deduplicate webview event spam.
- Single-pane-browser tabs show the page favicon + live page title in the tab
  bar via a new optional renderTabLabel prop on the panes library.
- BrowserPane subscribes to the workspace store for activeTabId to drive
  webview visibility, so inactive-tab webviews don't paint on top of the
  active tab's content.
- Destroy path: diff the set of browser pane ids across all tabs each render,
  destroying any that disappeared. Correctly handles tab closes, intra-tab
  split closes, and pane drags between tabs (moved panes stay in the set).

Known limitation: dropping panes onto another browser pane's body doesn't
register — the webview compositor layer intercepts drag events even with
pointer-events:none/visibility:hidden. Drops on pane headers work. Deferred.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds a full Browser pane feature: a browser runtime registry managing Electron webviews, new BrowserPane UI (toolbar, overflow menu, error overlay, URL autocomplete), URL sanitization/defaults, pane lifecycle hooks, and workspace/tab API extensions for custom tab titles/icons and cleanup.

Changes

Cohort / File(s) Summary
Browser Pane Core
apps/desktop/src/renderer/routes/.../BrowserPane/BrowserPane.tsx, apps/desktop/src/renderer/routes/.../BrowserPane/browserRuntimeRegistry.ts, apps/desktop/src/renderer/routes/.../BrowserPane/constants.ts, apps/desktop/src/renderer/routes/.../BrowserPane/sanitizeUrl.ts, apps/desktop/src/renderer/routes/.../BrowserPane/index.ts
Implements BrowserPane and BrowserPaneToolbar, adds a singleton browserRuntimeRegistry managing Electron webview lifecycle/state/events, exports tab helpers (getBrowserTabTitle, renderBrowserTabIcon), defines DEFAULT_BROWSER_URL, and adds URL sanitization.
Persistent Webview & Hooks
.../usePersistentWebview/usePersistentWebview.ts, .../usePersistentWebview/index.ts
Hook to attach/detach webviews via registry, sync pane data (url/title/favicon), subscribe to TRPC events (new window, context menu) and expose navigation helpers.
Toolbar & Autocomplete
.../BrowserToolbar/BrowserToolbar.tsx, .../BrowserToolbar/index.ts, .../BrowserToolbar/components/UrlSuggestions/*, .../BrowserToolbar/hooks/useUrlAutocomplete/*
Adds navigation toolbar with back/forward/reload, URL input with edit mode, URL display normalization, history-backed autocomplete hook, and UrlSuggestions dropdown component.
Overflow Menu & Error Overlay
.../BrowserOverflowMenu/BrowserOverflowMenu.tsx, .../BrowserOverflowMenu/index.ts, .../BrowserErrorOverlay/BrowserErrorOverlay.tsx, .../BrowserErrorOverlay/index.ts
Adds overflow dropdown (screenshot, hard reload, copy URL, clear data) and BrowserErrorOverlay with human-readable messages, details toggling, and retry action.
Pane Registry Integration
hooks/usePaneRegistry/usePaneRegistry.tsx
Replaces iframe-based rendering with BrowserPane component, adds renderToolbar, uses `pageTitle
Hotkeys & Default Actions
hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts, hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx, page.tsx
New browser tabs and split actions now initialize with url: "about:blank" and removed mode field; page passes getBrowserTabTitle and renderBrowserTabIcon into Workspace.
Types & Workspace/Tab API
types.ts, packages/panes/src/react/types.ts, packages/panes/src/react/components/Workspace/..., packages/panes/src/react/components/Workspace/components/TabBar/..., .../TabItem/TabItem.tsx
Removes mode from BrowserPaneData, adds pageTitle and faviconUrl; adds PaneDefinition.onRemoved, Workspace props renderTabIcon and getTabTitle, and updates Workspace/TabBar/TabItem to support tab icons and optional title renames.

Sequence Diagram

sequenceDiagram
    participant React as BrowserPane Component
    participant Registry as BrowserRuntimeRegistry
    participant Webview as Electron.WebviewTag
    participant TRPC as TRPC Client
    participant DOM as Placeholder/Window

    React->>Registry: attach(paneId, placeholderEl, initialUrl)
    Registry->>DOM: create root container & placeholder observer
    Registry->>Webview: create webview, set src, bind event listeners
    Webview->>Registry: dom-ready / did-start-loading
    Registry->>Registry: set isLoading, update state
    Webview->>Registry: did-navigate / page-title-updated / page-favicon-updated
    Registry->>TRPC: upsert history / register webContentsId
    Webview->>Registry: did-stop-loading / did-fail-load
    Registry->>React: notify onStateChange (url, title, favicon, error, nav state)
    React->>React: render toolbar, overlays, and url/autocomplete based on state
    React->>Registry: navigate/goBack/goForward/reload (user actions)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • Implement in-app browser #1519 — Implements similar in-app browser surface and related artifacts (BrowserPane, toolbar, error overlay, registry and BrowserPaneData changes).

Poem

🐇 I hopped in to patch the pane so spry,

about:blank first—no localhost in sight,
Webviews hum, toolbars blink, history sings,
Tabs wear favicons and titles take flight,
A little rabbit cheers: hop, click, delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(desktop): port browser pane to v2 workspaces with global persistence' accurately summarizes the main feature—porting the browser pane to v2 with a float-over webview architecture for persistence.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a detailed summary, context explaining the Electron reparenting issue, implementation details (browserRuntimeRegistry and keep-alive), manual QA checklist, known limitations, testing results, design decisions, and follow-ups.

✏️ 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 saddlepaddle/honorable-burn

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

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 11, 2026

Greptile Summary

This PR ports the v1 browser pane into v2 workspaces using a float-over webview architecture — every <webview> element lives permanently in a #browser-runtime-root div appended to document.body and is positioned via a ResizeObserver on a placeholder div. This elegantly solves Electron's fundamental constraint that reparenting a <webview> destroys its guest WebContents. The PR also introduces keep-alive tab rendering in @superset/panes (visited tabs stay mounted behind visibility: hidden), benefiting terminals, chat, and any other stateful pane kind.

Key design decisions are sound and well-reasoned:

  • The browserRuntimeRegistry singleton (modeled on terminalRuntimeRegistry) with HMR preservation via import.meta.hot.data is correct.
  • The diff-effect in page.tsx as the single source of truth for webview destruction correctly handles tab closes, split closes, and pane drags between tabs.
  • Using imperative electronTrpcClient calls throughout avoids the @trpc/react-query React-context hijack issue in v2 routes.
  • UrlSuggestions correctly uses onMouseDown + e.preventDefault() to prevent input blur from racing with suggestion selection.

Issues found:

  • The isHistoryNavigation guard inside handleDidStopLoading is effectively dead code. In Electron, did-navigate fires before did-stop-loading, so handleDidNavigate always consumes and resets the flag first. The practical impact is limited (history deduplication prevents corruption) but results in unnecessary browserHistory.upsert IPC calls on every back/forward navigation.
  • faviconUrl is not cleared when navigation starts (handleDidStartLoading), so navigating to a page without a favicon leaves the previous page's favicon visible indefinitely.
  • getTabTitle in page.tsx reads registry state imperatively and is not subscribed to changes, so the tab tooltip title lags behind the live BrowserTabLabel.

Confidence Score: 4/5

Safe to merge; the architecture is solid and the known issues are non-blocking bugs with limited practical impact.

The float-over webview design is the correct solution to Electron's reparenting constraint, and the implementation is well-structured. The isHistoryNavigation guard bug causes unnecessary IPC calls on back/forward navigation but does not corrupt history state (pushHistory deduplicates correctly). The stale favicon issue is a minor UX concern. The non-reactive getTabTitle only affects tooltip display. None of these block primary functionality. One targeted fix to handleDidNavigate to stop consuming the isHistoryNavigation flag would resolve the P1.

browserRuntimeRegistry.ts — specifically the handleDidNavigate / handleDidStopLoading interaction around the isHistoryNavigation flag.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts Core float-over registry; well-architected singleton with HMR preservation, but isHistoryNavigation guard in handleDidStopLoading is dead code because handleDidNavigate always resets the flag first, and faviconUrl is not cleared on navigation start.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx Clean, correctly uses useSyncExternalStore to subscribe to registry state; visibility toggling via isTabActive is correct and integrates properly with the keep-alive tab rendering.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts Correctly wires the placeholder ref to the registry's attach/detach lifecycle; imperative electronTrpcClient subscriptions for new-window and context menu avoid the React-context hijack issue.
packages/panes/src/react/components/Workspace/Workspace.tsx Keep-alive tab rendering via mountedTabIds is clean; inactive tabs rendered with invisible pointer-events-none; stale tab IDs are pruned from the set when tabs are removed.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Diff-effect for webview destruction is correct and handles pane drags safely; getTabTitle reads registry state imperatively so tooltip titles are not reactive, unlike BrowserTabLabel.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts Simple URL sanitization; handles http/https, localhost, dotted domains, and falls back to Google search for bare queries.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts Autocomplete fetches full history on every open (via imperative query()) and filters client-side; keyboard navigation and suggestion selection are correctly implemented.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/UrlSuggestions.tsx Correctly uses onMouseDown with e.preventDefault() to avoid losing input focus before the selection registers; favicon error fallback directly manipulates DOM siblings which is acceptable here.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserTabLabel/BrowserTabLabel.tsx Live-updating tab label via useSyncExternalStore; memo wrapper is appropriate; broken favicon tracking via local state is clean.
packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx Accepts optional label prop (for BrowserTabLabel) which overrides the plain text title; drag/drop and rename logic unchanged and correct.

Sequence Diagram

sequenceDiagram
    participant React as BrowserPane
    participant Hook as usePersistentWebview
    participant Registry as browserRuntimeRegistry
    participant DOM as browser-runtime-root
    participant WV as webview element
    participant Main as Main Process tRPC

    React->>Hook: mount with paneId and placeholder
    Hook->>Registry: attach(paneId, placeholder, initialUrl)
    Registry->>DOM: appendChild(webview) — first time only
    Registry->>Registry: ResizeObserver on placeholder
    Registry-->>WV: position via fixed CSS top/left/width/height

    Note over WV,Main: User navigates to URL
    WV->>Registry: did-start-loading — isLoading true
    WV->>Registry: did-navigate — setState currentUrl and pageTitle
    WV->>Registry: did-stop-loading — pushHistory and onDidStopLoading
    Registry->>Main: browserHistory.upsert
    Registry->>Hook: onDidStopLoading callback
    Hook->>React: ctx.actions.updateData

    Note over React,WV: Tab switch away — keep-alive
    React->>Registry: setVisibility(paneId, false)
    Registry-->>WV: visibility hidden — stays in DOM, no reload

    Note over React,WV: Tab switch back
    React->>Registry: setVisibility(paneId, true)
    Registry->>Registry: updateLayout entry
    Registry-->>WV: visibility visible

    Note over Hook,WV: Workspace unmount
    Hook->>Registry: detach(paneId)
    Registry-->>WV: hidden and observer stopped

    Note over React,Main: Tab closed via diff effect
    React->>Registry: destroy(paneId)
    Registry->>DOM: webview.remove()
    Registry->>Main: browser.unregister
Loading

Reviews (1): Last reviewed commit: "feat(desktop): port browser pane to v2 w..." | Re-trigger Greptile

Comment on lines +214 to +241
const handleDidStopLoading = () => {
if (entry.isHistoryNavigation) {
entry.isHistoryNavigation = false;
this.setState(paneId, { isLoading: false });
return;
}
const url = webview.getURL() ?? "";
const title = webview.getTitle() ?? "";
this.setState(paneId, {
isLoading: false,
currentUrl: url,
pageTitle: title,
});
if (url && url !== "about:blank") {
this.pushHistory(paneId, url, title);
electronTrpcClient.browserHistory.upsert
.mutate({ url, title, faviconUrl: entry.state.faviconUrl })
.catch((err) => {
console.error("[browserRuntimeRegistry] upsert history:", err);
});
}
entry.onDidStopLoading?.({ url, title });
};

const handleDidNavigate = (e: Electron.DidNavigateEvent) => {
if (entry.isHistoryNavigation) {
entry.isHistoryNavigation = false;
return;
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 isHistoryNavigation guard is dead code in handleDidStopLoading

In Electron, did-navigate always fires before did-stop-loading. That means handleDidNavigate (lines 238–241) resets entry.isHistoryNavigation = false and returns early — so by the time handleDidStopLoading reaches its own guard on line 215, the flag is already false.

As a result, for every back/forward navigation:

  • The guard if (entry.isHistoryNavigation) at line 215 is never true, making lines 216–219 dead code.
  • pushHistory is still called (though it correctly deduplicates because the target URL is already at the current historyIndex, so no extra entry is added).
  • entry.onDidStopLoading?.() is called, triggering ctx.actions.updateData (benign but unintended).
  • electronTrpcClient.browserHistory.upsert.mutate() fires for every back/forward navigation (extra IPC call).

The fix is to only reset the flag inside handleDidStopLoading, not in handleDidNavigate:

// handleDidNavigate — don't reset the flag; just skip state update
const handleDidNavigate = (e: Electron.DidNavigateEvent) => {
    if (entry.isHistoryNavigation) {
        // flag stays true — handleDidStopLoading will clear it
        return;
    }
    // ...
};

// handleDidStopLoading — single place that resets the flag
const handleDidStopLoading = () => {
    if (entry.isHistoryNavigation) {
        entry.isHistoryNavigation = false;
        this.setState(paneId, { isLoading: false });
        return;
    }
    // ...
};

The same issue applies to handleDidNavigateInPage (line 254) which also resets the flag early, though in-page navigations typically don't trigger did-stop-loading so it's less impactful in practice.

Comment on lines +210 to +212
const handleDidStartLoading = () => {
this.setState(paneId, { isLoading: true, error: null });
};
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 Stale favicon persists after navigation to a new page

handleDidStartLoading clears isLoading and error but doesn't reset faviconUrl. If a user navigates from a page that has a favicon to one that doesn't (or whose favicon loads slowly), the old favicon will persist in both the registry state and the BrowserTabLabel until page-favicon-updated fires for the new page — or indefinitely if it never fires.

Consider clearing faviconUrl when loading begins:

Suggested change
const handleDidStartLoading = () => {
this.setState(paneId, { isLoading: true, error: null });
};
const handleDidStartLoading = () => {
this.setState(paneId, { isLoading: true, error: null, faviconUrl: null });
};

Comment on lines +177 to +191
const getTabTitle = useCallback(
(tab: Tab<PaneViewerData>): string => {
const browserPaneId = getSingleBrowserPaneId(tab);
if (!browserPaneId) return tab.titleOverride ?? tab.id;
const state = browserRuntimeRegistry.getState(browserPaneId);
if (state.pageTitle) return state.pageTitle;
if (state.currentUrl && state.currentUrl !== "about:blank") {
try {
return new URL(state.currentUrl).hostname;
} catch {}
}
return tab.titleOverride ?? "Browser";
},
[getSingleBrowserPaneId],
);
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 getTabTitle is not reactive — tooltip shows stale browser tab titles

getTabTitle reads browserRuntimeRegistry.getState(browserPaneId) at render time but is not subscribed to registry state changes. TabItem uses this value for its <TooltipContent> (the title shown on hover), and won't re-render when browser state changes — only when the tab array itself changes (reorder, add, remove).

The visible label is handled correctly by BrowserTabLabel (via useSyncExternalStore), so the tab content is reactive. However, hovering the tab will show a stale title until the next unrelated re-render.

If the tooltip title needs to stay live, one approach is to have BrowserTabLabel own the full rendered label and include the title for the tooltip slot too. Alternatively, noting this as a known limitation is acceptable given the complexity involved.

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: 7

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts (1)

21-52: Remove unused suggestionsRef to simplify hook state.

suggestionsRef is written but never consumed, so it can be safely removed to reduce noise.

♻️ Suggested cleanup
-	const suggestionsRef = useRef<HistorySuggestion[]>([]);
@@
-	suggestionsRef.current = suggestions;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts
around lines 21 - 52, Remove the unused suggestionsRef to simplify state: delete
the const suggestionsRef = useRef<HistorySuggestion[]>([]); declaration and the
trailing assignment suggestionsRef.current = suggestions; in the
useUrlAutocomplete hook, and also remove any now-unused useRef import; keep all
existing logic that computes suggestions (allHistory, suggestions, useEffect)
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx:
- Around line 94-100: The copy button inside BrowserErrorOverlay is an icon-only
control with no accessible name; update the button (the element with
onClick={copyDetails}) to provide an accessible label by adding either an
aria-label (e.g., aria-label="Copy error details") or aria-labelledby/visually
hidden descriptive text, ensuring screen readers announce its purpose; keep the
existing onClick and classes intact and prefer a localized/descriptive string if
available.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx:
- Around line 67-72: The overflow trigger in BrowserOverflowMenu is an icon-only
button (the button wrapping TbDots) with no accessible name; add a clear
accessible label by giving that button an aria-label (e.g., "More options" or
"Open menu") or include visually hidden text inside the BrowserOverflowMenu
component so assistive tech can announce it, and optionally add
aria-haspopup="menu" to indicate it opens a menu.
- Around line 32-61: The handlers handleScreenshot, handleHardReload,
handleCopyUrl, handleClearCookies, handleClearHistory and handleClearAllData
currently swallow errors via `.catch(() => {})`; change each to surface failures
by catching the error and reporting it (e.g., call a user-facing
notification/toast API and log the error) so users see when destructive/browser
actions fail and can retry; implement either async/await with try/catch or
replace `.catch(() => {})` with `.catch(err => { logger.error(err, "action
failed"); notifyUser("Action failed: " + err.message) })` (use your project's
notify/log utilities) and ensure handleCopyUrl also reports if copyToClipboard
fails.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserTabLabel/BrowserTabLabel.tsx:
- Around line 46-52: BrowserTabLabel.tsx currently binds raw remote faviconUrl
(from page-favicon-updated in browserRuntimeRegistry.ts) directly to the <img
src>, causing the renderer to make external requests outside the webview
session; change this to use a session-backed source by either (a) proxying the
favicon through the main process (implement an IPC or main-endpoint that fetches
the favicon using the webview's session and returns a proxied URL) and update
BrowserTabLabel to request that proxied URL instead of faviconUrl, or (b) have
the main process fetch/convert the remote favicon to a data URL (via IPC) and
set the img src to that data URL; update usages of faviconUrl and
setBrokenFaviconUrl in BrowserTabLabel to accept the proxied/data URL and ensure
the browserRuntimeRegistry.ts emission triggers the main-process
fetch/conversion rather than exposing raw remote URLs to the renderer.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/UrlSuggestions.tsx:
- Around line 33-39: The button in UrlSuggestions.tsx currently only handles
selection via onMouseDown which breaks keyboard activation; keep the existing
onMouseDown (for blur prevention) and add an onClick handler on the same button
that calls onSelect(item.url) so keyboard/enter/space activation works (retain
key={item.url} and type="button" and do not remove e.preventDefault() from
onMouseDown).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts:
- Around line 1-11: sanitizeUrl currently treats any input with a dot as a
hostname; first trim the input and then treat inputs containing whitespace as
search queries; only auto-prefix `http(s)://` for inputs that either already
have a scheme (`^https?://`, `about:`) or match a host/path-like pattern (e.g.,
host labels separated by dots, optional port and path) or the explicit
`localhost`/`127.0.0.1` cases; otherwise return a google search URL with
encodeURIComponent of the trimmed input. Update the sanitizeUrl function to
perform trim(), check for spaces before the `url.includes(".")` branch, and
replace the broad dot-check with a stricter host/path regex to decide when to
prefix `https://`.

In `@packages/panes/src/react/components/Workspace/Workspace.tsx`:
- Around line 29-37: The active tab can render blank because activeTabId is
added to mountedTabIds only in useEffect after render; update the logic so the
active tab is considered mounted synchronously by including activeTabId in any
filters and in the setMountedTabIds updater (i.e., inside the useEffect that
calls setMountedTabIds, ensure you treat prev.has(activeTabId) as including
activeTabId immediately or merge activeTabId into the returned Set), and apply
the same change to the similar filter logic around the other occurrence (the
block referenced at lines ~100-102) so the active tab is always visible on
initial render.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts:
- Around line 21-52: Remove the unused suggestionsRef to simplify state: delete
the const suggestionsRef = useRef<HistorySuggestion[]>([]); declaration and the
trailing assignment suggestionsRef.current = suggestions; in the
useUrlAutocomplete hook, and also remove any now-unused useRef import; keep all
existing logic that computes suggestions (allHistory, suggestions, useEffect)
intact.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c07f4b1-1f6f-4226-8c1b-442265188673

📥 Commits

Reviewing files that changed from the base of the PR and between 1bdd700 and 1842175.

📒 Files selected for processing (28)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserTabLabel/BrowserTabLabel.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserTabLabel/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/UrlSuggestions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/constants.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/hooks/usePersistentWebview/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx
  • packages/panes/src/react/types.ts

Comment on lines +94 to +100
<button
type="button"
onClick={copyDetails}
className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground/70 transition-colors"
>
<TbCopy 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.

⚠️ Potential issue | 🟡 Minor

Add an accessible name to the copy button.

The icon-only control here has no text or aria-label, so screen readers will announce it as an unlabeled button.

Suggested fix
 						<button
 							type="button"
 							onClick={copyDetails}
+							aria-label="Copy error details"
 							className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground/70 transition-colors"
 						>
📝 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
<button
type="button"
onClick={copyDetails}
className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground/70 transition-colors"
>
<TbCopy className="size-4" />
</button>
<button
type="button"
onClick={copyDetails}
aria-label="Copy error details"
className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground/70 transition-colors"
>
<TbCopy className="size-4" />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx
around lines 94 - 100, The copy button inside BrowserErrorOverlay is an
icon-only control with no accessible name; update the button (the element with
onClick={copyDetails}) to provide an accessible label by adding either an
aria-label (e.g., aria-label="Copy error details") or aria-labelledby/visually
hidden descriptive text, ensuring screen readers announce its purpose; keep the
existing onClick and classes intact and prefer a localized/descriptive string if
available.

Comment on lines +32 to +61
const handleScreenshot = () => {
electronTrpcClient.browser.screenshot.mutate({ paneId }).catch(() => {});
};

const handleHardReload = () => {
electronTrpcClient.browser.reload
.mutate({ paneId, hard: true })
.catch(() => {});
};

const handleCopyUrl = () => {
if (currentUrl) {
copyToClipboard(currentUrl);
}
};

const handleClearCookies = () => {
electronTrpcClient.browser.clearBrowsingData
.mutate({ type: "cookies" })
.catch(() => {});
};

const handleClearHistory = () => {
electronTrpcClient.browserHistory.clear.mutate().catch(() => {});
};

const handleClearAllData = () => {
electronTrpcClient.browser.clearBrowsingData
.mutate({ type: "all" })
.catch(() => {});
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

Don't swallow browser action failures.

These mutations include destructive/global actions, but every failure is dropped with .catch(() => {}). If screenshot, reload, or data-clearing fails, the UI still looks successful and the user gets no recovery path.

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx
around lines 32 - 61, The handlers handleScreenshot, handleHardReload,
handleCopyUrl, handleClearCookies, handleClearHistory and handleClearAllData
currently swallow errors via `.catch(() => {})`; change each to surface failures
by catching the error and reporting it (e.g., call a user-facing
notification/toast API and log the error) so users see when destructive/browser
actions fail and can retry; implement either async/await with try/catch or
replace `.catch(() => {})` with `.catch(err => { logger.error(err, "action
failed"); notifyUser("Action failed: " + err.message) })` (use your project's
notify/log utilities) and ensure handleCopyUrl also reports if copyToClipboard
fails.

Comment on lines +67 to +72
<button
type="button"
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground"
>
<TbDots className="size-3.5" />
</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.

⚠️ Potential issue | 🟡 Minor

Label the overflow trigger for assistive tech.

This is an icon-only button with no accessible name.

Suggested fix
 				<button
 					type="button"
+					aria-label="Browser actions"
 					className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground"
 				>
📝 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
<button
type="button"
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground"
>
<TbDots className="size-3.5" />
</button>
<button
type="button"
aria-label="Browser actions"
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground"
>
<TbDots className="size-3.5" />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx
around lines 67 - 72, The overflow trigger in BrowserOverflowMenu is an
icon-only button (the button wrapping TbDots) with no accessible name; add a
clear accessible label by giving that button an aria-label (e.g., "More options"
or "Open menu") or include visually hidden text inside the BrowserOverflowMenu
component so assistive tech can announce it, and optionally add
aria-haspopup="menu" to indicate it opens a menu.

Comment on lines +33 to +39
<button
key={item.url}
type="button"
onMouseDown={(e) => {
e.preventDefault();
onSelect(item.url);
}}
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

Add onClick alongside onMouseDown for keyboard activation.

Current selection is mouse-driven only. Adding onClick preserves keyboard/button activation semantics while keeping onMouseDown for blur prevention.

♿ Suggested fix
 					onMouseDown={(e) => {
 						e.preventDefault();
 						onSelect(item.url);
 					}}
+					onClick={() => {
+						onSelect(item.url);
+					}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/UrlSuggestions.tsx
around lines 33 - 39, The button in UrlSuggestions.tsx currently only handles
selection via onMouseDown which breaks keyboard activation; keep the existing
onMouseDown (for blur prevention) and add an onClick handler on the same button
that calls onSelect(item.url) so keyboard/enter/space activation works (retain
key={item.url} and type="button" and do not remove e.preventDefault() from
onMouseDown).

Comment thread packages/panes/src/react/components/Workspace/Workspace.tsx Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 11, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
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.

11 issues found across 28 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="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx:14">
P2: Avoid stripping trailing slashes from URLs, because submitting an unchanged address bar can navigate to a different URL.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts:8">
P2: `url.includes(".")` is too broad for URL detection and misclassifies dotted search queries as URLs.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts:31">
P2: Do not silently swallow autocomplete history fetch errors; add observable handling (at least a warning log) so failures can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:210">
P2: `handleDidStartLoading` clears `error` but not `faviconUrl`. When navigating from a page with a favicon to one without, the old favicon persists indefinitely (since `page-favicon-updated` never fires for the new page). Clear `faviconUrl` when loading begins.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:215">
P1: `isHistoryNavigation` flag is consumed by `handleDidNavigate` before `handleDidStopLoading` gets to check it. In Electron, `did-navigate` fires before `did-stop-loading`, so the flag is always `false` by the time `handleDidStopLoading` runs. This causes back/forward navigations to fall through to the normal path — calling `pushHistory` (which can truncate forward history if the URL differs even slightly) and making a redundant `browserHistory.upsert` call.

Remove the flag reset here; let `handleDidStopLoading` be the sole consumer.</violation>

<violation number="3" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:407">
P2: Silent `.catch(() => {})` swallows errors with no logging. At minimum, log a warning so failures in `unregister` are observable during debugging.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx:63">
P2: Handle clipboard copy failures explicitly to avoid unhandled promise rejections from the async IPC call.</violation>
</file>

<file name="packages/panes/src/react/components/Workspace/Workspace.tsx">

<violation number="1" location="packages/panes/src/react/components/Workspace/Workspace.tsx:101">
P2: Include the active tab in the render filter immediately; relying on the post-render effect causes a blank frame when first switching to an unmounted tab.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx:78">
P2: Do not silently swallow `openDevTools` failures; log or otherwise surface the error so this failure path is observable.

(Based on your team's feedback about handling async errors and avoiding empty catch blocks.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:181">
P3: `getTabTitle` reads `browserRuntimeRegistry.getState()` at render time but is not subscribed to registry state changes. The tooltip will show a stale title until an unrelated re-render happens. The visible tab label is correctly reactive via `BrowserTabLabel` using `useSyncExternalStore`, but the tooltip title computed here will drift.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx:33">
P2: Every browser action failure is silently swallowed with `.catch(() => {})`. These include destructive/global operations like clearing cookies, clearing all browsing data, and taking screenshots. If any of these fail, the user gets no feedback and no recovery path. At minimum, log the error or show a toast on failure.</violation>
</file>

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


function displayUrl(url: string): string {
if (url === "about:blank") return "";
return url.endsWith("/") ? url.slice(0, -1) : url;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Avoid stripping trailing slashes from URLs, because submitting an unchanged address bar can navigate to a different URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx, line 14:

<comment>Avoid stripping trailing slashes from URLs, because submitting an unchanged address bar can navigate to a different URL.</comment>

<file context>
@@ -0,0 +1,215 @@
+
+function displayUrl(url: string): string {
+	if (url === "about:blank") return "";
+	return url.endsWith("/") ? url.slice(0, -1) : url;
+}
+
</file context>
Fix with Cubic

.then((items) => {
if (!cancelled) setAllHistory(items as HistorySuggestion[]);
})
.catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Do not silently swallow autocomplete history fetch errors; add observable handling (at least a warning log) so failures can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts, line 31:

<comment>Do not silently swallow autocomplete history fetch errors; add observable handling (at least a warning log) so failures can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.) </comment>

<file context>
@@ -0,0 +1,129 @@
+			.then((items) => {
+				if (!cancelled) setAllHistory(items as HistorySuggestion[]);
+			})
+			.catch(() => {});
+		return () => {
+			cancelled = true;
</file context>
Fix with Cubic

entry.webview.remove();
this.entries.delete(paneId);
this.listenersByPaneId.delete(paneId);
electronTrpcClient.browser.unregister.mutate({ paneId }).catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Silent .catch(() => {}) swallows errors with no logging. At minimum, log a warning so failures in unregister are observable during debugging.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts, line 407:

<comment>Silent `.catch(() => {})` swallows errors with no logging. At minimum, log a warning so failures in `unregister` are observable during debugging.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) </comment>

<file context>
@@ -0,0 +1,483 @@
+		entry.webview.remove();
+		this.entries.delete(paneId);
+		this.listenersByPaneId.delete(paneId);
+		electronTrpcClient.browser.unregister.mutate({ paneId }).catch(() => {});
+	}
+
</file context>
Fix with Cubic

Comment thread packages/panes/src/react/components/Workspace/Workspace.tsx Outdated
const state = useBrowserState(paneId);

const handleOpenDevTools = useCallback(() => {
electronTrpcClient.browser.openDevTools.mutate({ paneId }).catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Do not silently swallow openDevTools failures; log or otherwise surface the error so this failure path is observable.

(Based on your team's feedback about handling async errors and avoiding empty catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx, line 78:

<comment>Do not silently swallow `openDevTools` failures; log or otherwise surface the error so this failure path is observable.

(Based on your team's feedback about handling async errors and avoiding empty catch blocks.) </comment>

<file context>
@@ -0,0 +1,142 @@
+	const state = useBrowserState(paneId);
+
+	const handleOpenDevTools = useCallback(() => {
+		electronTrpcClient.browser.openDevTools.mutate({ paneId }).catch(() => {});
+	}, [paneId]);
+
</file context>
Fix with Cubic

const { copyToClipboard } = useCopyToClipboard();

const handleScreenshot = () => {
electronTrpcClient.browser.screenshot.mutate({ paneId }).catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Every browser action failure is silently swallowed with .catch(() => {}). These include destructive/global operations like clearing cookies, clearing all browsing data, and taking screenshots. If any of these fail, the user gets no feedback and no recovery path. At minimum, log the error or show a toast on failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx, line 33:

<comment>Every browser action failure is silently swallowed with `.catch(() => {})`. These include destructive/global operations like clearing cookies, clearing all browsing data, and taking screenshots. If any of these fail, the user gets no feedback and no recovery path. At minimum, log the error or show a toast on failure.</comment>

<file context>
@@ -0,0 +1,115 @@
+	const { copyToClipboard } = useCopyToClipboard();
+
+	const handleScreenshot = () => {
+		electronTrpcClient.browser.screenshot.mutate({ paneId }).catch(() => {});
+	};
+
</file context>
Fix with Cubic

- Float-over webviews already preserve state across tab switches via the
  registry's attach/detach pair, so keep-alive tab rendering was redundant.
  Revert packages/panes Workspace.tsx to rendering only the active tab.
- Move all browser-specific wiring from page.tsx into a new
  useBrowserPaneIntegration hook co-located with BrowserPane. page.tsx now
  consumes a single hook call instead of owning tab title derivation, tab
  label rendering, and the destroy diff effect.
- Registry attach takes over the visibility flip directly; setVisibility
  method is no longer needed. Drop the rAF deferral — React useEffect fires
  after layout commit so getBoundingClientRect reads correct values
  synchronously.
- BrowserPane no longer subscribes to the workspace store for isTabActive.
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:
- Around line 214-236: Replace any pre-mutation + webview.loadURL() usage in
goBack()/goForward() (and similar navigation helpers) with calls to the native
webview.goBack()/goForward() methods; remove the immediate this.setState(paneId,
{ isLoading: true }) and avoid calling webview.loadURL(savedUrl) so you don't
reload or desynchronize on failure. Instead, let the webview emit navigation
events (e.g., handleDidStopLoading, did-navigate, did-navigate-in-page,
did-start-loading) and derive/update UI state (this.setState and
this.pushHistory) from those events (use existing handleDidStopLoading,
entry.onDidStopLoading, pushHistory, setState, and webview.getURL()/getTitle()).
Also add error/fallback handling if goBack()/goForward() report failure so
registry state stays consistent.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66057b41-2d5f-41d2-b311-72f5751e91e8

📥 Commits

Reviewing files that changed from the base of the PR and between 1842175 and 7ecb775.

📒 Files selected for processing (6)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/useBrowserPaneIntegration.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • packages/panes/src/react/components/Workspace/Workspace.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx

Copy link
Copy Markdown
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.

3 issues found across 6 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="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/useBrowserPaneIntegration.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/useBrowserPaneIntegration.tsx:45">
P2: Avoid an empty `catch` when parsing the URL; handle parse failures explicitly instead of silently swallowing them.</violation>
</file>

<file name="packages/panes/src/react/components/Workspace/Workspace.tsx">

<violation number="1">
P1: Rendering only `activeTab` unmounts inactive tabs on every switch, regressing keep-alive behavior and state persistence across tabs.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx:4">
P1: Active tab visibility is no longer synchronized for browser panes. Since the webviews are floated at the document root and only hidden on detach, removing the tab-activity effect leaves inactive tab webviews visible, so they can paint over the active tab after a tab switch.</violation>
</file>

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

@@ -9,6 +9,8 @@ export function Workspace<TData>({
registry,
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P1: Rendering only activeTab unmounts inactive tabs on every switch, regressing keep-alive behavior and state persistence across tabs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/panes/src/react/components/Workspace/Workspace.tsx, line 69:

<comment>Rendering only `activeTab` unmounts inactive tabs on every switch, regressing keep-alive behavior and state persistence across tabs.</comment>

<file context>
@@ -96,36 +66,19 @@ export function Workspace<TData>({
-					</div>
-				)}
-			</div>
+			{activeTab ? (
+				<Tab
+					store={store}
</file context>
Fix with Cubic

import type { RendererContext } from "@superset/panes";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { GlobeIcon } from "lucide-react";
import { useCallback, useSyncExternalStore } from "react";
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P1: Active tab visibility is no longer synchronized for browser panes. Since the webviews are floated at the document root and only hidden on detach, removing the tab-activity effect leaves inactive tab webviews visible, so they can paint over the active tab after a tab switch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx, line 4:

<comment>Active tab visibility is no longer synchronized for browser panes. Since the webviews are floated at the document root and only hidden on detach, removing the tab-activity effect leaves inactive tab webviews visible, so they can paint over the active tab after a tab switch.</comment>

<file context>
@@ -1,10 +1,9 @@
 import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
 import { GlobeIcon } from "lucide-react";
-import { useCallback, useEffect, useSyncExternalStore } from "react";
+import { useCallback, useSyncExternalStore } from "react";
 import { TbDeviceDesktop } from "react-icons/tb";
 import { electronTrpcClient } from "renderer/lib/trpc-client";
</file context>
Fix with Cubic

- Add PaneDefinition.onRemoved(pane) lifecycle hook to @superset/panes.
  Workspace.tsx tracks a flat snapshot of panes across all tabs and fires
  onRemoved with the pre-removal Pane object when an id disappears. Pane
  moves between tabs keep the same id in the current set so moves correctly
  don't fire onRemoved.
- Wire browser cleanup declaratively via the new hook in usePaneRegistry.tsx:
  `onRemoved: (pane) => browserRuntimeRegistry.destroy(pane.id)`. Removes
  the need for useBrowserPaneDestroy in userland.
- Persist faviconUrl on BrowserPaneData alongside pageTitle. The registry's
  persist callback fires from handleDidStopLoading AND handlePageFaviconUpdated
  via a shared firePersist helper. Favicons now survive app restart.
- Replace renderTabLabel with renderTabIcon in @superset/panes. TabItem renders
  {icon}{title}{accessory} — icon leading, accessory trailing, native gap-2
  from the parent button. Drop redundant `| null` from the ReactNode types.
- Fix titleOverride precedence in Workspace.tsx: `tab.titleOverride ??
  getTabTitle(tab) ?? tab.id`. Renaming a browser tab now actually sticks.
- Empty tab rename clears titleOverride instead of being swallowed; thread
  `string | undefined` through TabItem → TabBar → Workspace so the derived
  title takes back over on clear.
- Collapse useBrowserPaneIntegration.tsx and BrowserTabLabel component into
  BrowserPane.tsx as pure functions (getBrowserTabTitle, renderBrowserTabIcon).
  Favicon is now a plain <img> reading from persisted pane data — no more
  subscribing component just for one field.
- page.tsx loses browser-specific imports, refs, effects, and callbacks. Just
  two pure helpers passed as props.
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.

🧹 Nitpick comments (3)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts (2)

88-107: Global event listeners are never cleaned up.

The dragstart, dragend, drop, and resize listeners are added once but never removed. While HMR preservation mitigates this during development, if the registry were ever destroyed and recreated (or in edge cases with multiple windows), these would accumulate.

Consider storing references and providing a cleanup path, or document that this singleton is expected to live for the app lifetime.

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
around lines 88 - 107, The installGlobalListeners method attaches window
listeners (dragstart, dragend, drop, resize) but never removes them; update
installGlobalListeners to save the listener callbacks (e.g., references to the
setPassthrough wrapper and the resize handler) on the instance (use properties
like this._globalListenerRefs) and add a public cleanup method (e.g.,
uninstallGlobalListeners or dispose) that removes each listener via
window.removeEventListener and clears this.globalListenersInstalled; ensure
uninstallGlobalListeners also undoes any pointer-events changes if needed and
call it from any lifecycle teardown where the registry might be destroyed or
recreated so entries/updateLayout behavior remains correct without leaking
listeners.

223-245: History upsert may fire with stale favicon.

In handleDidStopLoading, upsert is called with entry.state.faviconUrl, but the favicon event (page-favicon-updated) may arrive after did-stop-loading. This could persist a null favicon even when one is available shortly after.

The subsequent handlePageFaviconUpdated does call upsert again with the correct favicon, so this is self-correcting but causes an extra mutation. Consider deferring the history upsert slightly or relying solely on the favicon handler for favicon persistence.

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
around lines 223 - 245, handleDidStopLoading currently calls
electronTrpcClient.browserHistory.upsert with entry.state.faviconUrl which can
be stale because the page-favicon-updated handler (handlePageFaviconUpdated) may
arrive after did-stop-loading; change handleDidStopLoading to only call upsert
immediately when entry.state.faviconUrl is non-null (so you avoid persisting
null), and if favicon is null, skip the upsert here (still call
this.pushHistory/persist URL/title) and let handlePageFaviconUpdated perform the
upsert when the favicon becomes available; alternatively, if you prefer a short
delay, debounce the upsert in handleDidStopLoading (e.g., schedule a small
timeout and re-check entry.state.faviconUrl before calling
electronTrpcClient.browserHistory.upsert) so handlePageFaviconUpdated can supply
the real favicon and avoid an extra mutation.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx (1)

15-23: Guard against undefined pane access.

tab.panes[paneIds[0]] could be undefined if the panes object is inconsistent with its keys. While unlikely, a defensive check would prevent a runtime error.

🛡️ Suggested defensive check
 function getSingleBrowserPane(
 	tab: Tab<PaneViewerData>,
 ): { id: string; data: BrowserPaneData } | null {
 	const paneIds = Object.keys(tab.panes);
 	if (paneIds.length !== 1) return null;
 	const pane = tab.panes[paneIds[0]];
+	if (!pane) return null;
 	if (pane.kind !== "browser") return null;
 	return { id: pane.id, data: pane.data as BrowserPaneData };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx
around lines 15 - 23, getSingleBrowserPane currently assumes
tab.panes[paneIds[0]] exists and accesses its properties directly; add a
defensive null/undefined check after computing pane (e.g. if (!pane) return
null) and additionally validate expected properties (pane.kind, pane.id,
pane.data) before returning to avoid runtime errors when tab.panes is
inconsistent. Update the function getSingleBrowserPane to perform these guards
and return null on any missing/invalid pane.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx:
- Around line 15-23: getSingleBrowserPane currently assumes
tab.panes[paneIds[0]] exists and accesses its properties directly; add a
defensive null/undefined check after computing pane (e.g. if (!pane) return
null) and additionally validate expected properties (pane.kind, pane.id,
pane.data) before returning to avoid runtime errors when tab.panes is
inconsistent. Update the function getSingleBrowserPane to perform these guards
and return null on any missing/invalid pane.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:
- Around line 88-107: The installGlobalListeners method attaches window
listeners (dragstart, dragend, drop, resize) but never removes them; update
installGlobalListeners to save the listener callbacks (e.g., references to the
setPassthrough wrapper and the resize handler) on the instance (use properties
like this._globalListenerRefs) and add a public cleanup method (e.g.,
uninstallGlobalListeners or dispose) that removes each listener via
window.removeEventListener and clears this.globalListenersInstalled; ensure
uninstallGlobalListeners also undoes any pointer-events changes if needed and
call it from any lifecycle teardown where the registry might be destroyed or
recreated so entries/updateLayout behavior remains correct without leaking
listeners.
- Around line 223-245: handleDidStopLoading currently calls
electronTrpcClient.browserHistory.upsert with entry.state.faviconUrl which can
be stale because the page-favicon-updated handler (handlePageFaviconUpdated) may
arrive after did-stop-loading; change handleDidStopLoading to only call upsert
immediately when entry.state.faviconUrl is non-null (so you avoid persisting
null), and if favicon is null, skip the upsert here (still call
this.pushHistory/persist URL/title) and let handlePageFaviconUpdated perform the
upsert when the favicon becomes available; alternatively, if you prefer a short
delay, debounce the upsert in handleDidStopLoading (e.g., schedule a small
timeout and re-check entry.state.faviconUrl before calling
electronTrpcClient.browserHistory.upsert) so handlePageFaviconUpdated can supply
the real favicon and avoid an extra mutation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d718533d-ab59-4c6d-8ea8-cfa00e1ec3e9

📥 Commits

Reviewing files that changed from the base of the PR and between 7ecb775 and 504526c.

📒 Files selected for processing (11)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx
  • packages/panes/src/react/types.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts
  • packages/panes/src/react/types.ts
  • packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts

- Drop the custom history array and isHistoryNavigation flag. goBack/goForward
  now call webview.goBack()/goForward() directly, preserving scroll position,
  form state, and BFCache on back/forward navigation.
- Add refreshNavState helper that reads webview.canGoBack()/canGoForward() and
  writes them into runtime state. Called from did-navigate,
  did-navigate-in-page, did-stop-loading, and on re-attach of existing entries.
- The isHistoryNavigation guard was dead code: did-navigate fires before
  did-stop-loading, so the flag was always false by the time the guard ran,
  causing a redundant browserHistory.upsert IPC call on every back/forward nav.
- Clear faviconUrl in handleDidStartLoading so stale favicons don't persist
  when navigating to a page with no favicon.
- Tighten sanitizeUrl: reject dotted strings containing whitespace (e.g.
  "release v2.1") as URLs, trim input, tighten the localhost/127.0.0.1 regex
  to accept optional port and path. Dotted search queries now correctly fall
  through to Google search.
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.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts (1)

141-145: Consider logging suppressed errors for debugging.

The empty catch block silently swallows errors from canGoBack()/canGoForward(). While this may be intentional if the webview can be in an invalid state, adding minimal logging would aid debugging.

♻️ Suggested improvement
 try {
     canGoBack = entry.webview.canGoBack();
     canGoForward = entry.webview.canGoForward();
-} catch {}
+} catch (err) {
+    console.debug("[browserRuntimeRegistry] refreshNavState:", err);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
around lines 141 - 145, The empty catch around calling
entry.webview.canGoBack()/canGoForward() swallows errors; update the try/catch
in the block that computes canGoBack/canGoForward so the catch logs the caught
error (e.g., console.warn or the module's logger) along with the paneId and a
brief context string; keep the existing behavior of setting state via
this.setState(paneId, { canGoBack, canGoForward }) but ensure any exception from
entry.webview is surfaced in the log to aid debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:
- Around line 141-145: The empty catch around calling
entry.webview.canGoBack()/canGoForward() swallows errors; update the try/catch
in the block that computes canGoBack/canGoForward so the catch logs the caught
error (e.g., console.warn or the module's logger) along with the paneId and a
brief context string; keep the existing behavior of setting state via
this.setState(paneId, { canGoBack, canGoForward }) but ensure any exception from
entry.webview is surfaced in the log to aid debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15402c31-b218-4a88-8f5f-03478e62b895

📥 Commits

Reviewing files that changed from the base of the PR and between 504526c and 84a8f63.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/sanitizeUrl.ts

Copy link
Copy Markdown
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 2 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="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts:144">
P2: Avoid swallowing errors in `refreshNavState`; the empty catch hides webview failures and makes navigation-state bugs hard to diagnose. Log the failure (or otherwise surface it) with context.

(Based on your team's feedback about handling async/runtime failures explicitly and avoiding silent catch blocks.) [FEEDBACK_USED]</violation>
</file>

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

try {
canGoBack = entry.webview.canGoBack();
canGoForward = entry.webview.canGoForward();
} catch {}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Avoid swallowing errors in refreshNavState; the empty catch hides webview failures and makes navigation-state bugs hard to diagnose. Log the failure (or otherwise surface it) with context.

(Based on your team's feedback about handling async/runtime failures explicitly and avoiding silent catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts, line 144:

<comment>Avoid swallowing errors in `refreshNavState`; the empty catch hides webview failures and makes navigation-state bugs hard to diagnose. Log the failure (or otherwise surface it) with context.

(Based on your team's feedback about handling async/runtime failures explicitly and avoiding silent catch blocks.) </comment>

<file context>
@@ -138,34 +133,16 @@ class BrowserRuntimeRegistryImpl {
+		try {
+			canGoBack = entry.webview.canGoBack();
+			canGoForward = entry.webview.canGoForward();
+		} catch {}
+		this.setState(paneId, { canGoBack, canGoForward });
 	}
</file context>
Suggested change
} catch {}
} catch (err) {
console.warn("[browserRuntimeRegistry] refreshNavState failed:", err);
}
Fix with Cubic

@saddlepaddle saddlepaddle merged commit 2c67364 into main Apr 11, 2026
15 checks passed
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 11, 2026
…ence (superset-sh#3346)

* feat(desktop): port browser pane to v2 workspaces with global persistence

- Port v1 browser pane (webview, URL toolbar, history, error overlay, new-window
  handler) into v2 workspace tree as a native implementation. Uses the v2
  Workspace component's renderToolbar slot for a Chrome-style URL bar in the
  pane header.
- Float-over webview: each <webview> lives at document.body under a fixed
  #browser-runtime-root, positioned via ResizeObserver on a per-pane placeholder
  div. Elements never reparent, so Electron's guest WebContents survives tab
  switches, pane drags, and workspace route remounts — no page reloads.
- Keep-alive tab rendering in @superset/panes Workspace.tsx: tabs that have
  been visited stay mounted behind visibility:hidden so their React state
  (and persistent DOM like webviews) survives tab switches. Also benefits
  terminal scrollback, chat subscriptions, editor state.
- BrowserRuntimeRegistry: module-level singleton keyed by paneId, modeled on
  terminalRuntimeRegistry. Owns webview lifecycle, state subscriptions via
  useSyncExternalStore, drag-passthrough listeners, equality-guarded setState
  to deduplicate webview event spam.
- Single-pane-browser tabs show the page favicon + live page title in the tab
  bar via a new optional renderTabLabel prop on the panes library.
- BrowserPane subscribes to the workspace store for activeTabId to drive
  webview visibility, so inactive-tab webviews don't paint on top of the
  active tab's content.
- Destroy path: diff the set of browser pane ids across all tabs each render,
  destroying any that disappeared. Correctly handles tab closes, intra-tab
  split closes, and pane drags between tabs (moved panes stay in the set).

Known limitation: dropping panes onto another browser pane's body doesn't
register — the webview compositor layer intercepts drag events even with
pointer-events:none/visibility:hidden. Drops on pane headers work. Deferred.

* refactor(desktop): co-locate browser pane integration, remove keep-alive

- Float-over webviews already preserve state across tab switches via the
  registry's attach/detach pair, so keep-alive tab rendering was redundant.
  Revert packages/panes Workspace.tsx to rendering only the active tab.
- Move all browser-specific wiring from page.tsx into a new
  useBrowserPaneIntegration hook co-located with BrowserPane. page.tsx now
  consumes a single hook call instead of owning tab title derivation, tab
  label rendering, and the destroy diff effect.
- Registry attach takes over the visibility flip directly; setVisibility
  method is no longer needed. Drop the rAF deferral — React useEffect fires
  after layout commit so getBoundingClientRect reads correct values
  synchronously.
- BrowserPane no longer subscribes to the workspace store for isTabActive.

* refactor(desktop): simplify browser pane integration, add onRemoved hook

- Add PaneDefinition.onRemoved(pane) lifecycle hook to @superset/panes.
  Workspace.tsx tracks a flat snapshot of panes across all tabs and fires
  onRemoved with the pre-removal Pane object when an id disappears. Pane
  moves between tabs keep the same id in the current set so moves correctly
  don't fire onRemoved.
- Wire browser cleanup declaratively via the new hook in usePaneRegistry.tsx:
  `onRemoved: (pane) => browserRuntimeRegistry.destroy(pane.id)`. Removes
  the need for useBrowserPaneDestroy in userland.
- Persist faviconUrl on BrowserPaneData alongside pageTitle. The registry's
  persist callback fires from handleDidStopLoading AND handlePageFaviconUpdated
  via a shared firePersist helper. Favicons now survive app restart.
- Replace renderTabLabel with renderTabIcon in @superset/panes. TabItem renders
  {icon}{title}{accessory} — icon leading, accessory trailing, native gap-2
  from the parent button. Drop redundant `| null` from the ReactNode types.
- Fix titleOverride precedence in Workspace.tsx: `tab.titleOverride ??
  getTabTitle(tab) ?? tab.id`. Renaming a browser tab now actually sticks.
- Empty tab rename clears titleOverride instead of being swallowed; thread
  `string | undefined` through TabItem → TabBar → Workspace so the derived
  title takes back over on clear.
- Collapse useBrowserPaneIntegration.tsx and BrowserTabLabel component into
  BrowserPane.tsx as pure functions (getBrowserTabTitle, renderBrowserTabIcon).
  Favicon is now a plain <img> reading from persisted pane data — no more
  subscribing component just for one field.
- page.tsx loses browser-specific imports, refs, effects, and callbacks. Just
  two pure helpers passed as props.

* refactor(desktop): use native webview back/forward + tighten sanitizeUrl

- Drop the custom history array and isHistoryNavigation flag. goBack/goForward
  now call webview.goBack()/goForward() directly, preserving scroll position,
  form state, and BFCache on back/forward navigation.
- Add refreshNavState helper that reads webview.canGoBack()/canGoForward() and
  writes them into runtime state. Called from did-navigate,
  did-navigate-in-page, did-stop-loading, and on re-attach of existing entries.
- The isHistoryNavigation guard was dead code: did-navigate fires before
  did-stop-loading, so the flag was always false by the time the guard ran,
  causing a redundant browserHistory.upsert IPC call on every back/forward nav.
- Clear faviconUrl in handleDidStartLoading so stale favicons don't persist
  when navigating to a page with no favicon.
- Tighten sanitizeUrl: reject dotted strings containing whitespace (e.g.
  "release v2.1") as URLs, trim input, tighten the localhost/127.0.0.1 regex
  to accept optional port and path. Dotted search queries now correctly fall
  through to Google search.
@Kitenite Kitenite deleted the saddlepaddle/honorable-burn branch April 13, 2026 16:35
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