import { DeviceHostsSettings } from "./DeviceHostsSettings"; /** * Integrations settings - preferences for surfaces T3 Code embeds rather than * owns. Browser is the first section: the defaults a preview tab opens at, * applied to both hand-opened tabs and agent `preview_open` calls that don't * state their own size. * * @module IntegrationsSettings */ import { BrowserImportFailureReason, BROWSER_PROFILE_MAX_COUNT, type BrowserLinkTarget, type BrowserProfile, type EnvironmentId, type SshDeviceHostConfig, BROWSER_PROFILE_NAME_MAX_LENGTH, BROWSER_RECORDING_FRAME_RATES, DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW, DEFAULT_BROWSER_PROFILE_ID, DEFAULT_BROWSER_LINK_TARGET, DEFAULT_BROWSER_RECORDING_FRAME_RATE, DEFAULT_BROWSER_VIEWPORT, DEFAULT_PREVIEW_APPEARANCE, DEFAULT_PREVIEW_ZOOM_FACTOR, FILL_PREVIEW_VIEWPORT, PREVIEW_VIEWPORT_MAX_AREA, PREVIEW_VIEWPORT_MAX_DIMENSION, PREVIEW_VIEWPORT_MIN_DIMENSION, PREVIEW_ZOOM_LEVELS, findBrowserProfile, isBuiltInBrowserProfileId, resolveBrowserProfiles, type BrowserImportSource, type PreviewAppearancePreference, type PreviewViewportSetting, } from "@t3tools/contracts"; import { PREVIEW_VIEWPORT_PRESETS } from "@t3tools/shared/previewViewport"; import { Link } from "@tanstack/react-router"; import { MoreVertical, Plus as PlusIcon } from "lucide-react"; import { useCallback, useRef, useState } from "react"; import { ScreenRotationIcon } from "~/browser/ScreenRotationIcon"; import { AnimatedHeight } from "~/components/AnimatedHeight"; import { resolveEnvironmentOptionLabel } from "~/components/BranchToolbar.logic"; import { previewBridge } from "~/components/preview/previewBridge"; import { cn, randomUUID } from "~/lib/utils"; import { useEnvironments, usePrimaryEnvironment } from "~/state/environments"; import { deviceEnvironment, useDeviceState } from "~/state/device"; import { useAtomCommand } from "~/state/use-atom-command"; import { AgentDeviceSetupStatus, DeviceHubSetupStatus, PlatformStatus, platformSetupStatus, deviceHubDescription, agentDeviceDescription, } from "~/components/device/DeviceSetup"; import { isElectron } from "../../env"; import { Badge } from "../ui/badge"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuSeparator, MenuTrigger, } from "../ui/menu"; import { readLocalApi } from "~/localApi"; import { toastManager } from "../ui/toast"; import { AlertDialog, AlertDialogClose, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogPopup, AlertDialogTitle, } from "../ui/alert-dialog"; import { Button } from "../ui/button"; import { DraftInput } from "../ui/draft-input"; import { NumberField, NumberFieldGroup, NumberFieldInput } from "../ui/number-field"; import { Select, SelectGroup, SelectGroupLabel, SelectItem, SelectPopup, SelectTrigger, SelectValue, } from "../ui/select"; import { Switch } from "../ui/switch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { getClientSettings, persistClientSettingsUpdate, useClientSettings, useClientSettingsHydrated, useUpdatePrimarySettings, } from "~/hooks/useSettings"; import { SettingsUnavailableGroup, SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; import { BrowserImportWizard, type WizardTarget } from "./BrowserImportWizard"; import type { ImportOutcome } from "./browserImportWizard.logic"; const FILL_VALUE = "fill"; const RESPONSIVE_VALUE = "responsive"; type BrowserProfileDataBridge = Pick< NonNullable, "clearCookies" | "clearCache" >; export async function clearBrowserProfileData( bridge: BrowserProfileDataBridge | null, environmentIds: ReadonlyArray, profileId: string, ): Promise { if (bridge === null || environmentIds.length === 0) { throw new Error("Browser profile data is not available to clear."); } await Promise.all( environmentIds.flatMap((environmentId) => [ bridge.clearCookies(environmentId, profileId), bridge.clearCache(environmentId, profileId), ]), ); } export function browserProfileRemovalAvailable( bridgeAvailable: boolean, environmentsReady: boolean, environmentCount: number, ): boolean { return bridgeAvailable && environmentsReady && environmentCount > 0; } /** * The size a "Responsive" default falls back to when the user switches away * from Fill and hasn't typed dimensions yet. Fill has no dimensions to carry * over, so the picker needs something concrete to seed the inputs with. */ const RESPONSIVE_SEED_SIZE = { width: 1280, height: 800 } as const; const NO_GROUPING: Intl.NumberFormatOptions = { useGrouping: false }; const APPEARANCE_LABELS: Readonly> = { system: "System", light: "Light", dark: "Dark", }; const zoomLabel = (zoomFactor: number) => `${Math.round(zoomFactor * 100)}%`; /** * IPC flattens the failure to its message, so the reason token travels inside * it. Anything unrecognised reads as a plain read failure rather than leaking * the raw message into a toast. */ /** Thrown from the post-import settings updater when the cap was hit meanwhile. */ class ProfileLimitReachedError extends Error { constructor() { super("Browser profile limit reached."); this.name = "ProfileLimitReachedError"; } } export const importFailureReason = (cause: unknown): BrowserImportFailureReason => { const message = String((cause as { message?: unknown } | undefined)?.message ?? ""); return ( BrowserImportFailureReason.literals.find((reason) => message.includes(`failed: ${reason}.`)) ?? "readFailed" ); }; const viewportSelectValue = (viewport: PreviewViewportSetting): string => { if (viewport._tag === "fill") return FILL_VALUE; if ( viewport._tag === "preset" && PREVIEW_VIEWPORT_PRESETS.some((preset) => preset.id === viewport.presetId) ) { return viewport.presetId; } return RESPONSIVE_VALUE; }; /** * The trigger renders this rather than a bare `SelectValue`, which would fall * back to printing the raw stored value ("fill") because the options are built * inline instead of from an `items` map. */ const viewportSelectLabel = (viewport: PreviewViewportSetting): string => { const value = viewportSelectValue(viewport); if (value === FILL_VALUE) return "Fill panel"; if (value === RESPONSIVE_VALUE) return "Responsive"; return PREVIEW_VIEWPORT_PRESETS.find((preset) => preset.id === value)?.label ?? "Responsive"; }; const isValidDimension = (value: number) => Number.isInteger(value) && value >= PREVIEW_VIEWPORT_MIN_DIMENSION && value <= PREVIEW_VIEWPORT_MAX_DIMENSION; /** * A sized viewport with width and height swapped. Presets keep their identity * through a rotation — `resolvePreviewViewport` already stores rotated presets * as the preset id plus swapped dimensions — so a rotated iPad is still an * iPad, not an anonymous custom size. */ const rotateViewport = ( viewport: Exclude, ): PreviewViewportSetting => ({ ...viewport, width: viewport.height, height: viewport.width, }); function BrowserViewportSetting({ disabled }: { readonly disabled: boolean }) { const viewport = useClientSettings((settings) => settings.browserDefaultViewport); const updateSettings = useUpdatePrimarySettings(); const sized = viewport._tag === "fill" ? null : viewport; const presentedSize = { width: sized?.width ?? RESPONSIVE_SEED_SIZE.width, height: sized?.height ?? RESPONSIVE_SEED_SIZE.height, }; const selectViewport = (value: string | null) => { if (value === FILL_VALUE) { updateSettings({ browserDefaultViewport: FILL_PREVIEW_VIEWPORT }); return; } if (value === RESPONSIVE_VALUE) { updateSettings({ browserDefaultViewport: { _tag: "freeform", width: sized?.width ?? RESPONSIVE_SEED_SIZE.width, height: sized?.height ?? RESPONSIVE_SEED_SIZE.height, }, }); return; } const preset = PREVIEW_VIEWPORT_PRESETS.find((candidate) => candidate.id === value); if (!preset) return; updateSettings({ browserDefaultViewport: { _tag: "preset", width: preset.width, height: preset.height, presetId: preset.id, }, }); }; // Committed on blur rather than per keystroke: typing "2560" passes through // "256", which is a legal dimension, so an onValueChange handler would // persist that intermediate size and churn the settings file on every key. const commitDimension = (axis: "width" | "height", value: number | null) => { if (value === null || !isValidDimension(value)) return; const next = { ...presentedSize, [axis]: value }; if (next.width * next.height > PREVIEW_VIEWPORT_MAX_AREA) return; if (sized && next.width === sized.width && next.height === sized.height) return; // Typing a size means the preset no longer describes it. updateSettings({ browserDefaultViewport: { _tag: "freeform", ...next } }); }; return ( updateSettings({ browserDefaultViewport: DEFAULT_BROWSER_VIEWPORT })} /> ) : null } control={
{sized ? (
commitDimension("width", value)} > × commitDimension("height", value)} > = presentedSize.width ? "landscape" : "portrait" }`} onClick={() => updateSettings({ browserDefaultViewport: rotateViewport(sized) }) } > } /> Rotate
) : null}
} /> ); } function BrowserZoomSetting({ disabled }: { readonly disabled: boolean }) { const zoomFactor = useClientSettings((settings) => settings.browserDefaultZoomFactor); const updateSettings = useUpdatePrimarySettings(); return ( updateSettings({ browserDefaultZoomFactor: DEFAULT_PREVIEW_ZOOM_FACTOR }) } /> ) : null } control={ } /> ); } function BrowserAppearanceSetting({ disabled }: { readonly disabled: boolean }) { const appearance = useClientSettings((settings) => settings.browserDefaultAppearance); const updateSettings = useUpdatePrimarySettings(); return ( updateSettings({ browserDefaultAppearance: DEFAULT_PREVIEW_APPEARANCE })} /> ) : null } control={ } /> ); } function BrowserRecordingFrameRateSetting({ disabled }: { readonly disabled: boolean }) { const frameRate = useClientSettings((settings) => settings.browserRecordingFrameRate); const updateSettings = useUpdatePrimarySettings(); return ( updateSettings({ browserRecordingFrameRate: DEFAULT_BROWSER_RECORDING_FRAME_RATE }) } /> ) : null } control={ } /> ); } const LINK_TARGET_LABELS: Readonly> = { system: "Your default browser", app: "T3 Code", }; function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean }) { const linkTarget = useClientSettings((settings) => settings.browserLinkTarget); const updateSettings = useUpdatePrimarySettings(); return ( updateSettings({ browserLinkTarget: DEFAULT_BROWSER_LINK_TARGET })} /> ) : null } control={ } /> ); } function AgentBrowserAccessSetting() { return ( } size="sm" variant="outline" > Project settings } /> ); } function DeviceIntegrationSettings() { const primaryEnvironment = usePrimaryEnvironment(); const { environments } = useEnvironments(); const [selectedId, setSelectedId] = useState(null); const selected = environments.find((environment) => environment.environmentId === selectedId) ?? environments.find( (environment) => environment.environmentId === primaryEnvironment?.environmentId, ) ?? environments[0]; const connected = selected?.connection.phase === "connected" && selected.serverConfig !== null; const environmentId = connected ? selected.environmentId : null; return ( {environments.length > 1 ? ( setSelectedId(value)} > {selected?.label ?? "Select environment"} {environments.map((environment) => ( {environment.label} {environment.connection.phase === "connected" ? "" : " · Offline"} ))} } /> ) : null} ); } function DeviceIntegrationControls({ environmentId, hosts, enabled, agentAccessEnabled, }: { environmentId: EnvironmentId | null; hosts: ReadonlyArray; enabled: boolean; agentAccessEnabled: boolean; }) { const { state, loaded } = useDeviceState(environmentId); const configure = useAtomCommand(deviceEnvironment.configure); const list = useAtomCommand(deviceEnvironment.list, { reportFailure: false }); const [pending, setPending] = useState<"hub" | "check" | "agent" | null>(null); const busy = state.hostStatus === "installing" || state.hostStatus === "starting"; const [platformsRevealed, setPlatformsRevealed] = useState(false); // Keep diagnostics visible through subsequent agent setup and refresh phases. if (platformsRevealed && !enabled) setPlatformsRevealed(false); if (enabled && !platformsRevealed && state.hostStatus === "ready" && pending !== "hub") { setPlatformsRevealed(true); } const update = async ( kind: NonNullable, input: { enabled?: boolean; agentAccessEnabled?: boolean }, ) => { if (!environmentId) return; setPending(kind); try { const result = await configure({ environmentId, input }); if (result._tag === "Success" && input.enabled === true && !state.onboardingCompleted) { await configure({ environmentId, input: { onboardingCompleted: true } }); } } finally { setPending(null); } }; return ( <> {pending === "hub" ? : null} void update("hub", { enabled: Boolean(checked), ...(checked ? {} : { agentAccessEnabled: false }), }) } /> } /> {platformsRevealed ? ( } control={ } /> ) : null} {pending === "agent" ? : null} void update("agent", { agentAccessEnabled: Boolean(checked) }) } /> } /> {state.hostStatus === "failed" && state.hostStatusDetail ? (

{state.hostStatusDetail}

) : null} ); } function BrowserAutoShowFloatingPreviewSetting({ disabled }: { readonly disabled: boolean }) { const autoShow = useClientSettings((settings) => settings.browserAutoShowFloatingPreview); const updateSettings = useUpdatePrimarySettings(); return ( updateSettings({ browserAutoShowFloatingPreview: DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW, }) } /> ) : null } control={ updateSettings({ browserAutoShowFloatingPreview: Boolean(checked) }) } aria-label="Auto-show floating preview" /> } /> ); } /** * Profile list, its header menu, and the import flow. * * One menu creates profiles and imports into them, because the two are the * same decision from the user's side: "I want a profile that has my Helium * logins in it". Import targets include "New profile" so that case does not * require creating one first and then finding a second control. * * Built-ins render without a rename field: they are synthesized rather than * stored, so there is nothing to rename and removing them would strand every * tab that opened under them. * * Sources are listed lazily on open: detection touches the other browser's * files, and the answer changes while the app is running (quitting the browser * clears `browserRunning`), so a value cached at mount would go stale. */ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) { const userProfiles = useClientSettings((settings) => settings.browserProfiles); const defaultProfileId = useClientSettings((settings) => settings.browserDefaultProfileId); const settingsHydrated = useClientSettingsHydrated(); const updateSettings = useUpdatePrimarySettings(); const { environments, isReady: environmentsReady } = useEnvironments(); const primaryEnvironment = usePrimaryEnvironment(); const [sources, setSources] = useState | null>(null); const [importSession, setImportSession] = useState<{ readonly source: BrowserImportSource; readonly environmentId: EnvironmentId; readonly environmentName: string; } | null>(null); const [profilePendingRemoval, setProfilePendingRemoval] = useState(null); const [profileRemovalError, setProfileRemovalError] = useState(null); const [profileRemovalInFlight, setProfileRemovalInFlight] = useState(false); const removalAvailable = browserProfileRemovalAvailable( previewBridge !== null, environmentsReady, environments.length, ); const importInFlightRef = useRef(false); const [importInFlight, setImportInFlight] = useState(false); const profileWritesDisabled = disabled || !settingsHydrated; const profiles = resolveBrowserProfiles(userProfiles); // Incognito is deliberately not a row — it holds nothing to manage — so the // default has to resolve against the list that renders. A stored // `browserDefaultProfileId` of "incognito" would otherwise leave the section // with no Default badge at all. const listedProfiles = profiles.filter((profile) => profile.kind !== "incognito"); const resolvedDefaultId = findBrowserProfile(listedProfiles, defaultProfileId)?.id ?? DEFAULT_BROWSER_PROFILE_ID; const createProfile = (baseName: string) => { if (!settingsHydrated || importInFlightRef.current) return undefined; const currentProfiles = getClientSettings().browserProfiles; // Checked against the live settings, not the rendered list: two clicks // before a re-render would otherwise both pass the disabled control. if (currentProfiles.length >= BROWSER_PROFILE_MAX_COUNT) return undefined; const resolvedProfiles = resolveBrowserProfiles(currentProfiles); const taken = new Set(resolvedProfiles.map((profile) => profile.name)); let name = baseName; for (let index = 2; taken.has(name); index += 1) name = `${baseName} ${index}`; const profile = { id: `profile-${randomUUID()}`, name, kind: "persistent" as const }; updateSettings({ browserProfiles: [...currentProfiles, profile] }); return profile; }; const renameProfile = (id: string, next: string) => { if (!settingsHydrated || importInFlightRef.current) return; const name = next.trim().slice(0, BROWSER_PROFILE_NAME_MAX_LENGTH); if (name === "") return; const currentProfiles = getClientSettings().browserProfiles; updateSettings({ browserProfiles: currentProfiles.map((profile) => profile.id === id ? { ...profile, name } : profile, ), }); }; const clearProfileData = (id: string, name: string) => { if (!settingsHydrated || importInFlightRef.current) return; if (!previewBridge || !environmentsReady || environments.length === 0) { toastManager.add({ type: "error", title: `Could not clear ${name}'s data`, description: "You're not connected to a server yet.", }); return; } void clearBrowserProfileData( previewBridge, environments.map((environment) => environment.environmentId), id, ) .then(() => { toastManager.add({ type: "success", title: `Cleared ${name}'s cookies and cache` }); }) .catch(() => { toastManager.add({ type: "error", title: `Could not clear ${name}'s data` }); }); }; const removeProfile = async (id: string) => { if (!settingsHydrated || importInFlightRef.current) return; if (!removalAvailable) { setProfileRemovalError("Connect to an environment before removing this profile."); return; } setProfileRemovalError(null); setProfileRemovalInFlight(true); // Drop the partition's data too, otherwise a removed profile's cookies // stay on disk with nothing in the UI pointing at them. try { await clearBrowserProfileData( previewBridge, environmentsReady ? environments.map((environment) => environment.environmentId) : [], id, ); } catch { setProfileRemovalError("Profile data could not be deleted. Try again."); setProfileRemovalInFlight(false); return; } const currentSettings = getClientSettings(); updateSettings({ browserProfiles: currentSettings.browserProfiles.filter((profile) => profile.id !== id), // Reassign the default rather than leaving it pointing at nothing. ...(currentSettings.browserDefaultProfileId === id ? { browserDefaultProfileId: DEFAULT_BROWSER_PROFILE_ID } : {}), }); setProfileRemovalInFlight(false); setProfilePendingRemoval(null); }; // A browser that is not on this machine is left out rather than listed as a // dead row: there is nothing to act on, and the menu is a list of things you // can import from. An unsupported one is left out for the same reason — the // blocked wizard step can't be fixed from here. Every other unavailable // reason stays, since each names a step the user can take. const importableSources = (sources ?? []).filter( (source) => source.unavailable !== "notInstalled" && source.unavailable !== "unsupportedPlatform", ); // Refreshed without blanking the last result: the menu shows the cached list // straight away so it doesn't reflow on open, and the source list is stable // (names only) since choosing what to import happens in the wizard, not here. const loadSources = useCallback(() => { if (!previewBridge) return; void previewBridge .listBrowserImportSources() .then(setSources) .catch(() => setSources((previous) => previous ?? [])); }, []); // Runs one import for the wizard. A new profile is registered only once the // import succeeds — the cookies land in its partition first — so a blocked // attempt never leaves an empty profile behind. const runWizardImport = async ( source: BrowserImportSource, environmentId: EnvironmentId, input: { readonly sourceProfileDirectory: string; readonly target: WizardTarget }, ): Promise => { if (!previewBridge) return { kind: "blocked", reason: "sessionUnavailable" }; if (!settingsHydrated) return { kind: "blocked", reason: "sessionUnavailable" }; if ( input.target.kind === "existing" && !resolveBrowserProfiles(getClientSettings().browserProfiles).some( (profile) => profile.id === input.target.profileId, ) ) { return { kind: "blocked", reason: "readFailed" }; } if (importInFlightRef.current) return { kind: "blocked", reason: "readFailed" }; importInFlightRef.current = true; setImportInFlight(true); try { const result = await previewBridge.importBrowserCookies({ environmentId, sourceId: source.id, sourceProfileDirectory: input.sourceProfileDirectory, targetProfileId: input.target.profileId, }); if ( input.target.kind === "existing" && !resolveBrowserProfiles(getClientSettings().browserProfiles).some( (profile) => profile.id === input.target.profileId, ) ) { return { kind: "blocked", reason: "readFailed" }; } let targetName: string; if (input.target.kind === "new") { // Registered only when something actually came over: an import that // found no cookies should not leave a new, empty profile behind. if (result.imported > 0) { try { const persisted = await persistClientSettingsUpdate((current) => { const existing = current.browserProfiles.find( (profile) => profile.id === input.target.profileId, ); if (existing) return current; // The wizard refuses a new target at the cap, but the cap can be // reached while the import runs; the updater sees the newest // settings, so this is the check that holds. if (current.browserProfiles.length >= BROWSER_PROFILE_MAX_COUNT) { throw new ProfileLimitReachedError(); } const taken = new Set( resolveBrowserProfiles(current.browserProfiles).map((profile) => profile.name), ); let name = source.name; for (let index = 2; taken.has(name); index += 1) name = `${source.name} ${index}`; return { ...current, browserProfiles: [ ...current.browserProfiles, { id: input.target.profileId, name, kind: "persistent" as const }, ], }; }); targetName = persisted.browserProfiles.find((profile) => profile.id === input.target.profileId) ?.name ?? source.name; } catch (cause) { // This target id belongs only to the attempted new profile. Clear // its partition so a failed registration cannot strand imported // cookies behind a profile that disappears on restart. await clearBrowserProfileData( previewBridge, [environmentId], input.target.profileId, ).catch(() => undefined); // Not a read failure: the cookies came over and were cleared again // because the profile could not be kept. Name that, in the same // token form `importFailureReason` recovers from a bridge error. const reason = cause instanceof ProfileLimitReachedError ? "profileLimitReached" : "profileNotSaved"; throw new Error(`Importing cookies from ${source.id} failed: ${reason}.`, { cause }); } } else { targetName = source.name; } } else { targetName = input.target.name; } return { kind: "imported", imported: result.imported, skipped: result.skipped, skippedDomains: result.skippedDomains, targetName, }; } catch (cause) { return { kind: "blocked", reason: importFailureReason(cause) }; } finally { importInFlightRef.current = false; setImportInFlight(false); } }; // Re-checks a source's availability after the user quits the browser, and // keeps the cached list in step so the menu reflects it too. const refreshImportSource = async ( sourceId: BrowserImportSource["id"], ): Promise => { if (!previewBridge) return undefined; try { const latest = await previewBridge.listBrowserImportSources(); setSources(latest); return latest.find((source) => source.id === sourceId); } catch { return undefined; } }; const atProfileLimit = userProfiles.length >= BROWSER_PROFILE_MAX_COUNT; return ( open && loadSources()}> } > Add profile createProfile("New profile")} > Blank profile {atProfileLimit ? ( You’ve reached the profile limit ) : null} Import from {sources === null ? ( Looking for browsers… ) : importableSources.length === 0 ? ( No supported browsers found ) : ( // Every source is a plain row — running, needs-permission and // ready all look the same here. The wizard picks up whatever // state the source is in and walks the user forward from there. <> {importableSources.map((source) => ( { if (!settingsHydrated || primaryEnvironment == null) return; setImportSession({ source, environmentId: primaryEnvironment.environmentId, environmentName: resolveEnvironmentOptionLabel({ isPrimary: true, environmentId: primaryEnvironment.environmentId, runtimeLabel: primaryEnvironment.label, }), }); }} > {source.name} ))} {primaryEnvironment == null ? ( Connect to an environment to import cookies ) : null} )} } > {/* The bordered container groups rows unambiguously at any width, and carries the bottom spacing `SettingsRow` leaves to its children (`pt-3 pb-1`). */}
{listedProfiles.map((profile, index) => { const builtIn = isBuiltInBrowserProfileId(profile.id); const isDefault = profile.id === resolvedDefaultId; return (
0 && "border-t border-border/60", )} > {builtIn ? ( // Dimmed here rather than on the table: a wrapper-level dim // stacks with the rename field's and the row menu button's // own, landing them near 0.41 while every other disabled // control in the block sits at 0.64. {profile.name} ) : ( renameProfile(profile.id, next)} /> )} {/* Dimmed with the rest of the row: a `Badge` has no disabled treatment of its own, so a solid `bg-primary` pill would otherwise sit at full strength beside a name, rename field and menu button that are all at 0.64. */} {isDefault ? ( Default ) : null} } > { if (settingsHydrated) { updateSettings({ browserDefaultProfileId: profile.id }); } }} > Set as default clearProfileData(profile.id, profile.name)} > Clear cookies and cache {builtIn ? null : ( { if (settingsHydrated) setProfilePendingRemoval(profile); }} > Remove profile and data )} {!removalAvailable ? ( <> {environmentsReady ? "Connect to an environment to clear profile data" : "Checking environments…"} ) : null}
); })}
{ if (!open && !profileRemovalInFlight) { setProfilePendingRemoval(null); setProfileRemovalError(null); } }} > Remove “{profilePendingRemoval?.name}”? Its cookies and logins are deleted. Tabs already open in this profile stay open until you close them. {profileRemovalError ? (

{profileRemovalError}

) : null} {!removalAvailable ? (

Connect to an environment to remove this profile and its data.

) : null}
} > Cancel
{importSession ? ( ({ id: profile.id, name: profile.name }))} canCreateProfile={settingsHydrated && !atProfileLimit} onImport={(input) => runWizardImport(importSession.source, importSession.environmentId, input) } onRefreshSource={() => refreshImportSource(importSession.source.id)} onOpenFullDiskAccessSettings={() => { // Rejects outside the desktop shell (and on shells that predate the // method), so the one toast covers every way the link can fail. void readLocalApi() ?.shell.openSystemSettings("full-disk-access") .catch(() => { toastManager.add({ type: "error", title: "Could not open System Settings", description: "Open Privacy & Security → Full Disk Access manually.", }); }); }} onClose={() => setImportSession(null)} /> ) : null}
); } export function IntegrationsSettingsPanel() { // Client-local preview defaults are editable only where the preview exists. const previewDefaultsDisabled = !isElectron; const previewDefaults = ( <> ); return ( {/* Server-authoritative, so it stays editable on any client anchored to a server; `serverScoped` covers the hosted app, which has none. It sits outside the block covering the desktop-only defaults. */} {previewDefaultsDisabled ? ( {previewDefaults} ) : ( previewDefaults )} ); }