feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(desktop): browser profiles for the preview browser - #7254

Open
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles
Open

feat(desktop): browser profiles for the preview browser#7254
juliusmarminge wants to merge 31 commits into
mainfrom
browser-profiles

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Adds profiles to the desktop preview browser so tabs can keep separate cookies and site data. This is the bottom of the remaining stack; the browser-access setting is already merged.

Default preserves existing login storage. Incognito uses an in-memory partition. Other profiles use a separate partition namespace with unambiguous environment/profile scoping. A tab keeps its chosen profile for its lifetime.

Settings supports creating, renaming, choosing a default, and removing profiles. Removal waits for cookie/cache cleanup across all known environments and retains the profile if cleanup fails. Existing tabs remain open after removal and are labeled “Removed profile.” The Browser menu supports opening a chosen profile, including touch input.

Validation: focused profile, preview, settings-hydration, and partition-isolation tests; web/desktop typechecks; scoped formatting and lint. Latest CI results are shown below. Earlier desktop behavior was verified by the maintainer; this audit did not run another GUI pass.

Compatibility: Default partitions are unchanged. Data created in custom profiles by an earlier unmerged version of this stack is not migrated from the collision-prone partition mapping.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add browser profiles to the preview browser

  • Introduces BrowserProfile contracts with built-in default and incognito profiles, and adds settings UI to create, rename, delete, and set the default profile
  • Updates BrowserSession to support a profile namespace and ephemeral (in-memory) partitions for incognito mode, keeping legacy partitions disjoint
  • Updates PreviewManager and IPC handlers to pass profileId through snapshots, config requests, and partition-scoped clearing operations
  • Updates web components like RightPanelTabs to open specific profiles via a submenu, and keys previewWebviewConfigAtom by (environmentId, profileId)
  • Risk: DesktopPreviewBridge.clearCookies, clearCache, and getPreviewConfig now require (environmentId, profileId?) instead of taking no arguments or just environmentId
📊 Macroscope summarized f666480. 27 files reviewed, 4 issues evaluated, 4 issues filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/previewWebviewConfigState.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 77: parseConfigKey splits at the first NUL even though EnvironmentId is only a trimmed non-empty string and permits control characters. For an environment ID such as "env\u0000x", configKey produces a key which is parsed as environment "env" and profile "x\u0000..."; the webview configuration request is then made for the wrong environment/profile (and its profile value violates the profile-ID contract), so that environment cannot open its preview with the intended partition. [ Already posted ]
apps/web/src/components/settings/IntegrationsSettings.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 628: removeProfile clears the partition before it removes the profile while deliberately leaving its tabs open. An open tab can receive a response or run script that writes cookies/cache after clearBrowserProfileData resolves but before/after the settings update, leaving persistent data in the now-unreachable removed profile partition even though removal reports success. [ Already posted ]
  • line 629: After destructive cleanup succeeds, removeProfile invokes updateSettings and immediately reports success, but client persistence is fire-and-forget and explicitly catches write failures. If setClientSettings fails (for example, storage/IPC failure), the profile reappears after restart with its cookies/cache already erased, while the UI had claimed removal succeeded. [ Already posted ]
packages/contracts/src/settings.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 208: browserProfiles has no Schema.isMaxLength(BROWSER_PROFILE_MAX_COUNT) check. A hand-edited settings file (or a settings update outside the add-profile UI) with 25 or more individually valid profiles decodes successfully, even though the UI and exported limit cap profiles at 24; all excess profiles are then resolved and displayed/used. [ Out of scope (post-validation triage) ]

Note

Medium Risk
Changes Electron session partitioning and breaks the preview IPC surface for clear/config calls; incorrect scope mapping could mix or strand login data, though legacy Default partitions are explicitly preserved.

Overview
Adds named browser profiles so preview tabs can keep isolated cookies and storage, with built-in Default (legacy partition unchanged) and Incognito (ephemeral Chromium partition).

Contracts & server: New BrowserProfile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so tabs don't silently switch partitions.

Desktop:resolvePartitionScope maps (environmentId, profileId) to partition scope (JSON tuple for non-default profiles), persistence, and a profile- partition namespace disjoint from legacy defaults. getPreviewConfig, clearCookies, and clearCache IPC now take environmentId and optional profileId; per-profile clears load the session first so cleanup works after restart. BrowserSession supports ephemeral partitions and partition-scoped clears.

