import { DEFAULT_CLIENT_SETTINGS, DEFAULT_SERVER_SETTINGS, type EnvironmentId, type ModelSelection, type ProviderInstanceId, type ServerSettingsPatch, } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; import { useNavigate } from "@tanstack/react-router"; import { useRef, useState } from "react"; import { Trash2Icon } from "lucide-react"; import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; import { getCustomModelOptionsByInstance } from "../../modelSelection"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, resolveDefaultProviderModelSelection, sortProviderInstanceEntries, } from "../../providerInstances"; import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments"; import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { resolveEnvModeLabel } from "../BranchToolbar.logic"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { toastManager } from "../ui/toast"; import { Switch } from "../ui/switch"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { PROJECT_GROUPING_MODE_LABELS } from "./ProjectSettingsPanel"; import { ProjectDefaultActionsSettings } from "./ProjectDefaultActionsSettings"; import { searchableSetting } from "./settingsSearch"; import { SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, } from "./settingsLayout"; /** Defaults are written only to the machines selected on the projects settings page. */ export function ProjectDefaultsSettings({ environmentId, }: { environmentId: EnvironmentId | null; }) { const { environments } = useEnvironments(); const primaryEnvironmentId = usePrimaryEnvironmentId(); const clientSettings = useClientSettings(); const updateClientSettings = useUpdateClientSettings(); const navigate = useNavigate(); const updateSettings = useAtomCommand( serverEnvironment.updateSettings, "project defaults update", ); const savingRef = useRef(new Set()); const [saving, setSaving] = useState>(new Set()); const scoped = environments.filter( (environment) => environmentId === null || environment.environmentId === environmentId, ); const targets = scoped.filter( (environment) => environment.connection.phase === "connected" && environment.serverConfig !== null, ); const representative = targets.find((environment) => environment.environmentId === primaryEnvironmentId) ?? targets[0]; const serverSettings = representative?.serverConfig?.settings ?? DEFAULT_SERVER_SETTINGS; const providers = representative?.serverConfig?.providers ?? EMPTY_SERVER_PROVIDERS; const settings = { ...serverSettings, ...clientSettings }; const storedSelection = serverSettings.defaultModelSelection; const selection = resolveDefaultProviderModelSelection(providers, storedSelection); const entries = sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(providers), settings), ); const modelOptions = getCustomModelOptionsByInstance( settings, providers, selection?.instanceId, selection?.model, ); const activeEntry = entries.find((entry) => entry.instanceId === selection?.instanceId); const mixedModel = targets.some( (target) => JSON.stringify(target.serverConfig?.settings.defaultModelSelection) !== JSON.stringify(storedSelection), ); const mixedWorkspace = targets.some( (target) => target.serverConfig?.settings.defaultThreadEnvMode !== serverSettings.defaultThreadEnvMode, ); const mixedBrowser = targets.some( (target) => target.serverConfig?.settings.enableAgentBrowserAccess !== serverSettings.enableAgentBrowserAccess, ); const disabled = (key: keyof ServerSettingsPatch) => targets.length === 0 || saving.has(key); const mixedAutoPull = targets.some( (target) => target.serverConfig?.settings.defaultAutoPull !== serverSettings.defaultAutoPull, ); function modelDisabledReason(instanceId: ProviderInstanceId, model: string): string | null { const sourceEntry = entries.find((entry) => entry.instanceId === instanceId); for (const target of targets) { const config = target.serverConfig; if (!config) continue; const entry = applyProviderInstanceSettings( deriveProviderInstanceEntries(config.providers), config.settings, ).find((candidate) => candidate.instanceId === instanceId); const options = getCustomModelOptionsByInstance( { ...config.settings, ...clientSettings }, config.providers, ).get(instanceId); if ( !entry?.enabled || !entry.isAvailable || entry.driverKind !== sourceEntry?.driverKind || !options?.some((option) => option.slug === model && !option.isUnavailable) ) { return `This model is unavailable on ${target.label}. Select that machine to choose its default separately.`; } } return null; } async function save(patch: ServerSettingsPatch) { const keys = Object.keys(patch); if (targets.length === 0 || keys.some((key) => savingRef.current.has(key))) return; const nextModel = patch.defaultModelSelection; const reason = nextModel ? modelDisabledReason(nextModel.instanceId, nextModel.model) : null; if (reason) { toastManager.add({ type: "error", title: "Default model not saved", description: reason }); return; } for (const key of keys) savingRef.current.add(key); setSaving(new Set(savingRef.current)); try { const results = await Promise.all( targets.map((target) => updateSettings({ environmentId: target.environmentId, input: { patch } }), ), ); const failedTargets = targets.filter((_, index) => results[index]?._tag === "Failure"); if (failedTargets.length > 0) { toastManager.add({ type: "error", title: "Project defaults not saved on every machine", description: `Could not update ${failedTargets.map((target) => target.label).join(", ")}. Other machines may have saved the change.`, }); } } finally { for (const key of keys) savingRef.current.delete(key); setSaving(new Set(savingRef.current)); } } const setModel = (value: ModelSelection | null) => void save({ defaultModelSelection: value }); return ( } /> } /> {scoped.length > targets.length || targets.length === 0 ? (

{targets.length === 0 ? "Connect a machine to change its project defaults." : "Changes apply to connected machines only. Offline machines keep their current defaults."}

) : null} setModel(null)} /> ) : null } control={ selection && activeEntry ? (
{ if (representative) void navigate({ to: "/settings/providers", search: { environmentId: representative.environmentId, instanceId }, }); }} onInstanceModelChange={(instanceId, model) => setModel(createModelSelection(instanceId, model)) } /> {!mixedModel ? ( {}} modelOptions={selection.options ?? []} allowPromptInjectedEffort={false} planModeEnabled={settings.planModeEnabled} triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} onModelOptionsChange={(options) => setModel(createModelSelection(selection.instanceId, selection.model, options)) } /> ) : null}
) : ( No providers available ) } /> void save({ defaultThreadEnvMode: DEFAULT_SERVER_SETTINGS.defaultThreadEnvMode }) } /> ) : null } control={ } /> void save({ defaultAutoPull: false })} /> ) : null } control={ void save({ defaultAutoPull: enabled })} /> } /> void save({ enableAgentBrowserAccess: DEFAULT_SERVER_SETTINGS.enableAgentBrowserAccess, }) } /> ) : null } control={ } />
} /> void updateClientSettings({ sidebarProjectGroupingMode: DEFAULT_CLIENT_SETTINGS.sidebarProjectGroupingMode, }) } /> ) : null } control={ } /> Remove checkout } /> Remove project } />
); }