| author | |
| committer | |
| log | 79c57170c7fb9cc1844427aed93c5f297fb110b5 |
| tree | 9566c4e17cc4887f8485ad95363ce69e0988daf5 |
| parent | 3900c45f25049ea8df359736f0ea8ad97bd3624f |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>30 files changed, 254 insertions(+), 177 deletions(-)
apps/mobile/src/state/entities.ts+2-3| ... | ... | @@ -12,8 +12,7 @@ import type { |
| 12 | 12 | import { Atom } from "effect/unstable/reactivity"; |
| 13 | 13 | |
| 14 | 14 | import { environmentProjects } from "./projects"; |
| 15 | import { environmentServerConfigsAtom } from "./server"; | |
| 16 | import { environmentSession } from "./session"; | |
| 15 | import { environmentServerConfigsAtom, serverEnvironment } from "./server"; | |
| 17 | 16 | import { environmentThreadShells } from "./threads"; |
| 18 | 17 | |
| 19 | 18 | const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe( |
| ... | ... | @@ -50,7 +49,7 @@ export function useEnvironmentServerConfig( |
| 50 | 49 | return useAtomValue( |
| 51 | 50 | environmentId === null |
| 52 | 51 | ? EMPTY_SERVER_CONFIG_ATOM |
| 53 | : environmentSession.configValueAtom(environmentId), | |
| 52 | : serverEnvironment.configValueAtom(environmentId), | |
| 54 | 53 | ); |
| 55 | 54 | } |
| 56 | 55 |
apps/mobile/src/state/presentation.ts+2-2| ... | ... | @@ -5,12 +5,12 @@ import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | 5 | import { Atom } from "effect/unstable/reactivity"; |
| 6 | 6 | |
| 7 | 7 | import { environmentCatalog } from "../connection/catalog"; |
| 8 | import { environmentSession } from "./session"; | |
| 8 | import { serverEnvironment } from "./server"; | |
| 9 | 9 | |
| 10 | 10 | export const environmentPresentations = createEnvironmentPresentationAtoms({ |
| 11 | 11 | catalogValueAtom: environmentCatalog.catalogValueAtom, |
| 12 | 12 | stateAtom: environmentCatalog.stateAtom, |
| 13 | configValueAtom: environmentSession.configValueAtom, | |
| 13 | serverConfigValueAtom: serverEnvironment.configValueAtom, | |
| 14 | 14 | }); |
| 15 | 15 | |
| 16 | 16 | const EMPTY_ENVIRONMENT_PRESENTATION_ATOM = Atom.make<EnvironmentPresentation | null>(null).pipe( |
apps/mobile/src/state/server.ts+2-2| ... | ... | @@ -6,9 +6,9 @@ import { connectionAtomRuntime } from "../connection/runtime"; |
| 6 | 6 | import { environmentSession } from "./session"; |
| 7 | 7 | |
| 8 | 8 | export const serverEnvironment = createServerEnvironmentAtoms(connectionAtomRuntime, { |
| 9 | initialConfigValueAtom: environmentSession.configValueAtom, | |
| 9 | initialConfigValueAtom: environmentSession.initialConfigValueAtom, | |
| 10 | 10 | }); |
| 11 | 11 | export const environmentServerConfigsAtom = createEnvironmentServerConfigsAtom({ |
| 12 | 12 | catalogValueAtom: environmentCatalog.catalogValueAtom, |
| 13 | configValueAtom: serverEnvironment.configValueAtom, | |
| 13 | serverConfigValueAtom: serverEnvironment.configValueAtom, | |
| 14 | 14 | }); |
apps/web/src/components/ChatView.tsx+8-4| ... | ... | @@ -138,7 +138,7 @@ import { |
| 138 | 138 | } from "~/projectScripts"; |
| 139 | 139 | import { newDraftId, newMessageId, newThreadId } from "~/lib/utils"; |
| 140 | 140 | import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels"; |
| 141 | import { useSettings } from "../hooks/useSettings"; | |
| 141 | import { useEnvironmentSettings } from "../hooks/useSettings"; | |
| 142 | 142 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 143 | 143 | import { getTerminalFocusOwner } from "../lib/terminalFocus"; |
| 144 | 144 | import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; |
| ... | ... | @@ -1027,7 +1027,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1027 | 1027 | const activeThreadLastVisitedAt = useUiStateStore((store) => |
| 1028 | 1028 | routeKind === "server" ? store.threadLastVisitedAtById[routeThreadKey] : undefined, |
| 1029 | 1029 | ); |
| 1030 | const settings = useSettings(); | |
| 1030 | const settings = useEnvironmentSettings(environmentId); | |
| 1031 | 1031 | const setStickyComposerModelSelection = useComposerDraftStore( |
| 1032 | 1032 | (store) => store.setStickyModelSelection, |
| 1033 | 1033 | ); |
| ... | ... | @@ -1417,7 +1417,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1417 | 1417 | }, |
| 1418 | 1418 | [retryEnvironment], |
| 1419 | 1419 | ); |
| 1420 | const projectGroupingSettings = useSettings(selectProjectGroupingSettings); | |
| 1420 | const projectGroupingSettings = selectProjectGroupingSettings(settings); | |
| 1421 | 1421 | const logicalProjectEnvironments = useMemo(() => { |
| 1422 | 1422 | if (!activeProject) return []; |
| 1423 | 1423 | const logicalKey = deriveLogicalProjectKeyFromSettings(activeProject, projectGroupingSettings); |
| ... | ... | @@ -1586,7 +1586,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 1586 | 1586 | selectedProvider: selectedProviderByThreadId, |
| 1587 | 1587 | threadProvider, |
| 1588 | 1588 | }); |
| 1589 | const serverConfig = activeEnvironment?.serverConfig ?? primaryEnvironment?.serverConfig ?? null; | |
| 1589 | // Once a thread selects an environment, never substitute the primary | |
| 1590 | // environment's config while the selected environment is still loading. | |
| 1591 | const serverConfig = activeThread | |
| 1592 | ? (activeEnvironment?.serverConfig ?? null) | |
| 1593 | : (primaryEnvironment?.serverConfig ?? null); | |
| 1590 | 1594 | const versionMismatch = resolveServerConfigVersionMismatch(serverConfig); |
| 1591 | 1595 | const versionMismatchDismissKey = |
| 1592 | 1596 | versionMismatch && activeThread |
apps/web/src/components/CommandPalette.tsx+14-38| ... | ... | @@ -45,7 +45,7 @@ import { |
| 45 | 45 | import { useAtomValue } from "@effect/atom-react"; |
| 46 | 46 | import { OpenAddProjectCommandPaletteProvider } from "../commandPaletteContext"; |
| 47 | 47 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 48 | import { useSettings } from "../hooks/useSettings"; | |
| 48 | import { useClientSettings } from "../hooks/useSettings"; | |
| 49 | 49 | import { readLocalApi } from "../localApi"; |
| 50 | 50 | import { filesystemEnvironment } from "../state/filesystem"; |
| 51 | 51 | import { projectEnvironment } from "../state/projects"; |
| ... | ... | @@ -447,7 +447,7 @@ function OpenCommandPaletteDialog(props: { |
| 447 | 447 | const deferredQuery = useDeferredValue(query); |
| 448 | 448 | const isActionsOnly = deferredQuery.startsWith(">"); |
| 449 | 449 | const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null); |
| 450 | const settings = useSettings(); | |
| 450 | const clientSettings = useClientSettings(); | |
| 451 | 451 | const createProject = useAtomCommand(projectEnvironment.create, { |
| 452 | 452 | reportFailure: false, |
| 453 | 453 | }); |
| ... | ... | @@ -524,16 +524,14 @@ function OpenCommandPaletteDialog(props: { |
| 524 | 524 | const environment = environments.find( |
| 525 | 525 | (candidate) => candidate.environmentId === environmentId, |
| 526 | 526 | ); |
| 527 | const environmentSettings = | |
| 528 | environment?.serverConfig?.settings ?? | |
| 529 | (environmentId === primaryEnvironmentId ? settings : null); | |
| 527 | const environmentSettings = environment?.serverConfig?.settings ?? null; | |
| 530 | 528 | const baseDirectory = environmentSettings?.addProjectBaseDirectory?.trim() ?? ""; |
| 531 | 529 | if (baseDirectory.length === 0) { |
| 532 | 530 | return "~/"; |
| 533 | 531 | } |
| 534 | 532 | return ensureBrowseDirectoryPath(baseDirectory); |
| 535 | 533 | }, |
| 536 | [environments, primaryEnvironmentId, settings], | |
| 534 | [environments], | |
| 537 | 535 | ); |
| 538 | 536 | |
| 539 | 537 | const projectCwdById = useMemo( |
| ... | ... | @@ -589,7 +587,7 @@ function OpenCommandPaletteDialog(props: { |
| 589 | 587 | const latestThread = getLatestThreadForProject( |
| 590 | 588 | threads.filter((thread) => thread.environmentId === project.environmentId), |
| 591 | 589 | project.id, |
| 592 | settings.sidebarThreadSortOrder, | |
| 590 | clientSettings.sidebarThreadSortOrder, | |
| 593 | 591 | ); |
| 594 | 592 | if (latestThread) { |
| 595 | 593 | await navigate({ |
| ... | ... | @@ -601,17 +599,9 @@ function OpenCommandPaletteDialog(props: { |
| 601 | 599 | return; |
| 602 | 600 | } |
| 603 | 601 | |
| 604 | await handleNewThread(scopeProjectRef(project.environmentId, project.id), { | |
| 605 | envMode: settings.defaultThreadEnvMode, | |
| 606 | }); | |
| 602 | await handleNewThread(scopeProjectRef(project.environmentId, project.id)); | |
| 607 | 603 | }, |
| 608 | [ | |
| 609 | handleNewThread, | |
| 610 | navigate, | |
| 611 | settings.defaultThreadEnvMode, | |
| 612 | settings.sidebarThreadSortOrder, | |
| 613 | threads, | |
| 614 | ], | |
| 604 | [handleNewThread, navigate, clientSettings.sidebarThreadSortOrder, threads], | |
| 615 | 605 | ); |
| 616 | 606 | |
| 617 | 607 | const projectSearchItems = useMemo( |
| ... | ... | @@ -650,21 +640,13 @@ function OpenCommandPaletteDialog(props: { |
| 650 | 640 | activeDraftThread, |
| 651 | 641 | activeThread: activeThread ?? undefined, |
| 652 | 642 | defaultProjectRef, |
| 653 | defaultThreadEnvMode: settings.defaultThreadEnvMode, | |
| 654 | 643 | handleNewThread, |
| 655 | 644 | }, |
| 656 | 645 | scopeProjectRef(project.environmentId, project.id), |
| 657 | 646 | ); |
| 658 | 647 | }, |
| 659 | 648 | }), |
| 660 | [ | |
| 661 | activeDraftThread, | |
| 662 | activeThread, | |
| 663 | defaultProjectRef, | |
| 664 | handleNewThread, | |
| 665 | projects, | |
| 666 | settings.defaultThreadEnvMode, | |
| 667 | ], | |
| 649 | [activeDraftThread, activeThread, defaultProjectRef, handleNewThread, projects], | |
| 668 | 650 | ); |
| 669 | 651 | |
| 670 | 652 | const allThreadItems = useMemo( |
| ... | ... | @@ -673,7 +655,7 @@ function OpenCommandPaletteDialog(props: { |
| 673 | 655 | threads, |
| 674 | 656 | ...(activeThreadId ? { activeThreadId } : {}), |
| 675 | 657 | projectTitleById, |
| 676 | sortOrder: settings.sidebarThreadSortOrder, | |
| 658 | sortOrder: clientSettings.sidebarThreadSortOrder, | |
| 677 | 659 | icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />, |
| 678 | 660 | renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />, |
| 679 | 661 | renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />, |
| ... | ... | @@ -684,7 +666,7 @@ function OpenCommandPaletteDialog(props: { |
| 684 | 666 | }); |
| 685 | 667 | }, |
| 686 | 668 | }), |
| 687 | [activeThreadId, navigate, projectTitleById, settings.sidebarThreadSortOrder, threads], | |
| 669 | [activeThreadId, clientSettings.sidebarThreadSortOrder, navigate, projectTitleById, threads], | |
| 688 | 670 | ); |
| 689 | 671 | const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT); |
| 690 | 672 | |
| ... | ... | @@ -956,7 +938,6 @@ function OpenCommandPaletteDialog(props: { |
| 956 | 938 | activeDraftThread, |
| 957 | 939 | activeThread: activeThread ?? undefined, |
| 958 | 940 | defaultProjectRef, |
| 959 | defaultThreadEnvMode: settings.defaultThreadEnvMode, | |
| 960 | 941 | handleNewThread, |
| 961 | 942 | }); |
| 962 | 943 | }, |
| ... | ... | @@ -1072,7 +1053,7 @@ function OpenCommandPaletteDialog(props: { |
| 1072 | 1053 | const latestThread = getLatestThreadForProject( |
| 1073 | 1054 | threads.filter((thread) => thread.environmentId === existing.environmentId), |
| 1074 | 1055 | existing.id, |
| 1075 | settings.sidebarThreadSortOrder, | |
| 1056 | clientSettings.sidebarThreadSortOrder, | |
| 1076 | 1057 | ); |
| 1077 | 1058 | if (latestThread) { |
| 1078 | 1059 | await navigate({ |
| ... | ... | @@ -1083,9 +1064,7 @@ function OpenCommandPaletteDialog(props: { |
| 1083 | 1064 | }); |
| 1084 | 1065 | } else { |
| 1085 | 1066 | const navigationResult = await settlePromise(() => |
| 1086 | handleNewThread(scopeProjectRef(existing.environmentId, existing.id), { | |
| 1087 | envMode: settings.defaultThreadEnvMode, | |
| 1088 | }), | |
| 1067 | handleNewThread(scopeProjectRef(existing.environmentId, existing.id)), | |
| 1089 | 1068 | ); |
| 1090 | 1069 | if (navigationResult._tag === "Failure") { |
| 1091 | 1070 | const error = squashAtomCommandFailure(navigationResult); |
| ... | ... | @@ -1132,9 +1111,7 @@ function OpenCommandPaletteDialog(props: { |
| 1132 | 1111 | } |
| 1133 | 1112 | |
| 1134 | 1113 | const navigationResult = await settlePromise(() => |
| 1135 | handleNewThread(scopeProjectRef(browseEnvironmentId, projectId), { | |
| 1136 | envMode: settings.defaultThreadEnvMode, | |
| 1137 | }), | |
| 1114 | handleNewThread(scopeProjectRef(browseEnvironmentId, projectId)), | |
| 1138 | 1115 | ); |
| 1139 | 1116 | if (navigationResult._tag === "Failure") { |
| 1140 | 1117 | const error = squashAtomCommandFailure(navigationResult); |
| ... | ... | @@ -1158,8 +1135,7 @@ function OpenCommandPaletteDialog(props: { |
| 1158 | 1135 | navigate, |
| 1159 | 1136 | projects, |
| 1160 | 1137 | setOpen, |
| 1161 | settings.defaultThreadEnvMode, | |
| 1162 | settings.sidebarThreadSortOrder, | |
| 1138 | clientSettings.sidebarThreadSortOrder, | |
| 1163 | 1139 | threads, |
| 1164 | 1140 | ], |
| 1165 | 1141 | ); |
apps/web/src/components/DiffPanel.tsx+2-2| ... | ... | @@ -34,7 +34,7 @@ import { |
| 34 | 34 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 35 | 35 | import { useProject, useThread } from "../state/entities"; |
| 36 | 36 | import { resolveThreadRouteRef } from "../threadRoutes"; |
| 37 | import { useSettings } from "../hooks/useSettings"; | |
| 37 | import { useClientSettings } from "../hooks/useSettings"; | |
| 38 | 38 | import { formatShortTimestamp } from "../timestampFormat"; |
| 39 | 39 | import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; |
| 40 | 40 | import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableFileDiff"; |
| ... | ... | @@ -183,7 +183,7 @@ export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; |
| 183 | 183 | |
| 184 | 184 | export default function DiffPanel({ mode = "inline", composerDraftTarget }: DiffPanelProps) { |
| 185 | 185 | const { resolvedTheme } = useTheme(); |
| 186 | const settings = useSettings(); | |
| 186 | const settings = useClientSettings(); | |
| 187 | 187 | const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); |
| 188 | 188 | const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); |
| 189 | 189 | const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); |
apps/web/src/components/Sidebar.tsx+21-20| ... | ... | @@ -41,11 +41,11 @@ import { restrictToFirstScrollableAncestor, restrictToVerticalAxis } from "@dnd- |
| 41 | 41 | import { CSS } from "@dnd-kit/utilities"; |
| 42 | 42 | import { |
| 43 | 43 | type ContextMenuItem, |
| 44 | DEFAULT_SERVER_SETTINGS, | |
| 44 | 45 | ProjectId, |
| 45 | 46 | type ScopedThreadRef, |
| 46 | 47 | type ResolvedKeybindingsConfig, |
| 47 | 48 | type SidebarProjectGroupingMode, |
| 48 | type ThreadEnvMode, | |
| 49 | 49 | ThreadId, |
| 50 | 50 | } from "@t3tools/contracts"; |
| 51 | 51 | import { |
| ... | ... | @@ -77,6 +77,7 @@ import { |
| 77 | 77 | readThreadShell, |
| 78 | 78 | useProject, |
| 79 | 79 | useProjects, |
| 80 | useServerConfigs, | |
| 80 | 81 | useThreadShells, |
| 81 | 82 | useThreadShellsForProjectRefs, |
| 82 | 83 | } from "../state/entities"; |
| ... | ... | @@ -199,7 +200,7 @@ import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; |
| 199 | 200 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 200 | 201 | import { useIsMobile } from "~/hooks/useMediaQuery"; |
| 201 | 202 | import { CommandDialogTrigger } from "./ui/command"; |
| 202 | import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; | |
| 203 | import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; | |
| 203 | 204 | import { primaryServerConfigAtom, primaryServerKeybindingsAtom } from "../state/server"; |
| 204 | 205 | import { |
| 205 | 206 | derivePhysicalProjectKey, |
| ... | ... | @@ -1084,19 +1085,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1084 | 1085 | isManualProjectSorting, |
| 1085 | 1086 | dragHandleProps, |
| 1086 | 1087 | } = props; |
| 1087 | const threadSortOrder = useSettings<SidebarThreadSortOrder>( | |
| 1088 | const threadSortOrder = useClientSettings<SidebarThreadSortOrder>( | |
| 1088 | 1089 | (settings) => settings.sidebarThreadSortOrder, |
| 1089 | 1090 | ); |
| 1090 | const appSettingsConfirmThreadDelete = useSettings<boolean>( | |
| 1091 | const appSettingsConfirmThreadDelete = useClientSettings<boolean>( | |
| 1091 | 1092 | (settings) => settings.confirmThreadDelete, |
| 1092 | 1093 | ); |
| 1093 | const appSettingsConfirmThreadArchive = useSettings<boolean>( | |
| 1094 | const appSettingsConfirmThreadArchive = useClientSettings<boolean>( | |
| 1094 | 1095 | (settings) => settings.confirmThreadArchive, |
| 1095 | 1096 | ); |
| 1096 | const defaultThreadEnvMode = useSettings<ThreadEnvMode>( | |
| 1097 | (settings) => settings.defaultThreadEnvMode, | |
| 1098 | ); | |
| 1099 | const projectGroupingSettings = useSettings(selectProjectGroupingSettings); | |
| 1097 | const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); | |
| 1098 | const serverConfigs = useServerConfigs(); | |
| 1100 | 1099 | const deleteProject = useAtomCommand(projectEnvironment.delete, { |
| 1101 | 1100 | reportFailure: false, |
| 1102 | 1101 | }); |
| ... | ... | @@ -1106,8 +1105,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1106 | 1105 | const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, { |
| 1107 | 1106 | reportFailure: false, |
| 1108 | 1107 | }); |
| 1109 | const updateSettings = useUpdateSettings(); | |
| 1110 | const sidebarThreadPreviewCount = useSettings<SidebarThreadPreviewCount>( | |
| 1108 | const updateSettings = useUpdateClientSettings(); | |
| 1109 | const sidebarThreadPreviewCount = useClientSettings<SidebarThreadPreviewCount>( | |
| 1111 | 1110 | (settings) => settings.sidebarThreadPreviewCount, |
| 1112 | 1111 | ); |
| 1113 | 1112 | const router = useRouter(); |
| ... | ... | @@ -1840,7 +1839,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1840 | 1839 | const seedContext = resolveSidebarNewThreadSeedContext({ |
| 1841 | 1840 | projectId: member.id, |
| 1842 | 1841 | defaultEnvMode: resolveSidebarNewThreadEnvMode({ |
| 1843 | defaultEnvMode: defaultThreadEnvMode, | |
| 1842 | defaultEnvMode: | |
| 1843 | serverConfigs.get(member.environmentId)?.settings.defaultThreadEnvMode ?? | |
| 1844 | DEFAULT_SERVER_SETTINGS.defaultThreadEnvMode, | |
| 1844 | 1845 | }), |
| 1845 | 1846 | activeThread: |
| 1846 | 1847 | currentActiveThread && currentActiveThread.projectId === member.id |
| ... | ... | @@ -1889,7 +1890,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1889 | 1890 | } |
| 1890 | 1891 | })(); |
| 1891 | 1892 | }, |
| 1892 | [defaultThreadEnvMode, handleNewThread, isMobile, router, setOpenMobile], | |
| 1893 | [handleNewThread, isMobile, router, serverConfigs, setOpenMobile], | |
| 1893 | 1894 | ); |
| 1894 | 1895 | |
| 1895 | 1896 | const handleCreateThreadClick = useCallback( |
| ... | ... | @@ -2745,7 +2746,7 @@ interface SidebarProjectsContentProps { |
| 2745 | 2746 | threadSortOrder: SidebarThreadSortOrder; |
| 2746 | 2747 | projectGroupingMode: SidebarProjectGroupingMode; |
| 2747 | 2748 | threadPreviewCount: SidebarThreadPreviewCount; |
| 2748 | updateSettings: ReturnType<typeof useUpdateSettings>; | |
| 2749 | updateSettings: ReturnType<typeof useUpdateClientSettings>; | |
| 2749 | 2750 | openAddProject: () => void; |
| 2750 | 2751 | isManualProjectSorting: boolean; |
| 2751 | 2752 | projectDnDSensors: ReturnType<typeof useSensors>; |
| ... | ... | @@ -3014,12 +3015,12 @@ export default function Sidebar() { |
| 3014 | 3015 | const navigate = useNavigate(); |
| 3015 | 3016 | const pathname = useLocation({ select: (loc) => loc.pathname }); |
| 3016 | 3017 | const isOnSettings = pathname.startsWith("/settings"); |
| 3017 | const sidebarThreadSortOrder = useSettings((s) => s.sidebarThreadSortOrder); | |
| 3018 | const sidebarProjectSortOrder = useSettings((s) => s.sidebarProjectSortOrder); | |
| 3019 | const sidebarProjectGroupingMode = useSettings((s) => s.sidebarProjectGroupingMode); | |
| 3020 | const projectGroupingSettings = useSettings(selectProjectGroupingSettings); | |
| 3021 | const sidebarThreadPreviewCount = useSettings((s) => s.sidebarThreadPreviewCount); | |
| 3022 | const updateSettings = useUpdateSettings(); | |
| 3018 | const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder); | |
| 3019 | const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); | |
| 3020 | const sidebarProjectGroupingMode = useClientSettings((s) => s.sidebarProjectGroupingMode); | |
| 3021 | const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); | |
| 3022 | const sidebarThreadPreviewCount = useClientSettings((s) => s.sidebarThreadPreviewCount); | |
| 3023 | const updateSettings = useUpdateClientSettings(); | |
| 3023 | 3024 | const handleNewThread = useNewThreadHandler(); |
| 3024 | 3025 | const { archiveThread, deleteThread } = useThreadActions(); |
| 3025 | 3026 | const { isMobile, setOpenMobile } = useSidebar(); |
apps/web/src/components/chat/ModelPickerContent.tsx+3-3| ... | ... | @@ -19,7 +19,7 @@ import { |
| 19 | 19 | resolveShortcutCommand, |
| 20 | 20 | shortcutLabelForCommand, |
| 21 | 21 | } from "../../keybindings"; |
| 22 | import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; | |
| 22 | import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; | |
| 23 | 23 | import { cn } from "~/lib/utils"; |
| 24 | 24 | import { TooltipProvider } from "../ui/tooltip"; |
| 25 | 25 | import { |
| ... | ... | @@ -102,7 +102,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { |
| 102 | 102 | const searchInputRef = useRef<HTMLInputElement>(null); |
| 103 | 103 | const modelListRef = useRef<LegendListRef | null>(null); |
| 104 | 104 | const highlightedModelKeyRef = useRef<string | null>(null); |
| 105 | const favorites = useSettings((s) => s.favorites ?? []); | |
| 105 | const favorites = useClientSettings((s) => s.favorites ?? []); | |
| 106 | 106 | const [selectedInstanceId, setSelectedInstanceId] = useState<ProviderInstanceId | "favorites">( |
| 107 | 107 | () => { |
| 108 | 108 | if (props.lockedProvider !== null) { |
| ... | ... | @@ -117,7 +117,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { |
| 117 | 117 | () => providedKeybindings ?? [], |
| 118 | 118 | [providedKeybindings], |
| 119 | 119 | ); |
| 120 | const updateSettings = useUpdateSettings(); | |
| 120 | const updateSettings = useUpdateClientSettings(); | |
| 121 | 121 | |
| 122 | 122 | const focusSearchInput = useCallback(() => { |
| 123 | 123 | searchInputRef.current?.focus({ preventScroll: true }); |
apps/web/src/components/settings/AddProviderInstanceDialog.tsx+3-3| ... | ... | @@ -9,7 +9,7 @@ import { |
| 9 | 9 | type ProviderInstanceConfig, |
| 10 | 10 | } from "@t3tools/contracts"; |
| 11 | 11 | |
| 12 | import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; | |
| 12 | import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; | |
| 13 | 13 | import { cn } from "../../lib/utils"; |
| 14 | 14 | import { normalizeProviderAccentColor } from "../../providerInstances"; |
| 15 | 15 | import { Button } from "../ui/button"; |
| ... | ... | @@ -114,8 +114,8 @@ interface AddProviderInstanceDialogProps { |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { |
| 117 | const settings = useSettings(); | |
| 118 | const updateSettings = useUpdateSettings(); | |
| 117 | const settings = usePrimarySettings(); | |
| 118 | const updateSettings = useUpdatePrimarySettings(); | |
| 119 | 119 | |
| 120 | 120 | const [wizardStep, setWizardStep] = useState(0); |
| 121 | 121 | const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND); |
apps/web/src/components/settings/SettingsPanels.tsx+7-7| ... | ... | @@ -36,7 +36,7 @@ import { TraitsPicker } from "../chat/TraitsPicker"; |
| 36 | 36 | import { isElectron } from "../../env"; |
| 37 | 37 | import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; |
| 38 | 38 | import { useTheme } from "../../hooks/useTheme"; |
| 39 | import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; | |
| 39 | import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; | |
| 40 | 40 | import { useThreadActions } from "../../hooks/useThreadActions"; |
| 41 | 41 | import { useDesktopUpdateState } from "../../state/desktopUpdate"; |
| 42 | 42 | import { |
| ... | ... | @@ -373,8 +373,8 @@ function AboutVersionSection() { |
| 373 | 373 | |
| 374 | 374 | export function useSettingsRestore(onRestored?: () => void) { |
| 375 | 375 | const { theme, setTheme } = useTheme(); |
| 376 | const settings = useSettings(); | |
| 377 | const updateSettings = useUpdateSettings(); | |
| 376 | const settings = usePrimarySettings(); | |
| 377 | const updateSettings = useUpdatePrimarySettings(); | |
| 378 | 378 | |
| 379 | 379 | const isGitWritingModelDirty = !Equal.equals( |
| 380 | 380 | settings.textGenerationModelSelection ?? null, |
| ... | ... | @@ -479,8 +479,8 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 479 | 479 | |
| 480 | 480 | export function GeneralSettingsPanel() { |
| 481 | 481 | const { theme, setTheme } = useTheme(); |
| 482 | const settings = useSettings(); | |
| 483 | const updateSettings = useUpdateSettings(); | |
| 482 | const settings = usePrimarySettings(); | |
| 483 | const updateSettings = useUpdatePrimarySettings(); | |
| 484 | 484 | const observability = useAtomValue(primaryServerObservabilityAtom); |
| 485 | 485 | const serverProviders = useAtomValue(primaryServerProvidersAtom); |
| 486 | 486 | const diagnosticsDescription = formatDiagnosticsDescription({ |
| ... | ... | @@ -977,8 +977,8 @@ export function GeneralSettingsPanel() { |
| 977 | 977 | } |
| 978 | 978 | |
| 979 | 979 | export function ProviderSettingsPanel() { |
| 980 | const settings = useSettings(); | |
| 981 | const updateSettings = useUpdateSettings(); | |
| 980 | const settings = usePrimarySettings(); | |
| 981 | const updateSettings = useUpdatePrimarySettings(); | |
| 982 | 982 | const serverProviders = useAtomValue(primaryServerProvidersAtom); |
| 983 | 983 | const primaryEnvironment = usePrimaryEnvironment(); |
| 984 | 984 | const refreshServerProviders = useAtomCommand(serverEnvironment.refreshProviders, { |
apps/web/src/components/settings/SourceControlSettings.tsx+5-3| ... | ... | @@ -12,7 +12,7 @@ import type { |
| 12 | 12 | } from "@t3tools/contracts"; |
| 13 | 13 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; |
| 14 | 14 | |
| 15 | import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; | |
| 15 | import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; | |
| 16 | 16 | import { cn } from "../../lib/utils"; |
| 17 | 17 | import { usePrimaryEnvironment } from "../../state/environments"; |
| 18 | 18 | import { useEnvironmentQuery } from "../../state/query"; |
| ... | ... | @@ -291,8 +291,10 @@ function DiscoveryItemRow({ |
| 291 | 291 | } |
| 292 | 292 | |
| 293 | 293 | function GitFetchIntervalSettings() { |
| 294 | const automaticGitFetchInterval = useSettings((settings) => settings.automaticGitFetchInterval); | |
| 295 | const updateSettings = useUpdateSettings(); | |
| 294 | const automaticGitFetchInterval = usePrimarySettings( | |
| 295 | (settings) => settings.automaticGitFetchInterval, | |
| 296 | ); | |
| 297 | const updateSettings = useUpdatePrimarySettings(); | |
| 296 | 298 | const automaticGitFetchIntervalSeconds = durationToSeconds(automaticGitFetchInterval); |
| 297 | 299 | const defaultAutomaticGitFetchIntervalSeconds = durationToSeconds( |
| 298 | 300 | DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, |
apps/web/src/hooks/useHandleNewThread.ts+14-10| ... | ... | @@ -3,7 +3,11 @@ import { |
| 3 | 3 | scopeProjectRef, |
| 4 | 4 | scopeThreadRef, |
| 5 | 5 | } from "@t3tools/client-runtime/environment"; |
| 6 | import { DEFAULT_RUNTIME_MODE, type ScopedProjectRef } from "@t3tools/contracts"; | |
| 6 | import { | |
| 7 | DEFAULT_RUNTIME_MODE, | |
| 8 | DEFAULT_SERVER_SETTINGS, | |
| 9 | type ScopedProjectRef, | |
| 10 | } from "@t3tools/contracts"; | |
| 7 | 11 | import { useParams, useRouter } from "@tanstack/react-router"; |
| 8 | 12 | import { useCallback, useMemo } from "react"; |
| 9 | 13 | import { |
| ... | ... | @@ -19,18 +23,16 @@ import { |
| 19 | 23 | getProjectOrderKey, |
| 20 | 24 | selectProjectGroupingSettings, |
| 21 | 25 | } from "../logicalProject"; |
| 22 | import { readThreadShell, useProjects, useThread } from "../state/entities"; | |
| 26 | import { readThreadShell, useProjects, useServerConfigs, useThread } from "../state/entities"; | |
| 23 | 27 | import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; |
| 24 | 28 | import { resolveThreadRouteTarget } from "../threadRoutes"; |
| 25 | 29 | import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; |
| 26 | import { useSettings } from "./useSettings"; | |
| 30 | import { useClientSettings } from "./useSettings"; | |
| 27 | 31 | |
| 28 | 32 | export function useNewThreadHandler() { |
| 29 | 33 | const projects = useProjects(); |
| 30 | const projectGroupingSettings = useSettings(selectProjectGroupingSettings); | |
| 31 | const newWorktreesStartFromOrigin = useSettings( | |
| 32 | (settings) => settings.newWorktreesStartFromOrigin, | |
| 33 | ); | |
| 34 | const serverConfigs = useServerConfigs(); | |
| 35 | const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); | |
| 34 | 36 | const router = useRouter(); |
| 35 | 37 | const getCurrentRouteTarget = useCallback(() => { |
| 36 | 38 | const currentRouteParams = router.state.matches[router.state.matches.length - 1]?.params ?? {}; |
| ... | ... | @@ -61,6 +63,8 @@ export function useNewThreadHandler() { |
| 61 | 63 | candidate.id === projectRef.projectId && |
| 62 | 64 | candidate.environmentId === projectRef.environmentId, |
| 63 | 65 | ); |
| 66 | const environmentSettings = | |
| 67 | serverConfigs.get(projectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS; | |
| 64 | 68 | const logicalProjectKey = project |
| 65 | 69 | ? deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings) |
| 66 | 70 | : scopedProjectKey(projectRef); |
| ... | ... | @@ -155,7 +159,7 @@ export function useNewThreadHandler() { |
| 155 | 159 | const draftId = newDraftId(); |
| 156 | 160 | const threadId = newThreadId(); |
| 157 | 161 | const createdAt = new Date().toISOString(); |
| 158 | const initialEnvMode = options?.envMode ?? "local"; | |
| 162 | const initialEnvMode = options?.envMode ?? environmentSettings.defaultThreadEnvMode; | |
| 159 | 163 | return (async () => { |
| 160 | 164 | setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, draftId, { |
| 161 | 165 | threadId, |
| ... | ... | @@ -167,7 +171,7 @@ export function useNewThreadHandler() { |
| 167 | 171 | options?.startFromOrigin ?? |
| 168 | 172 | resolveNewDraftStartFromOrigin({ |
| 169 | 173 | envMode: initialEnvMode, |
| 170 | newWorktreesStartFromOrigin, | |
| 174 | newWorktreesStartFromOrigin: environmentSettings.newWorktreesStartFromOrigin, | |
| 171 | 175 | }), |
| 172 | 176 | runtimeMode: DEFAULT_RUNTIME_MODE, |
| 173 | 177 | }); |
| ... | ... | @@ -179,7 +183,7 @@ export function useNewThreadHandler() { |
| 179 | 183 | }); |
| 180 | 184 | })(); |
| 181 | 185 | }, |
| 182 | [newWorktreesStartFromOrigin, getCurrentRouteTarget, projectGroupingSettings, router, projects], | |
| 186 | [getCurrentRouteTarget, projectGroupingSettings, projects, router, serverConfigs], | |
| 183 | 187 | ); |
| 184 | 188 | } |
| 185 | 189 |
apps/web/src/hooks/useSettings.test.ts created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | import { | |
| 2 | DEFAULT_SERVER_SETTINGS, | |
| 3 | ProviderDriverKind, | |
| 4 | ProviderInstanceId, | |
| 5 | } from "@t3tools/contracts"; | |
| 6 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; | |
| 7 | import { describe, expect, it } from "vite-plus/test"; | |
| 8 | ||
| 9 | import { mergeEnvironmentSettings } from "./useSettings"; | |
| 10 | ||
| 11 | describe("mergeEnvironmentSettings", () => { | |
| 12 | it("combines the selected environment's server settings with client preferences", () => { | |
| 13 | const serverSettings = { | |
| 14 | ...DEFAULT_SERVER_SETTINGS, | |
| 15 | providerInstances: { | |
| 16 | [ProviderInstanceId.make("codex_remote")]: { | |
| 17 | driver: ProviderDriverKind.make("codex"), | |
| 18 | enabled: true, | |
| 19 | }, | |
| 20 | }, | |
| 21 | }; | |
| 22 | const clientSettings = { | |
| 23 | ...DEFAULT_CLIENT_SETTINGS, | |
| 24 | favorites: [ | |
| 25 | { | |
| 26 | provider: ProviderInstanceId.make("codex_remote"), | |
| 27 | model: "gpt-5.4", | |
| 28 | }, | |
| 29 | ], | |
| 30 | }; | |
| 31 | ||
| 32 | const settings = mergeEnvironmentSettings(serverSettings, clientSettings); | |
| 33 | ||
| 34 | expect(settings.providerInstances).toBe(serverSettings.providerInstances); | |
| 35 | expect(settings.favorites).toBe(clientSettings.favorites); | |
| 36 | }); | |
| 37 | }); |
apps/web/src/hooks/useSettings.ts+72-23| ... | ... | @@ -1,22 +1,27 @@ |
| 1 | 1 | /** |
| 2 | * Unified settings hook. | |
| 2 | * Environment-scoped settings hooks. | |
| 3 | 3 | * |
| 4 | 4 | * Abstracts the split between server-authoritative settings (persisted in |
| 5 | 5 | * `settings.json` on the server, fetched via `server.getConfig`) and |
| 6 | 6 | * client-only settings (persisted in localStorage). |
| 7 | 7 | * |
| 8 | * Consumers use `useSettings(selector)` to read, and `useUpdateSettings()` to | |
| 9 | * write. The hook transparently routes reads/writes to the correct backing | |
| 10 | * store. | |
| 8 | * Live server settings always require an environment id. Primary-environment | |
| 9 | * access is intentionally named as such so environment-sensitive consumers | |
| 10 | * cannot silently read the wrong server's settings. | |
| 11 | 11 | */ |
| 12 | 12 | import { useCallback, useMemo, useSyncExternalStore } from "react"; |
| 13 | 13 | import { useAtomValue } from "@effect/atom-react"; |
| 14 | import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts"; | |
| 14 | import { | |
| 15 | DEFAULT_SERVER_SETTINGS, | |
| 16 | type EnvironmentId, | |
| 17 | ServerSettings, | |
| 18 | type ServerSettingsPatch, | |
| 19 | } from "@t3tools/contracts"; | |
| 15 | 20 | import { |
| 16 | 21 | type ClientSettingsPatch, |
| 17 | 22 | type ClientSettings, |
| 18 | 23 | DEFAULT_CLIENT_SETTINGS, |
| 19 | UnifiedSettings, | |
| 24 | type UnifiedSettings, | |
| 20 | 25 | } from "@t3tools/contracts/settings"; |
| 21 | 26 | import { ensureLocalApi } from "~/localApi"; |
| 22 | 27 | import * as Struct from "effect/Struct"; |
| ... | ... | @@ -153,11 +158,6 @@ function splitPatch(patch: Partial<UnifiedSettings>): { |
| 153 | 158 | |
| 154 | 159 | // ── Hooks ──────────────────────────────────────────────────────────── |
| 155 | 160 | |
| 156 | /** | |
| 157 | * Read merged settings. Selector narrows the subscription so components | |
| 158 | * only re-render when the slice they care about changes. | |
| 159 | */ | |
| 160 | ||
| 161 | 161 | /** |
| 162 | 162 | * Non-hook accessor for the current merged client settings snapshot. |
| 163 | 163 | * Used by non-React code paths (e.g. runtime services) that need the latest |
| ... | ... | @@ -175,45 +175,77 @@ export function useClientSettingsHydrated(): boolean { |
| 175 | 175 | ); |
| 176 | 176 | } |
| 177 | 177 | |
| 178 | export function useSettings<T = UnifiedSettings>(selector?: (s: UnifiedSettings) => T): T { | |
| 179 | const serverSettings = useAtomValue(primaryServerSettingsAtom); | |
| 180 | const clientSettings = useSyncExternalStore( | |
| 178 | function useClientSettingsValue(): ClientSettings { | |
| 179 | return useSyncExternalStore( | |
| 181 | 180 | subscribeClientSettings, |
| 182 | 181 | getClientSettingsSnapshot, |
| 183 | 182 | () => DEFAULT_CLIENT_SETTINGS, |
| 184 | 183 | ); |
| 184 | } | |
| 185 | ||
| 186 | export function mergeEnvironmentSettings( | |
| 187 | serverSettings: ServerSettings, | |
| 188 | clientSettings: ClientSettings, | |
| 189 | ): UnifiedSettings { | |
| 190 | return { ...serverSettings, ...clientSettings }; | |
| 191 | } | |
| 192 | ||
| 193 | function useMergedSettings<T>( | |
| 194 | serverSettings: ServerSettings, | |
| 195 | selector: ((settings: UnifiedSettings) => T) | undefined, | |
| 196 | ): T { | |
| 197 | const clientSettings = useClientSettingsValue(); | |
| 185 | 198 | |
| 186 | 199 | const merged = useMemo<UnifiedSettings>( |
| 187 | () => ({ | |
| 188 | ...serverSettings, | |
| 189 | ...clientSettings, | |
| 190 | }), | |
| 200 | () => mergeEnvironmentSettings(serverSettings, clientSettings), | |
| 191 | 201 | [clientSettings, serverSettings], |
| 192 | 202 | ); |
| 193 | 203 | |
| 194 | 204 | return useMemo(() => (selector ? selector(merged) : (merged as T)), [merged, selector]); |
| 195 | 205 | } |
| 196 | 206 | |
| 207 | export function useClientSettings<T = ClientSettings>( | |
| 208 | selector?: (settings: ClientSettings) => T, | |
| 209 | ): T { | |
| 210 | const settings = useClientSettingsValue(); | |
| 211 | return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); | |
| 212 | } | |
| 213 | ||
| 214 | /** Read current settings for one environment, merged with client-local preferences. */ | |
| 215 | export function useEnvironmentSettings<T = UnifiedSettings>( | |
| 216 | environmentId: EnvironmentId, | |
| 217 | selector?: (settings: UnifiedSettings) => T, | |
| 218 | ): T { | |
| 219 | const serverSettings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId)); | |
| 220 | return useMergedSettings(serverSettings ?? DEFAULT_SERVER_SETTINGS, selector); | |
| 221 | } | |
| 222 | ||
| 223 | /** Primary-only settings access for the settings UI and other explicitly global surfaces. */ | |
| 224 | export function usePrimarySettings<T = UnifiedSettings>( | |
| 225 | selector?: (settings: UnifiedSettings) => T, | |
| 226 | ): T { | |
| 227 | return useMergedSettings(useAtomValue(primaryServerSettingsAtom), selector); | |
| 228 | } | |
| 229 | ||
| 197 | 230 | /** |
| 198 | 231 | * Returns an updater that routes each key to the correct backing store. |
| 199 | 232 | * |
| 200 | 233 | * Server keys are optimistically patched in atom-backed server state, then |
| 201 | 234 | * persisted via RPC. Client keys go through client persistence. |
| 202 | 235 | */ |
| 203 | export function useUpdateSettings() { | |
| 236 | function useUpdateSettingsTarget(environmentId: EnvironmentId | null) { | |
| 204 | 237 | const persistServerSettings = useAtomCommand( |
| 205 | 238 | serverEnvironment.updateSettings, |
| 206 | 239 | "server settings update", |
| 207 | 240 | ); |
| 208 | const primaryEnvironment = usePrimaryEnvironment(); | |
| 209 | 241 | const updateSettings = useCallback( |
| 210 | 242 | (patch: Partial<UnifiedSettings>) => { |
| 211 | 243 | const { serverPatch, clientPatch } = splitPatch(patch); |
| 212 | 244 | |
| 213 | 245 | if (Object.keys(serverPatch).length > 0) { |
| 214 | if (primaryEnvironment) { | |
| 246 | if (environmentId) { | |
| 215 | 247 | void persistServerSettings({ |
| 216 | environmentId: primaryEnvironment.environmentId, | |
| 248 | environmentId, | |
| 217 | 249 | input: { patch: serverPatch }, |
| 218 | 250 | }); |
| 219 | 251 | } |
| ... | ... | @@ -226,12 +258,29 @@ export function useUpdateSettings() { |
| 226 | 258 | }); |
| 227 | 259 | } |
| 228 | 260 | }, |
| 229 | [persistServerSettings, primaryEnvironment], | |
| 261 | [environmentId, persistServerSettings], | |
| 230 | 262 | ); |
| 231 | 263 | |
| 232 | 264 | return updateSettings; |
| 233 | 265 | } |
| 234 | 266 | |
| 267 | export function useUpdateEnvironmentSettings(environmentId: EnvironmentId) { | |
| 268 | return useUpdateSettingsTarget(environmentId); | |
| 269 | } | |
| 270 | ||
| 271 | export function useUpdatePrimarySettings() { | |
| 272 | return useUpdateSettingsTarget(usePrimaryEnvironment()?.environmentId ?? null); | |
| 273 | } | |
| 274 | ||
| 275 | export function useUpdateClientSettings() { | |
| 276 | return useCallback((patch: ClientSettingsPatch) => { | |
| 277 | persistClientSettings({ | |
| 278 | ...getClientSettingsSnapshot(), | |
| 279 | ...patch, | |
| 280 | }); | |
| 281 | }, []); | |
| 282 | } | |
| 283 | ||
| 235 | 284 | export function __resetClientSettingsPersistenceForTests(): void { |
| 236 | 285 | clientSettingsHydrationGeneration += 1; |
| 237 | 286 | clientSettingsSnapshot = DEFAULT_CLIENT_SETTINGS; |
apps/web/src/hooks/useThreadActions.ts+3-3| ... | ... | @@ -24,7 +24,7 @@ import { useTerminalUiStateStore } from "../terminalUiStateStore"; |
| 24 | 24 | import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes"; |
| 25 | 25 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 26 | 26 | import { stackedThreadToast, toastManager } from "../components/ui/toast"; |
| 27 | import { useSettings } from "./useSettings"; | |
| 27 | import { useClientSettings } from "./useSettings"; | |
| 28 | 28 | import { useAtomCommand } from "../state/use-atom-command"; |
| 29 | 29 | |
| 30 | 30 | export class ThreadArchiveBlockedError extends Data.TaggedError("ThreadArchiveBlockedError")<{ |
| ... | ... | @@ -49,8 +49,8 @@ export function useThreadActions() { |
| 49 | 49 | const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { |
| 50 | 50 | reportFailure: false, |
| 51 | 51 | }); |
| 52 | const sidebarThreadSortOrder = useSettings((settings) => settings.sidebarThreadSortOrder); | |
| 53 | const confirmThreadDelete = useSettings((settings) => settings.confirmThreadDelete); | |
| 52 | const sidebarThreadSortOrder = useClientSettings((settings) => settings.sidebarThreadSortOrder); | |
| 53 | const confirmThreadDelete = useClientSettings((settings) => settings.confirmThreadDelete); | |
| 54 | 54 | const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearDraftThread); |
| 55 | 55 | const clearProjectDraftThreadById = useComposerDraftStore( |
| 56 | 56 | (store) => store.clearProjectDraftThreadById, |
apps/web/src/lib/chatThreadActions.test.ts+2-6| ... | ... | @@ -18,7 +18,6 @@ function createContext(overrides: Partial<ChatThreadActionContext> = {}): ChatTh |
| 18 | 18 | activeDraftThread: null, |
| 19 | 19 | activeThread: undefined, |
| 20 | 20 | defaultProjectRef: scopeProjectRef(ENVIRONMENT_ID, FALLBACK_PROJECT_ID), |
| 21 | defaultThreadEnvMode: "local", | |
| 22 | 21 | handleNewThread: async () => {}, |
| 23 | 22 | ...overrides, |
| 24 | 23 | }; |
| ... | ... | @@ -118,21 +117,18 @@ describe("chatThreadActions", () => { |
| 118 | 117 | }); |
| 119 | 118 | }); |
| 120 | 119 | |
| 121 | it("starts a local thread with the configured default env mode", async () => { | |
| 120 | it("delegates the target environment defaults to the new-thread handler", async () => { | |
| 122 | 121 | const handleNewThread = vi.fn<ChatThreadActionContext["handleNewThread"]>(async () => {}); |
| 123 | 122 | |
| 124 | 123 | const didStart = await startNewLocalThreadFromContext( |
| 125 | 124 | createContext({ |
| 126 | 125 | defaultProjectRef: scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID), |
| 127 | defaultThreadEnvMode: "worktree", | |
| 128 | 126 | handleNewThread, |
| 129 | 127 | }), |
| 130 | 128 | ); |
| 131 | 129 | |
| 132 | 130 | expect(didStart).toBe(true); |
| 133 | expect(handleNewThread).toHaveBeenCalledWith(scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID), { | |
| 134 | envMode: "worktree", | |
| 135 | }); | |
| 131 | expect(handleNewThread).toHaveBeenCalledWith(scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID)); | |
| 136 | 132 | }); |
| 137 | 133 | |
| 138 | 134 | it("does not start a thread when there is no project context", async () => { |
apps/web/src/lib/chatThreadActions.ts+1-8| ... | ... | @@ -32,7 +32,6 @@ export interface ChatThreadActionContext { |
| 32 | 32 | readonly activeDraftThread: DraftThreadContextLike | null; |
| 33 | 33 | readonly activeThread: ThreadContextLike | undefined; |
| 34 | 34 | readonly defaultProjectRef: ScopedProjectRef | null; |
| 35 | readonly defaultThreadEnvMode: DraftThreadEnvMode; | |
| 36 | 35 | readonly handleNewThread: NewThreadHandler; |
| 37 | 36 | } |
| 38 | 37 | |
| ... | ... | @@ -72,12 +71,6 @@ function buildContextualThreadOptions(context: ChatThreadActionContext): NewThre |
| 72 | 71 | }; |
| 73 | 72 | } |
| 74 | 73 | |
| 75 | function buildDefaultThreadOptions(context: ChatThreadActionContext): NewThreadOptions { | |
| 76 | return { | |
| 77 | envMode: context.defaultThreadEnvMode, | |
| 78 | }; | |
| 79 | } | |
| 80 | ||
| 81 | 74 | export async function startNewThreadInProjectFromContext( |
| 82 | 75 | context: ChatThreadActionContext, |
| 83 | 76 | projectRef: ScopedProjectRef, |
| ... | ... | @@ -105,6 +98,6 @@ export async function startNewLocalThreadFromContext( |
| 105 | 98 | return false; |
| 106 | 99 | } |
| 107 | 100 | |
| 108 | await context.handleNewThread(projectRef, buildDefaultThreadOptions(context)); | |
| 101 | await context.handleNewThread(projectRef); | |
| 109 | 102 | return true; |
| 110 | 103 | } |
apps/web/src/routes/__root.tsx+2-2| ... | ... | @@ -26,7 +26,7 @@ import { |
| 26 | 26 | toastManager, |
| 27 | 27 | } from "../components/ui/toast"; |
| 28 | 28 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; |
| 29 | import { useSettings } from "../hooks/useSettings"; | |
| 29 | import { useClientSettings } from "../hooks/useSettings"; | |
| 30 | 30 | import { |
| 31 | 31 | deriveLogicalProjectKeyFromSettings, |
| 32 | 32 | derivePhysicalProjectKeyFromPath, |
| ... | ... | @@ -266,7 +266,7 @@ function AuthenticatedTracingBootstrap() { |
| 266 | 266 | function EventRouter() { |
| 267 | 267 | const navigate = useNavigate(); |
| 268 | 268 | const pathname = useLocation({ select: (loc) => loc.pathname }); |
| 269 | const projectGroupingSettings = useSettings(selectProjectGroupingSettings); | |
| 269 | const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); | |
| 270 | 270 | const primaryEnvironment = usePrimaryEnvironment(); |
| 271 | 271 | const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { |
| 272 | 272 | reportFailure: false, |
apps/web/src/routes/_chat.tsx-11| ... | ... | @@ -16,9 +16,7 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina |
| 16 | 16 | import { isPreviewSupportedInRuntime } from "../previewStateStore"; |
| 17 | 17 | import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; |
| 18 | 18 | import { useThreadSelectionStore } from "../threadSelectionStore"; |
| 19 | import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic"; | |
| 20 | 19 | import { stackedThreadToast, toastManager } from "~/components/ui/toast"; |
| 21 | import { useSettings } from "~/hooks/useSettings"; | |
| 22 | 20 | import { primaryServerKeybindingsAtom } from "~/state/server"; |
| 23 | 21 | |
| 24 | 22 | function ChatRouteGlobalShortcuts() { |
| ... | ... | @@ -40,8 +38,6 @@ function ChatRouteGlobalShortcuts() { |
| 40 | 38 | ? selectActiveRightPanel(state.byThreadKey, routeThreadRef) === "preview" |
| 41 | 39 | : false, |
| 42 | 40 | ); |
| 43 | const appSettings = useSettings(); | |
| 44 | ||
| 45 | 41 | useEffect(() => { |
| 46 | 42 | const onWindowKeyDown = (event: KeyboardEvent) => { |
| 47 | 43 | if (event.defaultPrevented) return; |
| ... | ... | @@ -71,9 +67,6 @@ function ChatRouteGlobalShortcuts() { |
| 71 | 67 | activeDraftThread, |
| 72 | 68 | activeThread: activeThread ?? undefined, |
| 73 | 69 | defaultProjectRef, |
| 74 | defaultThreadEnvMode: resolveSidebarNewThreadEnvMode({ | |
| 75 | defaultEnvMode: appSettings.defaultThreadEnvMode, | |
| 76 | }), | |
| 77 | 70 | handleNewThread, |
| 78 | 71 | }); |
| 79 | 72 | return; |
| ... | ... | @@ -86,9 +79,6 @@ function ChatRouteGlobalShortcuts() { |
| 86 | 79 | activeDraftThread, |
| 87 | 80 | activeThread: activeThread ?? undefined, |
| 88 | 81 | defaultProjectRef, |
| 89 | defaultThreadEnvMode: resolveSidebarNewThreadEnvMode({ | |
| 90 | defaultEnvMode: appSettings.defaultThreadEnvMode, | |
| 91 | }), | |
| 92 | 82 | handleNewThread, |
| 93 | 83 | }); |
| 94 | 84 | return; |
| ... | ... | @@ -153,7 +143,6 @@ function ChatRouteGlobalShortcuts() { |
| 153 | 143 | routeThreadRef, |
| 154 | 144 | selectedThreadKeysSize, |
| 155 | 145 | terminalOpen, |
| 156 | appSettings.defaultThreadEnvMode, | |
| 157 | 146 | ]); |
| 158 | 147 | |
| 159 | 148 | return null; |
apps/web/src/state/entities.ts+6| ... | ... | @@ -12,12 +12,14 @@ import type { |
| 12 | 12 | OrchestrationThreadActivity, |
| 13 | 13 | ScopedProjectRef, |
| 14 | 14 | ScopedThreadRef, |
| 15 | ServerConfig, | |
| 15 | 16 | } from "@t3tools/contracts"; |
| 16 | 17 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| 17 | 18 | import { Atom } from "effect/unstable/reactivity"; |
| 18 | 19 | import { useMemo } from "react"; |
| 19 | 20 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| 20 | 21 | import { environmentProjects } from "./projects"; |
| 22 | import { environmentServerConfigsAtom } from "./server"; | |
| 21 | 23 | import { environmentThreadDetails, environmentThreadShells } from "./threads"; |
| 22 | 24 | |
| 23 | 25 | const EMPTY_PROJECT_REFS: ReadonlyArray<ScopedProjectRef> = Object.freeze([]); |
| ... | ... | @@ -103,6 +105,10 @@ export function useProjects(): ReadonlyArray<EnvironmentProject> { |
| 103 | 105 | return useAtomValue(environmentProjects.projectsAtom); |
| 104 | 106 | } |
| 105 | 107 | |
| 108 | export function useServerConfigs(): ReadonlyMap<EnvironmentId, ServerConfig> { | |
| 109 | return useAtomValue(environmentServerConfigsAtom); | |
| 110 | } | |
| 111 | ||
| 106 | 112 | export function useThreadShells(): ReadonlyArray<EnvironmentThreadShell> { |
| 107 | 113 | return useAtomValue(environmentThreadShells.threadShellsAtom); |
| 108 | 114 | } |
apps/web/src/state/environments.ts+1-10| ... | ... | @@ -5,11 +5,11 @@ import { |
| 5 | 5 | } from "@t3tools/client-runtime/connection"; |
| 6 | 6 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 7 | 7 | import * as Option from "effect/Option"; |
| 8 | import { Atom } from "effect/unstable/reactivity"; | |
| 9 | 8 | import { useMemo } from "react"; |
| 10 | 9 | |
| 11 | 10 | import { environmentCatalog } from "../connection/catalog"; |
| 12 | 11 | import { environmentPresentations, useEnvironmentPresentation } from "./presentation"; |
| 12 | import { primaryEnvironmentIdAtom } from "./primaryEnvironment"; | |
| 13 | 13 | import { useEnvironmentQuery } from "./query"; |
| 14 | 14 | import { relayEnvironmentDiscovery } from "./relay"; |
| 15 | 15 | import { usePreparedConnection } from "./session"; |
| ... | ... | @@ -21,15 +21,6 @@ export interface EnvironmentPresentation extends BaseEnvironmentPresentation { |
| 21 | 21 | readonly relayManaged: boolean; |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | export const primaryEnvironmentIdAtom = Atom.make((get) => { | |
| 25 | for (const [environmentId, entry] of get(environmentCatalog.catalogValueAtom).entries) { | |
| 26 | if (entry.target._tag === "PrimaryConnectionTarget") { | |
| 27 | return environmentId; | |
| 28 | } | |
| 29 | } | |
| 30 | return null; | |
| 31 | }).pipe(Atom.withLabel("web-primary-environment-id")); | |
| 32 | ||
| 33 | 24 | function projectEnvironmentPresentation( |
| 34 | 25 | environmentId: EnvironmentId, |
| 35 | 26 | presentation: BaseEnvironmentPresentation, |
apps/web/src/state/presentation.ts+2-2| ... | ... | @@ -5,12 +5,12 @@ import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | 5 | import { Atom } from "effect/unstable/reactivity"; |
| 6 | 6 | |
| 7 | 7 | import { environmentCatalog } from "../connection/catalog"; |
| 8 | import { environmentSession } from "./session"; | |
| 8 | import { serverEnvironment } from "./server"; | |
| 9 | 9 | |
| 10 | 10 | export const environmentPresentations = createEnvironmentPresentationAtoms({ |
| 11 | 11 | catalogValueAtom: environmentCatalog.catalogValueAtom, |
| 12 | 12 | stateAtom: environmentCatalog.stateAtom, |
| 13 | configValueAtom: environmentSession.configValueAtom, | |
| 13 | serverConfigValueAtom: serverEnvironment.configValueAtom, | |
| 14 | 14 | }); |
| 15 | 15 | |
| 16 | 16 | const EMPTY_ENVIRONMENT_PRESENTATION_ATOM = Atom.make<EnvironmentPresentation | null>(null).pipe( |
apps/web/src/state/primaryEnvironment.ts created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | import { Atom } from "effect/unstable/reactivity"; | |
| 2 | ||
| 3 | import { environmentCatalog } from "../connection/catalog"; | |
| 4 | ||
| 5 | export const primaryEnvironmentIdAtom = Atom.make((get) => { | |
| 6 | for (const [environmentId, entry] of get(environmentCatalog.catalogValueAtom).entries) { | |
| 7 | if (entry.target._tag === "PrimaryConnectionTarget") { | |
| 8 | return environmentId; | |
| 9 | } | |
| 10 | } | |
| 11 | return null; | |
| 12 | }).pipe(Atom.withLabel("web-primary-environment-id")); |
apps/web/src/state/server.ts+3-3| ... | ... | @@ -15,15 +15,15 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 15 | 15 | |
| 16 | 16 | import { environmentCatalog } from "../connection/catalog"; |
| 17 | 17 | import { connectionAtomRuntime } from "../connection/runtime"; |
| 18 | import { primaryEnvironmentIdAtom } from "./environments"; | |
| 18 | import { primaryEnvironmentIdAtom } from "./primaryEnvironment"; | |
| 19 | 19 | import { environmentSession } from "./session"; |
| 20 | 20 | |
| 21 | 21 | export const serverEnvironment = createServerEnvironmentAtoms(connectionAtomRuntime, { |
| 22 | initialConfigValueAtom: environmentSession.configValueAtom, | |
| 22 | initialConfigValueAtom: environmentSession.initialConfigValueAtom, | |
| 23 | 23 | }); |
| 24 | 24 | export const environmentServerConfigsAtom = createEnvironmentServerConfigsAtom({ |
| 25 | 25 | catalogValueAtom: environmentCatalog.catalogValueAtom, |
| 26 | configValueAtom: serverEnvironment.configValueAtom, | |
| 26 | serverConfigValueAtom: serverEnvironment.configValueAtom, | |
| 27 | 27 | }); |
| 28 | 28 | |
| 29 | 29 | interface PrimaryServerState { |
packages/client-runtime/src/state/presentation.ts+3-2| ... | ... | @@ -26,7 +26,8 @@ export function createEnvironmentPresentationAtoms<E>(input: { |
| 26 | 26 | readonly stateAtom: ( |
| 27 | 27 | environmentId: EnvironmentId, |
| 28 | 28 | ) => Atom.Atom<AsyncResult.AsyncResult<SupervisorConnectionState, E>>; |
| 29 | readonly configValueAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>; | |
| 29 | /** Authoritative live server config, including streamed provider/settings updates. */ | |
| 30 | readonly serverConfigValueAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>; | |
| 30 | 31 | }) { |
| 31 | 32 | const presentationAtom = Atom.family((environmentId: EnvironmentId) => |
| 32 | 33 | Atom.make((get) => { |
| ... | ... | @@ -41,7 +42,7 @@ export function createEnvironmentPresentationAtoms<E>(input: { |
| 41 | 42 | return { |
| 42 | 43 | entry, |
| 43 | 44 | connection: presentEnvironmentConnection(state), |
| 44 | serverConfig: get(input.configValueAtom(environmentId)), | |
| 45 | serverConfig: get(input.serverConfigValueAtom(environmentId)), | |
| 45 | 46 | } satisfies EnvironmentPresentation; |
| 46 | 47 | }).pipe(Atom.withLabel(`environment-presentation:${environmentId}`)), |
| 47 | 48 | ); |
packages/client-runtime/src/state/projectGrouping.ts+2-2| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { scopedProjectKey, scopeProjectRef } from "../environment/scoped.ts"; |
| 2 | 2 | import type { ScopedProjectRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; |
| 3 | import type { UnifiedSettings } from "@t3tools/contracts/settings"; | |
| 3 | import type { ClientSettings } from "@t3tools/contracts/settings"; | |
| 4 | 4 | |
| 5 | 5 | import type { EnvironmentProject } from "./models.ts"; |
| 6 | 6 | import { normalizeProjectPathForComparison } from "./projects.ts"; |
| ... | ... | @@ -12,7 +12,7 @@ export interface ProjectGroupingSettings { |
| 12 | 12 | |
| 13 | 13 | export type ProjectGroupingMode = SidebarProjectGroupingMode; |
| 14 | 14 | |
| 15 | export function selectProjectGroupingSettings(settings: UnifiedSettings): ProjectGroupingSettings { | |
| 15 | export function selectProjectGroupingSettings(settings: ClientSettings): ProjectGroupingSettings { | |
| 16 | 16 | return { |
| 17 | 17 | sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode, |
| 18 | 18 | sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, |
packages/client-runtime/src/state/server.ts+12| ... | ... | @@ -118,9 +118,21 @@ export function createServerEnvironmentAtoms<R, E>( |
| 118 | 118 | return projection?.config ?? get(options.initialConfigValueAtom(environmentId)); |
| 119 | 119 | }).pipe(Atom.withLabel(`environment-data:server:config:${environmentId}`)); |
| 120 | 120 | }); |
| 121 | const settingsValueAtom = Atom.family((environmentId: EnvironmentId) => | |
| 122 | Atom.make((get) => get(configValueAtom(environmentId))?.settings ?? null).pipe( | |
| 123 | Atom.withLabel(`environment-data:server:settings:${environmentId}`), | |
| 124 | ), | |
| 125 | ); | |
| 126 | const providersValueAtom = Atom.family((environmentId: EnvironmentId) => | |
| 127 | Atom.make((get) => get(configValueAtom(environmentId))?.providers ?? null).pipe( | |
| 128 | Atom.withLabel(`environment-data:server:providers:${environmentId}`), | |
| 129 | ), | |
| 130 | ); | |
| 121 | 131 | |
| 122 | 132 | return { |
| 123 | 133 | configValueAtom, |
| 134 | settingsValueAtom, | |
| 135 | providersValueAtom, | |
| 124 | 136 | traceDiagnostics: createEnvironmentRpcQueryAtomFamily(runtime, { |
| 125 | 137 | label: "environment-data:server:trace-diagnostics", |
| 126 | 138 | tag: WS_METHODS.serverGetTraceDiagnostics, |
packages/client-runtime/src/state/session.ts+10-5| ... | ... | @@ -26,7 +26,7 @@ export function initialConfigOption<E>( |
| 26 | 26 | export function createEnvironmentSessionAtoms<R, E>( |
| 27 | 27 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>, |
| 28 | 28 | ) { |
| 29 | const configAtom = Atom.family((environmentId: EnvironmentId) => | |
| 29 | const initialConfigAtom = Atom.family((environmentId: EnvironmentId) => | |
| 30 | 30 | runtime.atom( |
| 31 | 31 | followStreamInEnvironment( |
| 32 | 32 | environmentId, |
| ... | ... | @@ -49,10 +49,15 @@ export function createEnvironmentSessionAtoms<R, E>( |
| 49 | 49 | ), |
| 50 | 50 | ); |
| 51 | 51 | |
| 52 | const configValueAtom = Atom.family((environmentId: EnvironmentId) => | |
| 52 | // This is only the bootstrap config captured when a transport session is | |
| 53 | // established. Consumers that need current provider/settings state must use | |
| 54 | // createServerEnvironmentAtoms(...).configValueAtom instead. | |
| 55 | const initialConfigValueAtom = Atom.family((environmentId: EnvironmentId) => | |
| 53 | 56 | Atom.make((get): ServerConfig | null => |
| 54 | 57 | Option.getOrNull( |
| 55 | Option.getOrElse(AsyncResult.value(get(configAtom(environmentId))), () => Option.none()), | |
| 58 | Option.getOrElse(AsyncResult.value(get(initialConfigAtom(environmentId))), () => | |
| 59 | Option.none(), | |
| 60 | ), | |
| 56 | 61 | ), |
| 57 | 62 | ).pipe(Atom.withLabel(`environment-config-value:${environmentId}`)), |
| 58 | 63 | ); |
| ... | ... | @@ -80,8 +85,8 @@ export function createEnvironmentSessionAtoms<R, E>( |
| 80 | 85 | ); |
| 81 | 86 | |
| 82 | 87 | return { |
| 83 | configAtom, | |
| 84 | configValueAtom, | |
| 88 | initialConfigAtom, | |
| 89 | initialConfigValueAtom, | |
| 85 | 90 | preparedConnectionAtom, |
| 86 | 91 | preparedConnectionValueAtom, |
| 87 | 92 | }; |
packages/client-runtime/src/state/shell.test.ts+1-1| ... | ... | @@ -75,7 +75,7 @@ function makeHarness() { |
| 75 | 75 | }); |
| 76 | 76 | const serverConfigsAtom = createEnvironmentServerConfigsAtom({ |
| 77 | 77 | catalogValueAtom, |
| 78 | configValueAtom: configAtoms, | |
| 78 | serverConfigValueAtom: configAtoms, | |
| 79 | 79 | }); |
| 80 | 80 | |
| 81 | 81 | return { |
packages/client-runtime/src/state/shell.ts+2-2| ... | ... | @@ -268,13 +268,13 @@ export function createEnvironmentShellSummaryAtom(input: { |
| 268 | 268 | |
| 269 | 269 | export function createEnvironmentServerConfigsAtom(input: { |
| 270 | 270 | readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>; |
| 271 | readonly configValueAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>; | |
| 271 | readonly serverConfigValueAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>; | |
| 272 | 272 | }) { |
| 273 | 273 | let previousServerConfigs = EMPTY_SERVER_CONFIGS; |
| 274 | 274 | return Atom.make((get) => { |
| 275 | 275 | const next = new Map<EnvironmentId, ServerConfig>(); |
| 276 | 276 | for (const environmentId of get(input.catalogValueAtom).entries.keys()) { |
| 277 | const config = get(input.configValueAtom(environmentId)); | |
| 277 | const config = get(input.serverConfigValueAtom(environmentId)); | |
| 278 | 278 | if (config !== null) { |
| 279 | 279 | next.set(environmentId, config); |
| 280 | 280 | } |