Skip to content

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
PostHog:mainfrom
ricardo-leiva:feat/hedge-browser
Open

feat(browser): Hedge Browser — embedded browser panel with tab management, error page, and welcome page#2797
ricardo-leiva wants to merge 12 commits into
PostHog:mainfrom
ricardo-leiva:feat/hedge-browser

Conversation

@ricardo-leiva

@ricardo-leiva ricardo-leiva commented Jun 20, 2026

Copy link
Copy Markdown

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.

Screenshot 2026-06-20 at 9 39 22 AM

Screen recording here: https://drive.google.com/file/d/1c-kbk0HQsL40J4YHela_DT2hyGuGVr6l/view?usp=drive_link

Core:

  • BrowserService (Electron main) — manages WebContentsView lifecycle: create, navigate, split-without-reload, close, title/favicon streaming via tRPC subscription
  • BrowserToolbar — address bar with back/forward/reload, loading spinner, favicon
  • BrowserPanel — wires toolbar to service; handles window.open intercept so popups open as new tabs instead of leaving the app
  • Tab management — browser tabs live in the same panelLayoutStore alongside file/shell/log tabs; split panel keeps each view independent

Welcome page (new tab):

  • Custom data:text/html page shown when no URL is provided — hoglet mascot, "Hedge Browser" title, tagline
  • Styled to match the error page (160 px hoglet, same font weights, drop-shadow)

Error page (failed loads):

  • Branded error page injected on did-fail-load — hoglet, message, URL display, Go Back / Try Again buttons, "POWERED BY POSTHOG CODE" badge

Chat link integration:

  • useOpenUrl — HTTP(S) links clicked inside agent messages open in the embedded browser instead of the system browser
  • AgentMessage — tracks BROWSER_TAB_OPENED with source: "chat_link"
  • PanelLayout — tracks with source: "user" (toolbar + button)
  • BrowserPanel — tracks with source: "window_open" (popup intercept)

Right-click context menu:

  • Native context menu on browser panel with Back, Forward, Reload, Copy URL, Open in Browser, Inspect (DevTools)
  • showLinkContextMenu tRPC procedure; context-menu core package for typed menu schemas

Architecture notes:

  • biome.jsonc allows @posthog/host-router/react in @posthog/ui for TabbedPanel — pre-existing pattern from 4fbb2cec7, noted as tech debt
  • Default URL in addBrowserTab (core) stays "about:blank"; BrowserService.create() intercepts the sentinel and swaps in the welcome page URL — keeps core free of host-specific content

Screenshots

How did you test this?

  • Full manual session: opened browser tabs from chat links, address bar, + button; navigated, split panels, closed tabs, triggered error page, right-clicked context menu, opened DevTools
  • Unit tests: errorPage.test.ts, eventSubscription.test.ts, context-menu.test.ts, panelLayoutStore.test.ts, copyContextMenu.integration.test.tsx
  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm test — 768/768 passing

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

ricardo-leiva and others added 9 commits June 19, 2026 11:00
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
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix 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

Comment thread packages/ui/src/features/browser/BrowserPanel.tsx
Comment thread packages/core/src/panels/panelLayoutTransforms.ts Outdated
Comment thread apps/code/src/main/services/browser/service.ts
ricardo-leiva and others added 2 commits June 20, 2026 09:54
… 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
@ricardo-leiva ricardo-leiva changed the title feat(browser): Hedge Browser — embedded browser panel with tab management, error page, and welcome page feat(skills): AI generation, grid/list view, collapsible sections, skip-modal create Jun 21, 2026
@ricardo-leiva ricardo-leiva changed the title feat(skills): AI generation, grid/list view, collapsible sections, skip-modal create feat(browser): Hedge Browser — embedded browser panel with tab management, error page, and welcome page Jun 21, 2026
…nalytics events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
@charlesvien charlesvien added the Stamphog This will request an autostamp by stamphog on small changes label Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants