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 for the preview browser with per-profile partition isolation

  • Adds browser profile contracts in browserProfile.ts with built-in Default and Incognito profiles, plus user-defined persistent profiles capped at a contract maximum
  • Extends BrowserSession partition derivation in BrowserSession.ts to support persistent, ephemeral, and profile-namespaced partitions, with escaping that prevents collisions from lone UTF-16 surrogates and backslash sequences
  • Adds profile management and default-profile selection UI in IntegrationsSettings.tsx; removing a user profile clears its cookies and cache across all environments first
  • Propagates profileId through preview open, navigate, and status snapshots in Manager.ts, through webview config caching keyed by environment+profile in previewWebviewConfigState.ts, and through all desktop IPC handlers in preview.ts
  • Surfaces a browser-profile submenu in RightPanelTabs.tsx where mouse clicks open the default profile directly and touch opens the submenu for selection
  • Risk: getPartition in BrowserSession.ts changes partition format for non-default profiles using a JSON tuple scope; existing sessions opened before this change remain on the legacy bare-environment persistent partition, and default-profile partitions are kept compatible with that legacy format

Macroscope summarized 1a5aa85.


Note

Medium Risk Changes Electron partition derivation and cookie/cache clearing semantics; default profile behavior is preserved, but wrong scoping could leak or wipe the wrong profile’s storage.

Overview Adds browser profiles so preview tabs can use separate Chromium session partitions (cookies, cache, logins). Built-in Default keeps the legacy bare-environment partition; Incognito uses a non-persistent partition; custom profiles get a namespaced partition with collision-safe scope encoding.

Contracts & server: New profile types and client settings (browserProfiles, browserDefaultProfileId). Preview open/snapshots carry optional profileId through navigate and status updates so a tab does not silently switch partitions.

Desktop: resolvePartitionScope maps environment + profile to partition scope; getPreviewConfig and clear-cookie/cache IPC take environmentId and optional profileId, with per-profile clears loading the session first so data is actually removed after restart.

Web: Settings to add/rename/remove profiles and pick a default (incognito excluded as default). New tabs and link/file opens send configured profileId; webview config is cached per environment+profile. UI: profile submenu on “Add browser,” profile badge in preview chrome, and profile-scoped clear actions in the more menu.

Note: Default partitions stay compatible; custom profiles from an earlier partition scheme are not migrated per the PR description.

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

apps/web/src/components/preview/PreviewMoreMenu.tsxline 201Outdated · b7395b6Open thread
……@@ -177,12 +188,27 @@ export function PreviewMoreMenu({
198+ {profileName ? <MenuGroupLabel>Profile: {profileName}</MenuGroupLabel> : null}
199+ <MenuItem
200+ onClick={() =>
201+ void bridge.clearCookies(environmentId, profileId).catch(() => undefined)
MmacroscopeappBoton line 201Aug 16, 2026

🟠 High preview/PreviewMoreMenu.tsx:201

The clear actions target the built-in Default partition when profileId is omitted, even though the menu displays the user's configured default profile. Because PreviewMoreMenu passes the raw optional ID to clearCookies and clearCache, profile-less legacy tabs can clear the wrong profile's data. Resolve the missing ID to DEFAULT_BROWSER_PROFILE_ID before invoking either action.

Also found in 1 other location(s)

apps/web/src/components/preview/PreviewView.tsx:150

activeProfile treats a missing snapshot.profileId as the user's currently configured default profile. However, desktop partition resolution treats undefined as the built-in Default profile, and PreviewMoreMenu still receives the original undefined ID. After upgrading with an existing profile-less tab and selecting a custom default profile, the menu labels the clear actions with that custom profile's name while the actions actually clear the built-in Default profile's cookies/cache. Resolve an absent snapshot ID as DEFAULT_BROWSER_PROFILE_ID so the displayed target matches the cleared partition.

No longer relevant as of 61d5a1136cade87d21fb98f42d5e93d91b3fdad5

apps/web/src/browser/previewWebviewConfigState.tslines 66–75Outdated · b7395b6Open thread
……@@ -45,32 +45,51 @@ type PreviewConfigBridge = Pick<DesktopPreviewBridge, "getPreviewConfig">;
72+ environmentId: environmentId as EnvironmentId,
73+ ...(profileId === "" ? {} : { profileId }),
74+ };
75+};
MmacroscopeappBoton lines 66–75Aug 16, 2026

🟡 Medium browser/previewWebviewConfigState.ts:66

A valid profile ID containing \u0000, such as foo\u0000bar, is truncated to foo, so getPreviewConfig loads and returns the partition for the wrong profile. parseConfigKey splits the key on the delimiter even though the ID schemas allow that character; encode the environment/profile pair unambiguously, such as a JSON tuple, instead of splitting on a raw delimiter.

No longer relevant as of 0009cb1daf2c6fea1dbe1643f7ea9fcf298640f1

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

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

apps/web/src/components/settings/IntegrationsSettings.tsxlines 562–567Outdated · b7395b6Open thread
……@@ -453,11 +465,172 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
564+ variant="ghost-muted"
565+ disabled={disabled}
566+ aria-label={`Remove ${profile.name}`}
567+ onClick={() => removeProfile(profile.id)}
MmacroscopeappBoton lines 562–567Aug 16, 2026

settings/IntegrationsSettings.tsx:567

This icon-only control deletes the profile and wipes its cookies/cache immediately and irreversibly, while every comparable destructive action in settings confirms first (ThemeSettings.tsx theme removal, ConnectionsSettings.tsx session revocation / Tailscale disable all route through AlertDialog). A stray click here silently destroys stored logins.

Suggest gating removeProfile behind the same AlertDialog confirmation used elsewhere in settings, naming the profile in the title, so the destructive path matches the established pattern.

No longer relevant as of 0009cb1daf2c6fea1dbe1643f7ea9fcf298640f1

packages/contracts/src/browserProfile.tslines 67–70Outdated · fe1bc48Open thread
……@@ -0,0 +1,78 @@
67+ return [
68+ ...BUILT_IN_BROWSER_PROFILES,
69+ ...userProfiles.filter((profile) => !isBuiltInBrowserProfileId(profile.id)),
70+ ];
MmacroscopeappBoton lines 67–70Aug 16, 2026

🟡 Medium src/browserProfile.ts:67

When userProfiles contains two distinct entries with the same id, resolveBrowserProfiles returns both identities even though they map to the same Electron partition, so cookies and logins leak between profiles the picker presents as isolated and profile lookup becomes ambiguous. The filter removes only built-in collisions; deduplicate user IDs (or reject duplicate IDs during settings validation) before returning the resolved list.

Also found in 1 other location(s)

packages/contracts/src/settings.ts:156

browserProfiles validates each entry but does not enforce unique BrowserProfile.id values. A hand-edited or otherwise persisted settings array containing two profiles with the same ID decodes successfully; both profiles are then presented separately but resolve to the same Electron partition, so cookies/logins unexpectedly leak between profiles that the UI represents as isolated.

No longer relevant as of 61d5a1136cade87d21fb98f42d5e93d91b3fdad5

packages/contracts/src/browserProfile.tsline 33Outdated · fe1bc48Open thread
……@@ -0,0 +1,78 @@
30+ * `persistent` profiles keep cookies on disk across restarts; `incognito`
31+ * uses an in-memory partition that Chromium discards with the process.
32+ */
33+export const BrowserProfileKind = Schema.Literals(["persistent", "incognito"]);

🟡 Medium src/browserProfile.ts:33

A custom profile with kind: "incognito" is shown as ephemeral but its cookies are persisted across restarts, because partition creation uses the special incognito ID rather than BrowserProfileKind. Reject kind: "incognito" for non-built-in IDs, or make partition creation derive persistence from kind.

No longer relevant as of 61d5a1136cade87d21fb98f42d5e93d91b3fdad5

apps/web/src/components/settings/IntegrationsSettings.tsxline 515Outdated · fe1bc48Open thread
……@@ -453,11 +465,172 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
512+ browserProfiles: userProfiles.filter((profile) => profile.id !== id),
513+ // Reassign the default rather than leaving it pointing at nothing.
514+ ...(defaultProfileId === id ? { browserDefaultProfileId: DEFAULT_BROWSER_PROFILE_ID } : {}),
515+ });
CcursorBoton line 515Aug 16, 2026

