feat(desktop): browser tab alongside Chat and Terminal in task panels - #84799
feat(desktop): browser tab alongside Chat and Terminal in task panels#84799DanielVisca wants to merge 2 commits into
Conversation
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
Hey @DanielVisca! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
HostHog preview —
|
1f4dbee to
ef76f65
Compare
🤖 CI report
|
A new "browser" panel tab type backed by an Electron WebContentsView the main process owns; the renderer drives bounds/visibility/navigation over the host router. Layered per the desktop architecture rules: - @posthog/platform: IEmbeddedBrowser interface + EMBEDDED_BROWSER token, and a new embeddedBrowser host capability flag (desktop true, web false) - @posthog/core: EmbeddedBrowserService (single http(s)-only URL policy, normalizeBrowserUrl), zod schemas, DI module; "browser" TabData variant with addBrowserTab/updateBrowserTabUrl transforms - host-router: embeddedBrowser router (one-line forwards + event subscription), mounted in both the shared and desktop assemblies - apps/code: ElectronEmbeddedBrowser adapter — sandboxed guest on its own persist:embedded-browser partition, all permission requests denied, UA cleaned at session + network-header level so OAuth popups aren't rejected as embedded webviews, http(s) popups allowed as same-partition child windows (popup SSO), zoom-aware bounds - @posthog/ui: BrowserPanel (toolbar/URL bar/error banner + pure chrome for stories), slot hook gluing the native view to the panel rect, visibility gated on active-tab state (inactive tabs stay mounted with non-zero rects), layout-diff reconciler destroying views on every tab-close path, browser-view teardown on task deletion Tabs persist per task through the existing panel-layout store; each tab restores its last page across restarts, and page logins survive restarts on the persistent partition. The whole feature is capability-gated: the web host hides it entirely and existing layouts are untouched (additive TabData variant, no persistence version bump). Generated-By: PostHog Desktop Task-Id: 1120fcea-fc07-4c54-8dbb-50b7b712f9cb
…ender useEmbeddedBrowserSlot wrote urlRef.current and visibleRef.current straight in the render body. React can discard or replay a render, so a value from a render that never commits could leak into the live native view. Both refs are only ever read from async callbacks (the rAF-scheduled bounds reporter and the .then() after open resolves), which always run after the commit that produced the value, so mirroring them in an effect keeps the existing behavior while making the write happen on commit only. Clears the two no-ref-current-in-render errors React Doctor reported. Generated-By: PostHog Desktop Task-Id: 1a7fb1f9-a18e-46ce-87f9-ffb6ad3400c9
ef76f65 to
c525868
Compare
Problem
Desktop users iterating on their product have to leave the app to see the thing they're building — the app has Chat and Terminal per task, but checking a change means switching to an external browser, losing the task context. The Product View spike (PostHog/code#4054, pre-monorepo-migration) validated an embedded browser plus an analytics overlay in one big PR; per the Slack thread on it, the work is being split into single-purpose PRs. This is PR 1: just the browser, done properly. The analytics overlay ships separately on top of it.
A task panel is the right home (rather than a global tab): the page being iterated on belongs next to that task's Chat and Terminal, and its state should live and die with the task.
Changes
A new browser panel tab type inside
products/desktop, next to Chat and Terminal — one native Chromium view per tab, owned by the Electron main process and glued under the panel's rect. The tab-bar gets a 🌐 "new browser" button beside "new terminal".A browser tab alongside Chat and Terminal (Storybook). In the running app the host paints the live page into the grey area — natively, above the renderer, so it cannot render in Storybook:
↑ the real dev app painting posthog.com in this PR's
WebContentsView, driven end-to-end through the host router in a headless sandbox (signed-out, hence no task chrome around it).New tab → URL prompt; load failure → banner with retry:
Layering (per
products/desktop/AGENTS.md):@posthog/platform—IEmbeddedBrowserinterface +EMBEDDED_BROWSERtoken; newembeddedBrowserhost-capability flag (desktoptrue, webfalse— the web host hides the feature entirely).@posthog/core—EmbeddedBrowserService: the single http(s)-only URL policy (normalizeBrowserUrl) every URL passes before reaching a native view; zod schemas; thebrowserTabDatavariant +addBrowserTab/updateBrowserTabUrltransforms.host-router—embeddedBrowserrouter: one-line forwards + an event subscription (page state, load failures), mounted in both the shared and desktop assemblies.apps/code—ElectronEmbeddedBrowseradapter (WebContentsViewper view id).@posthog/ui—BrowserPanel(toolbar: back/forward/reload, URL bar, open-external, DevTools) split into a pure chrome (storied + unit-tested) and a wiring container; a slot hook reporting bounds via ResizeObserver and driving visibility; a layout-diff reconciler destroying views whose tabs left the layout.Security posture:
sandbox: true,contextIsolation: true, no Node, no preload — nothing is injected into browsed pages in this PR.persist:embedded-browser) — browsed pages never see the app'spersist:maincookies; the user's product logins survive restarts.file:,javascript:, etc.), andwill-navigatein the adapter.disallowed_useragent), so theElectron/…/app tokens are stripped at session level and rewritten on every request header — a popup's first request is already in flight before any per-webContents override can run.Lifecycle & edge cases:
visibility: hidden(rects stay non-zero), so visibility is gated on the layout store's active-tab state, not the DOM.acquire()/release()store lets future overlays do the same.onClose. Task deletion tears down all of a task's views (mirrorsdestroyTaskTerminals).create()on an existing view re-glues and never re-navigates.ERR_ABORTEDnoise filtered). The tab label follows the page title, like the terminal tab follows its process name.Known limitations (follow-up candidates): a popup's first document still reports Electron in
navigator.userAgent(servers already see the clean UA via the header rewrite); browser tabs in a torn-off window would still glue to the main window; toasts aren't wired into the obscured store.How did you test this code?
/pricingwithcanGoBack; goBack → home withcanGoForward;file:///etc/passwd→ rejectedNot a loadable web URL;example.com→ normalized tohttps://example.com/; popupwindow.open→ same-partition child window,javascript:popup denied; destroy → page state null, no leaked CDP targets; server-visible UA verified clean via a header-echo service for the view and a popup's first request (this live pass caught the popup-UA bug the header rewrite now fixes). The live pass ran pre-migration in PostHog/code; after porting toproducts/desktop, all automated checks were re-run in this repo.localhost:8000misread as a custom scheme). Full@posthog/coreand@posthog/uisuites pass; the whole feature is additive and capability-gated, so existing suites double as the no-regression check.pnpm build:deps, typecheck acrosscore/ui/host-router/code/web,biome ciclean,check-host-boundariesclean (no new violations).Automatic notifications
Docs update
None — desktop-internal feature; screenshots live in
products/desktop/docs/images/embedded-browser/.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored with Claude Code (PostHog Desktop cloud task). Originally built and live-verified against the pre-migration PostHog/code repo (branch
posthog-code/session-browser-panel, unpushed due to an integration permissions gap), then ported here after the code moved toproducts/desktop— 40 of 44 files applied clean; 4 panel files were re-ported against their evolved monorepo versions (useIsCloudTaskrename, import drift) and everything re-verified. Design decisions along the way: the reconciler replaced per-tabonClosecleanup after close-others was found to leak views, and the request-header UA rewrite replaced a per-webContents override after live testing showed popups' first request raced it.Created with PostHog Desktop