feat(browser): Hedge Browser — embedded browser panel with tab management, error page, and welcome page#2797
Open
ricardo-leiva wants to merge 12 commits into
Open
feat(browser): Hedge Browser — embedded browser panel with tab management, error page, and welcome page#2797ricardo-leiva wants to merge 12 commits into
ricardo-leiva wants to merge 12 commits into
Conversation
Intercepts did-fail-load (net errors, DNS failures, timeouts) on each WebContentsView and replaces the blank screen with a dark-themed error page featuring the PostHog hedgehog (builder-hog, base64-embedded so it works in both dev and the packaged app). Error code -3 (ABORTED) is skipped to avoid flashing during normal stop/redirect flows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
Addresses findings from a protocol audit against AGENTS.md and the PostHog skills: - Host boundary: add service.ts to allowlist (WebContentsView is inherently Electron-coupled; no portable logic to extract); suppress noRestrictedImports with biome-ignore in service.ts for the same reason - Relative imports: replace ./browserStore and ./BrowserToolbar with @posthog/ui/... aliases in BrowserPanel, BrowserToolbar, and usePanelLayoutHooks (noRestrictedImports rule) - Analytics: add BROWSER_TAB_OPENED event with source/has_initial_url properties to @posthog/shared/analytics-events; fire track() in panelLayoutStore.addBrowserTab matching the FILE_OPENED pattern - Stop procedure: add IBrowserService.stop(), BrowserService.stop() (webContents.stop()), browser.router stop mutation, and fix the reload/stop toolbar button (both branches were calling reload) - Rule 11 / Biome: extract eventToAsyncIterator to @posthog/host-trpc/eventSubscription (plain AsyncIterable, no async function*) so tRPC subscriptions in browser.router.ts become one-line forwards; also fixes a Biome 2.2.4 hang on async generator patterns in method chains - Biome perf: add biome.jsonc override for BrowserPanel.tsx to disable useExhaustiveDependencies (Biome 2.2.4 bug: hangs indefinitely when multiple useEffect hooks capturing a deeply-typed tRPC client are combined with useSubscription calls in the same component) - a11y: add alt="" to decorative favicon img in usePanelLayoutHooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
…BrowserTab analytics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
… tab UX improvements - Chat links left-click open in the embedded browser tab; right-click shows a native context menu with Open in app browser / Open in system browser / Copy link - Track LINK_CLICKED_IN_CHAT analytics event with destination dimension; extend BROWSER_TAB_OPENED source to include "chat_link" - Add showLinkContextMenu to ContextMenuService, schema, and tRPC router - Add openBrowserUrl(taskId, url) store action that targets the focused panel - Tab bar "+" becomes a dropdown to choose terminal or browser tab - Add terminal icon to the right-side toolbar; add divider between terminal and globe icons; fix right-panel visibility condition - Fix browser tab visibility: use display:none (not visibility:hidden) for inactive browser tabs so IntersectionObserver correctly hides the native WebContentsView and prevents it bleeding through other tabs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
…mbedded browser Right-clicking in the embedded browser now shows a native context menu: - Back / Forward / Reload for in-browser navigation - "Open in system browser" for the current page (or "Open link in system browser" + "Copy link address" when right-clicking a hyperlink) - "Inspect element" that opens Chromium DevTools at the clicked position Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
…n, and dismissal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
- Add Hedge Browser welcome page (shown instead of Google on new tabs): dark-themed page with hedgehog logo, description, and footer note - Fix split panel without reload: skip destroy when tab moves to another panel; BrowserService.create() guards with browsers.has() so the WebContentsView is preserved across panel splits - Fix ghost browser view on hot-reload: setVisible(false) when skipping destroy so the native layer doesn't float over the UI - Fix address bar on error pages: track last real URL in a ref and emit synthetic navigate on create() so the toolbar is seeded immediately - Add loading animation bar at the bottom of the browser toolbar - Fix z-index: right-side tab-bar actions render above the scrollable tab list - Hide native WebContentsView when the "+" dropdown opens so it doesn't cover Radix dropdown menus (native OS layer ignores CSS z-index) - Fix silent no-op: "Open in app browser" in chat falls back to system browser when no active task is present - Freeze FALLBACK_BROWSER_STATE so mutations don't pollute the sentinel - Fix canOpenUrl regex in MentionChipView (require http/https prefix) - Add useOpenUrl hook centralising all external-link handling across the app (GithubRefChip, MarkdownRenderer, PRBadgeLink, FetchToolView, GitActionResult) - Update updateBrowserTabUrl to use typed wrappers for findTabInTree / updateTreeNode; fixes TypeScript error where core PanelNode (Tab with component: unknown) was not assignable to UI PanelNode (Tab with component: ReactNode) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
…seOpenUrl
Analytics for BROWSER_TAB_OPENED is tracked at call sites (PanelLayout,
BrowserPanel, AgentMessage) with source context — not in the store action
where source is unknown. Remove the two tests that incorrectly expected
the store to fire the event.
useOpenUrl no longer reads taskId from TanStack Router params; useSessionTaskId()
alone is sufficient since all real call sites are inside SessionTaskIdProvider,
and useParams({ strict: false }) was throwing in test environments without a
RouterProvider.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
Align hoglet size (72 → 160 px), font-weight (600 → 700), heading color (#f0f0f0 → #fff), body font-size (13.5 → 14 px), drop-shadow, and footer color so both pages look consistent side-by-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
Contributor
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
packages/ui/src/features/browser/BrowserPanel.tsx:165-174
**Global `onOpenUrl` subscription fires in every mounted BrowserPanel**
`trpc.browser.onOpenUrl` has no per-browser filter — the router subscribes a new listener to `BrowserService.on("openUrl", …)` for each active subscription. When any page calls `window.open()`, the service emits one event and every active subscriber (one per mounted `BrowserPanel`) calls `addBrowserTab`. With N browser tabs open, a single `window.open()` creates N new tabs across all panels.
Fix: either carry a `browserId` on the `BrowserOpenUrlEvent` and filter it inside `BrowserPanel` (so only the source browser opens a new tab), or lift the subscription out to a single site such as `PanelLayout` where it can be handled exactly once.
### Issue 2 of 3
packages/core/src/panels/panelLayoutTransforms.ts:712
`Date.now()` for browser ID generation can produce duplicate IDs when two tabs are opened within the same millisecond. `BrowserService.create` silently no-ops on a duplicate ID, so one tab ends up with no backing native view. A short random suffix removes the collision risk.
```suggestion
const browserId = `browser-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
```
### Issue 3 of 3
apps/code/src/main/services/browser/service.ts:252-258
`navigate` doesn't apply the same `about:blank` → welcome-page redirect that `create` does. If the user types `about:blank` in the address bar and presses Enter, the browser navigates to a truly blank page instead of showing the Hedge Browser welcome screen.
```suggestion
navigate(browserId: string, url: string): void {
const entry = this.browsers.get(browserId);
if (!entry) return;
const effectiveUrl =
!url || url === "about:blank" ? buildNewTabPage() : url;
entry.view.webContents.loadURL(effectiveUrl).catch((err) => {
log.warn("Failed to navigate", effectiveUrl, err);
});
}
```
Reviews (1): Last reviewed commit: "fix(browser): match new-tab page visual ..." | Re-trigger Greptile |
… about:blank in navigate - onOpenUrl subscription now takes browserId and filters at the router level (same as onNavigate/onTitle/onFavicon), so window.open() in one tab no longer creates N new tabs across all mounted BrowserPanels - BrowserOpenUrlEvent carries browserId; service emits it from the setWindowOpenHandler closure where browserId is in scope - browserId generation adds a random suffix to eliminate collisions when two tabs open within the same millisecond - navigate() applies the same about:blank → welcome-page redirect that create() uses, so typing about:blank in the address bar shows Hedge Browser instead of a truly blank page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
f2e7de8 to
d493b5b
Compare
…nalytics events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Opening external links from the agent chat (docs, PRs, tickets) breaks your flow — you leave the app, lose context, and come back cold.
PostHog Code has no built-in way to view web content without switching to a full browser.
Changes
Embedded browser panel (Hedge Browser)
A fully functional browser panel that lives inside the workspace layout, side-by-side with the terminal, editor, and agent output.
Screen recording here: https://drive.google.com/file/d/1c-kbk0HQsL40J4YHela_DT2hyGuGVr6l/view?usp=drive_link
Core:
BrowserService(Electron main) — managesWebContentsViewlifecycle: create, navigate, split-without-reload, close, title/favicon streaming via tRPC subscriptionBrowserToolbar— address bar with back/forward/reload, loading spinner, faviconBrowserPanel— wires toolbar to service; handleswindow.openintercept so popups open as new tabs instead of leaving the apppanelLayoutStorealongside file/shell/log tabs; split panel keeps each view independentWelcome page (new tab):
data:text/htmlpage shown when no URL is provided — hoglet mascot, "Hedge Browser" title, taglineError page (failed loads):
did-fail-load— hoglet, message, URL display, Go Back / Try Again buttons, "POWERED BY POSTHOG CODE" badgeChat link integration:
useOpenUrl— HTTP(S) links clicked inside agent messages open in the embedded browser instead of the system browserAgentMessage— tracksBROWSER_TAB_OPENEDwithsource: "chat_link"PanelLayout— tracks withsource: "user"(toolbar+button)BrowserPanel— tracks withsource: "window_open"(popup intercept)Right-click context menu:
showLinkContextMenutRPC procedure;context-menucore package for typed menu schemasArchitecture notes:
biome.jsoncallows@posthog/host-router/reactin@posthog/uiforTabbedPanel— pre-existing pattern from4fbb2cec7, noted as tech debtaddBrowserTab(core) stays"about:blank";BrowserService.create()intercepts the sentinel and swaps in the welcome page URL — keeps core free of host-specific contentScreenshots
How did you test this?
+button; navigated, split panels, closed tabs, triggered error page, right-clicked context menu, opened DevToolserrorPage.test.ts,eventSubscription.test.ts,context-menu.test.ts,panelLayoutStore.test.ts,copyContextMenu.integration.test.tsxpnpm typecheck— cleanpnpm lint— cleanpnpm test— 768/768 passingAutomatic notifications