Web UI: Integrations settings to create/rename/remove profiles (removal clears data across environments), default profile picker, and a Browser add-menu submenu to open a specific profile. Preview chrome shows the active profile and scopes "clear cookies/cache" to that tab's profile. Open paths (openPreviewSession, file/link opens) apply hydrated default profileId; webview config is cached per environment + profile.

Reviewed by Cursor Bugbot for commit f666480. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904d0a95-7510-4028-b553-9dcda458780c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actionsgithub-actionsBot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actionsgithub-actionsBot added the size:XL 500-999 changed lines (additions + deletions). label Aug 16, 2026
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
Comment threadapps/web/src/browser/previewWebviewConfigState.ts
Comment threadapps/web/src/components/preview/PreviewMoreMenu.tsx
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.1 KiB−168 B (−1.2%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.2 KiB−172 B (−2.6%)7.8 KiB
CodexLive turn WebSocket decoded55.6 KiB54.7 KiB−910 B (−1.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.2 KiB13.1 KiB−8 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+3 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.3 KiB6.2 KiB−11 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages880 (0.0%)21

Baseline: 04efa79 · PR result: f666480 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeappmacroscopeappBot left a comment

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.

Reviewed the web UI changes (RightPanelTabs, PreviewChromeRow/PreviewView, PreviewMoreMenu, IntegrationsSettings, menu.tsx) against the shared primitive contracts. Two consistency findings, both inline.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment threadapps/web/src/components/ui/menu.tsx Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 21:36
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/desktop/src/ipc/methods/preview.ts Outdated
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a cross-layer browser-profile capability that changes session partitioning, cookie/login isolation, storage deletion, and the default profile used for implicit preview opens. Its new configurable product default and sensitive browser-storage behavior warrant human review.

No code changes detected at f666480. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment threadpackages/contracts/src/browserProfile.ts
Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/browser/openFileInPreview.ts Outdated
Comment threadapps/web/src/components/RightPanelTabs.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

Two findings in the changed web UI. The MenuSubTrigger icon treatment added in this revision fixes the alignment of the new Browser sub-trigger, but the selector it uses also matches the trailing chevron on the two existing icon-less sub-triggers and overrides their ms-auto.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/ui/menu.tsx Outdated
Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One finding on the new profile badge in the preview chrome row: the truncate cap doesn't actually ellipsize because Badge is a flex container. Everything else in scope (the MenuSubTrigger icon-column fix, the MenuGroup/MenuGroupLabel wrapping in PreviewMoreMenu, the profile list rows reusing SettingsRow/ITEM_ROW_INNER_CLASSNAME/DraftInput/Button/AlertDialog, and the Select for the default profile) follows the existing primitives and settings conventions.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/preview/PreviewView.tsx Outdated
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadpackages/contracts/src/ipc.ts
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeappmacroscopeappBot left a comment

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.

One convention finding: the new browserProfile module (and its test) uses the consolidated import { Schema } from "effect" form instead of the subpath namespace import used by the rest of packages/contracts. Everything else in the diff (service interfaces on BrowserSession/PreviewManager, Schema.TaggedErrorClass errors with structural attributes, make/layer shape, dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/browserProfile.ts Outdated
Comment threadpackages/contracts/src/browserProfile.test.ts Outdated
Comment threadapps/web/src/components/settings/IntegrationsSettings.tsx
juliusmarmingeand others added 28 commits September 1, 2026 16:32
…nu row
Widening `onAddBrowser` to `(profileId?: string)` broke the empty-state
Browser card: it is passed straight to a DOM click handler, so React handed
the MouseEvent in as the profile id and the open silently failed schema
validation.
Fixed structurally rather than at the call site — `onAddBrowser` goes back to
taking no arguments, and choosing a profile is a separate
`onAddBrowserInProfile(profileId)`. The type now rejects wiring the profile
variant to a DOM handler, so this cannot recur; adding it surfaced all four
call sites immediately.
The "+" menu is one row again. The submenu trigger is itself clickable and
opens the default profile, with hover or arrow revealing the rest, so the
common case stays a single click. The menu is controlled so that action can
dismiss it, which a submenu trigger does not do on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MenuSubTrigger` was the only interactive menu primitive without a cursor:
`MenuItem`, `MenuCheckboxItem`, and `MenuRadioItem` all set `cursor-pointer`
plus `data-disabled:cursor-not-allowed`, so it kept the default arrow and read
as inert.
It always was clickable — clicking opens the submenu — and it is now also an
action in the add-surface menu, where the arrow cursor was actively
misleading. Fixed in the primitive rather than at that one call site, since
the gap applies to the other two submenu triggers (Diff scope, preview
Appearance) as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab's profile was only discoverable by opening the three-dot menu, which is
a poor place for something that changes what you are logged into.
The chrome row gains a leading slot before the URL bar, and the preview names
the tab's profile there. Only when it differs from the default: labelling
every tab "Default" would be noise on the common case, while a tab running in
another profile is exactly what needs calling out.
Also gives the three-dot menu's profile heading a `MenuGroup` ancestor —
`MenuGroupLabel` reads Base UI's group context and throws without one, which
took the app to its error boundary as soon as the menu opened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The More menu passed the tab's raw `profileId` to the clear actions. A tab
created before profiles existed has none, and the IPC layer reads an absent
profile as "every profile" — so a menu labelled "Clear cookies (Default)" wiped
every partition. The view now resolves the default the same way it resolves the
name it displays, and the prop is required so the gap cannot come back.
Clearing also only touched sessions already in the in-memory map. Deriving the
partition string does not create the session, so clearing a profile with no tab
open this run reported success and deleted nothing; the handler now loads the
session first.
`resolveBrowserProfiles` additionally drops repeated ids, which map to one
partition and would otherwise show as two isolated identities sharing every
cookie, and reports a custom `incognito` profile as persistent, since
persistence is keyed off the built-in id alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The leading-icon rules added for the Browser sub-trigger were scoped with
`:first-of-type`, which on a sub-trigger with no leading icon matches the
trailing chevron instead — the compound selector outranks its `ms-auto` and
took away the right alignment on the existing Appearance and Turn triggers.
Scoping away from the last child leaves the chevron alone.
The profile badge in the chrome row was unbounded while profile names run to 48
characters, so it took width from the URL input, the only flexible element
there. It is capped and truncated.
Removing a profile now confirms first, like every other destructive action in
Settings, and Incognito is no longer offered as — or resolved to — the default
profile: as a default it would open every new tab into storage discarded on
close, and the settings list and the resolved default now agree on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Badge` is an `inline-flex` with `whitespace-nowrap`, so `truncate` on the
badge never reached the name inside it: a long profile name was hard-clipped at
both ends with no ellipsis. The cap stays on the badge, the truncation moves to
an inner span, and the full name is available as a title.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built-in rows are a plain span and a badge rather than `h3`/`p` or disabled
controls, so the block's own dimming never reached them: on web they were the
only full-contrast content inside "only available in the desktop app".
Also switches `browserProfile` to the subpath namespace import the rest of
`packages/contracts` uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper-level dim stacked with each control's own: the rename field and
remove button composited to roughly 0.41 alpha while every other disabled
control in the desktop-only block sits at 0.64. Only the built-in row's name
and badge lack a disabled treatment, so the dim belongs there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profile names are user-supplied and run to 48 characters. The Browser
sub-menu rendered them bare inside an unbounded popup, so a long one widened
it to fit-content and wrapped; it is now capped and truncated like the other
name-bearing menus.
The clear actions repeated the name their own group heading already shows,
which drove the popup far past its width for no added information. The heading
keeps the profile and the actions keep fixed-length labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bare rows stack on narrow viewports with a larger gap inside a row than
between rows, so the remove button read as belonging to the profile below.
Each profile is now a bounded row, and the list carries the bottom spacing
`SettingsRow` leaves to its children.
`MenuGroupLabel` renders a block box, so `text-overflow` on an inline span
inside it never applied and a long profile name pushed the popup past its
width. The truncation sits on the label itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab created before profiles existed carries no profile of its own and runs
in the built-in `default` partition — the scope the browser used before
profiles. It was labelled with, and cleared against, whatever profile is
configured as the default now, so on a machine with a custom default the
active tab's data was left untouched while another profile's was wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Sep 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge