diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index a6da37c1551..71d00ec3bed 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -4,12 +4,13 @@ import { Radio as RadioPrimitive } from "@base-ui/react/radio"; import { CheckIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { + type EnvironmentId, ProviderInstanceId, ProviderDriverKind, type ProviderInstanceConfig, } from "@t3tools/contracts"; -import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; +import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; @@ -115,13 +116,20 @@ function validateInstanceId(id: string, existing: ReadonlySet): string | } interface AddProviderInstanceDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; + readonly open: boolean; + readonly environmentId: EnvironmentId; + readonly environmentLabel: string; + readonly onOpenChange: (open: boolean) => void; } -export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { - const settings = usePrimarySettings(); - const updateSettings = useUpdatePrimarySettings(); +export function AddProviderInstanceDialog({ + open, + environmentId, + environmentLabel, + onOpenChange, +}: AddProviderInstanceDialogProps) { + const settings = useEnvironmentSettings(environmentId); + const updateSettings = useUpdateEnvironmentSettings(environmentId); const [wizardStep, setWizardStep] = useState(0); const [driver, setDriver] = useState(DEFAULT_DRIVER_KIND); @@ -208,7 +216,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns toastManager.add({ type: "success", title: "Provider instance added", - description: `${driverOption.label} instance '${instanceId}' was added.`, + description: `${driverOption.label} instance '${instanceId}' was added to ${environmentLabel}.`, }); onOpenChange(false); } catch (error) { @@ -227,8 +235,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns Add provider instance - Configure an additional provider instance — for example, a second Codex install - pointed at a different workspace. + Configure an additional provider instance on {environmentLabel}. ; + readonly primaryEnvironmentId: EnvironmentId | null; + readonly onEnvironmentChange: (environmentId: EnvironmentId) => void; +} + +export function ProviderEnvironmentSelector({ + environmentId, + environments, + primaryEnvironmentId, + onEnvironmentChange, +}: ProviderEnvironmentSelectorProps) { + const selectedEnvironment = + environments.find((environment) => environment.environmentId === environmentId) ?? null; + const items = useMemo( + () => + environments.map((environment) => ({ + value: environment.environmentId, + label: environment.label, + })), + [environments], + ); + + return ( + + ); +} diff --git a/apps/web/src/components/settings/SettingsPanels.logic.test.ts b/apps/web/src/components/settings/SettingsPanels.logic.test.ts index 077991f8de0..f37a2d65d4e 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.test.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.test.ts @@ -1,5 +1,6 @@ import { DEFAULT_SERVER_SETTINGS, + EnvironmentId, ProviderDriverKind, ProviderInstanceId, type ProviderInstanceConfig, @@ -10,8 +11,69 @@ import { formatDiagnosticsDescription, isProjectGroupingEnabled, projectGroupingModeFromToggle, + resolveProviderSettingsEnvironmentId, } from "./SettingsPanels.logic"; +const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("00000000-0000-4000-8000-000000000001"); +const REMOTE_ENVIRONMENT_ID = EnvironmentId.make("00000000-0000-4000-8000-000000000002"); + +describe("provider settings environment selection", () => { + it("preserves an explicit selected environment", () => { + expect( + resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID, REMOTE_ENVIRONMENT_ID], + selectedEnvironmentId: REMOTE_ENVIRONMENT_ID, + primaryEnvironmentId: LOCAL_ENVIRONMENT_ID, + activeEnvironmentId: LOCAL_ENVIRONMENT_ID, + }), + ).toBe(REMOTE_ENVIRONMENT_ID); + }); + + it("defaults to the primary environment in managed mode", () => { + expect( + resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: [REMOTE_ENVIRONMENT_ID, LOCAL_ENVIRONMENT_ID], + selectedEnvironmentId: null, + primaryEnvironmentId: LOCAL_ENVIRONMENT_ID, + activeEnvironmentId: REMOTE_ENVIRONMENT_ID, + }), + ).toBe(LOCAL_ENVIRONMENT_ID); + }); + + it("defaults to the active remote environment in client-only mode", () => { + expect( + resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID, REMOTE_ENVIRONMENT_ID], + selectedEnvironmentId: null, + primaryEnvironmentId: null, + activeEnvironmentId: REMOTE_ENVIRONMENT_ID, + }), + ).toBe(REMOTE_ENVIRONMENT_ID); + }); + + it("falls back when the selected environment is no longer available", () => { + expect( + resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID], + selectedEnvironmentId: REMOTE_ENVIRONMENT_ID, + primaryEnvironmentId: LOCAL_ENVIRONMENT_ID, + activeEnvironmentId: REMOTE_ENVIRONMENT_ID, + }), + ).toBe(LOCAL_ENVIRONMENT_ID); + }); + + it("returns null when no environments are available", () => { + expect( + resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: [], + selectedEnvironmentId: null, + primaryEnvironmentId: null, + activeEnvironmentId: null, + }), + ).toBeNull(); + }); +}); + describe("project grouping toggle", () => { it("enables repository grouping and disables into separate projects", () => { expect(isProjectGroupingEnabled("repository")).toBe(true); diff --git a/apps/web/src/components/settings/SettingsPanels.logic.ts b/apps/web/src/components/settings/SettingsPanels.logic.ts index 51e318225ae..d61cf894047 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.ts @@ -1,4 +1,5 @@ import type { + EnvironmentId, ProviderDriverKind, ProviderInstanceConfig, ProviderInstanceId, @@ -8,6 +9,29 @@ import type { } from "@t3tools/contracts"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; +export function resolveProviderSettingsEnvironmentId(input: { + readonly availableEnvironmentIds: ReadonlyArray; + readonly selectedEnvironmentId: EnvironmentId | null; + readonly primaryEnvironmentId: EnvironmentId | null; + readonly activeEnvironmentId: EnvironmentId | null; +}): EnvironmentId | null { + const availableIds = new Set(input.availableEnvironmentIds); + const candidates = [ + input.selectedEnvironmentId, + input.primaryEnvironmentId, + input.activeEnvironmentId, + input.availableEnvironmentIds[0] ?? null, + ]; + + for (const candidate of candidates) { + if (candidate !== null && availableIds.has(candidate)) { + return candidate; + } + } + + return null; +} + export function isProjectGroupingEnabled(mode: SidebarProjectGroupingMode): boolean { return mode !== "separate"; } diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 310cdd5164c..a8f508ed2cb 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -6,6 +6,7 @@ import { useAtomValue } from "@effect/atom-react"; import { defaultInstanceIdForDriver, type DesktopUpdateChannel, + type EnvironmentId, PROVIDER_DISPLAY_NAMES, ProviderDriverKind, type ProviderInstanceConfig, @@ -43,7 +44,12 @@ 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 { + useEnvironmentSettings, + usePrimarySettings, + useUpdateEnvironmentSettings, + useUpdatePrimarySettings, +} from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { @@ -61,8 +67,12 @@ import { primaryServerProvidersAtom, serverEnvironment, } from "../../state/server"; -import { usePrimaryEnvironment } from "../../state/environments"; -import { useProjects } from "../../state/entities"; +import { + type EnvironmentPresentation, + useEnvironments, + usePrimaryEnvironmentId, +} from "../../state/environments"; +import { useActiveEnvironmentId, useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat"; import { Button } from "../ui/button"; @@ -80,6 +90,7 @@ import { type ProviderUpdateCandidate, } from "../ProviderUpdateLaunchNotification.logic"; import { ProviderInstanceCard } from "./ProviderInstanceCard"; +import { ProviderEnvironmentSelector } from "./ProviderEnvironmentSelector"; import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta"; import { buildProviderInstanceUpdatePatch, @@ -88,6 +99,7 @@ import { projectGroupingModeFromToggle, readLastEnabledProjectGroupingMode, rememberEnabledProjectGroupingMode, + resolveProviderSettingsEnvironmentId, } from "./SettingsPanels.logic"; import { SettingResetButton, @@ -1097,10 +1109,74 @@ export function GeneralSettingsPanel() { } export function ProviderSettingsPanel() { - const settings = usePrimarySettings(); - const updateSettings = useUpdatePrimarySettings(); - const serverProviders = useAtomValue(primaryServerProvidersAtom); - const primaryEnvironment = usePrimaryEnvironment(); + const { isReady, environments } = useEnvironments(); + const primaryEnvironmentId = usePrimaryEnvironmentId(); + const activeEnvironmentId = useActiveEnvironmentId(); + const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(null); + const environmentId = resolveProviderSettingsEnvironmentId({ + availableEnvironmentIds: environments.map((environment) => environment.environmentId), + selectedEnvironmentId, + primaryEnvironmentId, + activeEnvironmentId, + }); + const environment = + environmentId === null + ? null + : (environments.find((candidate) => candidate.environmentId === environmentId) ?? null); + + if (environmentId === null || environment === null) { + return ( + + + } size="xs" variant="outline"> + Open connections + + ) : null + } + /> + + + ); + } + + return ( + + ); +} + +function ProviderSettingsEnvironmentPanel({ + environmentId, + environmentLabel, + environments, + primaryEnvironmentId, + onEnvironmentChange, +}: { + readonly environmentId: EnvironmentId; + readonly environmentLabel: string; + readonly environments: ReadonlyArray; + readonly primaryEnvironmentId: EnvironmentId | null; + readonly onEnvironmentChange: (environmentId: EnvironmentId) => void; +}) { + const settings = useEnvironmentSettings(environmentId); + const updateSettings = useUpdateEnvironmentSettings(environmentId); + const serverSettings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId)); + const serverProviders = useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? []; const refreshServerProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); @@ -1115,6 +1191,15 @@ export function ProviderSettingsPanel() { const [openInstanceDetails, setOpenInstanceDetails] = useState>({}); const refreshingRef = useRef(false); + const environmentSelector = ( + + ); + const providerUpdateCandidates = useMemo( () => collectProviderUpdateCandidates(serverProviders), [serverProviders], @@ -1145,14 +1230,9 @@ export function ProviderSettingsPanel() { 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, + environmentId, input: {}, }); refreshingRef.current = false; @@ -1160,16 +1240,15 @@ export function ProviderSettingsPanel() { if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { console.warn("Failed to refresh providers", { operation: "refresh-providers", - environmentId: primaryEnvironment.environmentId, + environmentId, ...safeErrorLogAttributes(squashAtomCommandFailure(result)), }); } })(); - }, [primaryEnvironment, refreshServerProviders]); + }, [environmentId, refreshServerProviders]); const runProviderUpdate = useCallback( async (candidate: ProviderUpdateCandidate) => { - if (!primaryEnvironment) return; let started = false; setUpdatingProviderDrivers((previous) => { if (previous.has(candidate.driver)) { @@ -1185,7 +1264,7 @@ export function ProviderSettingsPanel() { } const result = await updateProvider({ - environmentId: primaryEnvironment.environmentId, + environmentId, input: { provider: candidate.driver, instanceId: candidate.instanceId, @@ -1213,9 +1292,22 @@ export function ProviderSettingsPanel() { return next; }); }, - [primaryEnvironment, updateProvider], + [environmentId, updateProvider], ); + if (serverSettings === null) { + return ( + + + + + + ); + } + interface InstanceRow { readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; @@ -1315,8 +1407,6 @@ export function ProviderSettingsPanel() { const deleteProviderInstance = (id: ProviderInstanceId) => { updateSettings({ providerInstances: withoutProviderInstanceKey(settings.providerInstances, id), - providerModelPreferences: withoutProviderInstanceKey(settings.providerModelPreferences, id), - favorites: withoutProviderInstanceFavorites(settings.favorites ?? [], id), }); }; @@ -1379,11 +1469,6 @@ export function ProviderSettingsPanel() { [driverKind]: defaultLegacyProvider, } as typeof settings.providers, providerInstances: withoutProviderInstanceKey(settings.providerInstances, defaultInstanceId), - providerModelPreferences: withoutProviderInstanceKey( - settings.providerModelPreferences, - defaultInstanceId, - ), - favorites: withoutProviderInstanceFavorites(settings.favorites ?? [], defaultInstanceId), }); }; @@ -1393,6 +1478,7 @@ export function ProviderSettingsPanel() { title="Providers" headerAction={
+ {environmentSelector} {isAddInstanceDialogOpen ? ( - + ) : null} );