Stacked on #7272 (shared-sqlite-client).

Imports cookies from Helium on macOS into a T3 Code browser profile. Saved passwords and browsing history remain out of scope.

The app requests the browser key through the in-process Keychain API, so consent belongs to T3 Code. Denial stops the import; there is no permission bypass. Source databases are opened read-only and snapshotted consistently with SQLite before extraction.

The wizard chooses a source and destination, prevents profile edits during an import, and reports imported/skipped counts and affected sites. Plaintext and encrypted cookie records are handled separately; domain-bound records are validated. Partitioned cookies are skipped when their partition semantics cannot be preserved by Electron.

Validation includes synthetic decryption records, plaintext and legacy records, domain-binding failures, partitioned-cookie rejection, WAL snapshot consistency, interrupted writes, and wizard state transitions, plus scoped typechecks/lint. Earlier Helium imports were verified in the desktop app; this audit did not request another live Keychain read.

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

Note

Add browser cookie import into profiles via BrowserImport service

  • Adds BrowserImport service, contracts, and IPC methods to list browser sources and import cookies into a target partition.
  • Implements macOS-only Chromium cookie extraction via keychain lookup, SQLite database snapshots, and AES-128-CBC decryption.
  • Introduces BrowserImportWizard and redesigns browser profile settings to support profile management and cookie imports.
  • Fixes client-settings persistence by adding a serialized queue in useSettings to prevent race conditions and ensure correct publish order.
  • Risk: persistClientSettingsUpdate in useSettings.ts now rejects on persistence failure and recomputes concurrent writes, changing publish timing.

Macroscope summarized c5dbc20.


Note

Medium Risk Touches macOS Keychain reads and cross-browser cookie data with new IPC surface area, plus a behavior change to how client settings are persisted and published.

Overview Adds cookie import from installed Chromium-family browsers (initially Helium on macOS) into T3 Code preview browser profiles, wired end-to-end from contracts through desktop main, preload, and Integrations settings.

The desktop app introduces a BrowserImport Effect service that lists sources, blocks imports while the source browser holds SingletonLock, validates profile directories against listed profiles (path traversal hardening), snapshots the SQLite cookie DB via VACUUM INTO, reads the encryption key through @napi-rs/keyring (in-process Keychain, no CLI bypass), decrypts Chromium cookies, and writes them into the same Electron partition as the target profile via new IPC channels listBrowserImportSources / importBrowserCookies.

Integrations → Browser profiles is reworked: an Add profile menu offers blank profiles or Import from detected browsers, a BrowserImportWizard handles quit-browser / configure / success / blocked flows, per-profile actions include Set as default, Clear cookies and cache, and remove; the standalone default-profile row is folded into the list. New profiles are persisted only after a successful import (with partition cleanup on save failures or profile cap).

persistClientSettingsUpdate plus a serialized persistence queue ensures awaited settings writes (e.g. registering an imported profile) don’t race optimistic patches. Desktop artifact builds stage keyring native binaries like other nested native deps.

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

MmacroscopeappBotreviewed · 4 inline threadsAug 16, 2026
Synced from GitHub

Reviewed the new BrowserImport service, its Chromium cookie helper, and the IPC/layer wiring against the Effect service conventions.

Service shape, module layout (Context.Service tag + inline interface, make, layer), namespace imports, and layer composition in main.ts all look correct. The findings below are about the error model: the new failure type is unstructured (reason: Schema.String) and every construction discards the underlying cause, including one that erases a structured BrowserSession error.

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tslines 50–58Outdated · 757f025Open thread
……@@ -0,0 +1,237 @@
55+ this.name = "ChromiumCookieReadError";
56+ this.failure = failure;
57+ }
58+}
MmacroscopeappBoton lines 50–58Aug 16, 2026

BrowserImport/ChromiumCookies.ts:58

This failure is translated straight into the BrowserImport service error channel, but it is a plain Error (the only one in apps/desktop/src) whose only data is a nested failure object, and readMacKeychainPassword throws it without retaining the keychain error as cause. Consider declaring it with Schema.TaggedErrorClass and flat structural attributes, e.g. reason: Schema.Literals([...]) plus cause: Schema.optional(Schema.Defect()), with message derived from reason, and pass the caught keychain error through as cause.

No longer relevant as of 58530950a4e4ec0a1743ac0ee2826ceba41af020

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 127–131Outdated · 757f025Open thread
……@@ -0,0 +1,177 @@
128+ new BrowserImportFailedError({
129+ sourceId: definition.id,
130+ reason: cause instanceof ChromiumCookieReadError ? cause.failure.reason : "readFailed",
131+ }),
MmacroscopeappBoton lines 127–131Aug 16, 2026

BrowserImport/BrowserImport.ts:131

The ChromiumCookieReadError (and any unexpected defect) is reduced to a reason string and the original error is dropped, so the stack and keychain details are unrecoverable. Suggest keeping it as cause alongside the structural fields, as BrowserSession does.

suggestion
1 catch: (cause) =>
2 new BrowserImportFailedError({
3 sourceId: definition.id,
4 reason: cause instanceof ChromiumCookieReadError ? cause.failure.reason : "readFailed",
5 cause,
6 }),

No longer relevant as of 58530950a4e4ec0a1743ac0ee2826ceba41af020

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 134–140Outdated · 757f025Open thread
……@@ -0,0 +1,177 @@
137+ Effect.mapError(
138+ () => new BrowserImportFailedError({ sourceId: definition.id, reason: "readFailed" }),
139+ ),
140+ );
MmacroscopeappBoton lines 134–140Aug 16, 2026

BrowserImport/BrowserImport.ts:140

This discards a structured domain error (BrowserSessionPartitionDerivationError / BrowserSessionCreationError) and relabels it as "readFailed", which reports a cookie-read problem for a partition/session failure. Suggest preserving the upstream error as cause and using a distinct reason.

suggestion
1 const session = yield* browserSession.getSession(input.scope, input.persistent).pipe(
2 Effect.mapError(
3 (cause) =>
4 new BrowserImportFailedError({
5 sourceId: definition.id,
6 reason: "sessionUnavailable",
7 cause,
8 }),
9 ),
10 );

No longer relevant as of 58530950a4e4ec0a1743ac0ee2826ceba41af020

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 35–45Outdated · 757f025Open thread
……@@ -0,0 +1,177 @@
42+ override get message(): string {
43+ return `Importing cookies from ${this.sourceId} failed: ${this.reason}.`;
44+ }
45+}
MmacroscopeappBoton lines 35–45Aug 16, 2026

