Skip to content

Commit 23b22b9

Browse files
fix(web): confirm profile removal and keep the table honest
Removing a profile deleted it and wiped its cookies and cache from a single icon-menu click, while every comparable destructive action in Settings confirms first. It now routes through the same AlertDialog. The Default badge resolved against the unfiltered profile list while the table renders only non-incognito rows, so a stored default of "incognito" left the section with no default marked at all. It now resolves against the rows that render. Reopening the import menu kept the previous source list on screen while the refresh was in flight, leaving a source that had since become unavailable selectable; the list is cleared first so the menu shows its loading state. A cookie sidecar that exists but cannot be copied is no longer ignored alongside the missing-file case. SQLite would open the snapshot without the write-ahead log and return a cookie set silently missing its newest transactions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d23545f commit 23b22b9

2 files changed

Lines changed: 116 additions & 60 deletions

File tree

apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,17 @@ const snapshotCookieDatabase = Effect.fn("ChromiumCookies.snapshotCookieDatabase
160160
const directory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-cookie-import-" });
161161
const target = path.join(directory, "Cookies");
162162
yield* fileSystem.copyFile(cookiePath, target);
163-
// The sidecars only exist while the browser holds the database open, so a
164-
// missing one is normal rather than a failure.
163+
// A sidecar only exists while the browser holds the database open, so an
164+
// absent one is normal. Anything else — a permission error, a partial read —
165+
// is not: SQLite would then open the snapshot without the write-ahead log
166+
// and quietly return a cookie set missing its most recent transactions.
165167
yield* Effect.forEach(["-wal", "-shm"], (suffix) =>
166-
fileSystem.copyFile(`${cookiePath}${suffix}`, `${target}${suffix}`).pipe(Effect.ignore),
168+
fileSystem.copyFile(`${cookiePath}${suffix}`, `${target}${suffix}`).pipe(
169+
Effect.catchIf(
170+
(error) => error.reason._tag === "NotFound",
171+
() => Effect.void,
172+
),
173+
),
167174
);
168175
return target;
169176
});

apps/web/src/components/settings/IntegrationsSettings.tsx

Lines changed: 106 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
BROWSER_IMPORT_UNAVAILABLE_COPY,
1212
BrowserImportFailureReason,
1313
BROWSER_PROFILE_MAX_COUNT,
14+
type BrowserProfile,
1415
BROWSER_PROFILE_NAME_MAX_LENGTH,
1516
DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW,
1617
DEFAULT_BROWSER_PROFILE_ID,
@@ -54,6 +55,15 @@ import {
5455
MenuTrigger,
5556
} from "../ui/menu";
5657
import { toastManager } from "../ui/toast";
58+
import {
59+
AlertDialog,
60+
AlertDialogClose,
61+
AlertDialogDescription,
62+
AlertDialogFooter,
63+
AlertDialogHeader,
64+
AlertDialogPopup,
65+
AlertDialogTitle,
66+
} from "../ui/alert-dialog";
5767
import { Button } from "../ui/button";
5868
import { DraftInput } from "../ui/draft-input";
5969
import { NumberField, NumberFieldGroup, NumberFieldInput } from "../ui/number-field";
@@ -523,11 +533,17 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
523533
const updateSettings = useUpdatePrimarySettings();
524534
const environmentId = usePrimaryEnvironment()?.environmentId;
525535
const [sources, setSources] = useState<ReadonlyArray<BrowserImportSource> | null>(null);
536+
const [profilePendingRemoval, setProfilePendingRemoval] = useState<BrowserProfile | null>(null);
526537
const [busy, setBusy] = useState(false);
527538

528539
const profiles = resolveBrowserProfiles(userProfiles);
540+
// Incognito is deliberately not a row — it holds nothing to manage — so the
541+
// default has to resolve against the list that renders. A stored
542+
// `browserDefaultProfileId` of "incognito" would otherwise leave the section
543+
// with no Default badge at all.
544+
const listedProfiles = profiles.filter((profile) => profile.kind !== "incognito");
529545
const resolvedDefaultId =
530-
findBrowserProfile(profiles, defaultProfileId)?.id ?? DEFAULT_BROWSER_PROFILE_ID;
546+
findBrowserProfile(listedProfiles, defaultProfileId)?.id ?? DEFAULT_BROWSER_PROFILE_ID;
531547

532548
const uniqueName = (base: string) => {
533549
const taken = new Set(profiles.map((profile) => profile.name));
@@ -573,6 +589,7 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
573589
};
574590

