diff --git a/apps/mobile/src/state/entities.ts b/apps/mobile/src/state/entities.ts index 9eec5dc1250..8199dee3486 100644 --- a/apps/mobile/src/state/entities.ts +++ b/apps/mobile/src/state/entities.ts @@ -12,8 +12,7 @@ import type { import { Atom } from "effect/unstable/reactivity"; import { environmentProjects } from "./projects"; -import { environmentServerConfigsAtom } from "./server"; -import { environmentSession } from "./session"; +import { environmentServerConfigsAtom, serverEnvironment } from "./server"; import { environmentThreadShells } from "./threads"; const EMPTY_PROJECT_ATOM = Atom.make(null).pipe( @@ -50,7 +49,7 @@ export function useEnvironmentServerConfig( return useAtomValue( environmentId === null ? EMPTY_SERVER_CONFIG_ATOM - : environmentSession.configValueAtom(environmentId), + : serverEnvironment.configValueAtom(environmentId), ); } diff --git a/apps/mobile/src/state/presentation.ts b/apps/mobile/src/state/presentation.ts index 83d1fdce462..96171d3ea5c 100644 --- a/apps/mobile/src/state/presentation.ts +++ b/apps/mobile/src/state/presentation.ts @@ -5,12 +5,12 @@ import type { EnvironmentId } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { environmentCatalog } from "../connection/catalog"; -import { environmentSession } from "./session"; +import { serverEnvironment } from "./server"; export const environmentPresentations = createEnvironmentPresentationAtoms({ catalogValueAtom: environmentCatalog.catalogValueAtom, stateAtom: environmentCatalog.stateAtom, - configValueAtom: environmentSession.configValueAtom, + serverConfigValueAtom: serverEnvironment.configValueAtom, }); const EMPTY_ENVIRONMENT_PRESENTATION_ATOM = Atom.make(null).pipe( diff --git a/apps/mobile/src/state/server.ts b/apps/mobile/src/state/server.ts index f72cc96e54a..1b7060571a5 100644 --- a/apps/mobile/src/state/server.ts +++ b/apps/mobile/src/state/server.ts @@ -6,9 +6,9 @@ import { connectionAtomRuntime } from "../connection/runtime"; import { environmentSession } from "./session"; export const serverEnvironment = createServerEnvironmentAtoms(connectionAtomRuntime, { - initialConfigValueAtom: environmentSession.configValueAtom, + initialConfigValueAtom: environmentSession.initialConfigValueAtom, }); export const environmentServerConfigsAtom = createEnvironmentServerConfigsAtom({ catalogValueAtom: environmentCatalog.catalogValueAtom, - configValueAtom: serverEnvironment.configValueAtom, + serverConfigValueAtom: serverEnvironment.configValueAtom, }); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 63674076151..cf5bb9de5e9 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -138,7 +138,7 @@ import { } from "~/projectScripts"; import { newDraftId, newMessageId, newThreadId } from "~/lib/utils"; import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels"; -import { useSettings } from "../hooks/useSettings"; +import { useEnvironmentSettings } from "../hooks/useSettings"; import { resolveAppModelSelectionForInstance } from "../modelSelection"; import { getTerminalFocusOwner } from "../lib/terminalFocus"; import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; @@ -1027,7 +1027,7 @@ function ChatViewContent(props: ChatViewProps) { const activeThreadLastVisitedAt = useUiStateStore((store) => routeKind === "server" ? store.threadLastVisitedAtById[routeThreadKey] : undefined, ); - const settings = useSettings(); + const settings = useEnvironmentSettings(environmentId); const setStickyComposerModelSelection = useComposerDraftStore( (store) => store.setStickyModelSelection, ); @@ -1417,7 +1417,7 @@ function ChatViewContent(props: ChatViewProps) { }, [retryEnvironment], ); - const projectGroupingSettings = useSettings(selectProjectGroupingSettings); + const projectGroupingSettings = selectProjectGroupingSettings(settings); const logicalProjectEnvironments = useMemo(() => { if (!activeProject) return []; const logicalKey = deriveLogicalProjectKeyFromSettings(activeProject, projectGroupingSettings); @@ -1586,7 +1586,11 @@ function ChatViewContent(props: ChatViewProps) { selectedProvider: selectedProviderByThreadId, threadProvider, }); - const serverConfig = activeEnvironment?.serverConfig ?? primaryEnvironment?.serverConfig ?? null; + // Once a thread selects an environment, never substitute the primary + // environment's config while the selected environment is still loading. + const serverConfig = activeThread + ? (activeEnvironment?.serverConfig ?? null) + : (primaryEnvironment?.serverConfig ?? null); const versionMismatch = resolveServerConfigVersionMismatch(serverConfig); const versionMismatchDismissKey = versionMismatch && activeThread diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index ad84fa72c2d..a11d6c4cb07 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -45,7 +45,7 @@ import { import { useAtomValue } from "@effect/atom-react"; import { OpenAddProjectCommandPaletteProvider } from "../commandPaletteContext"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; -import { useSettings } from "../hooks/useSettings"; +import { useClientSettings } from "../hooks/useSettings"; import { readLocalApi } from "../localApi"; import { filesystemEnvironment } from "../state/filesystem"; import { projectEnvironment } from "../state/projects"; @@ -447,7 +447,7 @@ function OpenCommandPaletteDialog(props: { const deferredQuery = useDeferredValue(query); const isActionsOnly = deferredQuery.startsWith(">"); const [highlightedItemValue, setHighlightedItemValue] = useState(null); - const settings = useSettings(); + const clientSettings = useClientSettings(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false, }); @@ -524,16 +524,14 @@ function OpenCommandPaletteDialog(props: { const environment = environments.find( (candidate) => candidate.environmentId === environmentId, ); - const environmentSettings = - environment?.serverConfig?.settings ?? - (environmentId === primaryEnvironmentId ? settings : null); + const environmentSettings = environment?.serverConfig?.settings ?? null; const baseDirectory = environmentSettings?.addProjectBaseDirectory?.trim() ?? ""; if (baseDirectory.length === 0) { return "~/"; } return ensureBrowseDirectoryPath(baseDirectory); }, - [environments, primaryEnvironmentId, settings], + [environments], ); const projectCwdById = useMemo( @@ -589,7 +587,7 @@ function OpenCommandPaletteDialog(props: { const latestThread = getLatestThreadForProject( threads.filter((thread) => thread.environmentId === project.environmentId), project.id, - settings.sidebarThreadSortOrder, + clientSettings.sidebarThreadSortOrder, ); if (latestThread) { await navigate({ @@ -601,17 +599,9 @@ function OpenCommandPaletteDialog(props: { return; } - await handleNewThread(scopeProjectRef(project.environmentId, project.id), { - envMode: settings.defaultThreadEnvMode, - }); + await handleNewThread(scopeProjectRef(project.environmentId, project.id)); }, - [ - handleNewThread, - navigate, - settings.defaultThreadEnvMode, - settings.sidebarThreadSortOrder, - threads, - ], + [handleNewThread, navigate, clientSettings.sidebarThreadSortOrder, threads], ); const projectSearchItems = useMemo( @@ -650,21 +640,13 @@ function OpenCommandPaletteDialog(props: { activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, - defaultThreadEnvMode: settings.defaultThreadEnvMode, handleNewThread, }, scopeProjectRef(project.environmentId, project.id), ); }, }), - [ - activeDraftThread, - activeThread, - defaultProjectRef, - handleNewThread, - projects, - settings.defaultThreadEnvMode, - ], + [activeDraftThread, activeThread, defaultProjectRef, handleNewThread, projects], ); const allThreadItems = useMemo( @@ -673,7 +655,7 @@ function OpenCommandPaletteDialog(props: { threads, ...(activeThreadId ? { activeThreadId } : {}), projectTitleById, - sortOrder: settings.sidebarThreadSortOrder, + sortOrder: clientSettings.sidebarThreadSortOrder, icon: , renderLeadingContent: (thread) => , renderTrailingContent: (thread) => , @@ -684,7 +666,7 @@ function OpenCommandPaletteDialog(props: { }); }, }), - [activeThreadId, navigate, projectTitleById, settings.sidebarThreadSortOrder, threads], + [activeThreadId, clientSettings.sidebarThreadSortOrder, navigate, projectTitleById, threads], ); const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT); @@ -956,7 +938,6 @@ function OpenCommandPaletteDialog(props: { activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, - defaultThreadEnvMode: settings.defaultThreadEnvMode, handleNewThread, }); }, @@ -1072,7 +1053,7 @@ function OpenCommandPaletteDialog(props: { const latestThread = getLatestThreadForProject( threads.filter((thread) => thread.environmentId === existing.environmentId), existing.id, - settings.sidebarThreadSortOrder, + clientSettings.sidebarThreadSortOrder, ); if (latestThread) { await navigate({ @@ -1083,9 +1064,7 @@ function OpenCommandPaletteDialog(props: { }); } else { const navigationResult = await settlePromise(() => - handleNewThread(scopeProjectRef(existing.environmentId, existing.id), { - envMode: settings.defaultThreadEnvMode, - }), + handleNewThread(scopeProjectRef(existing.environmentId, existing.id)), ); if (navigationResult._tag === "Failure") { const error = squashAtomCommandFailure(navigationResult); @@ -1132,9 +1111,7 @@ function OpenCommandPaletteDialog(props: { } const navigationResult = await settlePromise(() => - handleNewThread(scopeProjectRef(browseEnvironmentId, projectId), { - envMode: settings.defaultThreadEnvMode, - }), + handleNewThread(scopeProjectRef(browseEnvironmentId, projectId)), ); if (navigationResult._tag === "Failure") { const error = squashAtomCommandFailure(navigationResult); @@ -1158,8 +1135,7 @@ function OpenCommandPaletteDialog(props: { navigate, projects, setOpen, - settings.defaultThreadEnvMode, - settings.sidebarThreadSortOrder, + clientSettings.sidebarThreadSortOrder, threads, ], ); diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index a7309b44f4c..ae356fe08ef 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -34,7 +34,7 @@ import { import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useProject, useThread } from "../state/entities"; import { resolveThreadRouteRef } from "../threadRoutes"; -import { useSettings } from "../hooks/useSettings"; +import { useClientSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableFileDiff"; @@ -183,7 +183,7 @@ export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; export default function DiffPanel({ mode = "inline", composerDraftTarget }: DiffPanelProps) { const { resolvedTheme } = useTheme(); - const settings = useSettings(); + const settings = useClientSettings(); const [diffRenderMode, setDiffRenderMode] = useState("stacked"); const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 0d6b4b6d1c9..f6eb8602cf8 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -41,11 +41,11 @@ import { restrictToFirstScrollableAncestor, restrictToVerticalAxis } from "@dnd- import { CSS } from "@dnd-kit/utilities"; import { type ContextMenuItem, + DEFAULT_SERVER_SETTINGS, ProjectId, type ScopedThreadRef, type ResolvedKeybindingsConfig, type SidebarProjectGroupingMode, - type ThreadEnvMode, ThreadId, } from "@t3tools/contracts"; import { @@ -77,6 +77,7 @@ import { readThreadShell, useProject, useProjects, + useServerConfigs, useThreadShells, useThreadShellsForProjectRefs, } from "../state/entities"; @@ -199,7 +200,7 @@ import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useIsMobile } from "~/hooks/useMediaQuery"; import { CommandDialogTrigger } from "./ui/command"; -import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; +import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; import { primaryServerConfigAtom, primaryServerKeybindingsAtom } from "../state/server"; import { derivePhysicalProjectKey, @@ -1084,19 +1085,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec isManualProjectSorting, dragHandleProps, } = props; - const threadSortOrder = useSettings( + const threadSortOrder = useClientSettings( (settings) => settings.sidebarThreadSortOrder, ); - const appSettingsConfirmThreadDelete = useSettings( + const appSettingsConfirmThreadDelete = useClientSettings( (settings) => settings.confirmThreadDelete, ); - const appSettingsConfirmThreadArchive = useSettings( + const appSettingsConfirmThreadArchive = useClientSettings( (settings) => settings.confirmThreadArchive, ); - const defaultThreadEnvMode = useSettings( - (settings) => settings.defaultThreadEnvMode, - ); - const projectGroupingSettings = useSettings(selectProjectGroupingSettings); + const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); + const serverConfigs = useServerConfigs(); const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false, }); @@ -1106,8 +1105,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, { reportFailure: false, }); - const updateSettings = useUpdateSettings(); - const sidebarThreadPreviewCount = useSettings( + const updateSettings = useUpdateClientSettings(); + const sidebarThreadPreviewCount = useClientSettings( (settings) => settings.sidebarThreadPreviewCount, ); const router = useRouter(); @@ -1840,7 +1839,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec const seedContext = resolveSidebarNewThreadSeedContext({ projectId: member.id, defaultEnvMode: resolveSidebarNewThreadEnvMode({ - defaultEnvMode: defaultThreadEnvMode, + defaultEnvMode: + serverConfigs.get(member.environmentId)?.settings.defaultThreadEnvMode ?? + DEFAULT_SERVER_SETTINGS.defaultThreadEnvMode, }), activeThread: currentActiveThread && currentActiveThread.projectId === member.id @@ -1889,7 +1890,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec } })(); }, - [defaultThreadEnvMode, handleNewThread, isMobile, router, setOpenMobile], + [handleNewThread, isMobile, router, serverConfigs, setOpenMobile], ); const handleCreateThreadClick = useCallback( @@ -2745,7 +2746,7 @@ interface SidebarProjectsContentProps { threadSortOrder: SidebarThreadSortOrder; projectGroupingMode: SidebarProjectGroupingMode; threadPreviewCount: SidebarThreadPreviewCount; - updateSettings: ReturnType; + updateSettings: ReturnType; openAddProject: () => void; isManualProjectSorting: boolean; projectDnDSensors: ReturnType; @@ -3014,12 +3015,12 @@ export default function Sidebar() { const navigate = useNavigate(); const pathname = useLocation({ select: (loc) => loc.pathname }); const isOnSettings = pathname.startsWith("/settings"); - const sidebarThreadSortOrder = useSettings((s) => s.sidebarThreadSortOrder); - const sidebarProjectSortOrder = useSettings((s) => s.sidebarProjectSortOrder); - const sidebarProjectGroupingMode = useSettings((s) => s.sidebarProjectGroupingMode); - const projectGroupingSettings = useSettings(selectProjectGroupingSettings); - const sidebarThreadPreviewCount = useSettings((s) => s.sidebarThreadPreviewCount); - const updateSettings = useUpdateSettings(); + const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder); + const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); + const sidebarProjectGroupingMode = useClientSettings((s) => s.sidebarProjectGroupingMode); + const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); + const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount); + const updateSettings = useUpdateClientSettings(); const handleNewThread = useNewThreadHandler(); const { archiveThread, deleteThread } = useThreadActions(); const { isMobile, setOpenMobile } = useSidebar(); diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index f357218c22a..21570fb7125 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -19,7 +19,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand, } from "../../keybindings"; -import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; +import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { TooltipProvider } from "../ui/tooltip"; import { @@ -102,7 +102,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { const searchInputRef = useRef(null); const modelListRef = useRef(null); const highlightedModelKeyRef = useRef(null); - const favorites = useSettings((s) => s.favorites ?? []); + const favorites = useClientSettings((s) => s.favorites ?? []); const [selectedInstanceId, setSelectedInstanceId] = useState( () => { if (props.lockedProvider !== null) { @@ -117,7 +117,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { () => providedKeybindings ?? [], [providedKeybindings], ); - const updateSettings = useUpdateSettings(); + const updateSettings = useUpdateClientSettings(); const focusSearchInput = useCallback(() => { searchInputRef.current?.focus({ preventScroll: true }); diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index 59f069c9e2b..77c1813f110 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -9,7 +9,7 @@ import { type ProviderInstanceConfig, } from "@t3tools/contracts"; -import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; +import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; @@ -114,8 +114,8 @@ interface AddProviderInstanceDialogProps { } export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { - const settings = useSettings(); - const updateSettings = useUpdateSettings(); + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); const [wizardStep, setWizardStep] = useState(0); const [driver, setDriver] = useState(DEFAULT_DRIVER_KIND); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 20ebafbce40..6e08fa68ef1 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -36,7 +36,7 @@ import { TraitsPicker } from "../chat/TraitsPicker"; import { isElectron } from "../../env"; import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; import { useTheme } from "../../hooks/useTheme"; -import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; +import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { @@ -373,8 +373,8 @@ function AboutVersionSection() { export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme } = useTheme(); - const settings = useSettings(); - const updateSettings = useUpdateSettings(); + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, @@ -479,8 +479,8 @@ export function useSettingsRestore(onRestored?: () => void) { export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); - const settings = useSettings(); - const updateSettings = useUpdateSettings(); + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); const observability = useAtomValue(primaryServerObservabilityAtom); const serverProviders = useAtomValue(primaryServerProvidersAtom); const diagnosticsDescription = formatDiagnosticsDescription({ @@ -977,8 +977,8 @@ export function GeneralSettingsPanel() { } export function ProviderSettingsPanel() { - const settings = useSettings(); - const updateSettings = useUpdateSettings(); + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); const serverProviders = useAtomValue(primaryServerProvidersAtom); const primaryEnvironment = usePrimaryEnvironment(); const refreshServerProviders = useAtomCommand(serverEnvironment.refreshProviders, { diff --git a/apps/web/src/components/settings/SourceControlSettings.tsx b/apps/web/src/components/settings/SourceControlSettings.tsx index db1b2393626..b6d23de4f79 100644 --- a/apps/web/src/components/settings/SourceControlSettings.tsx +++ b/apps/web/src/components/settings/SourceControlSettings.tsx @@ -12,7 +12,7 @@ import type { } from "@t3tools/contracts"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; -import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; +import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { usePrimaryEnvironment } from "../../state/environments"; import { useEnvironmentQuery } from "../../state/query"; @@ -291,8 +291,10 @@ function DiscoveryItemRow({ } function GitFetchIntervalSettings() { - const automaticGitFetchInterval = useSettings((settings) => settings.automaticGitFetchInterval); - const updateSettings = useUpdateSettings(); + const automaticGitFetchInterval = usePrimarySettings( + (settings) => settings.automaticGitFetchInterval, + ); + const updateSettings = useUpdatePrimarySettings(); const automaticGitFetchIntervalSeconds = durationToSeconds(automaticGitFetchInterval); const defaultAutomaticGitFetchIntervalSeconds = durationToSeconds( DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index c99ae0af9b8..1b1c07b31c9 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -3,7 +3,11 @@ import { scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime/environment"; -import { DEFAULT_RUNTIME_MODE, type ScopedProjectRef } from "@t3tools/contracts"; +import { + DEFAULT_RUNTIME_MODE, + DEFAULT_SERVER_SETTINGS, + type ScopedProjectRef, +} from "@t3tools/contracts"; import { useParams, useRouter } from "@tanstack/react-router"; import { useCallback, useMemo } from "react"; import { @@ -19,18 +23,16 @@ import { getProjectOrderKey, selectProjectGroupingSettings, } from "../logicalProject"; -import { readThreadShell, useProjects, useThread } from "../state/entities"; +import { readThreadShell, useProjects, useServerConfigs, useThread } from "../state/entities"; import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; import { resolveThreadRouteTarget } from "../threadRoutes"; import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; -import { useSettings } from "./useSettings"; +import { useClientSettings } from "./useSettings"; export function useNewThreadHandler() { const projects = useProjects(); - const projectGroupingSettings = useSettings(selectProjectGroupingSettings); - const newWorktreesStartFromOrigin = useSettings( - (settings) => settings.newWorktreesStartFromOrigin, - ); + const serverConfigs = useServerConfigs(); + const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const router = useRouter(); const getCurrentRouteTarget = useCallback(() => { const currentRouteParams = router.state.matches[router.state.matches.length - 1]?.params ?? {}; @@ -61,6 +63,8 @@ export function useNewThreadHandler() { candidate.id === projectRef.projectId && candidate.environmentId === projectRef.environmentId, ); + const environmentSettings = + serverConfigs.get(projectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS; const logicalProjectKey = project ? deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings) : scopedProjectKey(projectRef); @@ -155,7 +159,7 @@ export function useNewThreadHandler() { const draftId = newDraftId(); const threadId = newThreadId(); const createdAt = new Date().toISOString(); - const initialEnvMode = options?.envMode ?? "local"; + const initialEnvMode = options?.envMode ?? environmentSettings.defaultThreadEnvMode; return (async () => { setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, draftId, { threadId, @@ -167,7 +171,7 @@ export function useNewThreadHandler() { options?.startFromOrigin ?? resolveNewDraftStartFromOrigin({ envMode: initialEnvMode, - newWorktreesStartFromOrigin, + newWorktreesStartFromOrigin: environmentSettings.newWorktreesStartFromOrigin, }), runtimeMode: DEFAULT_RUNTIME_MODE, }); @@ -179,7 +183,7 @@ export function useNewThreadHandler() { }); })(); }, - [newWorktreesStartFromOrigin, getCurrentRouteTarget, projectGroupingSettings, router, projects], + [getCurrentRouteTarget, projectGroupingSettings, projects, router, serverConfigs], ); } diff --git a/apps/web/src/hooks/useSettings.test.ts b/apps/web/src/hooks/useSettings.test.ts new file mode 100644 index 00000000000..7132c84c9d3 --- /dev/null +++ b/apps/web/src/hooks/useSettings.test.ts @@ -0,0 +1,37 @@ +import { + DEFAULT_SERVER_SETTINGS, + ProviderDriverKind, + ProviderInstanceId, +} from "@t3tools/contracts"; +import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; +import { describe, expect, it } from "vite-plus/test"; + +import { mergeEnvironmentSettings } from "./useSettings"; + +describe("mergeEnvironmentSettings", () => { + it("combines the selected environment's server settings with client preferences", () => { + const serverSettings = { + ...DEFAULT_SERVER_SETTINGS, + providerInstances: { + [ProviderInstanceId.make("codex_remote")]: { + driver: ProviderDriverKind.make("codex"), + enabled: true, + }, + }, + }; + const clientSettings = { + ...DEFAULT_CLIENT_SETTINGS, + favorites: [ + { + provider: ProviderInstanceId.make("codex_remote"), + model: "gpt-5.4", + }, + ], + }; + + const settings = mergeEnvironmentSettings(serverSettings, clientSettings); + + expect(settings.providerInstances).toBe(serverSettings.providerInstances); + expect(settings.favorites).toBe(clientSettings.favorites); + }); +}); diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 6759b227a13..bf8b3a7dd08 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -1,22 +1,27 @@ /** - * Unified settings hook. + * Environment-scoped settings hooks. * * Abstracts the split between server-authoritative settings (persisted in * `settings.json` on the server, fetched via `server.getConfig`) and * client-only settings (persisted in localStorage). * - * Consumers use `useSettings(selector)` to read, and `useUpdateSettings()` to - * write. The hook transparently routes reads/writes to the correct backing - * store. + * Live server settings always require an environment id. Primary-environment + * access is intentionally named as such so environment-sensitive consumers + * cannot silently read the wrong server's settings. */ import { useCallback, useMemo, useSyncExternalStore } from "react"; import { useAtomValue } from "@effect/atom-react"; -import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts"; +import { + DEFAULT_SERVER_SETTINGS, + type EnvironmentId, + ServerSettings, + type ServerSettingsPatch, +} from "@t3tools/contracts"; import { type ClientSettingsPatch, type ClientSettings, DEFAULT_CLIENT_SETTINGS, - UnifiedSettings, + type UnifiedSettings, } from "@t3tools/contracts/settings"; import { ensureLocalApi } from "~/localApi"; import * as Struct from "effect/Struct"; @@ -153,11 +158,6 @@ function splitPatch(patch: Partial): { // ── Hooks ──────────────────────────────────────────────────────────── -/** - * Read merged settings. Selector narrows the subscription so components - * only re-render when the slice they care about changes. - */ - /** * Non-hook accessor for the current merged client settings snapshot. * Used by non-React code paths (e.g. runtime services) that need the latest @@ -175,45 +175,77 @@ export function useClientSettingsHydrated(): boolean { ); } -export function useSettings(selector?: (s: UnifiedSettings) => T): T { - const serverSettings = useAtomValue(primaryServerSettingsAtom); - const clientSettings = useSyncExternalStore( +function useClientSettingsValue(): ClientSettings { + return useSyncExternalStore( subscribeClientSettings, getClientSettingsSnapshot, () => DEFAULT_CLIENT_SETTINGS, ); +} + +export function mergeEnvironmentSettings( + serverSettings: ServerSettings, + clientSettings: ClientSettings, +): UnifiedSettings { + return { ...serverSettings, ...clientSettings }; +} + +function useMergedSettings( + serverSettings: ServerSettings, + selector: ((settings: UnifiedSettings) => T) | undefined, +): T { + const clientSettings = useClientSettingsValue(); const merged = useMemo( - () => ({ - ...serverSettings, - ...clientSettings, - }), + () => mergeEnvironmentSettings(serverSettings, clientSettings), [clientSettings, serverSettings], ); return useMemo(() => (selector ? selector(merged) : (merged as T)), [merged, selector]); } +export function useClientSettings( + selector?: (settings: ClientSettings) => T, +): T { + const settings = useClientSettingsValue(); + return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); +} + +/** Read current settings for one environment, merged with client-local preferences. */ +export function useEnvironmentSettings( + environmentId: EnvironmentId, + selector?: (settings: UnifiedSettings) => T, +): T { + const serverSettings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId)); + return useMergedSettings(serverSettings ?? DEFAULT_SERVER_SETTINGS, selector); +} + +/** Primary-only settings access for the settings UI and other explicitly global surfaces. */ +export function usePrimarySettings( + selector?: (settings: UnifiedSettings) => T, +): T { + return useMergedSettings(useAtomValue(primaryServerSettingsAtom), selector); +} + /** * Returns an updater that routes each key to the correct backing store. * * Server keys are optimistically patched in atom-backed server state, then * persisted via RPC. Client keys go through client persistence. */ -export function useUpdateSettings() { +function useUpdateSettingsTarget(environmentId: EnvironmentId | null) { const persistServerSettings = useAtomCommand( serverEnvironment.updateSettings, "server settings update", ); - const primaryEnvironment = usePrimaryEnvironment(); const updateSettings = useCallback( (patch: Partial) => { const { serverPatch, clientPatch } = splitPatch(patch); if (Object.keys(serverPatch).length > 0) { - if (primaryEnvironment) { + if (environmentId) { void persistServerSettings({ - environmentId: primaryEnvironment.environmentId, + environmentId, input: { patch: serverPatch }, }); } @@ -226,12 +258,29 @@ export function useUpdateSettings() { }); } }, - [persistServerSettings, primaryEnvironment], + [environmentId, persistServerSettings], ); return updateSettings; } +export function useUpdateEnvironmentSettings(environmentId: EnvironmentId) { + return useUpdateSettingsTarget(environmentId); +} + +export function useUpdatePrimarySettings() { + return useUpdateSettingsTarget(usePrimaryEnvironment()?.environmentId ?? null); +} + +export function useUpdateClientSettings() { + return useCallback((patch: ClientSettingsPatch) => { + persistClientSettings({ + ...getClientSettingsSnapshot(), + ...patch, + }); + }, []); +} + export function __resetClientSettingsPersistenceForTests(): void { clientSettingsHydrationGeneration += 1; clientSettingsSnapshot = DEFAULT_CLIENT_SETTINGS; diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index f174ed8e6c6..35783348068 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -24,7 +24,7 @@ import { useTerminalUiStateStore } from "../terminalUiStateStore"; import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; import { stackedThreadToast, toastManager } from "../components/ui/toast"; -import { useSettings } from "./useSettings"; +import { useClientSettings } from "./useSettings"; import { useAtomCommand } from "../state/use-atom-command"; export class ThreadArchiveBlockedError extends Data.TaggedError("ThreadArchiveBlockedError")<{ @@ -49,8 +49,8 @@ export function useThreadActions() { const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false, }); - const sidebarThreadSortOrder = useSettings((settings) => settings.sidebarThreadSortOrder); - const confirmThreadDelete = useSettings((settings) => settings.confirmThreadDelete); + const sidebarThreadSortOrder = useClientSettings((settings) => settings.sidebarThreadSortOrder); + const confirmThreadDelete = useClientSettings((settings) => settings.confirmThreadDelete); const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearDraftThread); const clearProjectDraftThreadById = useComposerDraftStore( (store) => store.clearProjectDraftThreadById, diff --git a/apps/web/src/lib/chatThreadActions.test.ts b/apps/web/src/lib/chatThreadActions.test.ts index 62e5aa41d43..2b1d7b09b9f 100644 --- a/apps/web/src/lib/chatThreadActions.test.ts +++ b/apps/web/src/lib/chatThreadActions.test.ts @@ -18,7 +18,6 @@ function createContext(overrides: Partial = {}): ChatTh activeDraftThread: null, activeThread: undefined, defaultProjectRef: scopeProjectRef(ENVIRONMENT_ID, FALLBACK_PROJECT_ID), - defaultThreadEnvMode: "local", handleNewThread: async () => {}, ...overrides, }; @@ -118,21 +117,18 @@ describe("chatThreadActions", () => { }); }); - it("starts a local thread with the configured default env mode", async () => { + it("delegates the target environment defaults to the new-thread handler", async () => { const handleNewThread = vi.fn(async () => {}); const didStart = await startNewLocalThreadFromContext( createContext({ defaultProjectRef: scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID), - defaultThreadEnvMode: "worktree", handleNewThread, }), ); expect(didStart).toBe(true); - expect(handleNewThread).toHaveBeenCalledWith(scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID), { - envMode: "worktree", - }); + expect(handleNewThread).toHaveBeenCalledWith(scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID)); }); it("does not start a thread when there is no project context", async () => { diff --git a/apps/web/src/lib/chatThreadActions.ts b/apps/web/src/lib/chatThreadActions.ts index 63d0289d104..4f30885610a 100644 --- a/apps/web/src/lib/chatThreadActions.ts +++ b/apps/web/src/lib/chatThreadActions.ts @@ -32,7 +32,6 @@ export interface ChatThreadActionContext { readonly activeDraftThread: DraftThreadContextLike | null; readonly activeThread: ThreadContextLike | undefined; readonly defaultProjectRef: ScopedProjectRef | null; - readonly defaultThreadEnvMode: DraftThreadEnvMode; readonly handleNewThread: NewThreadHandler; } @@ -72,12 +71,6 @@ function buildContextualThreadOptions(context: ChatThreadActionContext): NewThre }; } -function buildDefaultThreadOptions(context: ChatThreadActionContext): NewThreadOptions { - return { - envMode: context.defaultThreadEnvMode, - }; -} - export async function startNewThreadInProjectFromContext( context: ChatThreadActionContext, projectRef: ScopedProjectRef, @@ -105,6 +98,6 @@ export async function startNewLocalThreadFromContext( return false; } - await context.handleNewThread(projectRef, buildDefaultThreadOptions(context)); + await context.handleNewThread(projectRef); return true; } diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index d01518a3858..a75b917bb0d 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -25,7 +25,7 @@ import { toastManager, } from "../components/ui/toast"; import { resolveAndPersistPreferredEditor } from "../editorPreferences"; -import { useSettings } from "../hooks/useSettings"; +import { useClientSettings } from "../hooks/useSettings"; import { deriveLogicalProjectKeyFromSettings, derivePhysicalProjectKeyFromPath, @@ -237,7 +237,7 @@ function AuthenticatedTracingBootstrap() { function EventRouter() { const navigate = useNavigate(); const pathname = useLocation({ select: (loc) => loc.pathname }); - const projectGroupingSettings = useSettings(selectProjectGroupingSettings); + const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const primaryEnvironment = usePrimaryEnvironment(); const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false, diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index cc24ed6090d..9fb1eae721e 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -16,9 +16,7 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina import { isPreviewSupportedInRuntime } from "../previewStateStore"; import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; -import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; -import { useSettings } from "~/hooks/useSettings"; import { primaryServerKeybindingsAtom } from "~/state/server"; function ChatRouteGlobalShortcuts() { @@ -40,8 +38,6 @@ function ChatRouteGlobalShortcuts() { ? selectActiveRightPanel(state.byThreadKey, routeThreadRef) === "preview" : false, ); - const appSettings = useSettings(); - useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; @@ -71,9 +67,6 @@ function ChatRouteGlobalShortcuts() { activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, - defaultThreadEnvMode: resolveSidebarNewThreadEnvMode({ - defaultEnvMode: appSettings.defaultThreadEnvMode, - }), handleNewThread, }); return; @@ -86,9 +79,6 @@ function ChatRouteGlobalShortcuts() { activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, - defaultThreadEnvMode: resolveSidebarNewThreadEnvMode({ - defaultEnvMode: appSettings.defaultThreadEnvMode, - }), handleNewThread, }); return; @@ -153,7 +143,6 @@ function ChatRouteGlobalShortcuts() { routeThreadRef, selectedThreadKeysSize, terminalOpen, - appSettings.defaultThreadEnvMode, ]); return null; diff --git a/apps/web/src/state/entities.ts b/apps/web/src/state/entities.ts index 2d827e36b3a..b4fc8cc5e80 100644 --- a/apps/web/src/state/entities.ts +++ b/apps/web/src/state/entities.ts @@ -12,12 +12,14 @@ import type { OrchestrationThreadActivity, ScopedProjectRef, ScopedThreadRef, + ServerConfig, } from "@t3tools/contracts"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { useMemo } from "react"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentProjects } from "./projects"; +import { environmentServerConfigsAtom } from "./server"; import { environmentThreadDetails, environmentThreadShells } from "./threads"; const EMPTY_PROJECT_REFS: ReadonlyArray = Object.freeze([]); @@ -103,6 +105,10 @@ export function useProjects(): ReadonlyArray { return useAtomValue(environmentProjects.projectsAtom); } +export function useServerConfigs(): ReadonlyMap { + return useAtomValue(environmentServerConfigsAtom); +} + export function useThreadShells(): ReadonlyArray { return useAtomValue(environmentThreadShells.threadShellsAtom); } diff --git a/apps/web/src/state/environments.ts b/apps/web/src/state/environments.ts index 38d19f90b54..211c981c5f6 100644 --- a/apps/web/src/state/environments.ts +++ b/apps/web/src/state/environments.ts @@ -5,11 +5,11 @@ import { } from "@t3tools/client-runtime/connection"; import type { EnvironmentId } from "@t3tools/contracts"; import * as Option from "effect/Option"; -import { Atom } from "effect/unstable/reactivity"; import { useMemo } from "react"; import { environmentCatalog } from "../connection/catalog"; import { environmentPresentations, useEnvironmentPresentation } from "./presentation"; +import { primaryEnvironmentIdAtom } from "./primaryEnvironment"; import { useEnvironmentQuery } from "./query"; import { relayEnvironmentDiscovery } from "./relay"; import { usePreparedConnection } from "./session"; @@ -21,15 +21,6 @@ export interface EnvironmentPresentation extends BaseEnvironmentPresentation { readonly relayManaged: boolean; } -export const primaryEnvironmentIdAtom = Atom.make((get) => { - for (const [environmentId, entry] of get(environmentCatalog.catalogValueAtom).entries) { - if (entry.target._tag === "PrimaryConnectionTarget") { - return environmentId; - } - } - return null; -}).pipe(Atom.withLabel("web-primary-environment-id")); - function projectEnvironmentPresentation( environmentId: EnvironmentId, presentation: BaseEnvironmentPresentation, diff --git a/apps/web/src/state/presentation.ts b/apps/web/src/state/presentation.ts index 0a4cfd12556..1c2fb7b6a62 100644 --- a/apps/web/src/state/presentation.ts +++ b/apps/web/src/state/presentation.ts @@ -5,12 +5,12 @@ import type { EnvironmentId } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { environmentCatalog } from "../connection/catalog"; -import { environmentSession } from "./session"; +import { serverEnvironment } from "./server"; export const environmentPresentations = createEnvironmentPresentationAtoms({ catalogValueAtom: environmentCatalog.catalogValueAtom, stateAtom: environmentCatalog.stateAtom, - configValueAtom: environmentSession.configValueAtom, + serverConfigValueAtom: serverEnvironment.configValueAtom, }); const EMPTY_ENVIRONMENT_PRESENTATION_ATOM = Atom.make(null).pipe( diff --git a/apps/web/src/state/primaryEnvironment.ts b/apps/web/src/state/primaryEnvironment.ts new file mode 100644 index 00000000000..e37931f74a8 --- /dev/null +++ b/apps/web/src/state/primaryEnvironment.ts @@ -0,0 +1,12 @@ +import { Atom } from "effect/unstable/reactivity"; + +import { environmentCatalog } from "../connection/catalog"; + +export const primaryEnvironmentIdAtom = Atom.make((get) => { + for (const [environmentId, entry] of get(environmentCatalog.catalogValueAtom).entries) { + if (entry.target._tag === "PrimaryConnectionTarget") { + return environmentId; + } + } + return null; +}).pipe(Atom.withLabel("web-primary-environment-id")); diff --git a/apps/web/src/state/server.ts b/apps/web/src/state/server.ts index 94561f2f207..3271eefd1e1 100644 --- a/apps/web/src/state/server.ts +++ b/apps/web/src/state/server.ts @@ -15,15 +15,15 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { environmentCatalog } from "../connection/catalog"; import { connectionAtomRuntime } from "../connection/runtime"; -import { primaryEnvironmentIdAtom } from "./environments"; +import { primaryEnvironmentIdAtom } from "./primaryEnvironment"; import { environmentSession } from "./session"; export const serverEnvironment = createServerEnvironmentAtoms(connectionAtomRuntime, { - initialConfigValueAtom: environmentSession.configValueAtom, + initialConfigValueAtom: environmentSession.initialConfigValueAtom, }); export const environmentServerConfigsAtom = createEnvironmentServerConfigsAtom({ catalogValueAtom: environmentCatalog.catalogValueAtom, - configValueAtom: serverEnvironment.configValueAtom, + serverConfigValueAtom: serverEnvironment.configValueAtom, }); interface PrimaryServerState { diff --git a/packages/client-runtime/src/state/presentation.ts b/packages/client-runtime/src/state/presentation.ts index 1321ece93f8..d6fed0cf5ed 100644 --- a/packages/client-runtime/src/state/presentation.ts +++ b/packages/client-runtime/src/state/presentation.ts @@ -26,7 +26,8 @@ export function createEnvironmentPresentationAtoms(input: { readonly stateAtom: ( environmentId: EnvironmentId, ) => Atom.Atom>; - readonly configValueAtom: (environmentId: EnvironmentId) => Atom.Atom; + /** Authoritative live server config, including streamed provider/settings updates. */ + readonly serverConfigValueAtom: (environmentId: EnvironmentId) => Atom.Atom; }) { const presentationAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => { @@ -41,7 +42,7 @@ export function createEnvironmentPresentationAtoms(input: { return { entry, connection: presentEnvironmentConnection(state), - serverConfig: get(input.configValueAtom(environmentId)), + serverConfig: get(input.serverConfigValueAtom(environmentId)), } satisfies EnvironmentPresentation; }).pipe(Atom.withLabel(`environment-presentation:${environmentId}`)), ); diff --git a/packages/client-runtime/src/state/projectGrouping.ts b/packages/client-runtime/src/state/projectGrouping.ts index 549942be277..ca804c13809 100644 --- a/packages/client-runtime/src/state/projectGrouping.ts +++ b/packages/client-runtime/src/state/projectGrouping.ts @@ -1,6 +1,6 @@ import { scopedProjectKey, scopeProjectRef } from "../environment/scoped.ts"; import type { ScopedProjectRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; -import type { UnifiedSettings } from "@t3tools/contracts/settings"; +import type { ClientSettings } from "@t3tools/contracts/settings"; import type { EnvironmentProject } from "./models.ts"; import { normalizeProjectPathForComparison } from "./projects.ts"; @@ -12,7 +12,7 @@ export interface ProjectGroupingSettings { export type ProjectGroupingMode = SidebarProjectGroupingMode; -export function selectProjectGroupingSettings(settings: UnifiedSettings): ProjectGroupingSettings { +export function selectProjectGroupingSettings(settings: ClientSettings): ProjectGroupingSettings { return { sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode, sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index 23bb7bff2a9..eb784183793 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -118,9 +118,21 @@ export function createServerEnvironmentAtoms( return projection?.config ?? get(options.initialConfigValueAtom(environmentId)); }).pipe(Atom.withLabel(`environment-data:server:config:${environmentId}`)); }); + const settingsValueAtom = Atom.family((environmentId: EnvironmentId) => + Atom.make((get) => get(configValueAtom(environmentId))?.settings ?? null).pipe( + Atom.withLabel(`environment-data:server:settings:${environmentId}`), + ), + ); + const providersValueAtom = Atom.family((environmentId: EnvironmentId) => + Atom.make((get) => get(configValueAtom(environmentId))?.providers ?? null).pipe( + Atom.withLabel(`environment-data:server:providers:${environmentId}`), + ), + ); return { configValueAtom, + settingsValueAtom, + providersValueAtom, traceDiagnostics: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:trace-diagnostics", tag: WS_METHODS.serverGetTraceDiagnostics, diff --git a/packages/client-runtime/src/state/session.ts b/packages/client-runtime/src/state/session.ts index 97a637a9c5a..84e9dbdd8eb 100644 --- a/packages/client-runtime/src/state/session.ts +++ b/packages/client-runtime/src/state/session.ts @@ -26,7 +26,7 @@ export function initialConfigOption( export function createEnvironmentSessionAtoms( runtime: Atom.AtomRuntime, ) { - const configAtom = Atom.family((environmentId: EnvironmentId) => + const initialConfigAtom = Atom.family((environmentId: EnvironmentId) => runtime.atom( followStreamInEnvironment( environmentId, @@ -49,10 +49,15 @@ export function createEnvironmentSessionAtoms( ), ); - const configValueAtom = Atom.family((environmentId: EnvironmentId) => + // This is only the bootstrap config captured when a transport session is + // established. Consumers that need current provider/settings state must use + // createServerEnvironmentAtoms(...).configValueAtom instead. + const initialConfigValueAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get): ServerConfig | null => Option.getOrNull( - Option.getOrElse(AsyncResult.value(get(configAtom(environmentId))), () => Option.none()), + Option.getOrElse(AsyncResult.value(get(initialConfigAtom(environmentId))), () => + Option.none(), + ), ), ).pipe(Atom.withLabel(`environment-config-value:${environmentId}`)), ); @@ -80,8 +85,8 @@ export function createEnvironmentSessionAtoms( ); return { - configAtom, - configValueAtom, + initialConfigAtom, + initialConfigValueAtom, preparedConnectionAtom, preparedConnectionValueAtom, }; diff --git a/packages/client-runtime/src/state/shell.test.ts b/packages/client-runtime/src/state/shell.test.ts index fcde2ad7d80..f1326e0a5cb 100644 --- a/packages/client-runtime/src/state/shell.test.ts +++ b/packages/client-runtime/src/state/shell.test.ts @@ -75,7 +75,7 @@ function makeHarness() { }); const serverConfigsAtom = createEnvironmentServerConfigsAtom({ catalogValueAtom, - configValueAtom: configAtoms, + serverConfigValueAtom: configAtoms, }); return { diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index a697a4e2a6b..428e99b76d0 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -268,13 +268,13 @@ export function createEnvironmentShellSummaryAtom(input: { export function createEnvironmentServerConfigsAtom(input: { readonly catalogValueAtom: Atom.Atom; - readonly configValueAtom: (environmentId: EnvironmentId) => Atom.Atom; + readonly serverConfigValueAtom: (environmentId: EnvironmentId) => Atom.Atom; }) { let previousServerConfigs = EMPTY_SERVER_CONFIGS; return Atom.make((get) => { const next = new Map(); for (const environmentId of get(input.catalogValueAtom).entries.keys()) { - const config = get(input.configValueAtom(environmentId)); + const config = get(input.serverConfigValueAtom(environmentId)); if (config !== null) { next.set(environmentId, config); }