BrowserImport/BrowserImport.ts:45

reason: Schema.String is an unstructured attribute, and the error carries no cause, so the real failure (keychain error, session creation error, Electron rejection) is lost. Consider typing reason as a literal union and adding an optional cause; the renderer currently recovers the reason by substring-matching this message (IntegrationsSettings.tsx runImport), which is exactly the fragility a typed discriminator avoids.

diff
1 {
2- sourceId: Schema.String,
3- reason: Schema.String,
4+ sourceId: Schema.String,
5+ reason: Schema.Literals([
6+ "unknownSource",
7+ "notInstalled",
8+ "needsKeychainApproval",
9+ "keychainItemMissing",
10+ "browserRunning",
11+ "unsupportedPlatform",
12+ "readFailed",
13+ "sessionUnavailable",
14+ ]),
15+ cause: Schema.optional(Schema.Defect()),
16 },

The "unknown source" construction below would become "unknownSource".

No longer relevant as of 58530950a4e4ec0a1743ac0ee2826ceba41af020

apps/desktop/src/preview/BrowserImport/BrowserImport.tsline 122Outdated · 757f025Open thread
……@@ -0,0 +1,177 @@
119+ const cookies: ReadonlyArray<ChromiumCookie> = yield* Effect.tryPromise({
120+ try: () =>
121+ readChromiumCookies({
122+ cookieDatabasePath: cookieDatabasePath(definition, input.input.sourceProfileDirectory),
MmacroscopeappBoton line 122Aug 16, 2026

🟠 High BrowserImport/BrowserImport.ts:122

importCookies passes the IPC-controlled sourceProfileDirectory directly to cookieDatabasePath, so .. segments let a forged request escape the browser user-data directory and read/import cookies from another reachable Cookies database. Validate the directory against listSourceProfiles(definition) and enforce resolved-path containment before calling readChromiumCookies.

Also found in 1 other location(s)

apps/desktop/src/ipc/methods/preview.ts:295

importBrowserCookies forwards the renderer-controlled sourceProfileDirectory without checking it against the profiles returned for the selected source. The existing callee builds the database path with NodePath.join(userDataDirectory, sourceProfileDirectory, &#34;Cookies&#34;), so a value containing ../ can escape the browser data directory and make the privileged main process open another SQLite cookie database. A compromised renderer can therefore make this new IPC endpoint read/import cookies from unintended filesystem locations; validate that the directory is one of listSourceProfiles(definition) (and remains beneath userDataDirectory) before calling browserImport.importCookies.

No longer relevant as of 58530950a4e4ec0a1743ac0ee2826ceba41af020

Synced from GitHub

UI Consistency

One finding in the changed browser-profiles UI (apps/web/src/components/settings/IntegrationsSettings.tsx).

The menu/primitive usage itself looks consistent with the shared system: Menu/MenuPopup/MenuItem/MenuSub from components/ui/menu, MenuTrigger render={<Button …/>} matching the existing pattern in PreviewMoreMenu/ProjectScriptsControl, existing size="icon-sm" / variant="ghost-muted" Button variants, min-w-* on popups honouring MenuPopup's width-defaulting contract, and the bordered list container matching the convention already used in ConnectionsSettings. aria-labels are preserved on both the rename input and the new row trigger.

The issue is a state-display regression introduced by removing the default-profile Select while filtering Incognito out of the new list: a stored default of incognito (which the removed Select allowed a user to pick) now leaves the section with no "Default" badge on any row.

Posted via Macroscope — UI Consistency

apps/desktop/src/preview/BrowserImport/BrowserImport.tsline 191Outdated · 5853095Open thread
……@@ -0,0 +1,207 @@
188+ ...(cookie.expirationDate === undefined
189+ ? {}
190+ : { expirationDate: cookie.expirationDate }),
191+ }),
CcursorBoton line 191Aug 16, 2026

Host-only cookies widened on import

High Severity

Imported cookies always pass domain into session.cookies.set. Electron only creates a host-only cookie when domain is omitted; any provided domain is normalized with a leading dot and becomes valid for subdomains. Chromium host-only rows (no leading dot in host_key) therefore get a wider scope after import than they had in the source browser.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 58530950a4e4ec0a1743ac0ee2826ceba41af020. Configure here.

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tsline 220Outdated · 5853095Open thread
……@@ -0,0 +1,258 @@
217+
218+ const snapshotPath = yield* snapshotCookieDatabase(source.cookieDatabasePath).pipe(
219+ Effect.mapError((cause) => new ChromiumCookieReadError({ reason: "readFailed", cause })),
220+ );
CcursorBoton line 220Aug 16, 2026

Snapshot races keychain consent wait

Medium Severity

browserRunning is checked once before the untimed Keychain modal, but the cookie DB snapshot runs only after that modal returns. If the source browser starts while the prompt is open, the import can copy a live WAL database anyway, which is the torn-read case the snapshot was meant to avoid.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 58530950a4e4ec0a1743ac0ee2826ceba41af020. Configure here.

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tsline 162Outdated · 23b22b9Open thread
……@@ -0,0 +1,265 @@
159+
160+ const directory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-cookie-import-" });
161+ const target = path.join(directory, "Cookies");
162+ yield* fileSystem.copyFile(cookiePath, target);
MmacroscopeappBoton line 162Aug 16, 2026

🟡 Medium BrowserImport/ChromiumCookies.ts:162

importCookies can snapshot a database while Chromium is writing it, producing a torn snapshot that fails to open or silently omits recent cookies. The earlier running-browser check can become stale while readMacKeychainPassword waits for approval, and copying Cookies, Cookies-wal, and Cookies-shm separately does not form an atomic SQLite snapshot; recheck immediately before snapshotting and use SQLite's backup mechanism or otherwise prevent writes for the entire snapshot.

Also found in 1 other location(s)

apps/desktop/src/preview/BrowserImport/BrowserImport.ts:147

importCookies checks isSourceRunning before invoking readChromiumCookies, but that invocation can wait indefinitely on the macOS Keychain approval modal. If the user launches Helium while the prompt is open, the import proceeds after approval without rechecking the lock and snapshots a live cookie database, defeating the running-browser guard and potentially importing a torn or incomplete cookie set. Recheck isSourceRunning after keychain approval/immediately before snapshotting, or otherwise make the check and snapshot atomic.

No longer relevant as of 3264ff7a4a32d791ea62c6b4e3338440b6e06a78

Synced from GitHub

Reviewed the new BrowserImport service, its Sources/ChromiumCookies helpers, the IPC method, and the layer wiring against the Effect service conventions. The service module follows the canonical shape (errors → Context.Service with inline interface → make → layer), dependencies are acquired from the environment, and the failure translations now keep a real cause. One remaining gap on error context.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

UI Consistency — 1 finding

The two issues flagged on earlier commits are addressed: loadSources now clears sources before each refresh, and resolvedDefaultId resolves against the rendered rows.

One consistency gap remains: with BrowserDefaultProfileSetting removed and Incognito no longer rendered as a row, the section can badge Default on a profile that is not the effective default (see inline comment).

Minor (not blocking): lines 507–529 now carry three consecutive doc comments for a single component — the "Create, rename, and remove browser profiles" and "Per-profile cookie import" blocks are leftovers from the removed/renamed pieces and could be folded into one.

Posted via Macroscope — UI Consistency

Synced from GitHub

Two findings in the reworked browser-profiles section. Both are about the settings UI now disagreeing with the runtime behaviour it configures, rather than styling.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxline 657Outdated · ea2efcbOpen thread
……@@ -523,69 +591,218 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
654+ runImport(source, sourceProfileDirectory, created.id, created.name);
655+ return;
656+ }
657+ runImport(source, sourceProfileDirectory, target.id, target.name);
CcursorBoton line 657Aug 16, 2026

Import creates profile then no-ops

Medium Severity

importInto with target "new" always calls createProfile before runImport. runImport returns silently when environmentId or previewBridge is missing—no toast. The user gets an empty profile and no import feedback whenever the primary environment is not ready yet.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit ea2efcb52ead0f58de45258f4961a1f7ffcfaa40. Configure here.

🟡 Medium

https://github.com/pingdotgg/t3code/blob/76fc9d21cb04161223f2cb6ce10ee2e146778199/apps/web/src/components/settings/IntegrationsSettings.tsx#L589

Removing a profile can leave its cookies or cache on disk while the profile entry is already gone, with no error or retry path. removeProfile closes the dialog and calls updateSettings without awaiting clearCookies/clearCache, and the per-call .catch(() => undefined) suppresses cleanup failures; await successful cleanup before updating settings, or retain the profile and expose a recoverable error when cleanup fails.

🚀 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 589:
2
3Removing a profile can leave its cookies or cache on disk while the profile entry is already gone, with no error or retry path. `removeProfile` closes the dialog and calls `updateSettings` without awaiting `clearCookies`/`clearCache`, and the per-call `.catch(() => undefined)` suppresses cleanup failures; await successful cleanup before updating settings, or retain the profile and expose a recoverable error when cleanup fails.
apps/web/src/components/settings/IntegrationsSettings.tsxlines 801–846Outdated · 76fc9d2Open thread
MmacroscopeappBoton lines 801–846Aug 17, 2026

🟡 Medium settings/IntegrationsSettings.tsx:801

With no primary environmentId, “Clear cookies and cache” remains enabled but clearProfileData returns immediately, so the menu closes without deleting anything or showing a toast. Disable this action when clearing is unavailable.

✅ Resolved in d35f0a79a17ce3d07afd8e9b24ef0515f120a47b

Synced from GitHub

One finding: the new Clear cookies and cache menu item silently no-ops in the same "no environment yet" window that importInto now reports with a toast. Everything else in the reworked profile list follows the existing settings patterns (bordered px-3 py-2 row list, Menu/MenuItem primitives, Button render triggers, aria-labeled icon triggers), and the resolvedDefaultId / browserDefaults mismatch raised earlier is now aligned.

Posted via Macroscope — UI Consistency

apps/desktop/src/preview/BrowserImport/BrowserImport.tsline 74Outdated · f047f21Open thread
……@@ -0,0 +1,207 @@
71+ if (!(yield* isSourceInstalled(definition, paths))) return "notInstalled";
72+ if (yield* isSourceRunning(definition, paths)) return "browserRunning";
73+ return undefined;
74+});
CcursorBoton line 74Aug 17, 2026