575591
const removeProfile = (id: string) => {
592+
setProfilePendingRemoval(null);
576593
// Drop the partition's data too, otherwise a removed profile's cookies
577594
// stay on disk with nothing in the UI pointing at them.
578595
if (environmentId) {
@@ -587,6 +604,10 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
587604

588605
const loadSources = () => {
589606
if (!previewBridge) return;
607+
// Cleared first: availability changes while the app runs (quitting a
608+
// browser clears `browserRunning`), and showing the previous answer during
609+
// the refresh lets the user start an import the source no longer supports.
610+
setSources(null);
590611
void previewBridge
591612
.listBrowserImportSources()
592613
.then(setSources)
@@ -728,70 +749,98 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
728749
}
729750
>
730751
<div className="mt-2 overflow-hidden rounded-lg border border-border/60">
731-
{profiles
732-
.filter((profile) => profile.kind !== "incognito")
733-
.map((profile, index) => {
734-
const builtIn = isBuiltInBrowserProfileId(profile.id);
735-
const isDefault = profile.id === resolvedDefaultId;
736-
return (
737-
<div
738-
key={profile.id}
739-
className={cn(
740-
"flex items-center gap-3 px-3 py-2",
741-
index > 0 && "border-t border-border/60",
752+
{listedProfiles.map((profile, index) => {
753+
const builtIn = isBuiltInBrowserProfileId(profile.id);
754+
const isDefault = profile.id === resolvedDefaultId;
755+
return (
756+
<div
757+
key={profile.id}
758+
className={cn(
759+
"flex items-center gap-3 px-3 py-2",
760+
index > 0 && "border-t border-border/60",
761+
)}
762+
>
763+
<span className="flex min-w-0 flex-1 items-center gap-2">
764+
{builtIn ? (
765+
<span className="truncate text-sm text-foreground">{profile.name}</span>
766+
) : (
767+
<DraftInput
768+
nativeInput
769+
size="sm"
770+
className="w-full max-w-56"
771+
aria-label={`Rename ${profile.name}`}
772+
disabled={disabled}
773+
maxLength={BROWSER_PROFILE_NAME_MAX_LENGTH}
774+
value={profile.name}
775+
onCommit={(next) => renameProfile(profile.id, next)}
776+
/>
742777
)}
743-
>
744-
<span className="flex min-w-0 flex-1 items-center gap-2">
745-
{builtIn ? (
746-
<span className="truncate text-sm text-foreground">{profile.name}</span>
747-
) : (
748-
<DraftInput
749-
nativeInput
750-
size="sm"
751-
className="w-full max-w-56"
752-
aria-label={`Rename ${profile.name}`}
778+
{isDefault ? <Badge>Default</Badge> : null}
779+
</span>
780+
<Menu>
781+
<MenuTrigger
782+
render={
783+
<Button
784+
size="icon-sm"
785+
variant="ghost-muted"
753786
disabled={disabled}
754-
maxLength={BROWSER_PROFILE_NAME_MAX_LENGTH}
755-
value={profile.name}
756-
onCommit={(next) => renameProfile(profile.id, next)}
787+
aria-label={`${profile.name} options`}
757788
/>
758-
)}
759-
{isDefault ? <Badge>Default</Badge> : null}
760-
</span>
761-
<Menu>
762-
<MenuTrigger
763-
render={
764-
<Button
765-
size="icon-sm"
766-
variant="ghost-muted"
767-
disabled={disabled}
768-
aria-label={`${profile.name} options`}
769-
/>
770-
}
789+
}
790+
>
791+
<MoreVertical />
792+
</MenuTrigger>
793+
<MenuPopup align="end" className="min-w-44">
794+
<MenuItem
795+
disabled={isDefault}
796+
onClick={() => updateSettings({ browserDefaultProfileId: profile.id })}
771797
>
772-
<MoreVertical />
773-
</MenuTrigger>
774-
<MenuPopup align="end" className="min-w-44">
798+
Set as default
799+
</MenuItem>
800+
<MenuItem onClick={() => clearProfileData(profile.id, profile.name)}>
801+
Clear cookies and cache
802+
</MenuItem>
803+
{builtIn ? null : (
775804
<MenuItem
776-
disabled={isDefault}
777-
onClick={() => updateSettings({ browserDefaultProfileId: profile.id })}
805+
variant="destructive"
806+
onClick={() => setProfilePendingRemoval(profile)}
778807
>
779-
Set as default
780-
</MenuItem>
781-
<MenuItem onClick={() => clearProfileData(profile.id, profile.name)}>
782-
Clear cookies and cache
808+
Remove profile and data
783809
</MenuItem>
784-
{builtIn ? null : (
785-
<MenuItem variant="destructive" onClick={() => removeProfile(profile.id)}>
786-
Remove profile and data
787-
</MenuItem>
788-
)}
789-
</MenuPopup>
790-
</Menu>
791-
</div>
792-
);
793-
})}
810+
)}
811+
</MenuPopup>
812+
</Menu>
813+
</div>
814+
);
815+
})}
794816
</div>
817+
<AlertDialog
818+
open={profilePendingRemoval !== null}
819+
onOpenChange={(open) => {
820+
if (!open) setProfilePendingRemoval(null);
821+
}}
822+
>
823+
<AlertDialogPopup>
824+
<AlertDialogHeader>
825+
<AlertDialogTitle>Remove “{profilePendingRemoval?.name}”?</AlertDialogTitle>
826+
<AlertDialogDescription>
827+
Its cookies, logins, and cache are deleted with it. Tabs open in this profile move to
828+
the default one.
829+
</AlertDialogDescription>
830+
</AlertDialogHeader>
831+
<AlertDialogFooter>
832+
<AlertDialogClose render={<Button variant="outline" />}>Cancel</AlertDialogClose>
833+
<Button
834+
variant="destructive"
835+
onClick={() => {
836+
if (profilePendingRemoval) removeProfile(profilePendingRemoval.id);
837+
}}
838+
>
839+
Remove profile
840+
</Button>
841+
</AlertDialogFooter>
842+
</AlertDialogPopup>
843+
</AlertDialog>
795844
</SettingsRow>
796845
);
797846
}

0 commit comments

Comments
 (0)