From 65c9f078f797aae03bbf36d3fac04160cc1b3ca6 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 22 Jul 2026 06:16:14 -0700 Subject: [PATCH] fix(web): read new-thread defaults from the primary environment The "New threads" default (local vs. new worktree) and "start from origin" are stored in the server-side settings.json, but the settings UI only ever writes to the primary environment's settings. New-thread code paths read the setting from the thread's own environment, so threads created on remote environments always fell back to the decoded defaults ("local" mode on the current branch) no matter what the user picked. Read the defaults from the primary server settings everywhere they seed new drafts: useNewThreadHandler, the sidebar's per-project new-thread seeding, and ChatView's env-mode/start-from-origin resolution. Co-Authored-By: Claude Fable 5 --- apps/web/src/components/ChatView.tsx | 11 ++++++++--- apps/web/src/components/Sidebar.tsx | 14 ++++++------- apps/web/src/hooks/useHandleNewThread.ts | 25 ++++++++++++------------ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f3145df4500..d0f6bababe9 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -188,6 +188,7 @@ import { useEnvironmentQuery } from "../state/query"; import { primaryServerAvailableEditorsAtom, primaryServerKeybindingsAtom, + primaryServerSettingsAtom, serverEnvironment, } from "../state/server"; import { terminalEnvironment } from "../state/terminal"; @@ -1139,6 +1140,10 @@ function ChatViewContent(props: ChatViewProps) { (store) => store.threadLastVisitedAtById[routeThreadKey], ); const settings = useEnvironmentSettings(environmentId); + // New-thread defaults live in the primary environment's settings.json (the + // settings UI never writes to remote environments), so read them from the + // primary server rather than the thread's environment. + const primaryServerSettings = useAtomValue(primaryServerSettingsAtom); const setStickyComposerModelSelection = useComposerDraftStore( (store) => store.setStickyModelSelection, ); @@ -3734,7 +3739,7 @@ function ChatViewContent(props: ChatViewProps) { ? (draftThread?.startFromOrigin ?? false) : canOverrideServerThreadEnvMode ? (pendingServerThreadStartFromOriginByThreadId[activeThread?.id ?? ""] ?? - settings.newWorktreesStartFromOrigin) + primaryServerSettings.newWorktreesStartFromOrigin) : false; const sendEnvMode = resolveSendEnvMode({ requestedEnvMode: envMode, @@ -5023,7 +5028,7 @@ function ChatViewContent(props: ChatViewProps) { envMode: mode, startFromOrigin: resolveNewDraftStartFromOrigin({ envMode: mode, - newWorktreesStartFromOrigin: settings.newWorktreesStartFromOrigin, + newWorktreesStartFromOrigin: primaryServerSettings.newWorktreesStartFromOrigin, }), ...(mode === "worktree" && draftThread?.worktreePath ? { worktreePath: null } : {}), }); @@ -5035,7 +5040,7 @@ function ChatViewContent(props: ChatViewProps) { composerDraftTarget, draftThread?.worktreePath, isLocalDraftThread, - settings.newWorktreesStartFromOrigin, + primaryServerSettings.newWorktreesStartFromOrigin, setPendingServerThreadEnvMode, scheduleComposerFocus, setDraftThreadContext, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index cf3fa30cf8d..217bdbd5af1 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -42,7 +42,6 @@ import { restrictToFirstScrollableAncestor, restrictToVerticalAxis } from "@dnd- import { CSS } from "@dnd-kit/utilities"; import { type ContextMenuItem, - DEFAULT_SERVER_SETTINGS, ProjectId, type ScopedThreadRef, type ResolvedKeybindingsConfig, @@ -80,7 +79,6 @@ import { readThreadShell, useProject, useProjects, - useServerConfigs, useThreadShells, useThreadShellsForProjectRefs, } from "../state/entities"; @@ -202,7 +200,7 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useIsMobile } from "~/hooks/useMediaQuery"; import { CommandDialogTrigger } from "./ui/command"; import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; -import { primaryServerKeybindingsAtom } from "../state/server"; +import { primaryServerKeybindingsAtom, primaryServerSettingsAtom } from "../state/server"; import { derivePhysicalProjectKey, deriveProjectGroupingOverrideKey, @@ -1106,7 +1104,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec (settings) => settings.confirmThreadArchive, ); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); - const serverConfigs = useServerConfigs(); + const primaryServerSettings = useAtomValue(primaryServerSettingsAtom); const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false, }); @@ -1894,10 +1892,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec : null; const seedContext = resolveSidebarNewThreadSeedContext({ projectId: member.id, + // The default env mode is a user preference stored on the primary + // environment's settings.json; remote environments never carry it. defaultEnvMode: resolveSidebarNewThreadEnvMode({ - defaultEnvMode: - serverConfigs.get(member.environmentId)?.settings.defaultThreadEnvMode ?? - DEFAULT_SERVER_SETTINGS.defaultThreadEnvMode, + defaultEnvMode: primaryServerSettings.defaultThreadEnvMode, }), activeThread: currentActiveThread && currentActiveThread.projectId === member.id @@ -1946,7 +1944,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec } })(); }, - [handleNewThread, isMobile, router, serverConfigs, setOpenMobile], + [handleNewThread, isMobile, primaryServerSettings.defaultThreadEnvMode, router, setOpenMobile], ); const handleCreateThreadClick = useCallback( diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index 232551349db..cd8546893b0 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -1,13 +1,10 @@ +import { useAtomValue } from "@effect/atom-react"; import { scopedProjectKey, scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime/environment"; -import { - DEFAULT_RUNTIME_MODE, - DEFAULT_SERVER_SETTINGS, - type ScopedProjectRef, -} from "@t3tools/contracts"; +import { DEFAULT_RUNTIME_MODE, type ScopedProjectRef } from "@t3tools/contracts"; import { useParams, useRouter } from "@tanstack/react-router"; import { useCallback, useMemo } from "react"; import { @@ -23,15 +20,21 @@ import { getProjectOrderKey, selectProjectGroupingSettings, } from "../logicalProject"; -import { readThreadShell, useProjects, useServerConfigs, useThread } from "../state/entities"; +import { readThreadShell, useProjects, useThread } from "../state/entities"; import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; +import { primaryServerSettingsAtom } from "../state/server"; import { resolveThreadRouteTarget } from "../threadRoutes"; import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; import { useClientSettings } from "./useSettings"; export function useNewThreadHandler() { const projects = useProjects(); - const serverConfigs = useServerConfigs(); + // New-thread defaults are a user preference, and the settings UI only ever + // edits the primary environment's settings.json. Reading the target + // environment's own settings here would silently reset remote projects to + // the decoded defaults ("local" mode, current branch), since nothing can + // set those values on a remote server. + const primaryServerSettings = useAtomValue(primaryServerSettingsAtom); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const router = useRouter(); const getCurrentRouteTarget = useCallback(() => { @@ -64,8 +67,6 @@ 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); @@ -161,7 +162,7 @@ export function useNewThreadHandler() { const draftId = newDraftId(); const threadId = newThreadId(); const createdAt = new Date().toISOString(); - const initialEnvMode = options?.envMode ?? environmentSettings.defaultThreadEnvMode; + const initialEnvMode = options?.envMode ?? primaryServerSettings.defaultThreadEnvMode; return (async () => { setLogicalProjectDraftThreadId(logicalProjectKey, projectRef, draftId, { threadId, @@ -173,7 +174,7 @@ export function useNewThreadHandler() { options?.startFromOrigin ?? resolveNewDraftStartFromOrigin({ envMode: initialEnvMode, - newWorktreesStartFromOrigin: environmentSettings.newWorktreesStartFromOrigin, + newWorktreesStartFromOrigin: primaryServerSettings.newWorktreesStartFromOrigin, }), runtimeMode: DEFAULT_RUNTIME_MODE, }); @@ -186,7 +187,7 @@ export function useNewThreadHandler() { }); })(); }, - [getCurrentRouteTarget, projectGroupingSettings, projects, router, serverConfigs], + [getCurrentRouteTarget, primaryServerSettings, projectGroupingSettings, projects, router], ); }