Unsupported sources clutter import menu

Low Severity

unavailableReason returns unsupportedPlatform before install detection, and the UI only hides notInstalled. On Windows/Linux, mac-only Helium therefore always appears as a disabled “unsupported platform” row even though it can never be imported there—the same “nothing to act on” case the notInstalled filter was written to remove.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit f047f210923aa3919adca37ec6a348555a485679. Configure here.

apps/desktop/src/preview/BrowserImport/BrowserImport.tsline 159Outdated · f047f21Open thread
……@@ -0,0 +1,207 @@
156+ (cause) =>
157+ new BrowserImportFailedError({ sourceId: definition.id, reason: cause.reason, cause }),
158+ ),
159+ );
CcursorBoton line 159Aug 17, 2026

Stale running check after Keychain

Medium Severity

browserRunning is checked only before the Keychain read, and that read is deliberately untimed. The source browser can start while the modal is up, then snapshotCookieDatabase runs against a live WAL-backed DB—the case SingletonLock was meant to block—producing torn or incomplete cookie imports without a clear failure.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit f047f210923aa3919adca37ec6a348555a485679. Configure here.

Synced from GitHub

One convention finding: the new contracts module imports the effect barrel instead of the effect/Schema subpath namespace used by the rest of packages/contracts and by the new desktop service modules. Everything else in the new BrowserImport service (tag with inline interface, exported make/layer, environment-acquired dependencies, Schema.TaggedErrorClass failures with structural attributes plus preserved cause) matches the conventions.

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/preview/BrowserImport/Sources.tsline 98Outdated · d35f0a7Open thread
……@@ -0,0 +1,160 @@
95+ Effect.map((entries) =>
96+ entries.map(([directory, info]) => ({ directory, name: info.name?.trim() || directory })),
97+ ),
98+ Effect.orElseSucceed(() => [] as ReadonlyArray<BrowserImportSourceProfile>),

🟠 High BrowserImport/Sources.ts:98

When Local State is unreadable or malformed, listSourceProfiles returns only Default, so isSourceInstalled checks only Default/Cookies and reports an installation with cookies in Profile 1 as notInstalled. This hides an otherwise valid source; the fallback should discover available profile directories (or otherwise check profile databases) instead of restricting the check to Default.

✅ Resolved in 40385efaf3d51c5a7b12f40de484d364c39b63d0

Synced from GitHub

One finding on the new profile row: the Default badge is outside the disabled-dim treatment the rest of the row uses.

Posted via Macroscope — UI Consistency

