import { ArchiveIcon, ArchiveX, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { useCallback, useMemo, useRef, useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import { defaultInstanceIdForDriver, type DesktopUpdateChannel, PROVIDER_DISPLAY_NAMES, ProviderDriverKind, type ProviderInstanceConfig, type ProviderInstanceId, type ScopedThreadRef, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; import { createModelSelection } from "@t3tools/shared/model"; import * as Arr from "effect/Array"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; import * as Result from "effect/Result"; import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, } from "../../components/desktopUpdate.logic"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { isElectron } from "../../env"; import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; import { useTheme } from "../../hooks/useTheme"; import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, } from "../../modelSelection"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries, } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; import { primaryServerObservabilityAtom, primaryServerProvidersAtom, serverEnvironment, } from "../../state/server"; import { usePrimaryEnvironment } from "../../state/environments"; import { useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat"; import { Button } from "../ui/button"; import { DraftInput } from "../ui/draft-input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; import { canOneClickUpdateProviderCandidate, collectProviderUpdateCandidates, hasOneClickUpdateProviderCandidate, isProviderUpdateActive, type ProviderUpdateCandidate, } from "../ProviderUpdateLaunchNotification.logic"; import { ProviderInstanceCard } from "./ProviderInstanceCard"; import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta"; import { buildProviderInstanceUpdatePatch, formatDiagnosticsDescription, } from "./SettingsPanels.logic"; import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useRelativeTimeTick, } from "./settingsLayout"; import { ProjectFavicon } from "../ProjectFavicon"; import { useAtomCommand } from "../../state/use-atom-command"; const THEME_OPTIONS = [ { value: "system", label: "System", }, { value: "light", label: "Light", }, { value: "dark", label: "Dark", }, ] as const; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour", } as const; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); function withoutProviderInstanceKey( record: Readonly> | undefined, key: ProviderInstanceId, ): Record { const next = { ...record } as Record; delete next[key]; return next; } function withoutProviderInstanceFavorites( favorites: ReadonlyArray<{ readonly provider: ProviderInstanceId; readonly model: string }>, instanceId: ProviderInstanceId, ) { return favorites.filter((favorite) => favorite.provider !== instanceId); } const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({ provider: definition.value, })); function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) { useRelativeTimeTick(); const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null; if (!lastCheckedRelative) { return null; } return ( {lastCheckedRelative.suffix ? ( <> Checked {lastCheckedRelative.value}{" "} {lastCheckedRelative.suffix} ) : ( <>Checked {lastCheckedRelative.value} )} ); } function AboutVersionTitle() { return ( Version {APP_VERSION} ); } function AboutVersionSection() { const updateState = useDesktopUpdateState(); const [isChangingUpdateChannel, setIsChangingUpdateChannel] = useState(false); const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); const selectedUpdateChannel = updateState?.channel ?? "latest"; const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL; const handleUpdateChannelChange = useCallback( (channel: DesktopUpdateChannel) => { const bridge = window.desktopBridge; if ( !bridge || typeof bridge.setUpdateChannel !== "function" || channel === selectedUpdateChannel ) { return; } setIsChangingUpdateChannel(true); void bridge .setUpdateChannel(channel) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not change update track", description: error instanceof Error ? error.message : "Update track change failed.", }), ); }) .finally(() => { setIsChangingUpdateChannel(false); }); }, [selectedUpdateChannel], ); const handleButtonClick = useCallback(() => { const bridge = window.desktopBridge; if (!bridge) return; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; if (action === "download") { void bridge.downloadUpdate().catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not download update", description: error instanceof Error ? error.message : "Download failed.", }), ); }); return; } if (action === "install") { const confirmed = window.confirm( getDesktopUpdateInstallConfirmationMessage( updateState ?? { availableVersion: null, downloadedVersion: null }, ), ); if (!confirmed) return; void bridge.installUpdate().catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not install update", description: error instanceof Error ? error.message : "Install failed.", }), ); }); return; } if (typeof bridge.checkForUpdate !== "function") return; void bridge .checkForUpdate() .then((result) => { if (!result.checked) { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: result.state.message ?? "Automatic updates are not available in this build.", }), ); } }) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }), ); }); }, [updateState]); const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; const buttonTooltip = updateState ? getDesktopUpdateButtonTooltip(updateState) : null; const buttonDisabled = action === "none" ? !canCheckForUpdate(updateState) : isDesktopUpdateButtonDisabled(updateState); const actionLabel: Record = { download: "Download", install: "Install" }; const statusLabel: Record = { checking: "Checking…", downloading: "Downloading…", "up-to-date": "Up to Date", }; const buttonLabel = actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates"; const description = action === "download" || action === "install" ? "Update available." : "Current version of the application."; return ( <> } description={description} control={ {buttonLabel} } /> {buttonTooltip ? {buttonTooltip} : null} } /> {hasDesktopBridge ? ( { handleUpdateChannelChange(value as DesktopUpdateChannel); }} > {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"} Stable Nightly } /> ) : selectedHostedAppChannel ? ( { if (value === selectedHostedAppChannel) return; window.location.assign( buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }), ); }} > {HOSTED_APP_CHANNEL_LABEL} Latest Nightly } /> ) : null} ); } export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const changedSettingLabels = useMemo( () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] : []), ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar ? ["Auto-open task panel"] : []), ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), ...(Duration.toMillis(settings.automaticGitFetchInterval) !== Duration.toMillis(DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval) ? ["Automatic Git fetch interval"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.newWorktreesStartFromOrigin !== DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin ? ["New worktrees start from origin"] : []), ...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory ? ["Add project base directory"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(isGitWritingModelDirty ? ["Git writing model"] : []), ], [ isGitWritingModelDirty, settings.autoOpenPlanSidebar, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.addProjectBaseDirectory, settings.defaultThreadEnvMode, settings.newWorktreesStartFromOrigin, settings.diffIgnoreWhitespace, settings.diffWordWrap, settings.automaticGitFetchInterval, settings.enableAssistantStreaming, settings.sidebarThreadPreviewCount, settings.timestampFormat, theme, ], ); const restoreDefaults = useCallback(async () => { if (changedSettingLabels.length === 0) return; const api = readLocalApi(); const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm( ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join( "\n", ), ); if (!confirmed) return; setTheme("system"); updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode, newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }); onRestored?.(); }, [changedSettingLabels, onRestored, setTheme, updateSettings]); return { changedSettingLabels, restoreDefaults, }; } export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const observability = useAtomValue(primaryServerObservabilityAtom); const serverProviders = useAtomValue(primaryServerProvidersAtom); const diagnosticsDescription = formatDiagnosticsDescription({ localTracingEnabled: observability?.localTracingEnabled ?? false, otlpTracesEnabled: observability?.otlpTracesEnabled ?? false, otlpTracesUrl: observability?.otlpTracesUrl, otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false, otlpMetricsUrl: observability?.otlpMetricsUrl, }); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenInstanceId = textGenerationModelSelection.instanceId; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const gitModelInstanceEntries = sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings), ); const textGenInstanceEntry = gitModelInstanceEntries.find( (entry) => entry.instanceId === textGenInstanceId, ); const textGenProvider: ProviderDriverKind = textGenInstanceEntry?.driverKind ?? DEFAULT_DRIVER_KIND; const gitModelOptionsByInstance = getCustomModelOptionsByInstance( settings, serverProviders, textGenInstanceId, textGenModel, ); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); return ( setTheme("system")} /> ) : null } control={ } /> updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, }) } /> ) : null } control={ } /> updateSettings({ diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, }) } /> ) : null } control={ updateSettings({ diffWordWrap: Boolean(checked) })} aria-label="Wrap diff lines by default" /> } /> updateSettings({ diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, }) } /> ) : null } control={ updateSettings({ diffIgnoreWhitespace: Boolean(checked) }) } aria-label="Hide whitespace changes by default" /> } /> updateSettings({ enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, }) } /> ) : null } control={ updateSettings({ enableAssistantStreaming: Boolean(checked) }) } aria-label="Stream assistant messages" /> } /> updateSettings({ enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, }) } /> ) : null } control={ updateSettings({ enableProviderUpdateChecks: Boolean(checked) }) } aria-label="Check provider versions" /> } /> updateSettings({ autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, }) } /> ) : null } control={ updateSettings({ autoOpenPlanSidebar: Boolean(checked) }) } aria-label="Open the task panel automatically" /> } /> updateSettings({ defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode, newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, }) } /> ) : null } control={ } /> {settings.defaultThreadEnvMode === "worktree" ? ( updateSettings({ newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, }) } /> ) : null } control={ updateSettings({ newWorktreesStartFromOrigin: Boolean(checked) }) } aria-label="Start new worktrees from origin by default" /> } /> ) : null} updateSettings({ addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, }) } /> ) : null } control={ updateSettings({ addProjectBaseDirectory: next })} placeholder="~/" spellCheck={false} aria-label="Add project base directory" /> } /> updateSettings({ confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, }) } /> ) : null } control={ updateSettings({ confirmThreadArchive: Boolean(checked) }) } aria-label="Confirm thread archiving" /> } /> updateSettings({ confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, }) } /> ) : null } control={ updateSettings({ confirmThreadDelete: Boolean(checked) }) } aria-label="Confirm thread deletion" /> } /> updateSettings({ textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }) } /> ) : null } control={
{ updateSettings({ textGenerationModelSelection: resolveAppModelSelectionState( { ...settings, textGenerationModelSelection: createModelSelection(instanceId, model), }, serverProviders, ), }); }} /> {}} modelOptions={textGenModelOptions} allowPromptInjectedEffort={false} triggerVariant="outline" triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground" onModelOptionsChange={(nextOptions) => { updateSettings({ textGenerationModelSelection: resolveAppModelSelectionState( { ...settings, textGenerationModelSelection: createModelSelection( textGenInstanceId, textGenModel, nextOptions, ), }, serverProviders, ), }); }} />
} />
{isElectron || HOSTED_APP_CHANNEL ? ( ) : ( } description="Current version of the application." /> )} } size="xs" variant="outline"> View diagnostics } />
); } export function ProviderSettingsPanel() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const serverProviders = useAtomValue(primaryServerProvidersAtom); const primaryEnvironment = usePrimaryEnvironment(); const refreshServerProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); const updateProvider = useAtomCommand(serverEnvironment.updateProvider, { reportFailure: false, }); const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); const [isAddInstanceDialogOpen, setIsAddInstanceDialogOpen] = useState(false); const [updatingProviderDrivers, setUpdatingProviderDrivers] = useState< ReadonlySet >(() => new Set()); const [openInstanceDetails, setOpenInstanceDetails] = useState>({}); const refreshingRef = useRef(false); const providerUpdateCandidates = useMemo( () => collectProviderUpdateCandidates(serverProviders), [serverProviders], ); const providerUpdateCandidateByInstanceId = useMemo( () => new Map(providerUpdateCandidates.map((candidate) => [candidate.instanceId, candidate])), [providerUpdateCandidates], ); const visibleProviderSettings = PROVIDER_SETTINGS.filter( (providerSettings) => providerSettings.provider !== "cursor" || serverProviders.some( (provider) => provider.instanceId === defaultInstanceIdForDriver(ProviderDriverKind.make("cursor")), ), ); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenInstanceId = textGenerationModelSelection.instanceId; const lastCheckedAt = serverProviders.length > 0 ? serverProviders.reduce( (latest, provider) => (provider.checkedAt > latest ? provider.checkedAt : latest), serverProviders[0]!.checkedAt, ) : null; const refreshProviders = useCallback(() => { if (refreshingRef.current) return; refreshingRef.current = true; setIsRefreshingProviders(true); if (!primaryEnvironment) { refreshingRef.current = false; setIsRefreshingProviders(false); return; } void (async () => { const result = await refreshServerProviders({ environmentId: primaryEnvironment.environmentId, input: {}, }); refreshingRef.current = false; setIsRefreshingProviders(false); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { console.warn("Failed to refresh providers", { operation: "refresh-providers", environmentId: primaryEnvironment.environmentId, ...safeErrorLogAttributes(squashAtomCommandFailure(result)), }); } })(); }, [primaryEnvironment, refreshServerProviders]); const runProviderUpdate = useCallback( async (candidate: ProviderUpdateCandidate) => { if (!primaryEnvironment) return; let started = false; setUpdatingProviderDrivers((previous) => { if (previous.has(candidate.driver)) { return previous; } started = true; const next = new Set(previous); next.add(candidate.driver); return next; }); if (!started) { return; } const result = await updateProvider({ environmentId: primaryEnvironment.environmentId, input: { provider: candidate.driver, instanceId: candidate.instanceId, }, }); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: `Could not update ${PROVIDER_DISPLAY_NAMES[candidate.driver] ?? candidate.driver}`, description: error instanceof Error ? error.message : "The provider update command could not be started.", }), ); } setUpdatingProviderDrivers((previous) => { if (!previous.has(candidate.driver)) { return previous; } const next = new Set(previous); next.delete(candidate.driver); return next; }); }, [primaryEnvironment, updateProvider], ); interface InstanceRow { readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; readonly driver: ProviderDriverKind; readonly isDefault: boolean; readonly isDirty?: boolean; } const instancesByDriver = new Map< ProviderDriverKind, Array<[ProviderInstanceId, ProviderInstanceConfig]> >(); for (const [rawId, instance] of Object.entries(settings.providerInstances ?? {})) { const driver = instance.driver; const list = instancesByDriver.get(driver) ?? []; list.push([rawId as ProviderInstanceId, instance]); instancesByDriver.set(driver, list); } const defaultSlotIdsBySource = new Set( visibleProviderSettings.map((providerSettings) => String(defaultInstanceIdForDriver(providerSettings.provider)), ), ); const rows: InstanceRow[] = []; const visibleDriverKinds = new Set( visibleProviderSettings.map((providerSettings) => providerSettings.provider), ); for (const providerSettings of visibleProviderSettings) { type LegacyProviderSettings = (typeof settings.providers)[keyof typeof settings.providers]; const legacyProviders = settings.providers as Record; const defaultLegacyProviders = DEFAULT_UNIFIED_SETTINGS.providers as Record< string, LegacyProviderSettings >; const driver = providerSettings.provider; const defaultInstanceId = defaultInstanceIdForDriver(driver); const explicitInstance = settings.providerInstances?.[defaultInstanceId]; const legacyConfig = legacyProviders[providerSettings.provider]!; const defaultLegacyConfig = defaultLegacyProviders[providerSettings.provider]!; const effectiveInstance: ProviderInstanceConfig = explicitInstance ?? ({ driver, enabled: legacyConfig.enabled, config: legacyConfig, } satisfies ProviderInstanceConfig); const isDirty = explicitInstance !== undefined || !Equal.equals(legacyConfig, defaultLegacyConfig); rows.push({ instanceId: defaultInstanceId, instance: effectiveInstance, driver, isDefault: true, isDirty, }); for (const [id, instance] of instancesByDriver.get(providerSettings.provider) ?? []) { if (id === defaultInstanceId) continue; rows.push({ instanceId: id, instance, driver: instance.driver, isDefault: false }); } } for (const [driver, list] of instancesByDriver) { if (visibleDriverKinds.has(driver)) continue; for (const [id, instance] of list) { rows.push({ instanceId: id, instance, driver: instance.driver, isDefault: defaultSlotIdsBySource.has(String(id)), }); } } const updateProviderInstance = ( row: InstanceRow, next: ProviderInstanceConfig, options?: { readonly textGenerationModelSelection?: Parameters< typeof buildProviderInstanceUpdatePatch >[0]["textGenerationModelSelection"]; }, ) => { updateSettings( buildProviderInstanceUpdatePatch({ settings, instanceId: row.instanceId, instance: next, driver: row.driver, isDefault: row.isDefault, textGenerationModelSelection: options?.textGenerationModelSelection, }), ); }; const deleteProviderInstance = (id: ProviderInstanceId) => { updateSettings({ providerInstances: withoutProviderInstanceKey(settings.providerInstances, id), providerModelPreferences: withoutProviderInstanceKey(settings.providerModelPreferences, id), favorites: withoutProviderInstanceFavorites(settings.favorites ?? [], id), }); }; const updateProviderModelPreferences = ( instanceId: ProviderInstanceId, next: { readonly hiddenModels: ReadonlyArray; readonly modelOrder: ReadonlyArray; }, ) => { const hiddenModels = [...new Set(next.hiddenModels.filter((slug) => slug.trim().length > 0))]; const modelOrder = [...new Set(next.modelOrder.filter((slug) => slug.trim().length > 0))]; const rest = withoutProviderInstanceKey(settings.providerModelPreferences, instanceId); updateSettings({ providerModelPreferences: hiddenModels.length === 0 && modelOrder.length === 0 ? rest : { ...rest, [instanceId]: { hiddenModels, modelOrder, }, }, }); }; const updateProviderFavoriteModels = ( instanceId: ProviderInstanceId, nextFavoriteModels: ReadonlyArray, ) => { const favoriteModels = [ ...new Set( Arr.filterMap(nextFavoriteModels, (slug) => { const trimmedSlug = slug.trim(); return trimmedSlug.length > 0 ? Result.succeed(trimmedSlug) : Result.failVoid; }), ), ]; updateSettings({ favorites: [ ...withoutProviderInstanceFavorites(settings.favorites ?? [], instanceId), ...favoriteModels.map((model) => ({ provider: instanceId, model })), ], }); }; const resetDefaultInstance = (driverKind: ProviderDriverKind) => { type LegacyProviderSettings = (typeof settings.providers)[keyof typeof settings.providers]; const defaultLegacyProviders = DEFAULT_UNIFIED_SETTINGS.providers as Record< string, LegacyProviderSettings | undefined >; const defaultInstanceId = defaultInstanceIdForDriver(driverKind); const defaultLegacyProvider = defaultLegacyProviders[driverKind]; if (defaultLegacyProvider === undefined) return; updateSettings({ providers: { ...settings.providers, [driverKind]: defaultLegacyProvider, } as typeof settings.providers, providerInstances: withoutProviderInstanceKey(settings.providerInstances, defaultInstanceId), providerModelPreferences: withoutProviderInstanceKey( settings.providerModelPreferences, defaultInstanceId, ), favorites: withoutProviderInstanceFavorites(settings.favorites ?? [], defaultInstanceId), }); }; return ( setIsAddInstanceDialogOpen(true)} aria-label="Add provider instance" > } /> Add provider instance void refreshProviders()} aria-label="Refresh provider status" > {isRefreshingProviders ? ( ) : ( )} } /> Refresh provider status } > {rows.map((row) => { const driverOption = getDriverOption(row.driver); const liveProvider = serverProviders.find( (candidate) => candidate.instanceId === row.instanceId, ); const updateCandidate = liveProvider ? providerUpdateCandidateByInstanceId.get(liveProvider.instanceId) : undefined; const isDriverUpdateRunning = updateCandidate !== undefined && (updatingProviderDrivers.has(updateCandidate.driver) || serverProviders.some( (provider) => provider.driver === updateCandidate.driver && isProviderUpdateActive(provider), )); const showInlineUpdateButton = updateCandidate !== undefined && hasOneClickUpdateProviderCandidate(updateCandidate, serverProviders); const canRunInlineUpdate = updateCandidate !== undefined && canOneClickUpdateProviderCandidate(updateCandidate, serverProviders) && !updatingProviderDrivers.has(updateCandidate.driver); const modelPreferences = settings.providerModelPreferences?.[row.instanceId] ?? { hiddenModels: [], modelOrder: [], }; const favoriteModels = Arr.filterMap(settings.favorites ?? [], (favorite) => favorite.provider === row.instanceId ? Result.succeed(favorite.model) : Result.failVoid, ); const resetLabel = driverOption?.label ?? String(row.driver); const headerAction = row.isDefault && row.isDirty ? ( resetDefaultInstance(row.driver)} /> ) : null; return ( setOpenInstanceDetails((existing) => ({ ...existing, [row.instanceId]: open, })) } onUpdate={(next) => { const wasEnabled = row.instance.enabled ?? true; const isDisabling = next.enabled === false && wasEnabled; const shouldClearTextGen = isDisabling && textGenInstanceId === row.instanceId; if (shouldClearTextGen) { updateProviderInstance(row, next, { textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }); } else { updateProviderInstance(row, next); } }} onDelete={row.isDefault ? undefined : () => deleteProviderInstance(row.instanceId)} headerAction={headerAction} hiddenModels={modelPreferences.hiddenModels} favoriteModels={favoriteModels} modelOrder={modelPreferences.modelOrder} onHiddenModelsChange={(hiddenModels) => updateProviderModelPreferences(row.instanceId, { ...modelPreferences, hiddenModels, }) } onFavoriteModelsChange={(favoriteModels) => updateProviderFavoriteModels(row.instanceId, favoriteModels) } onModelOrderChange={(modelOrder) => updateProviderModelPreferences(row.instanceId, { ...modelPreferences, modelOrder, }) } onRunUpdate={ showInlineUpdateButton && updateCandidate ? () => { if (!canRunInlineUpdate) { return; } void runProviderUpdate(updateCandidate); } : undefined } isUpdating={showInlineUpdateButton ? isDriverUpdateRunning : undefined} /> ); })} {isAddInstanceDialogOpen ? ( ) : null} ); } export function ArchivedThreadsPanel() { const projects = useProjects(); const { unarchiveThread, confirmAndDeleteThread } = useThreadActions(); const environmentIds = useMemo( () => [...new Set(projects.map((project) => project.environmentId))], [projects], ); const { snapshots: archivedSnapshots, error: archiveError, isLoading: isLoadingArchive, refresh: refreshArchivedThreads, } = useArchivedThreadSnapshots(environmentIds); const archivedGroups = useMemo(() => { const projectsByEnvironmentAndId = new Map( archivedSnapshots.flatMap(({ environmentId, snapshot }) => snapshot.projects.map( (project) => [ `${environmentId}:${project.id}`, { id: project.id, environmentId, name: project.title, cwd: project.workspaceRoot, }, ] as const, ), ), ); const threads = archivedSnapshots.flatMap(({ environmentId, snapshot }) => snapshot.threads.map((thread) => ({ ...thread, environmentId, })), ); const archivedProjects = Array.from(projectsByEnvironmentAndId.values()); const groups: Array<{ readonly project: (typeof archivedProjects)[number]; readonly threads: Array<(typeof threads)[number]>; }> = []; for (const project of archivedProjects) { const projectThreads: Array<(typeof threads)[number]> = []; for (const thread of threads) { if (thread.projectId === project.id && thread.environmentId === project.environmentId) { projectThreads.push(thread); } } if (projectThreads.length > 0) { groups.push({ project, threads: projectThreads.toSorted((left, right) => { const leftKey = left.archivedAt ?? left.createdAt; const rightKey = right.archivedAt ?? right.createdAt; return rightKey.localeCompare(leftKey) || right.id.localeCompare(left.id); }), }); } } return groups; }, [archivedSnapshots]); const handleArchivedThreadContextMenu = useCallback( async (threadRef: ScopedThreadRef, position: { x: number; y: number }) => { const api = readLocalApi(); if (!api) return; const clicked = await api.contextMenu.show( [ { id: "unarchive", label: "Unarchive" }, { id: "delete", label: "Delete", destructive: true }, ], position, ); if (clicked === "unarchive") { const result = await unarchiveThread(threadRef); if (result._tag === "Success") { refreshArchivedThreads(); } else if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to unarchive thread", description: error instanceof Error ? error.message : "An error occurred.", }), ); } return; } if (clicked === "delete") { const result = await confirmAndDeleteThread(threadRef); if (result._tag === "Success") { refreshArchivedThreads(); } else if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to delete thread", description: error instanceof Error ? error.message : "An error occurred.", }), ); } } }, [confirmAndDeleteThread, refreshArchivedThreads, unarchiveThread], ); return ( {archivedGroups.length === 0 ? ( {isLoadingArchive ? ( ) : ( )} {isLoadingArchive ? "Loading archived threads" : archiveError ? "Could not load archived threads" : "No archived threads"} } description={ isLoadingArchive ? "Checking connected environments." : (archiveError ?? "Archived threads will appear here.") } /> ) : ( archivedGroups.map(({ project, threads: projectThreads }) => ( } > {projectThreads.map((thread) => ( { event.preventDefault(); void (async () => { const result = await settlePromise(() => handleArchivedThreadContextMenu( scopeThreadRef(thread.environmentId, thread.id), { x: event.clientX, y: event.clientY, }, ), ); if (result._tag === "Failure") { const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Archived thread action failed", description: error instanceof Error ? error.message : "An error occurred.", }), ); } })(); }} title={thread.title} description={ <> Archived {formatRelativeTimeLabel(thread.archivedAt ?? thread.createdAt)} {" \u00b7 Created "} {formatRelativeTimeLabel(thread.createdAt)} } control={ } /> ))} )) )} ); }