Skip to content

Implement in-app browser#1519

Merged
saddlepaddle merged 11 commits into
mainfrom
browser-pane
Feb 16, 2026
Merged

Implement in-app browser#1519
saddlepaddle merged 11 commits into
mainfrom
browser-pane

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 16, 2026

Summary

  • URL autocomplete: Chrome-like dropdown in browser pane URL bar showing persistent browsing history (favicon + title + URL) with keyboard navigation, backed by SQLite via Drizzle
  • Favicon capture: Records favicons via page-favicon-updated webview event, persisted across sessions
  • Ports → browser pane: Clicking the external link icon on a detected port now opens it in an in-app browser tab instead of the system browser
  • Clear browsing history: New overflow menu item to wipe history
  • URL bar polish: Strip trailing slash from display (e.g. https://github.com/https://github.com)
  • CSP fix: Widen img-src to https: to allow favicons from arbitrary domains

Test plan

  • Navigate to 3+ URLs → entries appear in autocomplete dropdown
  • Click URL bar → dropdown shows recent history with favicons
  • Type partial URL → suggestions filter correctly
  • Arrow keys + Enter → navigates to highlighted suggestion
  • Click suggestion → navigates, dropdown closes
  • Escape once → dropdown closes; again → exits edit mode
  • Overflow menu → "Clear Browsing History" → history cleared, dropdown empty
  • Restart app → history persists
  • Detected port → click external link icon → opens in browser pane (not system browser)
  • Trailing slash not shown in URL bar display

Summary by CodeRabbit

  • New Features

    • Embedded browser pane with full navigation, address bar, URL autocomplete, history suggestions, and DevTools pane
    • Browser history recording, search, and upsert (visit counting)
    • Screenshot, copy-to-clipboard, and browsing-data clearing (cookies, cache, storage)
    • Error overlay with retry for failed page loads
    • Hotkey: Cmd+Shift+B to open a new browser tab
  • Infrastructure

    • Desktop browser extension support and new backend APIs for browser control and history

… cleanup

- Replace broken setDevToolsWebContents approach (Electron #15874) with
  CDP remote debugging URL loaded directly in the devtools webview
- Discover CDP port dynamically via app.commandLine.getSwitchValue
- Match webview targets by type "webview" (not "page") in /json endpoint
- Add PaneToolbarActions to DevToolsPane for close/split buttons
- Add "devtools" to pane type persistence schema to fix validation error
- Cascade removePane: closing a browser pane removes its devtools panes
…wser pane

Add Chrome-like URL autocomplete to the browser pane URL bar, backed by
persistent SQLite history. Navigating to a page records the URL, title,
and favicon in the local database. Clicking the URL bar shows a dropdown
of recent history filtered as you type, with keyboard navigation and
favicon display.

Also wires detected ports to open in the in-app browser pane instead of
the system browser.

Key changes:
- Add `browser_history` table to local-db with indexed URL and timestamp
- Add `browserHistory` tRPC router (getAll, search, upsert, clear)
- Capture favicons via `page-favicon-updated` webview event
- Create `useUrlAutocomplete` hook with client-side filtering
- Create `UrlSuggestions` dropdown component
- Add "Clear Browsing History" to overflow menu
- Widen CSP `img-src` to `https:` for arbitrary favicon domains
- Open port links in browser pane via `addBrowserTab`
- Strip trailing slash from URL bar display
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Adds an in-app browser feature: browser and browserHistory TRPC routers, browser_history DB table, BrowserPane/DevTools UI and hooks, browser pane state in tabs store/types, Electron main process extension loading, and related renderer, extension, and config updates.

Changes

Cohort / File(s) Summary
TRPC routers & DB schema
apps/desktop/src/lib/trpc/routers/browser/browser.ts, apps/desktop/src/lib/trpc/routers/browser-history/index.ts, apps/desktop/src/lib/trpc/routers/index.ts, packages/local-db/src/schema/schema.ts, packages/local-db/drizzle/0026_browser_history.sql, packages/local-db/drizzle/meta/...
Adds browser and browserHistory routers (navigation, history, console/devtools endpoints); introduces browser_history table, migration SQL, and drizzle snapshot.
Browser manager (main) & Electron setup
apps/desktop/src/main/lib/browser/browser-manager.ts, apps/desktop/src/main/index.ts, apps/desktop/src/lib/electron-app/factories/app/setup.ts
New BrowserManager singleton for webContents tracking, console capture, screenshots, eval, and DevTools URL; loads a browser extension in main init; avoids attaching will-navigate for webview contents.
Renderer: BrowserPane & DevTools UI
apps/desktop/src/renderer/.../BrowserPane/BrowserPane.tsx, .../DevToolsPane/DevToolsPane.tsx, .../BrowserToolbar/**, .../BrowserOverflowMenu/**, .../UrlSuggestions/**, .../BrowserErrorOverlay/**, .../constants.ts, .../index.ts
Introduces BrowserPane component, DevToolsPane, toolbar, overflow menu, URL autocomplete, error overlay, constants, and barrel exports.
Navigation hooks & autocomplete
.../hooks/useBrowserNavigation/useBrowserNavigation.ts, .../hooks/useBrowserNavigation/index.ts, .../useUrlAutocomplete/useUrlAutocomplete.ts, .../useUrlAutocomplete/index.ts
Adds useBrowserNavigation hook to manage webview lifecycle/events, history persistence, navigation controls; adds useUrlAutocomplete for history-based suggestions and keyboard handling.
Tabs store, utils & types
apps/desktop/src/renderer/stores/tabs/store.ts, apps/desktop/src/renderer/stores/tabs/types.ts, apps/desktop/src/renderer/stores/tabs/utils.ts
Extends tabs store/types with browser pane state and actions (addBrowserTab, updateBrowserUrl, navigateBrowserHistory, openDevToolsPane, viewport/error handling); bump persisted store version; new pane creators and helpers.
Shared pane typings & UI schema
apps/desktop/src/shared/tabs-types.ts, apps/desktop/src/lib/trpc/routers/ui-state/index.ts
Adds PaneType "devtools", BrowserPaneState, DevToolsPaneState, BrowserHistoryEntry, ViewportPreset, and BrowserLoadError shapes; updates ui-state schema to accept browser/devtools fields.
Tab bar, GroupStrip, Workspace wiring & hotkeys
apps/desktop/src/renderer/screens/.../GroupStrip/GroupStrip.tsx, .../$workspaceId/page.tsx, apps/desktop/src/shared/hotkeys.ts, apps/desktop/src/renderer/screens/.../TabView/index.tsx
Adds Browser action and NEW_BROWSER hotkey (meta+shift+b); wires addBrowserTab and renders BrowserPane/DevToolsPane for corresponding pane types.
Ports & open-in-browser flow
apps/desktop/src/renderer/.../MergedPortBadge/MergedPortBadge.tsx
Replaces direct window.open with navigation to workspace then addBrowserTab to open port URLs in browser tabs.
Browser extension resources & tsconfig exclusion
apps/desktop/src/resources/browser-extension/manifest.json, background.js, content.js, apps/desktop/tsconfig.json
Adds MV3 extension files (background ping handler, placeholder content script) and excludes extension resources from TypeScript compilation.
CSP & setup script
apps/desktop/src/renderer/index.html, .superset/lib/setup/steps.sh
Broadens img-src to allow https: and adds DESKTOP_AUTOMATION_PORT env var generation and exports in setup script.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as Renderer (BrowserPane)
  participant TRPC as TRPC Router (browser/history)
  participant Main as Electron Main (BrowserManager)
  participant DB as Local DB (drizzle)
  Renderer->>TRPC: register / navigate / screenshot / getDevToolsUrl
  TRPC->>Main: invoke browser action / query webContents
  Main->>Main: manage webContents, take screenshot, eval JS
  TRPC->>DB: upsert history (upsert)
  DB-->>TRPC: ack
  Main-->>TRPC: event callbacks (console, new-window)
  TRPC-->>Renderer: stream events / responses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I hopped into tabs with cheer,

Opened pages far and near,
History kept with every stride,
DevTools by my side—what pride!
New browser panes now spring to life, hooray—let’s glide!

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

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.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (52 files):

⚔️ .superset/lib/setup/steps.sh (content)
⚔️ apps/api/package.json (content)
⚔️ apps/api/src/app/.well-known/oauth-protected-resource/route.ts (content)
⚔️ apps/api/src/app/api/agent/[transport]/route.ts (content)
⚔️ apps/desktop/src/lib/electron-app/factories/app/setup.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/index.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/ui-state/index.ts (content)
⚔️ apps/desktop/src/main/index.ts (content)
⚔️ apps/desktop/src/renderer/index.html (content)
⚔️ apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/hooks/useEscapeToNavigate/useEscapeToNavigate.ts (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/page.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/TasksTableView.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/components/TaskContextMenu/TaskContextMenu.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/useTasksTable.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/page.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/components/TeardownLogsDialog/TeardownLogsDialog.tsx (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/components/TeardownLogsDialog/index.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRefs.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/DeleteWorktreeDialog.tsx (content)
⚔️ apps/desktop/src/renderer/stores/tabs/store.ts (content)
⚔️ apps/desktop/src/renderer/stores/tabs/terminal-callbacks.ts (content)
⚔️ apps/desktop/src/renderer/stores/tabs/types.ts (content)
⚔️ apps/desktop/src/renderer/stores/tabs/utils.ts (content)
⚔️ apps/desktop/src/shared/hotkeys.ts (content)
⚔️ apps/desktop/src/shared/tabs-types.ts (content)
⚔️ apps/desktop/tsconfig.json (content)
⚔️ apps/marketing/src/app/components/HeroSection/HeroSection.tsx (content)
⚔️ apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx (content)
⚔️ apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx (content)
⚔️ apps/marketing/src/app/components/HeroSection/components/ProductDemo/components/SelectorPill/SelectorPill.tsx (content)
⚔️ apps/marketing/src/app/components/TrustedBySection/TrustedBySection.tsx (content)
⚔️ bun.lock (content)
⚔️ packages/agent/package.json (content)
⚔️ packages/agent/src/superagent.ts (content)
⚔️ packages/desktop-mcp/package.json (content)
⚔️ packages/local-db/drizzle/meta/_journal.json (content)
⚔️ packages/local-db/src/schema/schema.ts (content)
⚔️ packages/mcp/package.json (content)
⚔️ packages/ui/src/components/ui/button.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement in-app browser' clearly and concisely summarizes the main objective of the changeset, which introduces a fully functional browser pane feature.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering key features (URL autocomplete, favicon capture, port handling, history clearing, URL bar polish, CSP fix) with a detailed test plan checklist.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch browser-pane
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch browser-pane
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 16, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

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

Thank you for your contribution! 🎉

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

🤖 Fix all issues with AI agents
In @.superset/lib/setup/steps.sh:
- Line 387: Add DESKTOP_AUTOMATION_PORT to the ports.json generation block so
the port written by write_env_var "DESKTOP_AUTOMATION_PORT" is documented;
locate the ports.json creation code (the block that currently writes
CODE_INSPECTOR_PORT and other ports) and insert an entry for
DESKTOP_AUTOMATION_PORT alongside the existing port entries so it appears in the
generated ports.json output.

In `@apps/desktop/src/main/index.ts`:
- Around line 281-292: The dev extension path for loading the browser extension
is incorrect and the resource isn't being copied into dist; update the dev
branch for extensionPath used in the app.isPackaged check so it points to the
correct source location (e.g., use ../../src/resources/browser-extension if you
want to load from source) OR add "browser-extension" to the RESOURCES_TO_COPY
array in vite/helpers.ts so the extension is copied into dist and the existing
path works; adjust the code around extensionPath and the
session.fromPartition("persist:superset").loadExtension call accordingly and
ensure electron-builder.ts continues to include src/resources/ for packaged
builds.

In `@apps/desktop/src/main/lib/browser/browser-manager.ts`:
- Around line 80-96: The current target lookup uses URL matching which can
return the wrong DevTools endpoint when multiple webviews/pages share the same
URL; replace the URL-based match with the CDP target ID obtained from the
webContents debugger: call wc.debugger.sendCommand('Target.getTargetInfo') to
read targetInfo.targetId and then find the correct entry in the fetched targets
list by comparing t.id === targetId (or use webContents.fromDevToolsTargetId()
if you need reverse lookup), updating the logic around wc, cdpPort, targets and
the find(...) call to use targetId instead of targetUrl.
- Around line 51-55: The evaluateJS method (evaluateJS/paneId/getWebContents)
allows arbitrary JS execution and is exposed via a publicProcedure in the trpc
router without auth; fix by adding an authorization check before executing code:
either attach a tRPC middleware that verifies the requesting user/context owns
the target paneId or add an explicit ownership/permission check inside
evaluateJS (validate paneId, confirm caller identity/session and pane ownership)
and throw if unauthorized; also ensure BrowserWindow creation uses
webPreferences.contextIsolation = true as an extra safety measure.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx`:
- Around line 127-153: The BrowserPane component (via webviewRefCallback and
useBrowserNavigation) currently lacks handling for webview load failures; add
listeners on the webview element for events such as "did-fail-load", "crashed",
and "unresponsive" (and optionally "did-finish-load" to clear errors), update
component state (e.g., an error/state variable in BrowserPane or exposed by
useBrowserNavigation) when these events fire, and render a concise error UI in
place of the blank overlay that shows the error message and a retry button that
reloads the webview (using the webview ref reload() or loadURL). Ensure you
remove listeners on cleanup to avoid leaks and surface error details
(code/url/message) from the event in the UI.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx`:
- Around line 59-61: The clear-all action in BrowserOverflowMenu is destructive
and currently executed immediately by handleClearAllData calling
clearBrowsingDataMutation.mutate({ type: "all" });—add a user confirmation step:
replace the direct mutate call with a confirmation dialog or modal (e.g.,
openConfirmDialog or useConfirm hook) from your UI toolkit inside
BrowserOverflowMenu, present a clear warning and confirm/cancel buttons, and
only call clearBrowsingDataMutation.mutate({ type: "all" }) when the user
confirms; ensure keyboard/escape cancels and consider disabling the menu item
while the mutation is pending to prevent double submissions.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/DevToolsPane/DevToolsPane.tsx`:
- Around line 31-36: The DevTools URL query
(electronTrpc.browser.getDevToolsUrl.useQuery with { browserPaneId: targetPaneId
}) lacks error handling causing the UI to stuck on "Connecting to DevTools...";
update the hook call to capture the error (and loading) from useQuery, derive
devToolsUrl from data?.url, and render three states: success -> render the
<webview>, error -> render a centered error UI (or trigger pane close) showing a
failure message, loading -> keep the connecting fallback. Ensure you reference
the existing targetPaneId/devToolsUrl flow and add the error variable from
useQuery to decide the UI path.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useBrowserNavigation/useBrowserNavigation.ts`:
- Around line 254-268: goBack and goForward set isHistoryNavigation.current =
true before attempting webviewRef.current?.loadURL(url), which can leave the
flag true if webviewRef is null; change both functions to first check
webviewRef.current exists (or store it to a local const) and only set
isHistoryNavigation.current = true immediately before calling loadURL, and
ensure no-ops (when webviewRef.current is null) do not flip the flag;
references: goBack, goForward, isHistoryNavigation, webviewRef,
navigateBrowserHistory, and the consumers handleDidNavigate /
handleDidStopLoading.
- Around line 274-286: The navigateTo function currently treats inputs without a
dot as searches, which makes localhost:PORT and host:port addresses open a
Google search; update navigateTo (and uses of webviewRef.current?.loadURL) to
first normalize and detect explicit schemes (http:, https:, file:, chrome:,
about:, etc.) and to explicitly handle 'localhost' and host:port patterns (e.g.,
strings matching /^localhost(:\d+)?$/i or /^[^\/:]+:\d+$/) by prepending the
appropriate scheme (default to http:// for localhost/host:port), otherwise keep
your existing dot-based heuristic and search fallback; ensure any encoded search
queries still use encodeURIComponent before constructing the Google search URL.
- Around line 46-52: The callback dependencies use the unstable mutation objects
causing handlers to be recreated; update handleDomReady to depend on
registerMutation.mutate instead of the whole registerMutation object and call
registerMutation.mutate(...) inside it, and apply the same pattern to
handleDidStopLoading and handlePageFaviconUpdated by using
upsertHistoryMutation.mutate and faviconMutation?.mutate (or the corresponding
.mutate stable references) in their implementations and deps so listeners are
stable and not torn down/readded every render.

In `@packages/local-db/drizzle/0026_add_browser_history.sql`:
- Around line 10-11: Remove the redundant non-unique index on url: delete the
CREATE INDEX `browser_history_url_idx` ON `browser_history` (`url`); line from
the migration (leave the unique index `browser_history_url_unique` intact) and
remove the corresponding index declaration named browser_history_url_idx from
the Drizzle schema definition for the browser_history table in schema.ts so only
the unique index browser_history_url_unique remains; update any migration
generation or schema sync steps as needed to keep DB/schema in sync.
🧹 Nitpick comments (13)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx (1)

42-49: handleCopyUrl has no error handling and no user feedback.

navigator.clipboard.writeText can reject (e.g., if the document isn't focused or permissions are denied). Additionally, there's no visual indication to the user that the copy succeeded.

Suggested improvement
 const handleCopyUrl = () => {
   const webview = document.querySelector(
     `webview[data-pane-id="${paneId}"]`,
   ) as Electron.WebviewTag | null;
   if (webview) {
-    navigator.clipboard.writeText(webview.getURL());
+    navigator.clipboard.writeText(webview.getURL()).catch((err) => {
+      console.error("[BrowserOverflowMenu] Failed to copy URL:", err);
+    });
   }
 };
apps/desktop/src/shared/tabs-types.ts (1)

11-16: Naming could be confusing: PaneType has no "browser" variant, but Pane has a browser? field.

The Pane.browser state field is populated for panes of type "webview". This implicit coupling between type: "webview" and the browser field may confuse future contributors. Consider either adding a code comment on the browser field clarifying it applies to "webview" panes, or renaming the pane type to "browser" if that better reflects its purpose.

Also applies to: 141-142

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/UrlSuggestions/UrlSuggestions.tsx (1)

46-62: Favicon fallback via DOM manipulation is fragile.

The onError handler uses e.currentTarget.style.display = "none" and nextElementSibling?.classList.remove("hidden") to swap the <img> for the globe icon. This couples rendering to sibling DOM order — any layout change will silently break the fallback.

A more React-idiomatic approach would track broken favicons in local state:

Sketch using state-based fallback
+import { useState, useEffect, useRef } from "react";
+
 export function UrlSuggestions({ suggestions, highlightedIndex, onSelect }: UrlSuggestionsProps) {
+  const [brokenFavicons, setBrokenFavicons] = useState<Set<string>>(new Set());
   const listRef = useRef<HTMLDivElement>(null);
   ...
             {item.faviconUrl ? (
-              <img
-                src={item.faviconUrl}
-                alt=""
-                className="size-4 shrink-0 rounded-sm"
-                onError={(e) => {
-                  e.currentTarget.style.display = "none";
-                  e.currentTarget.nextElementSibling?.classList.remove("hidden");
-                }}
-              />
-            ) : null}
-            {!item.faviconUrl ? (
-              <TbGlobe className="size-4 shrink-0 text-muted-foreground/50" />
-            ) : (
-              <TbGlobe className="hidden size-4 shrink-0 text-muted-foreground/50" />
-            )}
+              item.faviconUrl && !brokenFavicons.has(item.faviconUrl) ? (
+                <img
+                  src={item.faviconUrl}
+                  alt=""
+                  className="size-4 shrink-0 rounded-sm"
+                  onError={() => setBrokenFavicons(prev => new Set(prev).add(item.faviconUrl!))}
+                />
+              ) : (
+                <TbGlobe className="size-4 shrink-0 text-muted-foreground/50" />
+              )
+            )}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)

67-83: Consider extracting the inline type to align with the shared pane types.

The devtools shape { targetPaneId: string } is defined inline here. If this structure is already defined in renderer/stores/tabs/types.ts (which the AI summary suggests), consider importing and reusing it to keep the type in sync.

apps/desktop/src/lib/trpc/routers/browser-history/index.ts (1)

18-34: LIKE wildcard characters in user input are not escaped.

If input.query contains % or _, they act as LIKE wildcards, producing unexpected matches. This is a minor UX issue for a local autocomplete but worth noting.

🛡️ Suggested fix — escape LIKE meta-characters
 		search: publicProcedure
 			.input(z.object({ query: z.string() }))
 			.query(({ input }) => {
-				const pattern = `%${input.query}%`;
+				const escaped = input.query.replace(/[%_]/g, "\\$&");
+				const pattern = `%${escaped}%`;
 				return localDb
 					.select()
 					.from(browserHistory)
 					.where(
 						or(
-							like(browserHistory.url, pattern),
-							like(browserHistory.title, pattern),
+							sql`${browserHistory.url} LIKE ${pattern} ESCAPE '\\'`,
+							sql`${browserHistory.title} LIKE ${pattern} ESCAPE '\\'`,
 						),
 					)
 					.orderBy(sql`${browserHistory.lastVisitedAt} desc`)
 					.limit(10)
 					.all();
 			}),
packages/local-db/drizzle/meta/0026_snapshot.json (1)

55-69: Remove redundant index on url column in browser_history table.

Line 321 defines url: text("url").notNull().unique(), which creates a unique constraint and implicit index for lookups. Line 330's explicit non-unique index("browser_history_url_idx").on(table.url) is redundant — SQLite will use the unique index for all url-based queries. This adds unnecessary write I/O overhead on every insert/update without performance benefit.

Fix in packages/local-db/src/schema/schema.ts by removing line 330: index("browser_history_url_idx").on(table.url),

apps/desktop/src/renderer/stores/tabs/utils.ts (1)

241-241: Remove duplicate DEFAULT_BROWSER_URL constant and import from the shared location.

This constant is already exported from apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/constants.ts. Instead of defining it locally on line 241, import it from the existing constants file to maintain a single source of truth and prevent accidental divergence.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx (1)

63-68: autocomplete object reference as dependency defeats useCallback memoization.

useUrlAutocomplete returns a plain object on each render, so autocomplete is a new reference every time. Using it as a dependency in enterEditMode, exitEditMode, handleSubmit, handleInputChange, and handleKeyDown means none of these callbacks are ever memoized.

Destructure the stable individual functions instead:

Destructure hook return for stable deps
-	const autocomplete = useUrlAutocomplete({
+	const {
+		isOpen: autocompleteIsOpen,
+		suggestions: autocompleteSuggestions,
+		highlightedIndex: autocompleteHighlightedIndex,
+		open: autocompleteOpen,
+		close: autocompleteClose,
+		updateQuery: autocompleteUpdateQuery,
+		selectSuggestion: autocompleteSelectSuggestion,
+		handleKeyDown: autocompleteHandleKeyDown,
+	} = useUrlAutocomplete({
 		onSelect: (selectedUrl) => {
 			onNavigate(selectedUrl);
 			setIsEditing(false);
 		},
 	});

Then use these individual references in the dependency arrays.

packages/local-db/src/schema/schema.ts (1)

329-332: Redundant index on url — SQLite's UNIQUE constraint already creates one.

The url column has .unique() (Line 321), which implicitly creates an index in SQLite. The explicit browser_history_url_idx on Line 330 is redundant and wastes storage.

Remove the redundant index
 	(table) => [
-		index("browser_history_url_idx").on(table.url),
 		index("browser_history_last_visited_at_idx").on(table.lastVisitedAt),
 	],
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/hooks/useUrlAutocomplete/useUrlAutocomplete.ts (1)

20-20: suggestionsRef is assigned but never read — dead code.

suggestionsRef is created on Line 20 and kept in sync on Line 44, but handleKeyDown (and all other code) reads from the suggestions state value directly, not from suggestionsRef.current. The ref serves no purpose.

Remove dead ref
-	const suggestionsRef = useRef<HistorySuggestion[]>([]);
 ...
-	// Keep ref in sync for keyboard handler
-	suggestionsRef.current = suggestions;

Also applies to: 43-44

apps/desktop/src/lib/trpc/routers/browser/browser.ts (2)

23-30: Consider validating the URL protocol before calling loadURL.

wc.loadURL(input.url) accepts any string from the renderer process. While this is a desktop app, a malicious or buggy extension/component could navigate the webview to file:///etc/passwd or other sensitive local resources. Consider restricting to http: and https: protocols.

Add protocol validation
 		navigate: publicProcedure
 			.input(z.object({ paneId: z.string(), url: z.string() }))
 			.mutation(({ input }) => {
 				const wc = browserManager.getWebContents(input.paneId);
 				if (!wc) throw new Error(`No webContents for pane ${input.paneId}`);
+				const parsed = new URL(input.url);
+				if (!['http:', 'https:'].includes(parsed.protocol)) {
+					throw new Error(`Unsupported protocol: ${parsed.protocol}`);
+				}
 				wc.loadURL(input.url);
 				return { success: true };
 			}),

148-174: Extract the session partition name to a shared constant.

The partition name "persist:superset" is hardcoded in multiple places across the codebase (BrowserPane.tsx, main/index.ts, main/windows/main.ts, main/lib/sentry.ts, and this file). Define it as a constant in apps/desktop/src/shared/constants.ts and import it here for consistency and maintainability.

apps/desktop/src/renderer/stores/tabs/store.ts (1)

1135-1172: addBrowserTab duplicates the history-stack management pattern from addTab and addChatTab.

This is the third instance of the same "push current active tab to history, create tab, set active" pattern. Not a blocker since it matches the existing convention in this file, but worth noting as a future DRY candidate.

Consider extracting a shared helper like addTabToWorkspace(state, workspaceId, tab, pane) that handles the common history-stack and active-tab bookkeeping.

write_env_var "ELECTRIC_PORT" "$ELECTRIC_PORT"
write_env_var "CADDY_ELECTRIC_PORT" "$CADDY_ELECTRIC_PORT"
write_env_var "CODE_INSPECTOR_PORT" "$CODE_INSPECTOR_PORT"
write_env_var "DESKTOP_AUTOMATION_PORT" "$DESKTOP_AUTOMATION_PORT"
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

🧩 Analysis chain

🏁 Script executed:

rg -n "DESKTOP_AUTOMATION_PORT" --type-all -C 3

Repository: superset-sh/superset

Length of output: 502


🏁 Script executed:

# Check the ports.json generation section
fd "steps.sh" -type f | head -5

Repository: superset-sh/superset

Length of output: 234


🏁 Script executed:

rg "DESKTOP_AUTOMATION_PORT" -C 3

Repository: superset-sh/superset

Length of output: 2639


🏁 Script executed:

fd steps.sh

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Find and read the steps.sh file to check lines mentioned
fd "steps.sh" -type f | xargs wc -l

Repository: superset-sh/superset

Length of output: 253


🏁 Script executed:

fd "steps.sh" -o fd "ports.json"

Repository: superset-sh/superset

Length of output: 187


🏁 Script executed:

# Search more broadly for the file
find . -name "steps.sh" -o -name "ports.json" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 128


🏁 Script executed:

# Check if .superset directory exists
ls -la .superset 2>/dev/null || echo ".superset not found"

Repository: superset-sh/superset

Length of output: 408


🏁 Script executed:

wc -l ./.superset/lib/setup/steps.sh

Repository: superset-sh/superset

Length of output: 98


🏁 Script executed:

# Read the steps.sh file to check the relevant sections
cat -n ./.superset/lib/setup/steps.sh | sed -n '375,395p'

Repository: superset-sh/superset

Length of output: 1445


🏁 Script executed:

# Check the ports.json generation section
cat -n ./.superset/lib/setup/steps.sh | sed -n '425,450p'

Repository: superset-sh/superset

Length of output: 1227


Add DESKTOP_AUTOMATION_PORT to ports.json.

The DESKTOP_AUTOMATION_PORT is written to the .env file at line 387, but it's missing from the ports.json file generated at lines 428-445. All other ports (including CODE_INSPECTOR_PORT) are included in that file. Since this port is actively used for CDP/automation in the application, it should be documented in ports.json for consistency.

Add the following entry:

Suggested diff
    { "port": $CODE_INSPECTOR_PORT, "label": "Code Inspector" }
+   { "port": $DESKTOP_AUTOMATION_PORT, "label": "Desktop Automation" }
  ]
🤖 Prompt for AI Agents
In @.superset/lib/setup/steps.sh at line 387, Add DESKTOP_AUTOMATION_PORT to the
ports.json generation block so the port written by write_env_var
"DESKTOP_AUTOMATION_PORT" is documented; locate the ports.json creation code
(the block that currently writes CODE_INSPECTOR_PORT and other ports) and insert
an entry for DESKTOP_AUTOMATION_PORT alongside the existing port entries so it
appears in the generated ports.json output.

Comment on lines +281 to +292
// Load browser extension for webview panes
try {
const extensionPath = app.isPackaged
? path.join(process.resourcesPath, "browser-extension")
: path.join(__dirname, "../src/resources/browser-extension");
await session
.fromPartition("persist:superset")
.loadExtension(extensionPath);
console.log("[main] Browser extension loaded");
} catch (error) {
console.error("[main] Failed to load browser extension:", error);
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find electron-builder and electron.vite config files
fd -e "json" -e "config" | grep -E "(electron|build)" | head -20

Repository: superset-sh/superset

Length of output: 96


🏁 Script executed:

# Also check the actual desktop app structure
find apps/desktop -type f -name "*.json" | head -20

Repository: superset-sh/superset

Length of output: 280


🏁 Script executed:

# Look for the browser-extension directory
find . -type d -name "browser-extension" 2>/dev/null

Repository: superset-sh/superset

Length of output: 110


🏁 Script executed:

# Check for electron.vite config in apps/desktop
find apps/desktop -name "electron.vite*" -o -name "vite.config*" | head -10

Repository: superset-sh/superset

Length of output: 100


🏁 Script executed:

# Read the package.json to see build configuration and electron-builder setup
cat apps/desktop/package.json | head -100

Repository: superset-sh/superset

Length of output: 4148


🏁 Script executed:

# Read the electron.vite.config.ts to understand build output structure
cat apps/desktop/electron.vite.config.ts

Repository: superset-sh/superset

Length of output: 7208


🏁 Script executed:

# Read the electron-builder.ts to check for extraResources configuration
cat apps/desktop/electron-builder.ts

Repository: superset-sh/superset

Length of output: 5025


🏁 Script executed:

# Find and read the vite/helpers file
find apps/desktop/vite -type f -name "helpers*" -exec cat {} \;

Repository: superset-sh/superset

Length of output: 2307


Fix the dev extension path: browser-extension is not copied to the dist/ directory and the relative path is incorrect.

The dev path path.join(__dirname, "../src/resources/browser-extension") resolves to dist/src/resources/browser-extension (one level too shallow), and browser-extension is not listed in RESOURCES_TO_COPY in vite/helpers.ts, so it won't be copied to dist/ at all during build. Either add browser-extension to the copy list (alongside sounds and tray) or change the path to ../../src/resources/browser-extension if relying on the source directory directly. The packaged path using process.resourcesPath works correctly since electron-builder.ts includes all of src/resources/ via the files configuration.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/index.ts` around lines 281 - 292, The dev extension
path for loading the browser extension is incorrect and the resource isn't being
copied into dist; update the dev branch for extensionPath used in the
app.isPackaged check so it points to the correct source location (e.g., use
../../src/resources/browser-extension if you want to load from source) OR add
"browser-extension" to the RESOURCES_TO_COPY array in vite/helpers.ts so the
extension is copied into dist and the existing path works; adjust the code
around extensionPath and the
session.fromPartition("persist:superset").loadExtension call accordingly and
ensure electron-builder.ts continues to include src/resources/ for packaged
builds.

Comment on lines +51 to +55
async evaluateJS(paneId: string, code: string): Promise<unknown> {
const wc = this.getWebContents(paneId);
if (!wc) throw new Error(`No webContents for pane ${paneId}`);
return wc.executeJavaScript(code);
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and find the tRPC routers
find apps/desktop -type f -name "*.ts" -path "*/trpc/*" | head -20

Repository: superset-sh/superset

Length of output: 1578


🏁 Script executed:

# Check how evaluateJS is exposed in the tRPC router
rg -n "evaluateJS" --type=ts -C5 -g '*/trpc/routers/**'

Repository: superset-sh/superset

Length of output: 212


🏁 Script executed:

# Also search more broadly for evaluateJS references
rg -n "evaluateJS" --type=ts apps/desktop/src

Repository: superset-sh/superset

Length of output: 381


🏁 Script executed:

# Read the browser router implementation around the evaluateJS procedure
cat -n apps/desktop/src/lib/trpc/routers/browser/browser.ts | sed -n '60,85p'

Repository: superset-sh/superset

Length of output: 953


🏁 Script executed:

# Check the full browser router file to understand the context better
wc -l apps/desktop/src/lib/trpc/routers/browser/browser.ts

Repository: superset-sh/superset

Length of output: 120


🏁 Script executed:

# Get broader context around the evaluateJS procedure
cat -n apps/desktop/src/lib/trpc/routers/browser/browser.ts | head -80

Repository: superset-sh/superset

Length of output: 3055


🏁 Script executed:

# Check the tRPC router definition and understand publicProcedure
cat -n apps/desktop/src/lib/trpc/index.ts

Repository: superset-sh/superset

Length of output: 1642


🏁 Script executed:

# Check if publicProcedure is defined with any context/middleware
rg -n "publicProcedure" apps/desktop/src/lib/trpc -B5 -A5 | head -60

Repository: superset-sh/superset

Length of output: 4576


🏁 Script executed:

# Search for how tRPC is exposed and whether webview can call it
rg -n "createTRPCClient\|useClient\|trpc\." apps/desktop/src/renderer -l | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for where tRPC is set up as a client in the renderer
rg -n "createTRPCReact\|useContext\|trpc\." apps/desktop/src/renderer -A3 | head -40

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check how the tRPC router is exposed/registered with Electron IPC
rg -n "ipcHandle\|ipcMain\|ipcRenderer\|handle\(" apps/desktop/src/main -l | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Look for electron IPC setup or preload bridge
find apps/desktop/src -name "preload*" -o -name "*ipc*" -o -name "*bridge*"

Repository: superset-sh/superset

Length of output: 146


🏁 Script executed:

# Check main.ts or app initialization for tRPC server setup
rg -n "createTRPCProxyClient\|nodeHTTPBatchStreamLink\|createContext" apps/desktop/src/main -B3 -A3 | head -60

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check preload directory contents
ls -la apps/desktop/src/preload/

Repository: superset-sh/superset

Length of output: 232


🏁 Script executed:

# Look at preload files
cat -n apps/desktop/src/preload/*.ts 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 2951


🏁 Script executed:

# Search for trpc-electron or similar in the codebase
rg -n "trpc-electron\|createTRPCClient\|ipcLink" apps/desktop/src --type=ts | head -20

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the main app entry point
find apps/desktop/src/main -name "*.ts" -maxdepth 1 -type f

Repository: superset-sh/superset

Length of output: 128


🏁 Script executed:

# Check main/index.ts for tRPC setup
cat -n apps/desktop/src/main/index.ts | head -150

Repository: superset-sh/superset

Length of output: 5198


🏁 Script executed:

# Search for context isolation and sandbox settings in Electron config
rg -n "contextIsolation\|sandbox\|nodeIntegration" apps/desktop/src/main --type=ts -B2 -A2

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if there's any authentication context passed to tRPC
rg -n "createContext\|context:" apps/desktop/src/lib/trpc -B3 -A5

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Look for how browser manager relates to security and pane context
cat -n apps/desktop/src/main/lib/browser/browser-manager.ts | head -80

Repository: superset-sh/superset

Length of output: 3083


🏁 Script executed:

# Check if there's any paneId validation or authorization in browser router
cat -n apps/desktop/src/lib/trpc/routers/browser/browser.ts | sed -n '1,100p'

Repository: superset-sh/superset

Length of output: 3704


🏁 Script executed:

# Search for how panes are managed and if there's ownership/scope checking
rg -n "paneId\|pane.*owner\|pane.*auth\|register.*pane" apps/desktop/src/main -B2 -A2 | head -60

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if there's context passed to tRPC procedures in main setup
rg -n "createProxySSGHelpers\|createInnerTRPCContext\|router\(" apps/desktop/src/main/lib -A5 | head -40

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for BrowserWindow creation and context isolation settings
rg -n "new BrowserWindow\|contextIsolation\|preload" apps/desktop/src/main -B3 -A5 | head -80

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the MainWindow class to see security settings
find apps/desktop/src/main -name "*window*" -o -name "*main*" | grep -i window

Repository: superset-sh/superset

Length of output: 285


🏁 Script executed:

# Look for how webContents/panes are created and if there's scope checking
rg -n "webContents\|createWindow\|new.*Window" apps/desktop/src/main/lib -l

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the main window implementation
cat -n apps/desktop/src/main/windows/main.ts

Repository: superset-sh/superset

Length of output: 10573


🏁 Script executed:

# Search for context isolation and webPreferences
rg -n "webPreferences\|contextIsolation\|sandbox" apps/desktop/src/main/windows -B2 -A3

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if there's any window.open/webview handling that might create untrusted contexts
cat -n apps/desktop/src/main/lib/browser/browser-manager.ts | sed -n '60,130p'

Repository: superset-sh/superset

Length of output: 2481


🏁 Script executed:

# Check how webContents are registered - is there origin/URL validation?
rg -n "register\|webContentsId" apps/desktop/src/lib/trpc/routers/browser -B3 -A3

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for any authorization/access control in tRPC context or middleware
rg -n "createInnerTRPCContext\|ctx\.\|context:" apps/desktop/src/lib/trpc/routers --type=ts -B2 -A4 | head -40

Repository: superset-sh/superset

Length of output: 46


Arbitrary JS execution via evaluateJS — tRPC route lacks authorization checks.

The evaluateJS procedure is exposed via publicProcedure (line 68 of apps/desktop/src/lib/trpc/routers/browser/browser.ts), which applies only Sentry error middleware with no authentication or authorization checks. Any code with access to the tRPC client can invoke evaluateJS on any registered paneId, regardless of ownership or scope. The paneId parameter receives no validation beyond being a string, allowing arbitrary enumeration or hijacking of browser panes.

While this may be acceptable if all renderer content is trusted, consider:

  • Adding an authorization middleware to verify pane ownership before executing JS
  • Implementing per-pane access control tied to the requesting context
  • Explicitly enabling context isolation in BrowserWindow webPreferences if not already enforced
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/browser-manager.ts` around lines 51 - 55,
The evaluateJS method (evaluateJS/paneId/getWebContents) allows arbitrary JS
execution and is exposed via a publicProcedure in the trpc router without auth;
fix by adding an authorization check before executing code: either attach a tRPC
middleware that verifies the requesting user/context owns the target paneId or
add an explicit ownership/permission check inside evaluateJS (validate paneId,
confirm caller identity/session and pane ownership) and throw if unauthorized;
also ensure BrowserWindow creation uses webPreferences.contextIsolation = true
as an extra safety measure.

Comment on lines +80 to +96
const targetUrl = wc.getURL();

try {
const res = await fetch(`http://127.0.0.1:${cdpPort}/json`);
const targets = (await res.json()) as Array<{
id: string;
url: string;
type: string;
webSocketDebuggerUrl?: string;
}>;

// Webview guests have type "webview", not "page"
const target = targets.find(
(t) =>
(t.type === "webview" || t.type === "page") && t.url === targetUrl,
);
if (!target) return null;
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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/desktop/src/main/lib/browser/browser-manager.ts

Repository: superset-sh/superset

Length of output: 5496


🏁 Script executed:

# Check webContents registration and tracking patterns in the file
rg -n "register\|webContentsId\|getProcessId\|debugger" apps/desktop/src/main/lib/browser/browser-manager.ts -B2 -A2

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for any existing CDP target matching patterns in the codebase
rg -n "devToolsUrl\|cdpPort\|\/json" apps/desktop/src --type=ts -B2 -A5

Repository: superset-sh/superset

Length of output: 180


🏁 Script executed:

# Check how webContents IDs are used in the codebase
rg -n "wc\.id\|webContents\.id\|processId" apps/desktop/src --type=ts | head -20

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for other uses of CDP or debugger APIs in the desktop app
rg -n "remote-debugging-port|cdpPort|devToolsUrl|/json" apps/desktop/src --type=ts -B3 -A3

Repository: superset-sh/superset

Length of output: 6976


🏁 Script executed:

# Check if there are patterns for handling multiple webContents with same URL
rg -n "browserPaneId|paneId.*same.*url|multiple.*pane" apps/desktop/src --type=ts -i -B2 -A2

Repository: superset-sh/superset

Length of output: 8880


🏁 Script executed:

# Look for how panes are created and what identifiers are available
rg -n "new.*BrowserPane|createPane|register.*pane" apps/desktop/src --type=ts -B2 -A5

Repository: superset-sh/superset

Length of output: 34079


🏁 Script executed:

# Check webContents property usage to see if id or processId is available
rg -n "webContents\.(id|processId|getProcessId|debugger)" apps/desktop/src --type=ts -B2 -A2

Repository: superset-sh/superset

Length of output: 46


🌐 Web query:

Electron webContents to CDP target matching identifier debugger

💡 Result:

To match an Electron webContents with a Chrome DevTools Protocol (CDP) TargetID (the identifier you see in CDP / “targets”), use Electron’s built-in mapping:

  • CDP TargetID → WebContents: webContents.fromDevToolsTargetId(targetId) (electronjs.org)
  • WebContents → CDP TargetID: attach the debugger and call Target.getTargetInfo, then read targetInfo.targetId (electronjs.org)

Example (main process)

const { BrowserWindow, webContents } = require('electron');

const win = new BrowserWindow();

(async () => {
  const wc = win.webContents;

  wc.debugger.attach('1.3');

  const { targetInfo } = await wc.debugger.sendCommand('Target.getTargetInfo');
  const targetId = targetInfo.targetId; // CDP TargetID

  const sameWc = webContents.fromDevToolsTargetId(targetId); // -> WebContents
  console.log({ wcId: wc.id, targetId, same: sameWc?.id === wc.id });

  wc.debugger.detach();
})();

Target.getTargetInfo / targetInfo.targetId are defined by the CDP Target domain. (chromedevtools.github.io)

Citations:


🌐 Web query:

Chromium DevTools Protocol target id webview page type distinguish multiple

💡 Result:

In CDP, you distinguish “page” vs “webview” (and multiple instances of either) via Target.TargetInfo returned by Target.getTargets (or via Target.targetCreated/targetInfoChanged events after Target.setDiscoverTargets(true)):

  1. Use targetInfo.type to tell kind of target

    • TargetInfo.type is the canonical field; Chromium emits types including "page" and "webview" (among many others). (chromedevtools.github.io)
  2. Use targetInfo.targetId as the unique identifier

    • Every discovered target has a unique targetId; that’s what you attach to (via Target.attachToTarget / auto-attach) and what you use to route messages. (chromedevtools.github.io)
  3. To distinguish multiple webviews/pages, don’t rely on type alone—also key on:

    • targetInfo.url and targetInfo.title (often the simplest discriminator once navigated). (chromedevtools.github.io)
    • targetInfo.openerId to associate a popup/child target back to the opener target (often helps group which page spawned which webview/popup). (chromedevtools.github.io)
    • targetInfo.browserContextId if you’re using multiple browser contexts/profiles. (chromedevtools.github.io)
    • If URLs are identical/about:blank, listen for targetInfoChanged and re-read url/title after navigation (common with newly created webviews). (chromedevtools.github.io)
  4. If you’re using Puppeteer, the same concepts map directly

    • Puppeteer exposes target types including WEBVIEW and PAGE, and Target.page() works for "page", "webview", "background_page" targets. (pptr.dev)

If you share a sample Target.getTargets output (a few TargetInfo objects), I can tell you exactly which fields will reliably separate your multiple webviews (some environments reuse titles/URLs, so the best key can vary).

Citations:


Use CDP targetId instead of URL matching to reliably identify DevTools targets.

When multiple browser panes navigate to the same URL, targets.find(t => … && t.url === targetUrl) will match the first one, potentially returning the wrong DevTools endpoint. Use Electron's debugger API to get the CDP target ID instead:

const { targetInfo } = await wc.debugger.sendCommand('Target.getTargetInfo');
const targetId = targetInfo.targetId;
// Then find: targets.find(t => t.id === targetId)

Alternatively, use webContents.fromDevToolsTargetId() for the reverse lookup if needed.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/browser-manager.ts` around lines 80 - 96,
The current target lookup uses URL matching which can return the wrong DevTools
endpoint when multiple webviews/pages share the same URL; replace the URL-based
match with the CDP target ID obtained from the webContents debugger: call
wc.debugger.sendCommand('Target.getTargetInfo') to read targetInfo.targetId and
then find the correct entry in the fetched targets list by comparing t.id ===
targetId (or use webContents.fromDevToolsTargetId() if you need reverse lookup),
updating the logic around wc, cdpPort, targets and the find(...) call to use
targetId instead of targetUrl.

Comment on lines +31 to +36
// Query the CDP debug server for the DevTools frontend URL
const { data } = electronTrpc.browser.getDevToolsUrl.useQuery(
{ browserPaneId: targetPaneId },
{ refetchOnWindowFocus: false },
);
const devToolsUrl = data?.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

No error handling if the DevTools URL query fails.

If the target browser pane is closed or the CDP server isn't reachable, useQuery will error out but the component will perpetually show "Connecting to DevTools..." with no feedback. Consider handling the error state (e.g., showing an error message or auto-closing the pane).

-	const { data } = electronTrpc.browser.getDevToolsUrl.useQuery(
+	const { data, error } = electronTrpc.browser.getDevToolsUrl.useQuery(
 		{ browserPaneId: targetPaneId },
 		{ refetchOnWindowFocus: false },
 	);
 	const devToolsUrl = data?.url;

Then render an error state alongside the loading fallback:

{devToolsUrl ? (
  <webview ... />
) : error ? (
  <div className="flex h-full w-full items-center justify-center text-destructive text-xs">
    Failed to connect to DevTools
  </div>
) : (
  <div className="...">Connecting to DevTools...</div>
)}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/DevToolsPane/DevToolsPane.tsx`
around lines 31 - 36, The DevTools URL query
(electronTrpc.browser.getDevToolsUrl.useQuery with { browserPaneId: targetPaneId
}) lacks error handling causing the UI to stuck on "Connecting to DevTools...";
update the hook call to capture the error (and loading) from useQuery, derive
devToolsUrl from data?.url, and render three states: success -> render the
<webview>, error -> render a centered error UI (or trigger pane close) showing a
failure message, loading -> keep the connecting fallback. Ensure you reference
the existing targetPaneId/devToolsUrl flow and add the error variable from
useQuery to decide the UI path.

Comment on lines +274 to +286
const navigateTo = useCallback((url: string) => {
let finalUrl = url;
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
// If it looks like a domain (has a dot), add https
if (url.includes(".")) {
finalUrl = `https://${url}`;
} else {
// Otherwise treat as search
finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
}
webviewRef.current?.loadURL(finalUrl);
}, []);
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

localhost:PORT URLs are broken — they'll trigger a Google search instead of navigation.

navigateTo checks for a dot to distinguish domain-like input from search queries. localhost:3000 has no dot, so it falls through to the Google search branch. For a developer-facing browser pane this is a critical gap — local dev servers are the primary use case.

Consider also handling other common schemes (file:, chrome:, etc.) and localhost explicitly.

🐛 Proposed fix
 const navigateTo = useCallback((url: string) => {
 	let finalUrl = url;
 	if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
-		// If it looks like a domain (has a dot), add https
-		if (url.includes(".")) {
+		// If it looks like a domain (has a dot) or is localhost, add https
+		if (url.includes(".") || /^localhost(:\d+)?(\/|$)/i.test(url)) {
 			finalUrl = `https://${url}`;
 		} else {
 			// Otherwise treat as search
 			finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
 		}
 	}
 	webviewRef.current?.loadURL(finalUrl);
 }, []);

Also note: detected-port links from the sidebar are likely http:// (not https://). You may want to default to http:// for localhost targets specifically.

📝 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
const navigateTo = useCallback((url: string) => {
let finalUrl = url;
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
// If it looks like a domain (has a dot), add https
if (url.includes(".")) {
finalUrl = `https://${url}`;
} else {
// Otherwise treat as search
finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
}
webviewRef.current?.loadURL(finalUrl);
}, []);
const navigateTo = useCallback((url: string) => {
let finalUrl = url;
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
// If it looks like a domain (has a dot) or is localhost, add https
if (url.includes(".") || /^localhost(:\d+)?(\/|$)/i.test(url)) {
finalUrl = `https://${url}`;
} else {
// Otherwise treat as search
finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
}
webviewRef.current?.loadURL(finalUrl);
}, []);
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useBrowserNavigation/useBrowserNavigation.ts`
around lines 274 - 286, The navigateTo function currently treats inputs without
a dot as searches, which makes localhost:PORT and host:port addresses open a
Google search; update navigateTo (and uses of webviewRef.current?.loadURL) to
first normalize and detect explicit schemes (http:, https:, file:, chrome:,
about:, etc.) and to explicitly handle 'localhost' and host:port patterns (e.g.,
strings matching /^localhost(:\d+)?$/i or /^[^\/:]+:\d+$/) by prepending the
appropriate scheme (default to http:// for localhost/host:port), otherwise keep
your existing dot-based heuristic and search fallback; ensure any encoded search
queries still use encodeURIComponent before constructing the Google search URL.

Comment on lines +10 to +11
CREATE UNIQUE INDEX `browser_history_url_unique` ON `browser_history` (`url`);--> statement-breakpoint
CREATE INDEX `browser_history_url_idx` ON `browser_history` (`url`);--> statement-breakpoint
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

🧩 Analysis chain

🏁 Script executed:

# First, check the SQL migration file content
cat -n packages/local-db/drizzle/0026_add_browser_history.sql

Repository: superset-sh/superset

Length of output: 687


🏁 Script executed:

# Search for the browser_history schema definition in the Drizzle schema file
find . -name "schema.ts" -path "*packages/local-db*" | head -5

Repository: superset-sh/superset

Length of output: 104


🏁 Script executed:

# Look for browser_history table definition in schema files
rg -n "browser_history" --type=ts -C 10 -g '*schema*'

Repository: superset-sh/superset

Length of output: 2284


Redundant index: Remove browser_history_url_idx as it duplicates the unique index on the same column.

The browser_history_url_unique unique index on url already provides B-tree index lookups. The additional browser_history_url_idx regular index on url duplicates this functionality and wastes storage and write overhead. Remove it from both the migration file and the Drizzle schema definition.

Required changes

Migration file (packages/local-db/drizzle/0026_add_browser_history.sql):

 CREATE UNIQUE INDEX `browser_history_url_unique` ON `browser_history` (`url`);--> statement-breakpoint
-CREATE INDEX `browser_history_url_idx` ON `browser_history` (`url`);--> statement-breakpoint
 CREATE INDEX `browser_history_last_visited_at_idx` ON `browser_history` (`last_visited_at`);

Schema definition (packages/local-db/src/schema/schema.ts lines 329-332):

 	},
 	(table) => [
-		index("browser_history_url_idx").on(table.url),
 		index("browser_history_last_visited_at_idx").on(table.lastVisitedAt),
 	],
📝 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
CREATE UNIQUE INDEX `browser_history_url_unique` ON `browser_history` (`url`);--> statement-breakpoint
CREATE INDEX `browser_history_url_idx` ON `browser_history` (`url`);--> statement-breakpoint
CREATE UNIQUE INDEX `browser_history_url_unique` ON `browser_history` (`url`);--> statement-breakpoint
CREATE INDEX `browser_history_last_visited_at_idx` ON `browser_history` (`last_visited_at`);
🤖 Prompt for AI Agents
In `@packages/local-db/drizzle/0026_add_browser_history.sql` around lines 10 - 11,
Remove the redundant non-unique index on url: delete the CREATE INDEX
`browser_history_url_idx` ON `browser_history` (`url`); line from the migration
(leave the unique index `browser_history_url_unique` intact) and remove the
corresponding index declaration named browser_history_url_idx from the Drizzle
schema definition for the browser_history table in schema.ts so only the unique
index browser_history_url_unique remains; update any migration generation or
schema sync steps as needed to keep DB/schema in sync.

…location

DevToolsPane is a sibling pane type dispatched by TabView's renderPane(),
not a child of BrowserPane. Move it from BrowserPane/components/ to
TabView/ to match the co-location rules.
@saddlepaddle saddlepaddle changed the title feat(desktop): browser history autocomplete + open ports in browser pane Implement in-app browser Feb 16, 2026
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/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/DevToolsPane/DevToolsPane.tsx (1)

56-56: All pane types intentionally share the CLOSE_TERMINAL hotkey — but the naming is misleading

The hotkey is reused across DevToolsPane, ChatPane, BrowserPane, and the actual TabPane (Terminal), with the workspace handler closing whichever pane is focused. This is intentional, but the hotkey's label ("Close Terminal") and category don't reflect that it's a generic pane-closing mechanism. Since PaneToolbarActions already defaults to "CLOSE_PANE", consider either:

  • Renaming CLOSE_TERMINAL to CLOSE_PANE for clarity, or
  • Adding a brief comment explaining the shared hotkey design

…history flag

- Handle localhost/127.0.0.1 URLs with http:// instead of Google search
- Destructure stable .mutate refs from mutations to avoid listener churn
- Only set isHistoryNavigation flag when webview ref exists
Shows a friendly error page with human-readable label, error description,
show/hide details toggle, and retry button when webview navigation fails
(connection refused, DNS errors, timeouts, etc.).
Left-align content, add friendly message, dot separator before
Show/Hide Details toggle, bordered details box with copy button,
and "Restart Browser" label.
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

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/stores/tabs/store.ts (1)

778-809: ⚠️ Potential issue | 🟠 Major

adjacentPaneId may point to a devtools pane that is also being removed, leaving focusedPaneIds stale.

When removing a browser pane that has an associated devtools pane, getAdjacentPaneId is computed from the original layout using only the primary paneId. If the devtools pane happens to be the adjacent pane in visual order, adjacentPaneId will reference a pane that is also in paneIdsToRemove. The ?? fallback on Line 807 won't trigger because adjacentPaneId is a non-null string.

Example: layout [browser, devtools, terminal] → close browserpaneIdsToRemove = [browser, devtools]adjacentPaneId = devtools → focused pane set to the deleted devtools ID.

🐛 Proposed fix
 // Must get adjacent pane BEFORE removing from layout
 const adjacentPaneId = getAdjacentPaneId(tab.layout, paneId);
+// If adjacent is also being removed, fall through to getFirstPaneId
+const safeAdjacentPaneId = adjacentPaneId && !paneIdsToRemove.includes(adjacentPaneId)
+	? adjacentPaneId
+	: null;

 // ...

 let newFocusedPaneIds = state.focusedPaneIds;
 if (paneIdsToRemove.includes(state.focusedPaneIds[tab.id])) {
 	newFocusedPaneIds = {
 		...state.focusedPaneIds,
-		[tab.id]: adjacentPaneId ?? getFirstPaneId(newLayout),
+		[tab.id]: safeAdjacentPaneId ?? getFirstPaneId(newLayout),
 	};
 }
🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx`:
- Line 104: The BrowserPane component currently passes
closeHotkeyId="CLOSE_TERMINAL" which causes HotkeyTooltipContent to show "Close
Terminal"; change the prop to use the generic CLOSE_PANE identifier instead
(i.e., set closeHotkeyId="CLOSE_PANE") so PaneToolbarActions and tooltips
display the correct "Close Pane" label; update the JSX in BrowserPane where
closeHotkeyId is set and ensure no other references rely on CLOSE_TERMINAL for
this pane type.
🧹 Nitpick comments (5)
apps/desktop/src/shared/tabs-types.ts (1)

181-188: Nit: error and viewport use both ? and | null.

error?: BrowserLoadError | null means the field can be undefined, null, or a BrowserLoadError. If this is intentional (e.g., undefined = "not yet set", null = "explicitly cleared"), this is fine. Otherwise, consider using just ? (optional, defaulting to undefined) or just | null (always present) to reduce ambiguity for consumers.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserErrorOverlay/BrowserErrorOverlay.tsx (1)

60-62: navigator.clipboard.writeText can reject — no feedback on success or failure.

The clipboard API can throw (e.g., document not focused, permission denied in some contexts). Consider adding a .catch() and optionally brief user feedback (e.g., a transient "Copied!" state).

♻️ Suggested improvement
+const [copied, setCopied] = useState(false);
+
 const copyDetails = useCallback(() => {
-	navigator.clipboard.writeText(detailsText);
+	navigator.clipboard.writeText(detailsText).then(() => {
+		setCopied(true);
+		setTimeout(() => setCopied(false), 1500);
+	}).catch(() => {
+		// Silently fail — clipboard may not be available
+	});
 }, [detailsText]);
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useBrowserNavigation/useBrowserNavigation.ts (2)

300-312: localhost detection is loose — startsWith can match unintended strings.

url.startsWith("localhost") would match inputs like "localhostevil.com". A word-boundary check would be more precise. The past review comment about this area is still relevant.

♻️ Proposed fix
 const navigateTo = useCallback((url: string) => {
 	let finalUrl = url;
 	if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
-		if (url.startsWith("localhost") || url.startsWith("127.0.0.1")) {
+		if (/^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/i.test(url)) {
 			finalUrl = `http://${url}`;
 		} else if (url.includes(".")) {
 			finalUrl = `https://${url}`;

57-76: History navigation triggers URL update from handleDidStopLoading after handleDidNavigate resets the flag.

During history navigations, handleDidNavigate fires first and resets isHistoryNavigation to false. Then handleDidStopLoading fires, sees the flag is false, and proceeds to call updateBrowserUrl and upsertHistory again. This isn't a correctness bug since updateBrowserUrl detects same-URL updates and upsertHistory is idempotent, but it results in unnecessary work (extra store update + tRPC mutation).

Consider resetting the flag only in handleDidStopLoading (the last event in the sequence), or using a counter instead of a boolean.

Also applies to: 104-125

apps/desktop/src/renderer/stores/tabs/store.ts (1)

1332-1380: openDevToolsPane should check for an existing devtools pane targeting the same browser pane.

Clicking the DevTools button multiple times creates duplicate devtools panes for the same browser pane. Add a guard to check if a devtools pane already exists for browserPaneId and focus it instead.

♻️ Suggested guard
 openDevToolsPane: (tabId, browserPaneId, path) => {
 	const state = get();
 	const tab = state.tabs.find((t) => t.id === tabId);
 	if (!tab) return null;

 	const sourcePane = state.panes[browserPaneId];
 	if (!sourcePane || sourcePane.tabId !== tabId) return null;

+	// Check if devtools pane already exists for this browser pane
+	const existingDevTools = Object.values(state.panes).find(
+		(p) => p.type === "devtools" && p.devtools?.targetPaneId === browserPaneId,
+	);
+	if (existingDevTools) {
+		set({
+			focusedPaneIds: {
+				...state.focusedPaneIds,
+				[tabId]: existingDevTools.id,
+			},
+		});
+		return existingDevTools.id;
+	}
+
 	const newPane = createDevToolsPane(tabId, browserPaneId);

splitOrientation={handlers.splitOrientation}
onSplitPane={handlers.onSplitPane}
onClosePane={handlers.onClosePane}
closeHotkeyId="CLOSE_TERMINAL"
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for CLOSE_TERMINAL definition and usage
rg -n "CLOSE_TERMINAL" --type ts --type tsx -B2 -A2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for other closeHotkeyId usages to see what values are used
rg -n "closeHotkeyId=" --type ts --type tsx -B2 -A2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for hotkey ID definitions/constants
rg -n "CLOSE_" --type ts --type tsx | grep -i "const\|export\|enum" | head -20

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for CLOSE_TERMINAL definition and usage (without file type restriction)
rg -n "CLOSE_TERMINAL" -C2

Repository: superset-sh/superset

Length of output: 3945


🏁 Script executed:

#!/bin/bash
# Search for closeHotkeyId usages
rg -n "closeHotkeyId=" -C2

Repository: superset-sh/superset

Length of output: 3093


🏁 Script executed:

#!/bin/bash
# Find all hotkey ID constants/enums
rg -n "CLOSE_|TERMINAL" --type ts | head -30

Repository: superset-sh/superset

Length of output: 2847


🏁 Script executed:

#!/bin/bash
# Get the full definition of CLOSE_PANE and CLOSE_TERMINAL hotkeys
rg -n "CLOSE_PANE|CLOSE_TERMINAL" -A3 apps/desktop/src/shared/hotkeys.ts

Repository: superset-sh/superset

Length of output: 290


🏁 Script executed:

#!/bin/bash
# Check how closeHotkeyId is used - is it displayed to users?
rg -n "closeHotkeyId" -B5 -A5 | head -100

Repository: superset-sh/superset

Length of output: 11976


Use CLOSE_PANE instead of CLOSE_TERMINAL for the closeHotkeyId prop.

The closeHotkeyId value is displayed to users in tooltips via HotkeyTooltipContent. CLOSE_TERMINAL produces a "Close Terminal" label in the UI, which is semantically incorrect for a browser pane. The PaneToolbarActions component defaults to CLOSE_PANE (with label "Close Pane"), which is the appropriate hotkey for all pane types.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx`
at line 104, The BrowserPane component currently passes
closeHotkeyId="CLOSE_TERMINAL" which causes HotkeyTooltipContent to show "Close
Terminal"; change the prop to use the generic CLOSE_PANE identifier instead
(i.e., set closeHotkeyId="CLOSE_PANE") so PaneToolbarActions and tooltips
display the correct "Close Pane" label; update the JSX in BrowserPane where
closeHotkeyId is set and ensure no other references rely on CLOSE_TERMINAL for
this pane type.

@saddlepaddle saddlepaddle merged commit d86341e into main Feb 16, 2026
15 checks passed
@Kitenite Kitenite deleted the browser-pane branch February 19, 2026 21:44
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