……@@ -0,0 +1,340 @@
32+const AES_IV = Buffer.alloc(16, 0x20);
33+const V10_PREFIX = "v10";
34+
35+export interface ChromiumCookie {

🟠 High BrowserImport/ChromiumCookies.ts:35

Partitioned Chromium cookies are imported as ordinary unpartitioned cookies, so a CHIPS cookie becomes available outside its original top-level-site partition and may send a session identifier in unrelated browsing contexts. ChromiumCookie carries no partition-key metadata, and the import path therefore cannot pass it to Electron; preserve and write the partition key, or skip and count partitioned rows.

No longer relevant as of 05fe2c1c07af19afd480b84df6fc54f11a5ef45a

apps/desktop/src/preview/BrowserImport/Sources.tsline 111Outdated · 40385efOpen thread
……@@ -0,0 +1,185 @@
108+ ),
109+ Effect.orElseSucceed(() => [] as ReadonlyArray<BrowserImportSourceProfile>),
110+ );
111+ if (declared.length > 0) return declared;
CcursorBoton line 111Aug 17, 2026

Stale Local State profiles listed

Medium Severity

When Local State is present, listSourceProfiles returns every declared profile without checking for a Cookies database. The filesystem scan fallback already requires a cookie DB. Profiles that remain in metadata after their directory was removed still appear in the import menu and then fail with a generic readFailed.

Reviewed by Cursor Bugbot for commit 40385efaf3d51c5a7b12f40de484d364c39b63d0. Configure here.

apps/desktop/src/preview/BrowserImport/Sources.tsline 105Outdated · 7b8e294Open thread
……@@ -0,0 +1,185 @@
102+ // segment is dropped: `..` or a path separator would otherwise be handed
103+ // to `cookieDatabasePath` and read a database outside the user-data
104+ // directory.
105+ Effect.map((entries) => entries.filter(([directory]) => isSafeProfileDirectory(directory))),
MmacroscopeappBoton line 105Aug 17, 2026

🟠 High BrowserImport/Sources.ts:105

A symlinked profile such as <userData>/Default lets cookieDatabasePath resolve and import <external>/Cookies outside the browser user-data directory. isSafeProfileDirectory only validates the key lexically, so it does not enforce the path-containment guarantee described here; resolve the profile path and verify it remains under root (or reject symlinked profile components) before returning it.

No longer relevant as of a7d72de122b9cf679eeed4ac8fada652fc3455fd

Synced from GitHub

One finding: the new profile badge in the preview chrome row uses a native title tooltip where every other hover hint in that row goes through the shared Tooltip primitive.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review of the new browser-import surfaces. Three findings in apps/web/src/components/settings/BrowserImportWizard.tsx; the earlier findings in IntegrationsSettings.tsx (default-badge dimming, Clear cookies and cache silent no-op, default resolution against the rendered list) all look addressed at this head.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/BrowserImportWizard.tsxlines 257–263Outdated · 05fe2c1Open thread
……@@ -0,0 +1,323 @@
260+ <Spinner className="size-4 text-muted-foreground" />
261+ <span className="text-sm text-muted-foreground">Importing cookies…</span>
262+ </DialogPanel>
263+ );
MmacroscopeappBoton lines 257–263Aug 17, 2026

ImportingStep renders a bare DialogPanel, so while it is on screen the popup has no DialogTitle. Base UI derives the dialog's accessible name from that title, so the dialog goes unnamed for assistive tech mid-flow, and visually the popup collapses to a spinner-only box between the configure and done screens. Every other step here supplies a header.

suggestion
1function ImportingStep() {
2 return (
3 <>
4 <DialogHeader>
5 <DialogTitle>Importing cookies…</DialogTitle>
6 </DialogHeader>
7 <DialogPanel className="flex items-center gap-3 py-6">
8 <Spinner className="size-4 text-muted-foreground" />
9 <span className="text-sm text-muted-foreground">This can take a moment.</span>
10 </DialogPanel>
11 </>
12 );
13}

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxline 690Outdated · 05fe2c1Open thread
……@@ -523,91 +598,206 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
543- Add profile
544- </Button>
689+ <Menu onOpenChange={(open) => open && loadSources()}>
690+ <MenuTrigger render={<Button size="sm" variant="outline" disabled={disabled} />}>
CcursorBoton line 690Aug 17, 2026

Untracked imports after wizard close

High Severity

The prior busy lock on Add profile was removed, and dismissing the wizard during importing only clears importSource—it does not cancel or track the IPC work. Users can start another import (or hit the profile cap) while the first run is still finishing, so registrations race and target can stay new after canCreateProfile flips false.

Additional Locations (2)

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

apps/web/src/components/settings/IntegrationsSettings.tsxline 656Outdated · 05fe2c1Open thread
……@@ -523,91 +598,206 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
653+ ...userProfiles,
654+ { id: input.target.profileId, name: targetName, kind: "persistent" as const },
655+ ],
656+ });
CcursorBoton line 656Aug 17, 2026

Stale profiles wiped after import

High Severity

After importBrowserCookies returns, a new profile is registered by spreading a render-closed userProfiles into updateSettings, which replaces the whole browserProfiles list. The Keychain prompt and cookie write can outlive the wizard, so any profile created or removed in that window is overwritten when registration finally runs.

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

Synced from GitHub

Two findings in the import wizard's changed lines. Earlier findings on this PR (the importing step doing double duty for the post-quit re-check, and ImportingStep rendering without a DialogTitle) are still open but not re-posted here.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/BrowserImportWizard.tsxline 371Outdated · 2d762e5Open thread
……@@ -0,0 +1,406 @@
368+ </p>
369+ <p className="mt-1 text-sm text-foreground">{formatSkippedDomains(skippedDomains)}</p>
370+ </DialogPanel>
371+ ) : null}
CcursorBoton line 371Aug 17, 2026

Done copy contradicts skips

Low Severity

When imported is 0 but cookies were skipped, DoneStep still says there were no cookies to bring over while also rendering the new Skipped hosts panel. That pairing is contradictory for all-decrypt or all-write failures.

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

Synced from GitHub

One finding on the BrowserSession test double. The service modules, errors, make/layer shape, and namespace imports in the new BrowserImport code otherwise follow the conventions, and the earlier error-modelling feedback has been addressed.

Posted via Macroscope — Effect Service Conventions

apps/web/src/components/settings/BrowserImportWizard.tsxlines 356–361Outdated · 09ce84dOpen thread
……@@ -0,0 +1,406 @@
358+ <DialogDescription>
359+ {imported > 0
360+ ? `Into ${targetName}.${skipped > 0 ? ` ${skipped} couldn't be brought over.` : ""}`
361+ : "There were no cookies to bring over."}
MmacroscopeappBoton lines 356–361Aug 28, 2026