Deleted profile cookies can remain

Medium Severity

Removing a profile calls clear cookies/cache for that profile, but clear only touches sessions already in the in-memory map. resolveClearPartitions derives the partition string without creating the session first, so after a restart—or if no tab under that profile was opened this process—the clear is a no-op and persistent cookies stay on disk with no UI entry left.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit fe1bc48d0b0d5c13e5282191ae42e0973f15d539. Configure here.

apps/web/src/browser/openFileInPreview.tsline 54Outdated · 61d5a11Open thread
……@@ -43,7 +45,15 @@ export async function openUrlInPreview<E>(input: {
51+ // Built here rather than via `openPreviewSession` because this path
52+ // maps the result differently, so the configured defaults have to be
53+ // applied explicitly or file/link opens would ignore them.
54+ viewport: browserDefaultOpenViewport(),

🟡 Medium browser/openFileInPreview.ts:54

Opening a file or link before client-settings hydration permanently creates the tab with the schema-default viewport and profile instead of the user's configured defaults. browserDefaultOpenViewport() and browserDefaultOpenProfileId() read the pre-hydration snapshot synchronously; resolve the hydrated defaults first, as openPreviewSession does, before invoking openPreview.

Also found in 1 other location(s)

apps/web/src/components/preview/openTerminalLinkInPreview.ts:93

openTerminalLinkInPreview reads the viewport/profile via synchronous browserDefaultOpenViewport() and browserDefaultOpenProfileId(). Those accessors use the current settings snapshot, while resolveBrowserDefaults() explicitly waits for client-settings hydration because pre-hydration values are schema defaults. If a terminal link is opened before hydration finishes, the new tab is permanently created with the schema-default viewport/profile instead of the user's configured defaults. This path should resolve hydrated defaults before invoking openPreview.

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in 4dca51e1f5e. File/URL and terminal-link opens await settings hydration and use viewport/profile from the same hydrated snapshot; focused tests cover a deferred hydration promise.

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

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

apps/web/src/components/ui/menu.tsxlines 238–242Outdated · 61d5a11Open thread
……@@ -235,7 +235,11 @@ function MenuSubTrigger({
239+ // same column as the items around it, so its icon has to align and dim
240+ // with theirs. Scoped to the first svg because the chevron below is
241+ // also a direct child, and `-mx-0.5` would override its `-me-0.5`.
242+ "[&>svg:first-of-type]:-mx-0.5 flex min-h-8 cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:cursor-not-allowed data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&>svg:first-of-type:not([class*='opacity-'])]:opacity-80 [&_svg]:pointer-events-none [&>svg]:shrink-0",
MmacroscopeappBoton lines 238–242Aug 16, 2026

ui/menu.tsx:242

:first-of-type doesn't exclude the chevron when the sub-trigger has no leading icon — in that case the chevron is the first (and only) svg child, so it picks up [&>svg:first-of-type]:-mx-0.5. That compound selector has specificity (0,2,1) versus .ms-auto's (0,1,0), so margin-inline: -0.125rem beats the chevron's own ms-auto regardless of class order and the chevron stops being pushed to the right edge. That regresses the two existing icon-less sub-triggers: Appearance in PreviewMoreMenu.tsx:127 and Turn in DiffPanel.tsx:557, whose chevrons would sit flush against the label instead of right-aligned.

Since the chevron is always rendered last, scoping to :not(:last-child) matches only a real leading icon and leaves the chevron's own margins intact.

suggestion
1 // Leading-icon treatment matches `MenuItem`: a sub-trigger sits in the
2 // same column as the items around it, so its icon has to align and dim
3 // with theirs. Scoped to non-last children because the chevron below is
4 // also a direct child and must keep its own `ms-auto`/`-me-0.5`.
5 "[&>svg:not(:last-child)]:-mx-0.5 flex min-h-8 cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:cursor-not-allowed data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&>svg:not(:last-child):not([class*='opacity-'])]:opacity-80 [&_svg]:pointer-events-none [&>svg]:shrink-0",

No longer relevant as of 0009cb1daf2c6fea1dbe1643f7ea9fcf298640f1

apps/web/src/components/settings/IntegrationsSettings.tsxline 515Outdated · 61d5a11Open thread
……@@ -453,11 +465,172 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
512+ browserProfiles: userProfiles.filter((profile) => profile.id !== id),
513+ // Reassign the default rather than leaving it pointing at nothing.
514+ ...(defaultProfileId === id ? { browserDefaultProfileId: DEFAULT_BROWSER_PROFILE_ID } : {}),
515+ });
CcursorBoton line 515Aug 16, 2026

Profile delete skips other environments

Medium Severity

Removing a profile clears cookies/cache only for the primary environmentId. Partitions are scoped per environment, so other connected environments keep that profile’s data on disk after the profile disappears from settings.

Triggered by learned rule: Multi-environment identity scoping: keys and lookups must carry environmentId

Reviewed by Cursor Bugbot for commit 61d5a1136cade87d21fb98f42d5e93d91b3fdad5. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxline 524Outdated · 0009cb1Open thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
521+ void previewBridge?.clearCookies(environmentId, id).catch(() => undefined);
522+ void previewBridge?.clearCache(environmentId, id).catch(() => undefined);
523+ }
524+ updateSettings({
MmacroscopeappBoton line 524Aug 16, 2026

🟡 Medium settings/IntegrationsSettings.tsx:524

Removing a profile leaves existing tabs attached to its Electron partition, so those tabs can recreate cookies/cache after clearCookies and clearCache, leaving data with no profile UI to manage it. The dialog promises that open tabs move to the default profile, but this code only changes browserDefaultProfileId for future tabs; close or migrate the profile's existing tabs before clearing and removing it.

No longer relevant as of 3045949155d67b9a31a9426eb832c3d0c24ecf64

apps/web/src/components/settings/IntegrationsSettings.tsxline 520Outdated · 0009cb1Open thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
517+ setProfilePendingRemoval(null);
518+ // Drop the partition's data too, otherwise a removed profile's cookies
519+ // stay on disk with nothing in the UI pointing at them.
520+ if (environmentId) {
MmacroscopeappBoton line 520Aug 16, 2026

🟡 Medium settings/IntegrationsSettings.tsx:520

Deleting a browser profile removes its global entry but clears cookies and cache only for the current primary environmentId, leaving data orphaned in every other environment that used the profile. Clear the profile partition for all environments where it may exist, or retain cleanup state until those partitions have been removed.

No longer relevant as of 3045949155d67b9a31a9426eb832c3d0c24ecf64

Synced from GitHub

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

apps/web/src/components/settings/IntegrationsSettings.tsxline 521Outdated · 1adce5cOpen thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
518+ // Drop the partition's data too, otherwise a removed profile's cookies
519+ // stay on disk with nothing in the UI pointing at them.
520+ if (environmentId) {
521+ void previewBridge?.clearCookies(environmentId, id).catch(() => undefined);
MmacroscopeappBoton line 521Aug 17, 2026

🟡 Medium settings/IntegrationsSettings.tsx:521

The profile is removed from settings even when clearCookies or clearCache fails, so its cookies or cache can remain on disk with no profile entry available for retry. Both IPC calls are fire-and-forget and their errors are suppressed; await the deletions and only update settings after they succeed, while surfacing failure to the user.

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in 210d0f82b3 and c8840bc05b. Removal awaits cookie/cache cleanup across all known environments, retains the profile on failure, and exposes an inline retryable error.

Synced from GitHub

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

apps/web/src/components/settings/IntegrationsSettings.tsxline 499Outdated · 316224dOpen thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
496+ let name = "New profile";
497+ for (let index = 2; taken.has(name); index += 1) name = `New profile ${index}`;
498+ updateSettings({
499+ browserProfiles: [
MmacroscopeappBoton line 499Aug 17, 2026

🟡 Medium settings/IntegrationsSettings.tsx:499

Clicking Add profile twice before React rerenders creates only one profile instead of two, because both calls build browserProfiles from the same render-captured userProfiles array and the second updateSettings replaces the first patch. Disable or debounce the action while updating, or use an atomic updater based on the latest settings snapshot.

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in 2d101531ae. Add, rename, and post-cleanup removal re-read the current settings snapshot before writing, avoiding stale render snapshots.

apps/web/src/browser/openFileInPreview.tsline 56Outdated · 316224dOpen thread
……@@ -43,7 +45,15 @@ export async function openUrlInPreview<E>(input: {
53+ // applied explicitly or file/link opens would ignore them.
54+ viewport: browserDefaultOpenViewport(),
55+ profileId: browserDefaultOpenProfileId(),
56+ },
CcursorBoton line 56Aug 17, 2026

Sync opens skip settings hydration

Medium Severity

File and terminal-link preview opens newly apply browserDefaultOpenViewport / browserDefaultOpenProfileId via the sync getters. Unlike openPreviewSession, they never await resolveBrowserDefaults(), so a tab opened before client settings hydrate is born on schema Default and never corrected.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 316224d9edd554e1126e946505f7f4fa26472673. Configure here.

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

Reviewed the changed web UI surfaces. The two issues flagged in earlier runs (MenuSubTrigger leading-icon column and the unbounded profile badge in the preview chrome row) are resolved — [&>svg:not(:last-child)] now excludes the chevron, and the badge is capped with truncation on an inner span, which does ellipsize because the span's overflow: hidden zeroes its automatic minimum size.

Two remaining layout notes on the new browser-profiles settings list, both about the row primitives it composes.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 551–551Outdated · 316224dOpen thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
548+ {resolveBrowserProfiles(userProfiles).map((profile) => {
549+ const builtIn = isBuiltInBrowserProfileId(profile.id);
550+ return (
551+ <div key={profile.id} className={ITEM_ROW_INNER_CLASSNAME}>
MmacroscopeappBoton lines 551–551Aug 17, 2026

settings/IntegrationsSettings.tsx:551

ITEM_ROW_INNER_CLASSNAME stacks (flex flex-col gap-3) below sm, and it is written for rows that also carry ITEM_ROW_CLASSNAME's py-3 (see ConnectionsSettings.tsx and CloudEnvironmentConnectList.tsx, which always pair the two). Here the rows are bare inside a space-y-1 list, so on a narrow viewport each profile's Remove button drops 12px below its own name input while the next profile's input is only 4px away — the button visually groups with the wrong profile.

These rows are one input plus one icon button, so a plain single-line row avoids the stacking entirely:

diff
1- <div key={profile.id} className={ITEM_ROW_INNER_CLASSNAME}>
2+ <div key={profile.id} className="flex items-center justify-between gap-2">

This is the only use of ITEM_ROW_INNER_CLASSNAME in the file, so its import can go with it.

No longer relevant as of 3045949155d67b9a31a9426eb832c3d0c24ecf64

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the new browser-profiles list escapes the DesktopOnlyBrowserDefaults dimming on non-desktop clients, so it reads as live content inside the "only available in the desktop app" block.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 547–547Outdated · 316224dOpen thread
……@@ -453,11 +476,205 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
544+ </Button>
545+ }
546+ >
547+ <div className="mt-1 space-y-1">
MmacroscopeappBoton lines 547–547Aug 17, 2026

settings/IntegrationsSettings.tsx:547

On non-Electron clients this row renders inside DesktopOnlyBrowserDefaults, whose unavailability treatment only dims h3/p ([&_h3]:opacity-64 [&_p]:opacity-64) and relies on each control's own disabled opacity for the rest. The built-in rows here are neither: <span className="… text-foreground"> plus Badge render at full contrast, and with no stored profiles they are the only content in the block, so "Default / Incognito" reads as active while every neighbouring row is dimmed and disabled.

Suggest dimming the list when disabled, e.g. className={cn("mt-1 space-y-1", disabled && "opacity-64")} (needs cn added to the ~/lib/utils import), so the profile rows follow the same unavailable treatment as the rest of the block.

No longer relevant as of 5b85b1db9841333a84c08e3dff77ebaa59fbb923

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

One finding on the profile list's unavailable-state dimming. Everything else previously raised (badge cap/truncation, removal confirmation, sub-trigger icon column) looks addressed.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 553–559Outdated · 5b85b1dOpen thread
……@@ -453,11 +476,211 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
556+ return (
557+ <div key={profile.id} className={ITEM_ROW_INNER_CLASSNAME}>
558+ {builtIn ? (
559+ <span className="flex min-w-0 items-center gap-2 text-sm text-foreground">
MmacroscopeappBoton lines 553–559Aug 17, 2026

settings/IntegrationsSettings.tsx:559

The container-level opacity-64 stacks with each control's own disabled treatment, so the user-profile rows end up dimmer than every other row in the block. Input's wrapper carries has-disabled:opacity-64 and Button carries disabled:opacity-64, and both children here also receive disabled, so the rename field and the remove button composite to ~0.41 alpha while the Select, Switch, and NumberField in the neighbouring rows sit at 0.64.

Only the built-in rows lack a self-dimming treatment, so scoping the dim to that span keeps the whole block at one contrast level (the note above can move with it):

suggestion
1 <div className="mt-1 space-y-1">
2 {resolveBrowserProfiles(userProfiles).map((profile) => {
3 const builtIn = isBuiltInBrowserProfileId(profile.id);
4 return (
5 <div key={profile.id} className={ITEM_ROW_INNER_CLASSNAME}>
6 {builtIn ? (
7 <span
8 className={cn(
9 "flex min-w-0 items-center gap-2 text-sm text-foreground",
10 disabled && "opacity-64",
11 )}
12 >

No longer relevant as of a7801b6d775068e26dfe115a25bb11c3a7aa27d5

MmacroscopeappBotreviewed · 2 inline threadsAug 17, 2026
Synced from GitHub

Reviewed the changed web UI (RightPanelTabs, PreviewMoreMenu, PreviewView, PreviewChromeRow, menu.tsx, IntegrationsSettings) against the shared primitives. The sub-trigger icon alignment, the capped profile badge, the confirmation dialog, and the disabled dimming all now line up with their neighbours. Two remaining spots render user-supplied profile names (up to BROWSER_PROFILE_NAME_MAX_LENGTH = 48) inside menu rows without the truncation treatment the rest of the app applies to user-named menu rows.

Posted via Macroscope — UI Consistency

apps/web/src/components/RightPanelTabs.tsxlines 735–744Outdated · a7801b6Open thread
……@@ -695,14 +714,45 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
741+ {profile.name}
742+ </MenuItem>
743+ ))}
744+ </MenuSubPopup>
MmacroscopeappBoton lines 735–744Aug 17, 2026

components/RightPanelTabs.tsx:744

Profile names are user-supplied up to BROWSER_PROFILE_NAME_MAX_LENGTH (48), and these rows render the name bare in an unbounded popup. Menu rows carrying user text elsewhere cap and ellipsize instead (ProjectScriptsControl uses <span className="truncate"> inside MenuItem, PullRequestDetailPanel and ComposerStashMenu use min-w-0 flex-1 truncate), so a long name here widens the submenu to fit-content and wraps the row rather than ellipsizing like every other name-bearing menu.

Suggest capping the sub-popup and truncating the label:

suggestion
1 <MenuSubPopup className="min-w-40 max-w-56">
2 {browserProfiles.map((profile) => (
3 <MenuItem
4 key={profile.id}
5 onClick={() => props.onAddBrowserInProfile(profile.id)}
6 >
7 <span className="min-w-0 truncate">{profile.name}</span>
8 </MenuItem>
9 ))}
10 </MenuSubPopup>

No longer relevant as of 7aa90aad3cfd7b8c54854e2f404989a2ce390355

apps/web/src/components/preview/PreviewMoreMenu.tsxlines 204–215Outdated · a7801b6Open thread
……@@ -177,12 +193,27 @@ export function PreviewMoreMenu({
212+ onClick={() => void bridge.clearCache(environmentId, profileId).catch(() => undefined)}
213+ >
214+ {profileName ? `Clear cache (${profileName})` : "Clear cache"}
215+ </MenuItem>
MmacroscopeappBoton lines 204–215Aug 17, 2026

preview/PreviewMoreMenu.tsx:215

The parenthetical repeats the profile name that the MenuGroupLabel directly above already states, and it is the width driver for this popup: with a 48-character name the item label reaches ~65 characters, so the popup grows well past its min-w-56 and the rows wrap (menu rows here have no truncating span, unlike the name-bearing rows in ProjectScriptsControl/ComposerStashMenu).

Suggest letting the group heading carry the profile and keeping the action labels fixed-length:

suggestion
1 <MenuItem
2 onClick={() =>
3 void bridge.clearCookies(environmentId, profileId).catch(() => undefined)
4 }
5 >
6 Clear cookies
7 </MenuItem>
8 <MenuItem
9 onClick={() => void bridge.clearCache(environmentId, profileId).catch(() => undefined)}
10 >
11 Clear cache
12 </MenuItem>

No longer relevant as of 3045949155d67b9a31a9426eb832c3d0c24ecf64

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

One new finding: the profile heading in PreviewMoreMenu can't ellipsize, so a long profile name still widens the popup. Previously reported findings that remain open (ITEM_ROW_INNER_CLASSNAME narrow-viewport stacking and the missing bottom spacing on the profile list in IntegrationsSettings.tsx) are not repeated here.

Posted via Macroscope — UI Consistency

apps/web/src/components/preview/PreviewMoreMenu.tsxlines 209–211Outdated · 3045949Open thread
……@@ -177,12 +193,36 @@ export function PreviewMoreMenu({
208+ {profileName ? (
209+ <MenuGroupLabel>
210+ <span className="min-w-0 truncate">Profile: {profileName}</span>
211+ </MenuGroupLabel>
MmacroscopeappBoton lines 209–211Aug 17, 2026

preview/PreviewMoreMenu.tsx:211

truncate on an inner <span> has no effect here: MenuGroupLabel renders a block div, so the span stays an inline box, and overflow/text-overflow don't apply to inline non-replaced boxes. Only white-space: nowrap takes effect, so a 48-character profile name can't wrap and expands the popup's max-content width well past min-w-56 — the exact widening the comment above says this avoids.

Moving the truncation onto the label itself (a block box) with a max width both caps the popup and yields a real ellipsis, matching the max-w-56 + truncating-item pattern used for the profile submenu in RightPanelTabs:

suggestion
1 <MenuGroupLabel className="max-w-64 truncate">Profile: {profileName}</MenuGroupLabel>

No longer relevant as of 7aa90aad3cfd7b8c54854e2f404989a2ce390355

apps/web/src/components/settings/IntegrationsSettings.tsxlines 520–523Outdated · 438fc61Open thread
……@@ -453,11 +476,227 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
520+ if (environmentId) {
521+ void previewBridge?.clearCookies(environmentId, id).catch(() => undefined);
522+ void previewBridge?.clearCache(environmentId, id).catch(() => undefined);
523+ }
MmacroscopeappBoton lines 520–523Aug 28, 2026

settings/IntegrationsSettings.tsx:523

The partition scope is ${environmentId}::${profileId}, but this settings panel has no environment scope of its own and falls back to the primary environment, so removal only clears the profile's partition in that one environment. A tab opened under this profile from a thread in another environment (PreviewMoreMenu passes threadRef.environmentId) keeps its cookies and cache on disk, and when environmentId is still null nothing is cleared at all — while the confirmation dialog states "Its cookies, logins, and cache are deleted with it."

Suggest either clearing across the client's known environments (useEnvironments()) so the promise holds, or softening the dialog copy to match what is actually deleted rather than guessing a single environment.

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in c8840bc05b. Profile removal clears the partition in every known environment before removing the global profile record.

apps/web/src/components/RightPanelTabs.tsxlines 937–945Outdated · 438fc61Open thread
……@@ -909,6 +926,41 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
942+ >
943+ <Icon />
944+ {action.label}
945+ </MenuSubTrigger>
MmacroscopeappBoton lines 937–945Aug 28, 2026

components/RightPanelTabs.tsx:945

Routing Browser through MenuSubTrigger drops the shortcut affordance that SurfaceMenuItem gave it: the row no longer renders <MenuShortcut>B</MenuShortcut> and no longer carries aria-keyshortcuts="B", while Terminal/Files/Diff/Pull request/Agents in the same popup still show theirs (and the launcher empty state still shows a Kbd "B"). The shortcut itself still fires via handleAddSurfaceMenuKeyDown, so the hint disappeared while the behaviour stayed — the one row users can't discover is the one that now has a submenu.

Wrapping the label in a flex-1 span keeps the kbd flush right ahead of the chevron (whose own ms-auto would otherwise split the free space with the kbd's):

suggestion
1 <MenuSubTrigger
2 aria-keyshortcuts={action.shortcut}
3 onClick={() => {
4 setAddSurfaceMenuOpen(false);
5 action.onClick();
6 }}
7 >
8 <Icon />
9 <span className="flex-1">{action.label}</span>
10 <MenuShortcut className="ms-0">{action.shortcut}</MenuShortcut>
11 </MenuSubTrigger>

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in f5bb21e27d6 and ee989a8c07. Browser displays its B shortcut and aria-keyshortcuts; the shortcut and chevron share a stable right-aligned group.

apps/desktop/src/ipc/methods/preview.tslines 235–235Outdated · 05ae35cOpen thread
……@@ -196,33 +199,77 @@ export const closePictureInPicture = tabMethod(
232+ return { scope: environmentId, persistent: true };
233+ }
234+ return {
235+ scope: `${environmentId}::${profileId}`,
MmacroscopeappBoton lines 235–235Aug 28, 2026

🟠 High methods/preview.ts:235

Distinct environment/profile pairs can resolve to the same Electron partition, causing tabs to share cookies and storage across supposedly isolated contexts. Because EnvironmentId and BrowserProfileId permit :, the ::-joined scope is ambiguous; encode each identifier before joining them.

Also found in 1 other location(s)

packages/contracts/src/browserProfile.ts:29

BrowserProfileId allows : (including ::), while the desktop partition scope is constructed as ${environmentId}::${profileId}; EnvironmentId is also only a trimmed nonempty string. Consequently distinct pairs such as environment a/profile b::c and environment a::b/profile c generate the same Chromium partition. Tabs from those environments then share cookies and storage, defeating the profile/environment isolation this feature introduces.

✅ Resolved in 2d101531aec510d521de30e8772dd011b05b7232

Fixed in 4271e1541ac and 082c63134a. Scope components are encoded independently, and nondefault profiles use a disjoint partition namespace. Tests cover delimiter-pair collisions, default environment a::b versus environment a/profile b, and unchanged legacy Default partition names.

MmacroscopeappBotreviewed · 1 inline threadAug 28, 2026
Synced from GitHub

One finding: the new profile badge in the preview chrome uses a native title tooltip instead of the shared Tooltip primitive.

Posted via Macroscope — UI Consistency

apps/web/src/components/preview/PreviewView.tsxlines 708–708Outdated · 05ae35cOpen thread
……@@ -683,9 +693,29 @@ export function PreviewView({
705+ // `inline-flex` with `whitespace-nowrap` — `text-overflow` never
706+ // reaches a bare text node inside it, so the name would be cut off
707+ // at both ends with no ellipsis.
708+ <Badge variant="outline" className="max-w-28 shrink-0" title={activeProfile.name}>
MmacroscopeappBoton lines 708–708Aug 28, 2026

preview/PreviewView.tsx:708

The full profile name is surfaced with a native title, which the repo has moved away from (t3code/no-native-title-tooltip, "Use Tooltip + TooltipTrigger + TooltipPopup from components/ui/tooltip"). It only escapes the rule because Badge is a capitalized component — Badge renders a <span> and forwards the prop straight through, so the DOM ends up with exactly the banned native tooltip. Every other hover label in this same chrome row (PreviewChromeRow's nav buttons) and every other truncated label elsewhere (ServerUpdateAction, ProviderModelPicker, DiffPanel) uses the styled Tooltip, which is themed and shows on keyboard focus; the native one is neither.

Suggest wrapping the badge in Tooltip (also needs import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";):

diff
1- <Badge variant="outline" className="max-w-28 shrink-0" title={activeProfile.name}>
2- <span className="truncate">{activeProfile.name}</span>
3- </Badge>
4+ <Tooltip>
5+ <TooltipTrigger render={<Badge variant="outline" className="max-w-28 shrink-0" />}>
6+ <span className="truncate">{activeProfile.name}</span>
7+ </TooltipTrigger>
8+ <TooltipPopup side="bottom">{activeProfile.name}</TooltipPopup>
9+ </Tooltip>

No longer relevant as of 2d101531aec510d521de30e8772dd011b05b7232

Fixed in ee363dafcd3. The profile badge uses the shared Tooltip components and preserves truncation.

apps/web/src/components/settings/IntegrationsSettings.tsxline 528Outdated · 05ae35cOpen thread
……@@ -453,11 +476,227 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
525+ browserProfiles: userProfiles.filter((profile) => profile.id !== id),
526+ // Reassign the default rather than leaving it pointing at nothing.
527+ ...(defaultProfileId === id ? { browserDefaultProfileId: DEFAULT_BROWSER_PROFILE_ID } : {}),
528+ });
CcursorBoton line 528Aug 28, 2026

Deleted tabs do not switch profiles

Medium Severity

Removing a profile only wipes that partition and drops it from settings. Open tabs keep the deleted profileId and stay attached to the now-empty partition. The confirmation dialog still says those tabs move to the default profile, which does not happen.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 05ae35c3b79430e2247eaba8101036fb860a29aa. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxlines 677–677Outdated · 2d10153Open thread
……@@ -453,11 +499,252 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
674+ ) : null}
675+ </AlertDialogHeader>
676+ <AlertDialogFooter>
677+ <AlertDialogClose render={<Button variant="outline" />}>Cancel</AlertDialogClose>
MmacroscopeappBoton lines 677–677Aug 29, 2026

While removal is in flight the onOpenChange guard above swallows every close request, so this Cancel button stays enabled, focusable, and hoverable but does nothing (same for Esc). The other in-flight confirmations in settings disable the close explicitly — ConnectionsSettings does this for the Tailscale disable, WSL backend, and server-exposure dialogs — so the pending state reads the same everywhere.

suggestion
1 <AlertDialogClose
2 disabled={profileRemovalInFlight}
3 render={<Button variant="outline" disabled={profileRemovalInFlight} />}
4 >
5 Cancel
6 </AlertDialogClose>

Posted via Macroscope — UI Consistency

apps/web/src/components/RightPanelTabs.tsxlines 944–947Outdated · 2d10153Open thread
MmacroscopeappBoton lines 944–947Aug 29, 2026

Putting MenuShortcut inside a MenuSubTrigger gives this row two auto inline-start margins: the kbd (ms-auto) and the primitive's chevron (-me-0.5 ms-auto). Flexbox distributes the leftover space equally between auto margins, so "B" lands roughly halfway between the label and the chevron and its position shifts with the popup width — while Terminal/Files/Diff/Pull request/Agents in the same popup render their shortcut flush against the right padding edge (single ms-auto in MenuItem).

Neutralizing the chevron's auto margin lets the shortcut absorb the free space, so the shortcut/chevron pair sits flush right and the shortcut column stays stable:

suggestion
1 className="[&>svg:last-child]:ms-0"
2 aria-keyshortcuts={action.shortcut}

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxline 639Outdated · 2d10153Open thread
……@@ -453,11 +499,252 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
636+ <Button
637+ size="icon-sm"
638+ variant="ghost-muted"
639+ disabled={disabled || !environmentsReady}
CcursorBoton line 639Aug 29, 2026

Empty catalog blocks profile removal

Low Severity

The remove control only waits for environmentsReady, but clearBrowserProfileData rejects an empty environment list. A ready catalog with no entries still enables Remove, then every confirmation fails and the profile stays.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 2d101531aec510d521de30e8772dd011b05b7232. Configure here.

Fixed in fccf30726a8. Removal availability includes a desktop bridge and a ready, non-empty environment catalog. Both the row action and an already-open confirmation reflect unavailable cleanup, with an explanation; the handler retains its guard instead of deleting metadata without clearing data. Focused availability tests pass.

apps/desktop/src/ipc/methods/preview.tslines 241–241Outdated · e5a0a33Open thread
……@@ -196,33 +199,84 @@ export const closePictureInPicture = tabMethod(
238+ // Encode each component independently so delimiters inside either id cannot
239+ // make two distinct pairs hash to the same Electron partition.
240+ return {
241+ scope: `${encodeURIComponent(environmentId)}::${encodeURIComponent(profileId)}`,
MmacroscopeappBoton lines 241–241Aug 29, 2026

🟠 High methods/preview.ts:241

resolvePartitionScope throws URIError for a profile or environment ID containing a lone surrogate, so getPreviewConfig and profile-scoped data clearing fail even though the IPC schema accepts the ID. Normalize the strings to well-formed Unicode before calling encodeURIComponent so these requests still derive a partition.

diff
1- scope: `${encodeURIComponent(environmentId)}::${encodeURIComponent(profileId)}`,
2+ scope: `${encodeURIComponent(environmentId.toWellFormed())}::${encodeURIComponent(profileId.toWellFormed())}`,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/ipc/methods/preview.ts around line 241:
2
3`resolvePartitionScope` throws `URIError` for a profile or environment ID containing a lone surrogate, so `getPreviewConfig` and profile-scoped data clearing fail even though the IPC schema accepts the ID. Normalize the strings to well-formed Unicode before calling `encodeURIComponent` so these requests still derive a partition.
MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One finding: the remove-profile tooltip that explains why removal is unavailable can never be shown, because its trigger is the disabled button itself.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 651–663Outdated · 91f9334Open thread
……@@ -453,11 +507,277 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
660+ <Trash2Icon />
661+ </Button>
662+ }
663+ />
MmacroscopeappBoton lines 651–663Aug 29, 2026

The tooltip's !removalAvailable branch ("Connect to an environment to remove this profile") is unreachable: buttonVariants carries disabled:pointer-events-none, and a natively disabled button also can't be hovered or focused — so exactly when the explanation is needed, the trigger emits no events. Settings already handles this by making a wrapper the trigger (ConnectionsSettings.tsx's CloudLinkSwitch renders <TooltipTrigger render={<span className="inline-flex">{control}</span>} /> for its disabled-reason tooltip).

Suggest wrapping the button the same way so the reason stays reachable:

suggestion
1 <TooltipTrigger
2 render={
3 <span className="inline-flex">
4 <Button
5 size="icon-sm"
6 variant="ghost-muted"
7 disabled={disabled || !removalAvailable}
8 aria-label={`Remove ${profile.name}`}
9 onClick={() => setProfilePendingRemoval(profile)}
10 >
11 <Trash2Icon />
12 </Button>
13 </span>
14 }
15 />

Posted via Macroscope — UI Consistency

Fixed in 11dc366428c. An enabled, focusable wrapper now owns the unavailable-reason tooltip around the disabled removal button, so mouse and keyboard users can reach the explanation.

apps/web/src/components/settings/IntegrationsSettings.tsxline 531Outdated · 11dc366Open thread
……@@ -453,11 +507,279 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
528+ );
529+
530+ const addProfile = () => {
531+ const currentProfiles = getClientSettings().browserProfiles;
MmacroscopeappBoton line 531Aug 29, 2026

🟡 Medium settings/IntegrationsSettings.tsx:531

Clicking Add profile before client settings hydration completes writes DEFAULT_CLIENT_SETTINGS plus the new profile, overwriting persisted profiles and other client-local preferences. addProfile reads getClientSettings() during the hydration race; disable the action until hydration finishes or apply the update only after using the hydrated snapshot.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/IntegrationsSettings.tsx around line 531:
2
3Clicking `Add profile` before client settings hydration completes writes `DEFAULT_CLIENT_SETTINGS` plus the new profile, overwriting persisted profiles and other client-local preferences. `addProfile` reads `getClientSettings()` during the hydration race; disable the action until hydration finishes or apply the update only after using the hydrated snapshot.

Fixed at #7254 fe5480b817 and preserved through the table rewrite in #7255 d95c81c935. Profile creation, rename, removal, default selection/reset, and import setup now use the existing client-settings hydration gate, including handler guards. The upper import path reads latest hydrated settings for registration after await. Lower web typecheck and 50 focused tests pass.

apps/web/src/components/settings/IntegrationsSettings.tsxlines 825–831Outdated · 1c013abOpen thread
……@@ -501,11 +556,292 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
828+ {profile.name}
829+ </SelectItem>
830+ ))}
831+ </SelectPopup>
MmacroscopeappBoton lines 825–831Sep 2, 2026

These rows render user-supplied profile names (up to BROWSER_PROFILE_NAME_MAX_LENGTH, 48) bare in an uncapped popup, so the popup takes its width from the longest name — the same profile list in RightPanelTabs' submenu now caps and ellipsizes (min-w-40 max-w-56 + <span className="min-w-0 truncate">), and KeybindingsSettings does the same for its long option values (w-fit min-w-44 + truncating span). The trigger already truncates via SelectValue, so today the selected name ellipsizes in the row while the same name blows the popup out.

Suggest capping the list and truncating the item text so both profile pickers behave the same:

suggestion
1 <SelectPopup align="end" alignItemWithTrigger={false} className="max-w-64">
2 {profiles.map((profile) => (
3 <SelectItem hideIndicator key={profile.id} value={profile.id}>
4 <span className="min-w-0 truncate">{profile.name}</span>
5 </SelectItem>
6 ))}
7 </SelectPopup>

Posted via Macroscope — UI Consistency

CcursorBotreviewed · 2 inline threadsSep 2, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit cd654b4cf8597af890c8311852acda84bacde5e5. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxline 833Outdated · cd654b4Open thread
……@@ -501,11 +556,297 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
830+ <SelectPopup align="end" alignItemWithTrigger={false} className="max-w-64">
831+ {profiles.map((profile) => (
832+ <SelectItem hideIndicator key={profile.id} value={profile.id}>
833+ <span className="min-w-0 truncate">{profile.name}</span>
CcursorBoton line 833Sep 2, 2026

Profile names do not truncate

Medium Severity

The default-profile picker caps the list at max-w-64 and wraps names in a truncate span, but SelectItem renders children inside ItemText, so that span is an inline descendant rather than a flex item. text-overflow never applies, and whitespace-nowrap from truncate stops wrapping. Long profile names (up to 48 characters) overflow or clip without an ellipsis after the width cap.

Reviewed by Cursor Bugbot for commit cd654b4cf8597af890c8311852acda84bacde5e5. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxline 830Outdated · cd654b4Open thread
……@@ -501,11 +556,297 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
827+ user-supplied and run to 48 characters, which would otherwise
828+ widen the popup to fit the longest one.
829+ */}
830+ <SelectPopup align="end" alignItemWithTrigger={false} className="max-w-64">
CcursorBoton line 830Sep 2, 2026

Width cap misses the popup shell

Low Severity

max-w-64 is passed as SelectPopup className, which styles the list, not the glass wrapper. matchTriggerWidth still sets min-w-(--anchor-width) on that wrapper. When the trigger is w-full (settings rows stack below sm), the shell stays as wide as the trigger while the list is capped at 16rem, leaving empty popup chrome.

Reviewed by Cursor Bugbot for commit cd654b4cf8597af890c8311852acda84bacde5e5. Configure here.

Fixed in 1cb5acff41. The cap now goes on the glass shell via popupClassName as well as the list, and matchTriggerWidth={false} stops the shell tracking a full-width trigger — same shape as the keybinding pickers.

apps/web/src/components/settings/IntegrationsSettings.tsxlines 834–840Outdated · 1cb5acfOpen thread
……@@ -501,11 +556,307 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
837+ matchTriggerWidth={false}
838+ popupClassName="max-w-64"
839+ className="max-w-64"
840+ >
MmacroscopeappBoton lines 834–840Sep 2, 2026

matchTriggerWidth={false} drops the only min-width the popup shell has: dropdown-glass sets none, and with alignItemWithTrigger={false} the in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] on SelectItem never applies either. So this popup is purely content-sized — with just "Default" in the list it renders ~80px wide under a sm:w-44 trigger, while every other settings select (BrowserViewportSetting, zoom, appearance, frame rate, SettingsPanels, ConnectionsSettings) keeps the default matchTriggerWidth, so its popup is never narrower than the control it opens from.

The empty-chrome case the comment above describes comes from the list not filling the shell, not from matching the trigger: with w-full on the list, the shell stays ≥ trigger width (and ≤ max-w-64 wherever the trigger is narrower than 16rem) and the items truncate against whatever width the shell ends up with.

suggestion
1 <SelectPopup
2 align="end"
3 alignItemWithTrigger={false}
4 popupClassName="max-w-64"
5 className="w-full"
6 >

The explanatory comment just above would need its trigger-width sentence updated to match.

Posted via Macroscope — UI Consistency

Fixed in 06e1411676. Restored the trigger-width match and put w-full on the list, so the shell is never narrower than the trigger (and never wider than 16rem); comment updated.

apps/desktop/src/preview/BrowserSession.tsline 153Outdated · 56bf2ceOpen thread
……@@ -99,19 +109,47 @@ export class BrowserSession extends Context.Service<
150+ persistent = true,
151+ namespace?: BrowserSessionPartitionNamespace,
152+ ) {
115153 const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe(
MmacroscopeappBoton line 153Sep 2, 2026

🟠 High preview/BrowserSession.ts:153

getPartition assigns the same partition to distinct scopes such as "p\ud800" and "p�", so those profiles share cookies and site storage. TextEncoder().encode(scope) replaces lone surrogates with U+FFFD before hashing; use an encoding that preserves UTF-16 code units when deriving the digest.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserSession.ts around line 153:
2
3`getPartition` assigns the same partition to distinct scopes such as `"p\ud800"` and `"p�"`, so those profiles share cookies and site storage. `TextEncoder().encode(scope)` replaces lone surrogates with U+FFFD before hashing; use an encoding that preserves UTF-16 code units when deriving the digest.

Fixed in 4414469f35. Lone UTF-16 surrogates are escaped to \\uXXXX before the digest (with a literal backslash doubled so the escape can't be forged), so "p\\ud800" and "p\\ufffd" hash to different partitions. Every well-formed scope passes through byte-for-byte unchanged, so existing partitions and the logins in them stay put — the test pins both the separation and the historical scope-a hash.

Added 35 commits

  • fix(web): keep the profile chrome from crowding its neighbours040e929
  • feat(web): show which profile a browser tab is running in0a9e51f
  • fix(web): give menu submenu triggers the same cursor as menu items2fbfa0e
  • feat(desktop): partition preview browsers by profile3503088
  • fix(web): give the profile badge a real ellipsisa1568c0
  • feat(web): open browser tabs in a chosen profileb5435d2
  • feat(web): manage browser profiles from Settings → Integrationsbd3f910
  • fix(web): repair the Browser surface card and collapse the profile menu rowbd58fab
  • fix(web): clear the profile the menu names, and only that profilefa3e6b5
  • fix(web): make each profile its own row, and truncate the menu heading1baec7e
  • fix(web): clear profiles from every environment2a74df7
  • fix(web): update browser profiles from current settings3239ee8
  • fix(web): disable cancel during profile removal3c8ad20
  • fix(desktop): isolate browser profile partitions3cd048f
  • fix(web): theme profile badge tooltip52548a2
  • fix(web): dim only the row content that has no disabled state of its own56b0482
  • style(web): format hosted browser webview59cc0ff
  • fix(web): dim the profile list with the rest of the desktop-only block5cd1e0c
  • fix(desktop): encode browser profile scopes safely69b6cfb
  • fix(web): keep profile names from stretching the menus904f7bd
  • fix(web): open browser profiles on touch944d63d
  • fix(web): disable unavailable profile removal989e2ed
  • fix(web): show browser panel shortcutac3c431
  • fix(web): clear the partition a legacy tab actually runs inbccf188
  • fix(web): hydrate preview open defaultsccff475
  • fix(web): group browser submenu affordancescf5f060
  • fix(web): identify tabs from removed profilesd642f4b
  • fix(desktop): avoid browser partition scope collisionse190ae5
  • fix(web): keep profiles when cleanup failsefd9522
  • fix(desktop): keep lone-surrogate scopes in distinct browser partitions1a5aa85
  • fix(web): actually truncate long names in the default-profile picker32e789f
  • fix(web): keep long profile names from stretching the default-profile picker81742f8
  • fix(web): expose unavailable profile removal reasona6c5b56
  • fix(web): wait for settings before profile writesb1c6b4a
  • fix(web): keep the default-profile popup at least as wide as its triggerfe5a26f
……@@ -911,6 +934,55 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
911934 >
912935 {addSurfaceActions.map((action) => {
913936 const Icon = action.icon;
937+ // Browser collapses into one row: clicking the trigger opens
MmacroscopeappBoton line 937Sep 2, 2026

🟡 Medium components/RightPanelTabs.tsx:937

When the panel has no surfaces, users cannot open Incognito or a custom browser profile directly; RightPanelEmptyState only invokes onAddBrowser, which always creates the default-profile tab. The profile submenu is mounted only when props.surfaces.length > 0, so users must first create an unwanted default tab to reach it. Make the profile choices available from the empty-panel launcher as well.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/RightPanelTabs.tsx around line 937:
2
3When the panel has no surfaces, users cannot open Incognito or a custom browser profile directly; `RightPanelEmptyState` only invokes `onAddBrowser`, which always creates the default-profile tab. The profile submenu is mounted only when `props.surfaces.length > 0`, so users must first create an unwanted default tab to reach it. Make the profile choices available from the empty-panel launcher as well.