From cf1ba3d7da98357243ee056d2fd358eed8171981 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 11 Sep 2026 22:14:17 -0300 Subject: [PATCH] feat(settings): configure default permissions for new threads (#11346) --- .../threads/new-task-flow-provider.tsx | 8 ++- apps/server/src/serverRuntimeStartup.test.ts | 43 +++++++++++-- apps/server/src/serverRuntimeStartup.ts | 3 +- apps/web/src/components/ChatView.tsx | 17 +++-- apps/web/src/components/chat/ChatComposer.tsx | 33 +--------- .../src/components/chat/runtimeModeConfig.ts | 30 +++++++++ .../settings/ProjectDefaultsSettings.tsx | 64 +++++++++++++++++++ .../src/components/settings/settingsSearch.ts | 9 +++ apps/web/src/hooks/useHandleNewThread.test.ts | 25 ++++++++ apps/web/src/hooks/useHandleNewThread.ts | 23 ++----- docs/user/permission-modes.md | 6 +- packages/contracts/src/settings.test.ts | 29 +++++++++ packages/contracts/src/settings.ts | 13 +++- 13 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 apps/web/src/components/chat/runtimeModeConfig.ts diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index d0687e7e7b66beaea19c3e419d4a40a3ee779099..58fc8f1e3fa329de43b3b210e166566f4fa862a3 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -467,7 +467,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { const draftStartFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin; const startFromOrigin = draftStartFromOrigin ?? projectSettings.settings.newWorktreesStartFromOrigin; - const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; + const defaultRuntimeMode = editingPendingTask + ? (editingPendingTask.runtimeMode ?? DEFAULT_RUNTIME_MODE) + : projectSettings.settings.defaultRuntimeMode; + const runtimeMode = selectedProjectDraft.runtimeMode ?? defaultRuntimeMode; // Antigravity keeps unavailable selections so sign-out or a catalog change // cannot switch the user's model. Other providers retain their fallback @@ -978,7 +981,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { text, attachments: draft.attachments, modelSelection: draftModelSelection, - runtimeMode: draft.runtimeMode ?? DEFAULT_RUNTIME_MODE, + runtimeMode: draft.runtimeMode ?? defaultRuntimeMode, interactionMode: resolvePendingTaskInteractionMode({ preferenceLoaded: planModePreferenceLoaded, planModeEnabled: legacyPlanModeEnabled, @@ -1014,6 +1017,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { }; }, [ + defaultRuntimeMode, editingPendingProject, editingPendingTask, selectedEnvironmentServerConfig, diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index 37dbd7394a00b2b9ea155677661de8cf05525f95..60c93c2426cbc0e80679deeec85071da3145fa16 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -225,12 +225,37 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa }); it.effect.each([ - { existing: false, machineModel: null, projectModel: null }, - { existing: false, machineModel: "claude-sonnet-4-6", projectModel: null }, - { existing: true, machineModel: "claude-sonnet-4-6", projectModel: null }, - { existing: true, machineModel: "claude-sonnet-4-6", projectModel: "gpt-5.4" }, -])("auto-bootstrap model precedence: %j", ({ existing, machineModel, projectModel }) => + { + existing: false, + machineModel: null, + projectModel: null, + machineMode: "full-access", + projectMode: null, + }, + { + existing: false, + machineModel: "claude-sonnet-4-6", + projectModel: null, + machineMode: "approval-required", + projectMode: null, + }, + { + existing: true, + machineModel: "claude-sonnet-4-6", + projectModel: null, + machineMode: "auto", + projectMode: null, + }, + { + existing: true, + machineModel: "claude-sonnet-4-6", + projectModel: "gpt-5.4", + machineMode: "full-access", + projectMode: "auto-accept-edits", + }, +] as const)("auto-bootstrap model and permissions precedence: %j", (options) => Effect.gen(function* () { + const { existing, machineModel, projectModel, machineMode, projectMode } = options; const machineSelection = machineModel ? { instanceId: ProviderInstanceId.make("claude-code"), model: machineModel } : null; @@ -242,16 +267,21 @@ it.effect.each([ readonly type: string; readonly defaultModelSelection?: unknown; readonly modelSelection?: unknown; + readonly runtimeMode?: unknown; }> >([]); const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( Effect.provide( ServerSettings.layerTest({ defaultModelSelection: machineSelection, + defaultRuntimeMode: machineMode, projectSettingsOverrides: existing && projectSelection ? { - [ProjectId.make("existing-project")]: { defaultModelSelection: projectSelection }, + [ProjectId.make("existing-project")]: { + defaultModelSelection: projectSelection, + ...(projectMode ? { defaultRuntimeMode: projectMode } : {}), + }, } : {}, }), @@ -321,6 +351,7 @@ it.effect.each([ existing ? ["thread.create"] : ["project.create", "thread.create"], ); if (!existing) assert.equal("defaultModelSelection" in commands[0]!, false); + assert.equal(commands.at(-1)?.runtimeMode, projectMode ?? machineMode); assert.deepStrictEqual( commands.at(-1)?.modelSelection, projectSelection ?? diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index 90d6c3c576a0711c051883915665adf464402535..ec820e2f0e6f6052261f408afce77d8aab3f4083 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -248,7 +248,8 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { title: "New thread", modelSelection: nextThreadModelSelection, interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", + runtimeMode: resolveProjectSettings(settings, nextProjectId).settings + .defaultRuntimeMode, branch: null, worktreePath: null, createdAt, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index b74f5a073250edb2004b24d2c24cda7fea32d39d..4b09121b26b3edc56097b42bd965f2e3add114ef 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -154,7 +154,6 @@ import { } from "../proposedPlan"; import { DEFAULT_INTERACTION_MODE, - DEFAULT_RUNTIME_MODE, DEFAULT_THREAD_TERMINAL_ID, MAX_TERMINALS_PER_GROUP, type ChatMessage, @@ -1839,7 +1838,11 @@ export default function ChatView(props: ChatViewProps) { // session.lastError. Bump a tick so the banner hides immediately. Mirrors // the branch mismatch banner. const [, setThreadErrorBannerDismissTick] = useState(0); - const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; + const defaultRuntimeMode = resolveProjectSettings(settings, activeThread?.projectId ?? null) + .settings.defaultRuntimeMode; + // Implicit drafts follow their current project/environment, including retargets. + // Explicit composer choices and existing server threads retain their permissions. + const runtimeMode = composerRuntimeMode ?? activeServerThread?.runtimeMode ?? defaultRuntimeMode; const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; const canCheckoutPullRequestIntoThread = isLocalDraftThread; const activeThreadId = activeThread?.id ?? null; @@ -2293,7 +2296,8 @@ export default function ChatView(props: ChatViewProps) { setLogicalProjectDraftThreadId(logicalProjectKey, activeProjectRef, nextDraftId, { threadId: nextThreadId, createdAt: new Date().toISOString(), - runtimeMode: DEFAULT_RUNTIME_MODE, + runtimeMode: resolveProjectSettings(settings, activeProject.id, activeProject).settings + .defaultRuntimeMode, interactionMode: DEFAULT_INTERACTION_MODE, ...input, }); @@ -2312,6 +2316,7 @@ export default function ChatView(props: ChatViewProps) { navigate, projectGroupingSettings, routeKind, + settings, setDraftThreadContext, setLogicalProjectDraftThreadId, ], @@ -7890,7 +7895,7 @@ export default function ChatView(props: ChatViewProps) { projectId: activeProject.id, title: nextThreadTitle, modelSelection: nextThreadModelSelection, - runtimeMode, + runtimeMode: defaultRuntimeMode, interactionMode: "default", branch: activeThreadBranch, worktreePath: activeThread.worktreePath, @@ -7913,7 +7918,7 @@ export default function ChatView(props: ChatViewProps) { }, modelSelection: ctxSelectedModelSelection, titleSeed: nextThreadTitle, - runtimeMode, + runtimeMode: defaultRuntimeMode, interactionMode: "default", sourceProposedPlan: { threadId: activeThread.id, @@ -7987,7 +7992,7 @@ export default function ChatView(props: ChatViewProps) { isServerThread, navigate, resetLocalDispatch, - runtimeMode, + defaultRuntimeMode, startThreadTurn, environmentId, composerRef, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 7ecb5e2dc6352af3389266ab1b94c0a630f32368..79f343d2d88c6f950879726b64a2314f0b7a30ae 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1,3 +1,4 @@ +import { runtimeModeConfig, runtimeModeOptions } from "./runtimeModeConfig"; import { RefreshIcon } from "~/components/ui/refresh-icon"; import { questionAttachmentDraftId, @@ -850,11 +851,6 @@ import { PaperclipIcon, PencilRulerIcon, PlayIcon, - type LucideIcon, - LockIcon, - LockOpenIcon, - PenLineIcon, - SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -898,33 +894,6 @@ import type { ReviewCommentContext } from "../../reviewCommentContext"; const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000; -const runtimeModeConfig: Record< - RuntimeMode, - { label: string; description: string; icon: LucideIcon } -> = { - "approval-required": { - label: "Supervised", - description: "Ask before commands and file changes.", - icon: LockIcon, - }, - "auto-accept-edits": { - label: "Auto-accept edits", - description: "Auto-approve edits, ask before other actions.", - icon: PenLineIcon, - }, - auto: { - label: "Auto", - description: "Supported providers approve routine actions; others still ask.", - icon: SparklesIcon, - }, - "full-access": { - label: "Full access", - description: "Allow commands and edits without prompts.", - icon: LockOpenIcon, - }, -}; - -const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; const extendReplacementRangeForTrailingSpace = ( text: string, rangeEnd: number, diff --git a/apps/web/src/components/chat/runtimeModeConfig.ts b/apps/web/src/components/chat/runtimeModeConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a4ce149b222c22cd9447bb5353ba56569dd8a81 --- /dev/null +++ b/apps/web/src/components/chat/runtimeModeConfig.ts @@ -0,0 +1,30 @@ +import type { RuntimeMode } from "@t3tools/contracts"; +import { type LucideIcon, LockIcon, LockOpenIcon, PenLineIcon, SparklesIcon } from "lucide-react"; + +export const runtimeModeConfig: Record< + RuntimeMode, + { label: string; description: string; icon: LucideIcon } +> = { + "approval-required": { + label: "Supervised", + description: "Ask before commands and file changes.", + icon: LockIcon, + }, + "auto-accept-edits": { + label: "Auto-accept edits", + description: "Auto-approve edits, ask before other actions.", + icon: PenLineIcon, + }, + auto: { + label: "Auto", + description: "Supported providers approve routine actions; others still ask.", + icon: SparklesIcon, + }, + "full-access": { + label: "Full access", + description: "Allow commands and edits without prompts.", + icon: LockOpenIcon, + }, +}; + +export const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; diff --git a/apps/web/src/components/settings/ProjectDefaultsSettings.tsx b/apps/web/src/components/settings/ProjectDefaultsSettings.tsx index bb2c3e99bbb85245fb7b54e44c4e963f250390e6..a08b918be4c5f9e86156ca709ef976956df3e7a1 100644 --- a/apps/web/src/components/settings/ProjectDefaultsSettings.tsx +++ b/apps/web/src/components/settings/ProjectDefaultsSettings.tsx @@ -19,6 +19,7 @@ import { useEnvironments } from "../../state/environments"; import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; import { resolveEnvModeLabel } from "../BranchToolbar.logic"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; +import { runtimeModeConfig, runtimeModeOptions } from "../chat/runtimeModeConfig"; import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic"; import { TraitsPicker } from "../chat/TraitsPicker"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; @@ -67,6 +68,8 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting ); const activeEntry = entries.find((entry) => entry.instanceId === selection?.instanceId); const mixedModel = useScopedSettingsMixed(["defaultModelSelection"]); + const mixedPermissions = useScopedSettingsMixed(["defaultRuntimeMode"]); + const PermissionIcon = runtimeModeConfig[settings.defaultRuntimeMode].icon; const mixedWorkspace = useScopedSettingsMixed(["defaultThreadEnvMode"]); const mixedBrowser = useScopedSettingsMixed(["enableAgentBrowserAccess"]); const mixedAutoPull = useScopedSettingsMixed(["defaultAutoPull"]); @@ -220,6 +223,67 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting ) } /> + + updateSettings({ + defaultRuntimeMode: DEFAULT_SERVER_SETTINGS.defaultRuntimeMode, + }) + } + /> + ) : null + } + control={ + + } + /> { let completeProjectFileRead: (value: null) => void = () => undefined; @@ -7,6 +8,7 @@ const testState = vi.hoisted(() => { defaultThreadEnvMode: "local" as "local" | "worktree", newWorktreesStartFromOrigin: false, defaultModelSelection: null, + defaultRuntimeMode: "full-access" as RuntimeMode, }; let storedDraft: { readonly draftId: string; @@ -55,6 +57,7 @@ const testState = vi.hoisted(() => { defaultThreadEnvMode: workspaceDefaults.envMode, newWorktreesStartFromOrigin: workspaceDefaults.startFromOrigin, defaultModelSelection: null, + defaultRuntimeMode: "full-access", }; router.state.location.href = "/"; router.navigate.mockClear(); @@ -184,6 +187,28 @@ describe.each([ }, ], ])("useNewThreadHandler with a %s draft", (_, draft) => { + it.each(["approval-required", "auto-accept-edits", "auto", "full-access"] as const)( + "uses the target environment's %s permissions for new threads", + async (runtimeMode) => { + testState.reset(draft); + testState.targetSettings.defaultRuntimeMode = runtimeMode; + const projectRef = { + environmentId: "environment-ssh", + projectId: "project-remote", + } as never; + const pendingOpen = useNewThreadHandler()(projectRef); + testState.completeProjectFileRead(null); + const opened = await pendingOpen; + + expect(testState.draftStore.setLogicalProjectDraftThreadId).toHaveBeenCalledWith( + "remote-project", + projectRef, + opened!.draftId, + expect.objectContaining({ runtimeMode }), + ); + }, + ); + it("abandons a delayed draft open when the user navigates elsewhere", async () => { testState.reset(draft); const openThread = useNewThreadHandler(); diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index c72bef079967b4b77e0b87d90ce264277b401cd9..65ac2307ce89fb05e4b4027a444548a2b274055e 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -4,12 +4,7 @@ import { scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime/environment"; -import { - DEFAULT_RUNTIME_MODE, - DEFAULT_SERVER_SETTINGS, - type ScopedProjectRef, - type ThreadId, -} from "@t3tools/contracts"; +import { DEFAULT_SERVER_SETTINGS, type ScopedProjectRef, type ThreadId } from "@t3tools/contracts"; import { useParams, useRouter } from "@tanstack/react-router"; import { useCallback, useMemo } from "react"; import { @@ -100,8 +95,8 @@ export function useNewThreadHandler() { const routeChangedSinceRequest = () => router.state.location.href !== requestingRouteHref; const currentRouteTarget = getCurrentRouteTarget(); // A new thread carries the user's working mode from the thread being - // viewed. The target project's configured model still wins; runtime and - // interaction modes carry independently. Branch, worktree, and env mode + // viewed. The target project's configured model still wins; interaction + // mode carries independently. Permissions, branch, worktree, and env mode // come from configured defaults unless the caller passes them explicitly. const carrySourceShell = currentRouteTarget?.kind === "server" @@ -124,11 +119,6 @@ export function useNewThreadHandler() { : null; const carryModelSelection = composerModelSelection ?? carrySourceShell?.modelSelection ?? null; - const carryRuntimeMode = - carrySourceComposer?.runtimeMode ?? - carrySourceShell?.runtimeMode ?? - carrySourceDraft?.runtimeMode ?? - null; const carryInteractionMode = carrySourceComposer?.interactionMode ?? carrySourceShell?.interactionMode ?? @@ -147,6 +137,7 @@ export function useNewThreadHandler() { project, ); const projectDefaultModelSelection = projectSettings.settings.defaultModelSelection; + const defaultRuntimeMode = projectSettings.settings.defaultRuntimeMode; const projectThreadEnvMode = projectSettings.sources.defaultThreadEnvMode === "project" ? projectSettings.settings.defaultThreadEnvMode @@ -276,7 +267,7 @@ export function useNewThreadHandler() { if (workspaceContext) { setDraftThreadContext(emptyStoredDraftThread.draftId, { ...workspaceContext, - ...(carryRuntimeMode ? { runtimeMode: carryRuntimeMode } : {}), + ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), }); } @@ -313,7 +304,7 @@ export function useNewThreadHandler() { { threadId: emptyStoredDraftThread.threadId, ...workspaceContext, - ...(carryRuntimeMode ? { runtimeMode: carryRuntimeMode } : {}), + ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), }, ); @@ -426,7 +417,7 @@ export function useNewThreadHandler() { envMode: initialEnvMode, newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, }), - runtimeMode: carryRuntimeMode ?? DEFAULT_RUNTIME_MODE, + runtimeMode: defaultRuntimeMode, ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), }); applyStickyState(draftId); diff --git a/docs/user/permission-modes.md b/docs/user/permission-modes.md index 3d37850c5e20c48a9ad54e5dd2da7ec8b243ae91..dfc0c448c858ae9b4db968fa5b7313000fbf7f86 100644 --- a/docs/user/permission-modes.md +++ b/docs/user/permission-modes.md @@ -3,8 +3,10 @@ Permission modes control when an agent needs your approval to act. Choose a mode in the message composer; it applies to that thread. -New threads start in **Full access** unless you choose another mode before sending. A thread -created from another thread inherits its mode. +Set the default for new threads in **Settings → General → New threads → Permissions**. +Projects can override the environment default. New threads use this setting rather than the +mode of the thread you were viewing. The initial default is **Full access**; existing threads +and modes you choose in a draft keep their permissions. | Mode | Behavior | | --------------------- | ------------------------------------------------------------------------------------- | diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 7322307c1f89f0b8ec937ac53731bc1c3c78abaf..2fb6ead09c1618bb7c940c7a64e051daaedce99f 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -20,6 +20,35 @@ const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); const encodeServerSettings = Schema.encodeSync(ServerSettings); const decodeClaudeSettings = Schema.decodeUnknownSync(ClaudeSettings); +describe("ServerSettings default permissions", () => { + it("keeps full access for settings saved before a default was configured", () => { + expect(decodeServerSettings({}).defaultRuntimeMode).toBe("full-access"); + expect(DEFAULT_SERVER_SETTINGS.defaultRuntimeMode).toBe("full-access"); + }); + + it.each(["approval-required", "auto-accept-edits", "auto", "full-access"])( + "round-trips %s as an environment default and project override", + (defaultRuntimeMode) => { + const input = { + defaultRuntimeMode, + projectSettingsOverrides: { project: { defaultRuntimeMode } }, + }; + expect(encodeServerSettings(decodeServerSettings(input))).toMatchObject(input); + expect(decodeServerSettingsPatch(input)).toEqual(input); + }, + ); + + it("rejects unsupported permission defaults", () => { + expect(() => decodeServerSettings({ defaultRuntimeMode: "unsupported" })).toThrow(); + expect(() => decodeServerSettingsPatch({ defaultRuntimeMode: "unsupported" })).toThrow(); + expect(() => + decodeServerSettingsPatch({ + projectSettingsOverrides: { project: { defaultRuntimeMode: "unsupported" } }, + }), + ).toThrow(); + }); +}); + describe("ServerSettings usage price overrides", () => { const prices = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 }; diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 54aa20db73886c5116f10cff2f2d7b9bfea5f9fc..6b931f4ef3ea485e28655aef05dd0262a0e82f26 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -18,7 +18,12 @@ import { DEFAULT_TEXT_GENERATION_REASONING_EFFORT, ProviderOptionSelections, } from "./model.ts"; -import { ModelSelection, ProjectScript } from "./orchestration.ts"; +import { + DEFAULT_RUNTIME_MODE, + ModelSelection, + ProjectScript, + RuntimeMode, +} from "./orchestration.ts"; import { BrowserProfile, BrowserProfileId, DEFAULT_BROWSER_PROFILE_ID } from "./browserProfile.ts"; import { DEFAULT_PREVIEW_APPEARANCE, @@ -934,6 +939,7 @@ export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; */ export const PROJECT_SCOPED_SERVER_SETTING_KEYS = [ "defaultModelSelection", + "defaultRuntimeMode", "defaultThreadEnvMode", "newWorktreesStartFromOrigin", "defaultAutoPull", @@ -958,6 +964,7 @@ export type ProjectScopedServerSettingKey = (typeof PROJECT_SCOPED_SERVER_SETTIN */ export const ProjectSettingsOverrides = Schema.Struct({ defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), + defaultRuntimeMode: Schema.optionalKey(RuntimeMode), defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode), newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean), defaultAutoPull: Schema.optionalKey(Schema.Boolean), @@ -1015,6 +1022,9 @@ export const ServerSettings = Schema.Struct({ defaultModelSelection: Schema.NullOr(ModelSelection).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), + defaultRuntimeMode: RuntimeMode.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE)), + ), /** * Per-project overrides of the keys in `PROJECT_SCOPED_SERVER_SETTING_KEYS`. * The source of truth for project settings; `projectAgentBrowserAccessOverrides`, @@ -1328,6 +1338,7 @@ export const ServerSettingsPatch = Schema.Struct({ Schema.Record(ProjectId, Schema.NullOr(Schema.Boolean)), ), defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), + defaultRuntimeMode: Schema.optionalKey(RuntimeMode), /** * Per-project entry replacement: each entry replaces that project's whole * override set and `null` removes it. Clearing one override means resending -- 2.54.0