🟡 Medium settings/BrowserImportWizard.tsx:356

When imported is 0 and skipped is positive, DoneStep renders “Nothing to import” and says there were no cookies, omitting the failed skipped count. The title and description should distinguish an all-skipped import from an empty cookie store.

diff
1- {imported > 0 ? `Imported ${imported} cookies` : "Nothing to import"}
2+ {imported > 0
3+ ? `Imported ${imported} cookies`
4+ : skipped > 0
5+ ? "Couldn't import cookies"
6+ : "Nothing to import"}
7 </DialogTitle>
8 <DialogDescription>
9- {imported > 0
10- ? `Into ${targetName}.${skipped > 0 ? ` ${skipped} couldn't be brought over.` : ""}`
11- : "There were no cookies to bring over."}
12+ {imported > 0
13+ ? `Into ${targetName}.${skipped > 0 ? ` ${skipped} couldn't be brought over.` : ""}`
14+ : skipped > 0
15+ ? `${skipped} cookies couldn't be brought over.`
16+ : "There were no cookies to bring over."}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/BrowserImportWizard.tsx around lines 356-361:
2
3When `imported` is `0` and `skipped` is positive, `DoneStep` renders “Nothing to import” and says there were no cookies, omitting the failed `skipped` count. The title and description should distinguish an all-skipped import from an empty cookie store.

Fixed in 37cdc249ab. An all-skipped result is distinguished from a source with no cookies; it reports failure to import and the skipped count/sites.

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tsline 224Outdated · 09ce84dOpen thread
……@@ -0,0 +1,352 @@
221+
222+const decryptValue = (encrypted: Uint8Array, key: Buffer, domain: string): string | null => {
223+ const buffer = Buffer.from(encrypted);
224+ if (buffer.length === 0) return "";
MmacroscopeappBoton line 224Aug 28, 2026

🟠 High BrowserImport/ChromiumCookies.ts:224

Cookies with an empty encrypted_value are imported with value: "" instead of their stored plaintext value. Chromium keeps plaintext cookies in the value column when encryption is unavailable, but this query neither selects nor retains that column; select value and use it when encrypted_value is empty.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts around line 224:
2
3Cookies with an empty `encrypted_value` are imported with `value: ""` instead of their stored plaintext value. Chromium keeps plaintext cookies in the `value` column when encryption is unavailable, but this query neither selects nor retains that column; select `value` and use it when `encrypted_value` is empty.
……@@ -0,0 +1,406 @@
38+ | { readonly kind: "new"; readonly profileId: string }
39+ | { readonly kind: "existing"; readonly profileId: string; readonly name: string };
40+
41+const NEW_TARGET_VALUE = "new";

🟡 Medium settings/BrowserImportWizard.tsx:41

A persisted profile whose id is "new" is displayed as an existing target but runImport treats the selection as kind: "new", so importing after selecting that profile updates a generated profile instead and can create one even at the profile cap. Use a sentinel outside the valid BrowserProfileId space or model the new/existing choice separately.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/BrowserImportWizard.tsx around line 41:
2
3A persisted profile whose id is `"new"` is displayed as an existing target but `runImport` treats the selection as `kind: "new"`, so importing after selecting that profile updates a generated profile instead and can create one even at the profile cap. Use a sentinel outside the valid `BrowserProfileId` space or model the new/existing choice separately.

Fixed in 5483e8d898. New versus existing targets are tagged variants; a real profile whose id is new is no longer mistaken for the create-profile action. Regression coverage includes that id.

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 222–222Outdated · 09ce84dOpen thread
……@@ -0,0 +1,228 @@
219+
220+ // Capped: a broken key can skip thousands, and the user only needs a sense
221+ // of which sites didn't come over, not an exhaustive list.
222+ return { imported, skipped, skippedDomains: [...skippedDomains].slice(0, 20) };
MmacroscopeappBoton lines 222–222Aug 28, 2026

🟡 Medium BrowserImport/BrowserImport.ts:222

The import reports success before persistent cookies are flushed, so an import of fewer than 512 cookies can be lost on an app crash or forced termination despite cookies.set having succeeded. Call and await session.cookies.flushStore() before returning the result, and propagate a flush failure.

No longer relevant as of a7d72de122b9cf679eeed4ac8fada652fc3455fd

apps/web/src/components/settings/BrowserImportWizard.tsxlines 356–356Outdated · 09ce84dOpen thread
……@@ -0,0 +1,406 @@
353+ <>
354+ <DialogHeader>
355+ <DialogTitle>
356+ {imported > 0 ? `Imported ${imported} cookies` : "Nothing to import"}
MmacroscopeappBoton lines 356–356Aug 28, 2026

🟢 Low settings/BrowserImportWizard.tsx:356

The success title renders Imported 1 cookies when exactly one cookie is imported. Make the noun singular for imported === 1, consistent with cookieCountLabel.

suggestion
1 {imported > 0 ? `Imported ${imported} ${imported === 1 ? "cookie" : "cookies"}` : "Nothing to import"}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/BrowserImportWizard.tsx around line 356:
2
3The success title renders `Imported 1 cookies` when exactly one cookie is imported. Make the noun singular for `imported === 1`, consistent with `cookieCountLabel`.
apps/desktop/src/preview/BrowserImport/Sources.tsline 156Outdated · 09ce84dOpen thread
……@@ -0,0 +1,234 @@
153+ ),
154+ Effect.orElseSucceed(() => [] as ReadonlyArray<BrowserImportSourceProfile>),
155+ );
156+ if (declared.length > 0) return yield* withCookieCounts(definition, paths, declared);
MmacroscopeappBoton line 156Aug 28, 2026

🟡 Medium BrowserImport/Sources.ts:156

isSourceInstalled reports false and hides the source when Local State contains a stale entry such as Default while an actual Profile 1/Cookies database exists. Because line 156 skips the directory scan whenever declared.length > 0, the real profile is never discovered; scan and merge filesystem profiles even when the cache is non-empty.

No longer relevant as of a7d72de122b9cf679eeed4ac8fada652fc3455fd

MmacroscopeappBotreviewed · 1 inline threadAug 28, 2026
Synced from GitHub

One finding on the new skipped-sites panel in BrowserImportWizard. Two earlier findings on this file (the stale TEMP comment above ConfigureStep, and SelectableTile exposing no aria-pressed/aria-checked selection state) are unchanged at this head and are not re-posted.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/BrowserImportWizard.tsxlines 355–362Outdated · 0891a8cOpen thread
……@@ -0,0 +1,406 @@
359+ {imported > 0
360+ ? `Into ${targetName}.${skipped > 0 ? ` ${skipped} couldn't be brought over.` : ""}`
361+ : "There were no cookies to bring over."}
362+ </DialogDescription>
MmacroscopeappBoton lines 355–362Aug 28, 2026

When every cookie is skipped (imported === 0, skipped > 0 — e.g. an unusable key), this screen now contradicts itself: the header says "Nothing to import" / "There were no cookies to bring over", while the panel added just below lists the sites whose cookies were skipped, and the skipped count is never shown because it only appears on the imported > 0 branch. A user who picked "You — 5,065 cookies" on the configure step is told nothing was there.

Consider giving the all-skipped case its own copy so the header agrees with the panel.

suggestion
1 <DialogTitle>
2 {imported > 0
3 ? `Imported ${imported} cookies`
4 : skipped > 0
5 ? "No cookies could be brought over"
6 : "Nothing to import"}
7 </DialogTitle>
8 <DialogDescription>
9 {imported > 0
10 ? `Into ${targetName}.${skipped > 0 ? ` ${skipped} couldn't be brought over.` : ""}`
11 : skipped > 0
12 ? `None of the ${skipped} cookies found could be read.`
13 : "There were no cookies to bring over."}
14 </DialogDescription>

Posted via Macroscope — UI Consistency

Fixed in 37cdc249ab. All-skipped and genuinely empty results are separate cases, so zero imported does not claim the source had no cookies.

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tsline 104Outdated · 0891a8cOpen thread
……@@ -0,0 +1,352 @@
101+const sameSiteFromColumn = (value: number): ChromiumCookie["sameSite"] => {
102+ if (value === 0) return "no_restriction";
103+ if (value === 2) return "strict";
104+ return "lax";
MmacroscopeappBoton line 104Aug 28, 2026

🟠 High BrowserImport/ChromiumCookies.ts:104

sameSiteFromColumn converts Chromium's stored UNSPECIFIED value (-1) to explicit `

✅ Resolved in 779b12083c33dab8537e94273cd2b679abc690d9

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tsline 119Outdated · 0891a8cOpen thread
……@@ -0,0 +1,352 @@
116+const toUnixSeconds = (webkitSeconds: number): number | undefined => {
117+ if (webkitSeconds <= 0) return undefined;
118+ return webkitSeconds - WEBKIT_EPOCH_OFFSET_SECONDS;
119+};
CcursorBoton line 119Aug 28, 2026

Expired cookies overwrite target sessions

Medium Severity

Already-expired source cookies are converted to a past expirationDate and written with cookies.set. Chromium treats that as a delete (expired-overwrite), so importing into an existing profile can remove matching live cookies. Those rows also increment imported even though they do not persist, which disagrees with BrowserImportResult treating expired cookies as skipped.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 0891a8c5ccb60adac2864c3450fe14934eeb5319. Configure here.

Synced from GitHub

Reviewed the changed web UI (BrowserImportWizard.tsx, browserImportWizard.logic.ts, IntegrationsSettings.tsx, settingsSearch.ts). One new finding on the done screen's count copy; earlier findings on this file (the TEMP note above ConfigureStep, the unnamed ImportingStep dialog, the busy screen reused for the post-quit re-check, the missing selection state on SelectableTile, and the all-skipped done screen) are still open from previous runs and are not repeated here.

Posted via Macroscope — UI Consistency

apps/desktop/src/preview/BrowserImport/ChromiumCookies.tslines 225–225Outdated · 779b120Open thread
……@@ -0,0 +1,352 @@
222+const decryptValue = (encrypted: Uint8Array, key: Buffer, domain: string): string | null => {
223+ const buffer = Buffer.from(encrypted);
224+ if (buffer.length === 0) return "";
225+ if (buffer.subarray(0, 3).toString("latin1") !== V10_PREFIX) return null;
MmacroscopeappBoton lines 225–225Aug 29, 2026

🟡 Medium BrowserImport/ChromiumCookies.ts:225

Legacy clear-text values in encrypted_value are returned as null, so readChromiumCookies counts those rows as undecryptable and drops otherwise valid cookies. Chromium treats values without the v10 prefix as legacy clear text; return buffer.toString("utf8") for that branch instead.

suggestion
1 if (buffer.subarray(0, 3).toString("latin1") !== V10_PREFIX) return buffer.toString("utf8");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts around line 225:
2
3Legacy clear-text values in `encrypted_value` are returned as `null`, so `readChromiumCookies` counts those rows as undecryptable and drops otherwise valid cookies. Chromium treats values without the `v10` prefix as legacy clear text; return `buffer.toString("utf8")` for that branch instead.

Fixed in 9bad21232a. Unversioned encrypted_value blobs use Chromium’s legacy plaintext behavior on macOS only. Linux unknown schemes remain unsupported. Fixtures cover both paths.

MmacroscopeappBotreviewed · 2 inline threadsAug 29, 2026
Synced from GitHub

Two spots where a whole Cause is caught, which also swallows defects and interruption and drops the underlying failure entirely. Both are in the new BrowserImport service code.

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 207–210Outdated · 779b120Open thread
……@@ -0,0 +1,228 @@
207+ catch: () => undefined,
208+ }).pipe(
209+ Effect.as(true),
210+ Effect.catchCause(() => Effect.succeed(false)),
MmacroscopeappBoton lines 207–210Aug 29, 2026

catch: () => undefined throws the Electron rejection away (no cause, no log), and Effect.catchCause then also converts defects and interruption into written = false, so an interrupted import silently keeps looping over the remaining cookies instead of stopping. Consider keeping the rejection as the typed error, logging it, and handling only the error channel with Effect.catch.

suggestion
1 catch: (cause) => cause,
2 }).pipe(
3 Effect.as(true),
4 Effect.tapError((cause) =>
5 Effect.logDebug("Skipped a cookie the target session rejected", { cause }),
6 ),
7 Effect.catch(() => Effect.succeed(false)),

Posted via Macroscope — Effect Service Conventions

Fixed in 461ee21f37. Only typed cookie-write rejections are counted as skipped; interruption and defects are not swallowed. The interruption test proves the write loop stops.

apps/desktop/src/preview/BrowserImport/Sources.tslines 187–187Outdated · 779b120Open thread
……@@ -0,0 +1,234 @@
184+const entryExists = Effect.fnUntraced(function* (path: string) {
185+ const fileSystem = yield* FileSystem.FileSystem;
186+ return yield* fileSystem.stat(path).pipe(
187+ Effect.catchCause(() => fileSystem.readLink(path)),
MmacroscopeappBoton lines 187–187Aug 29, 2026

stat fails with a typed PlatformError, so catching the full cause here is wider than intended — it also turns a defect or an interrupt into a readLink attempt. Effect.catch handles the whole (statically known) error channel without touching defects or interruption.

suggestion
1 Effect.catch(() => fileSystem.readLink(path)),

Posted via Macroscope — Effect Service Conventions

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One new finding in this revision's copy change; the previously flagged wizard issues (ImportingStep with no DialogTitle, SelectableTile without selection state for assistive tech, the all-skipped DoneStep wording, the leftover TEMP note above ConfigureStep, and the native title on the profile badge in PreviewChromeRow) are still present and are not re-posted here.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 821–821Outdated · 779b120Open thread
……@@ -622,8 +818,7 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
624820 <AlertDialogDescription>
625- Its cookies, logins, and cache are deleted with it. Tabs open in this profile move to
626- the default one.
821+ Its cookies and logins are deleted. Any open tabs switch to your default profile.
MmacroscopeappBoton lines 821–821Aug 29, 2026

The new wording promises something the app doesn't do: a tab's profile is fixed at attach (HostedBrowserWebview documents this, and usePreviewWebviewConfig derives the partition from snapshot.profileId), so a tab already open in the removed profile keeps running in that partition — it doesn't move to the default. What the user sees instead is the profile badge in PreviewChromeRow disappearing (because activeProfile no longer resolves), which makes the tab look like it switched while its clear actions still target the removed profile's partition.

Consider describing what actually happens.

suggestion
1 Its cookies and logins are deleted. Tabs already open in it stay open, signed out.

Posted via Macroscope — UI Consistency

Fixed in 8560cddf38 and preserved in 7ee3c4f0c5. Removal copy no longer claims open tabs switch profiles. Tabs keep their original partition and show Removed profile when metadata is gone.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One finding in this run: the settings-search catalog lost its entry for the default browser profile, so that setting is no longer reachable by name from settings search even though the capability still exists in each profile row's menu. Previously reported issues in BrowserImportWizard.tsx and IntegrationsSettings.tsx are unchanged at this head and are not repeated here.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/settingsSearch.tslines 227–345Outdated · 56e3f72Open thread
MmacroscopeappBoton lines 227–345Aug 29, 2026

Dropping the browser-default-profile entry removes the only way to reach this setting from settings search, but the setting itself still exists — it moved into each profile row's Set as default. searchSettings substring-matches titles only, so "default browser profile" (and "default profile") now return nothing, while the three sibling defaults — viewport, zoom, appearance — still resolve.

Since targetId ?? id is what the jump scrolls to, an entry can point at the profiles row without owning an anchor of its own:

suggestion
1 },
2 {
3 // No row of its own any more: "Set as default" lives in each profile's
4 // menu, so this result lands on the profiles list.
5 id: "browser-default-profile",
6 title: "Default browser profile",
7 to: "/settings/integrations",
8 targetId: "browser-profiles",
9 },

Posted via Macroscope — UI Consistency

Fixed in 23950d7c31. The Default browser profile search entry targets the existing browser-profiles row, with search coverage.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One convention violation found in the new BrowserImport service: a single-tag Effect.catchTag where the convention requires Effect.catchTags. Everything else in the new Effect modules (subpath namespace imports, Context.Service with inline interface, exported make/layer, Schema.TaggedErrorClass errors with structural attributes and preserved cause, environment-based dependency acquisition) matches the conventions.

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/preview/BrowserImport/BrowserImport.tslines 127–127Outdated · adef296Open thread
……@@ -0,0 +1,246 @@
124+ }).pipe(
125+ Effect.as(true),
126+ Effect.tapError((error) => Effect.logDebug(error.message, { cause: error.cause })),
127+ Effect.catchTag("BrowserCookieWriteError", () => Effect.succeed(false)),
MmacroscopeappBoton lines 127–127Aug 29, 2026

Effect.catchTag isn't the convention here — statically known tagged failures should be recovered with Effect.catchTags({ ... }), including when only one tag is handled.

suggestion
1 Effect.catchTags({ BrowserCookieWriteError: () => Effect.succeed(false) }),

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/preview/BrowserImport/Sources.tsline 207Outdated · adef296Open thread
……@@ -0,0 +1,234 @@
204+ // `stat` and `exists` follow links — so they report every running browser as
205+ // closed, which would let an import read a live, mid-write database.
206+ // `readLink` is the one probe that answers for the entry itself.
207+ return yield* entryExists(lock);
MmacroscopeappBoton line 207Aug 29, 2026

🟠 High BrowserImport/Sources.ts:207

A stale SingletonLock symlink makes isSourceRunning return true, so after a crash or force-quit every import is rejected as browserRunning even though no browser is using the database. The lock target contains the host/PID but is never checked; validate that process is still alive (or remove stale locks) before reporting the source as running.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/Sources.ts around line 207:
2
3A stale `SingletonLock` symlink makes `isSourceRunning` return `true`, so after a crash or force-quit every import is rejected as `browserRunning` even though no browser is using the database. The lock target contains the host/PID but is never checked; validate that process is still alive (or remove stale locks) before reporting the source as running.

Fixed in a47eec8048 and 0524c56a20. Chromium host-PID locks are ignored only when a same-host PID is positively absent (ESRCH from signal 0). Live, denied, foreign-host, malformed, and unknown cases remain conservative. No lock is removed and no actual process signal is sent. Focused liveness/lock tests pass.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One finding on the profile row menu: the environment gate on the trigger now also blocks the environment-independent Set as default action that this PR moved into it.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 849–849Outdated · adef296Open thread
……@@ -584,90 +656,227 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
846+ <Button
847+ size="icon-sm"
848+ variant="ghost-muted"
849+ disabled={disabled || !environmentsReady || importInFlight}
MmacroscopeappBoton lines 849–849Aug 29, 2026

The row menu now hosts Set as default, which is a purely local client setting, but the trigger stays disabled until an environment catalog is ready. This PR also deletes BrowserDefaultProfileSetting, whose select was enabled whenever isElectron — so on desktop before a server connects there is no longer any way to change the default profile, and the dead icon button carries no explanation (the removal button it replaced had a Tooltip saying why).

The two environment-dependent items already gate themselves with disabled={!removalAvailable} (which is stricter than environmentsReady), so the trigger gate is redundant for them.

suggestion
1 disabled={disabled || importInFlight}

Posted via Macroscope — UI Consistency

Fixed in 649be8a030. The profile menu no longer requires environment readiness, so Set as default remains available. Clear/remove retain their stricter cleanup-availability guards, and import/runtime pending restrictions remain.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One finding: the browser cookie import routes to the primary environment only, while the sibling profile actions in the same component fan out across every known environment.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 674–800Outdated · adef296Open thread
MmacroscopeappBoton lines 674–800Aug 29, 2026

The import targets whatever usePrimaryEnvironment() happens to be, but a browser profile is one Electron partition per environment (resolvePartitionScope(environmentId, profileId)), which is exactly why the two sibling actions in this component enumerate environments instead: clearProfileData and removeProfile call clearBrowserProfileData(..., environments.map(...)), with a test asserting it "clears every known environment". So with two environments connected, the cookies land in the primary one's partition only, while the wizard reports Imported N cookies — Added to <profile> with no mention of which environment; a preview tab opened under that profile in the other environment is still signed out, and the settings surface never names the environment it picked.

Consider either fanning the import out the way the clear path does (one importBrowserCookies per known environment id, reporting the aggregate once), or — if a single target is intentional — naming the environment in the configure and done copy so the result isn't presented as profile-wide.

No diff: the fix spans the IPC call site and the wizard's result copy.

Posted via Macroscope — UI Consistency

Fixed in 6dd58889ef and 52c95d7d2f. Import intentionally targets one environment: the wizard captures its id and label when opened, uses that id throughout retries, and names the environment in configuration and results. Copy says for the environment because storage remains desktop-local; cookies are not fanned out to other environments.

apps/web/src/components/settings/BrowserImportWizard.tsxlines 114–114Outdated · ef25cffOpen thread
……@@ -0,0 +1,428 @@
111+
112+ return (
113+ <Dialog open onOpenChange={(open) => (open || !canCloseWizard(step) ? undefined : onClose())}>
114+ <DialogPopup className="max-w-lg">
MmacroscopeappBoton lines 114–114Aug 29, 2026

DialogPopup renders its close button by default, but onOpenChange refuses to close while step.step === "importing" — so during the import the X is visible, focusable and enabled, and clicking it (or pressing Escape) silently does nothing. The repo's other non-dismissible-while-busy dialog ties the two together instead (RelayClientInstallDialog uses showCloseButton={isConfirming}, and SshPasswordPromptDialog/RenameEntryDialog pass false), so the affordance is only shown when it works.

suggestion
1 <DialogPopup className="max-w-lg" showCloseButton={canCloseWizard(step)}>

Posted via Macroscope — UI Consistency

Synced from GitHub

One consistency gap found in the browser-profile settings menu; everything else in the web-side changes (wizard steps, row menu, badge dimming, settings-search entry) now lines up with the shared primitives and prior feedback.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding: the new selection tile in the import wizard misses the pointer cursor every other clickable surface in the app sets explicitly. Inline comment below.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding: Sources.ts reads the host name from the node:os global instead of the shared HostProcessHostname reference the rest of the codebase injects.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

One finding on the import wizard's result copy. (A previously flagged item — SelectableTile still missing cursor-pointer — remains open; not re-posted here.)

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding: the import wizard renders the primary environment's raw catalog label, which reads "Local environment" where every other surface calls the same machine "This device".

Posted via Macroscope — UI Consistency

Added 50 commits

  • fix(desktop): preserve cookie write interruption60d5d49
  • fix(desktop): import plaintext Chromium cookiesa74dc8f
  • fix(desktop): snapshot browser cookies consistentlyff2493c
  • fix(web): identify tabs from removed profiles208563d
  • refactor(web): clean up import configure step20c2d6d
  • fix(desktop): read legacy macOS Chromium cookie values4f35fdf
  • fix(desktop): preserve defects in browser probes55952e0
  • fix(web): expose import tile selection6428df6
  • test(desktop): type browser import session mock69e6b69
  • fix(web): lock profiles during browser import6d238ee
  • fix(desktop): validate Chromium domain-bound cookies7233757
  • fix(web): tag browser import targets8d3752d
  • fix(web): clarify browser import results9408e9c
  • fix(web): label browser import progressab78749
  • fix(web): find default browser profilebc46a13
  • fix(desktop): skip partitioned Chromium cookiesd09731d
  • fix(desktop): guard Chromium liveness errors16e9561
  • fix(web): preserve browser import source selection1c72d5f
  • fix(desktop): follow tagged error catch convention245f43d
  • fix(desktop): ignore stale Chromium locks461c055
  • fix(web): clarify browser import destination copy488ad59
  • fix(web): hide browser import close while writing5a6163a
  • fix(desktop): normalize IPv6 cookie URLs5d480fd
  • fix(desktop): decode Chromium text schema versions5f4e008
  • fix(web): keep browser profile menus available62e6985
  • fix(web): pin browser import environment6d4eab5
  • fix(web): explain unavailable browser imports970eba9
  • fix(web): explain unavailable profile cleanupa04cdde
  • fix(web): block imports without source profilesa29de13
  • fix(web): hydrate settings before profile importsb16760f
  • fix(desktop): target imports at profile partitioncfcfd2d
  • chore(desktop): drop a diagnostics suppression that no longer suppresses anything014e159
  • fix(web): say why Blank profile is disabled, and drop a dead probe02672a0
  • fix(web): serialize client settings persistence0cd9fd3
  • fix(web): explain why Import is disabled once the profile cap is hit27e528d
  • fix(web): revalidate browser import targets28ac6d9
  • fix(web): keep unsupported browsers out of the import menu29003db
  • fix(web): await imported profile persistence316675a
  • fix(web): clarify empty browser imports5f981a1
  • fix(web): show pointer on import choices7f04afc
  • fix(web): explain removed import targets8ddc0f9
  • fix(desktop): read browser lock hostname from service9bbc6b3
  • fix: require a real cookie database, gate the cap, and let a missing key retrybda98c8
  • fix(web): normalize import destination labelsc932203
  • fix(web): carry main's control sizing through the profile table rebase4b73907
  • fix(web): say the profile wasn't saved instead of blaming the cookie database5ca6e11
  • fix: read the live Network/Cookies jar, and don't let a double-click abort an import9ca0e4a
  • fix(web): enforce the browser profile cap on every writeae2c73d
  • chore(web): drop a lint directive that no longer suppresses anythingc5dbc20
  • fix(desktop): flush imported cookies to disk before reporting doneceee903