| author | |
| committer | |
| log | cf1ba3d7da98357243ee056d2fd358eed8171981 |
| tree | 4f470486463d6bb8c2e52b1683b46b9174fe7a46 |
| parent | 4d16e6bc6f0be52c5d44b8864000c70c7825e4f7 |
| signature | Signed by PGP key B5690EEEBB952194 |
13 files changed, 237 insertions(+), 66 deletions(-)
apps/mobile/src/features/threads/new-task-flow-provider.tsx+6-2| ... | @@ -467,7 +467,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -467,7 +467,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 467 | const draftStartFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin; | 467 | const draftStartFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin; |
| 468 | const startFromOrigin = | 468 | const startFromOrigin = |
| 469 | draftStartFromOrigin ?? projectSettings.settings.newWorktreesStartFromOrigin; | 469 | draftStartFromOrigin ?? projectSettings.settings.newWorktreesStartFromOrigin; |
| 470 | const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; | 470 | const defaultRuntimeMode = editingPendingTask |
| 471 | ? (editingPendingTask.runtimeMode ?? DEFAULT_RUNTIME_MODE) | ||
| 472 | : projectSettings.settings.defaultRuntimeMode; | ||
| 473 | const runtimeMode = selectedProjectDraft.runtimeMode ?? defaultRuntimeMode; | ||
| 471 | 474 | ||
| 472 | // Antigravity keeps unavailable selections so sign-out or a catalog change | 475 | // Antigravity keeps unavailable selections so sign-out or a catalog change |
| 473 | // cannot switch the user's model. Other providers retain their fallback | 476 | // cannot switch the user's model. Other providers retain their fallback |
| ... | @@ -978,7 +981,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -978,7 +981,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 978 | text, | 981 | text, |
| 979 | attachments: draft.attachments, | 982 | attachments: draft.attachments, |
| 980 | modelSelection: draftModelSelection, | 983 | modelSelection: draftModelSelection, |
| 981 | runtimeMode: draft.runtimeMode ?? DEFAULT_RUNTIME_MODE, | 984 | runtimeMode: draft.runtimeMode ?? defaultRuntimeMode, |
| 982 | interactionMode: resolvePendingTaskInteractionMode({ | 985 | interactionMode: resolvePendingTaskInteractionMode({ |
| 983 | preferenceLoaded: planModePreferenceLoaded, | 986 | preferenceLoaded: planModePreferenceLoaded, |
| 984 | planModeEnabled: legacyPlanModeEnabled, | 987 | planModeEnabled: legacyPlanModeEnabled, |
| ... | @@ -1014,6 +1017,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -1014,6 +1017,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 1014 | }; | 1017 | }; |
| 1015 | }, | 1018 | }, |
| 1016 | [ | 1019 | [ |
| 1020 | defaultRuntimeMode, | ||
| 1017 | editingPendingProject, | 1021 | editingPendingProject, |
| 1018 | editingPendingTask, | 1022 | editingPendingTask, |
| 1019 | selectedEnvironmentServerConfig, | 1023 | selectedEnvironmentServerConfig, |
apps/server/src/serverRuntimeStartup.test.ts+37-6| ... | @@ -225,12 +225,37 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa | ... | @@ -225,12 +225,37 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa |
| 225 | }); | 225 | }); |
| 226 | 226 | ||
| 227 | it.effect.each([ | 227 | it.effect.each([ |
| 228 | { existing: false, machineModel: null, projectModel: null }, | 228 | { |
| 229 | { existing: false, machineModel: "claude-sonnet-4-6", projectModel: null }, | 229 | existing: false, |
| 230 | { existing: true, machineModel: "claude-sonnet-4-6", projectModel: null }, | 230 | machineModel: null, |
| 231 | { existing: true, machineModel: "claude-sonnet-4-6", projectModel: "gpt-5.4" }, | 231 | projectModel: null, |
| 232 | ])("auto-bootstrap model precedence: %j", ({ existing, machineModel, projectModel }) => | 232 | machineMode: "full-access", |
| 233 | projectMode: null, | ||
| 234 | }, | ||
| 235 | { | ||
| 236 | existing: false, | ||
| 237 | machineModel: "claude-sonnet-4-6", | ||
| 238 | projectModel: null, | ||
| 239 | machineMode: "approval-required", | ||
| 240 | projectMode: null, | ||
| 241 | }, | ||
| 242 | { | ||
| 243 | existing: true, | ||
| 244 | machineModel: "claude-sonnet-4-6", | ||
| 245 | projectModel: null, | ||
| 246 | machineMode: "auto", | ||
| 247 | projectMode: null, | ||
| 248 | }, | ||
| 249 | { | ||
| 250 | existing: true, | ||
| 251 | machineModel: "claude-sonnet-4-6", | ||
| 252 | projectModel: "gpt-5.4", | ||
| 253 | machineMode: "full-access", | ||
| 254 | projectMode: "auto-accept-edits", | ||
| 255 | }, | ||
| 256 | ] as const)("auto-bootstrap model and permissions precedence: %j", (options) => | ||
| 233 | Effect.gen(function* () { | 257 | Effect.gen(function* () { |
| 258 | const { existing, machineModel, projectModel, machineMode, projectMode } = options; | ||
| 234 | const machineSelection = machineModel | 259 | const machineSelection = machineModel |
| 235 | ? { instanceId: ProviderInstanceId.make("claude-code"), model: machineModel } | 260 | ? { instanceId: ProviderInstanceId.make("claude-code"), model: machineModel } |
| 236 | : null; | 261 | : null; |
| ... | @@ -242,16 +267,21 @@ it.effect.each([ | ... | @@ -242,16 +267,21 @@ it.effect.each([ |
| 242 | readonly type: string; | 267 | readonly type: string; |
| 243 | readonly defaultModelSelection?: unknown; | 268 | readonly defaultModelSelection?: unknown; |
| 244 | readonly modelSelection?: unknown; | 269 | readonly modelSelection?: unknown; |
| 270 | readonly runtimeMode?: unknown; | ||
| 245 | }> | 271 | }> |
| 246 | >([]); | 272 | >([]); |
| 247 | const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( | 273 | const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( |
| 248 | Effect.provide( | 274 | Effect.provide( |
| 249 | ServerSettings.layerTest({ | 275 | ServerSettings.layerTest({ |
| 250 | defaultModelSelection: machineSelection, | 276 | defaultModelSelection: machineSelection, |
| 277 | defaultRuntimeMode: machineMode, | ||
| 251 | projectSettingsOverrides: | 278 | projectSettingsOverrides: |
| 252 | existing && projectSelection | 279 | existing && projectSelection |
| 253 | ? { | 280 | ? { |
| 254 | [ProjectId.make("existing-project")]: { defaultModelSelection: projectSelection }, | 281 | [ProjectId.make("existing-project")]: { |
| 282 | defaultModelSelection: projectSelection, | ||
| 283 | ...(projectMode ? { defaultRuntimeMode: projectMode } : {}), | ||
| 284 | }, | ||
| 255 | } | 285 | } |
| 256 | : {}, | 286 | : {}, |
| 257 | }), | 287 | }), |
| ... | @@ -321,6 +351,7 @@ it.effect.each([ | ... | @@ -321,6 +351,7 @@ it.effect.each([ |
| 321 | existing ? ["thread.create"] : ["project.create", "thread.create"], | 351 | existing ? ["thread.create"] : ["project.create", "thread.create"], |
| 322 | ); | 352 | ); |
| 323 | if (!existing) assert.equal("defaultModelSelection" in commands[0]!, false); | 353 | if (!existing) assert.equal("defaultModelSelection" in commands[0]!, false); |
| 354 | assert.equal(commands.at(-1)?.runtimeMode, projectMode ?? machineMode); | ||
| 324 | assert.deepStrictEqual( | 355 | assert.deepStrictEqual( |
| 325 | commands.at(-1)?.modelSelection, | 356 | commands.at(-1)?.modelSelection, |
| 326 | projectSelection ?? | 357 | projectSelection ?? |
apps/server/src/serverRuntimeStartup.ts+2-1| ... | @@ -248,7 +248,8 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { | ... | @@ -248,7 +248,8 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { |
| 248 | title: "New thread", | 248 | title: "New thread", |
| 249 | modelSelection: nextThreadModelSelection, | 249 | modelSelection: nextThreadModelSelection, |
| 250 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | 250 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 251 | runtimeMode: "full-access", | 251 | runtimeMode: resolveProjectSettings(settings, nextProjectId).settings |
| 252 | .defaultRuntimeMode, | ||
| 252 | branch: null, | 253 | branch: null, |
| 253 | worktreePath: null, | 254 | worktreePath: null, |
| 254 | createdAt, | 255 | createdAt, |
apps/web/src/components/ChatView.tsx+11-6| ... | @@ -154,7 +154,6 @@ import { | ... | @@ -154,7 +154,6 @@ import { |
| 154 | } from "../proposedPlan"; | 154 | } from "../proposedPlan"; |
| 155 | import { | 155 | import { |
| 156 | DEFAULT_INTERACTION_MODE, | 156 | DEFAULT_INTERACTION_MODE, |
| 157 | DEFAULT_RUNTIME_MODE, | ||
| 158 | DEFAULT_THREAD_TERMINAL_ID, | 157 | DEFAULT_THREAD_TERMINAL_ID, |
| 159 | MAX_TERMINALS_PER_GROUP, | 158 | MAX_TERMINALS_PER_GROUP, |
| 160 | type ChatMessage, | 159 | type ChatMessage, |
| ... | @@ -1839,7 +1838,11 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1839,7 +1838,11 @@ export default function ChatView(props: ChatViewProps) { |
| 1839 | // session.lastError. Bump a tick so the banner hides immediately. Mirrors | 1838 | // session.lastError. Bump a tick so the banner hides immediately. Mirrors |
| 1840 | // the branch mismatch banner. | 1839 | // the branch mismatch banner. |
| 1841 | const [, setThreadErrorBannerDismissTick] = useState(0); | 1840 | const [, setThreadErrorBannerDismissTick] = useState(0); |
| 1842 | const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; | 1841 | const defaultRuntimeMode = resolveProjectSettings(settings, activeThread?.projectId ?? null) |
| 1842 | .settings.defaultRuntimeMode; | ||
| 1843 | // Implicit drafts follow their current project/environment, including retargets. | ||
| 1844 | // Explicit composer choices and existing server threads retain their permissions. | ||
| 1845 | const runtimeMode = composerRuntimeMode ?? activeServerThread?.runtimeMode ?? defaultRuntimeMode; | ||
| 1843 | const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; | 1846 | const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; |
| 1844 | const canCheckoutPullRequestIntoThread = isLocalDraftThread; | 1847 | const canCheckoutPullRequestIntoThread = isLocalDraftThread; |
| 1845 | const activeThreadId = activeThread?.id ?? null; | 1848 | const activeThreadId = activeThread?.id ?? null; |
| ... | @@ -2293,7 +2296,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2293,7 +2296,8 @@ export default function ChatView(props: ChatViewProps) { |
| 2293 | setLogicalProjectDraftThreadId(logicalProjectKey, activeProjectRef, nextDraftId, { | 2296 | setLogicalProjectDraftThreadId(logicalProjectKey, activeProjectRef, nextDraftId, { |
| 2294 | threadId: nextThreadId, | 2297 | threadId: nextThreadId, |
| 2295 | createdAt: new Date().toISOString(), | 2298 | createdAt: new Date().toISOString(), |
| 2296 | runtimeMode: DEFAULT_RUNTIME_MODE, | 2299 | runtimeMode: resolveProjectSettings(settings, activeProject.id, activeProject).settings |
| 2300 | .defaultRuntimeMode, | ||
| 2297 | interactionMode: DEFAULT_INTERACTION_MODE, | 2301 | interactionMode: DEFAULT_INTERACTION_MODE, |
| 2298 | ...input, | 2302 | ...input, |
| 2299 | }); | 2303 | }); |
| ... | @@ -2312,6 +2316,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2312,6 +2316,7 @@ export default function ChatView(props: ChatViewProps) { |
| 2312 | navigate, | 2316 | navigate, |
| 2313 | projectGroupingSettings, | 2317 | projectGroupingSettings, |
| 2314 | routeKind, | 2318 | routeKind, |
| 2319 | settings, | ||
| 2315 | setDraftThreadContext, | 2320 | setDraftThreadContext, |
| 2316 | setLogicalProjectDraftThreadId, | 2321 | setLogicalProjectDraftThreadId, |
| 2317 | ], | 2322 | ], |
| ... | @@ -7890,7 +7895,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7890,7 +7895,7 @@ export default function ChatView(props: ChatViewProps) { |
| 7890 | projectId: activeProject.id, | 7895 | projectId: activeProject.id, |
| 7891 | title: nextThreadTitle, | 7896 | title: nextThreadTitle, |
| 7892 | modelSelection: nextThreadModelSelection, | 7897 | modelSelection: nextThreadModelSelection, |
| 7893 | runtimeMode, | 7898 | runtimeMode: defaultRuntimeMode, |
| 7894 | interactionMode: "default", | 7899 | interactionMode: "default", |
| 7895 | branch: activeThreadBranch, | 7900 | branch: activeThreadBranch, |
| 7896 | worktreePath: activeThread.worktreePath, | 7901 | worktreePath: activeThread.worktreePath, |
| ... | @@ -7913,7 +7918,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7913,7 +7918,7 @@ export default function ChatView(props: ChatViewProps) { |
| 7913 | }, | 7918 | }, |
| 7914 | modelSelection: ctxSelectedModelSelection, | 7919 | modelSelection: ctxSelectedModelSelection, |
| 7915 | titleSeed: nextThreadTitle, | 7920 | titleSeed: nextThreadTitle, |
| 7916 | runtimeMode, | 7921 | runtimeMode: defaultRuntimeMode, |
| 7917 | interactionMode: "default", | 7922 | interactionMode: "default", |
| 7918 | sourceProposedPlan: { | 7923 | sourceProposedPlan: { |
| 7919 | threadId: activeThread.id, | 7924 | threadId: activeThread.id, |
| ... | @@ -7987,7 +7992,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7987,7 +7992,7 @@ export default function ChatView(props: ChatViewProps) { |
| 7987 | isServerThread, | 7992 | isServerThread, |
| 7988 | navigate, | 7993 | navigate, |
| 7989 | resetLocalDispatch, | 7994 | resetLocalDispatch, |
| 7990 | runtimeMode, | 7995 | defaultRuntimeMode, |
| 7991 | startThreadTurn, | 7996 | startThreadTurn, |
| 7992 | environmentId, | 7997 | environmentId, |
| 7993 | composerRef, | 7998 | composerRef, |
apps/web/src/components/chat/ChatComposer.tsx+1-32| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { runtimeModeConfig, runtimeModeOptions } from "./runtimeModeConfig"; | ||
| 1 | import { RefreshIcon } from "~/components/ui/refresh-icon"; | 2 | import { RefreshIcon } from "~/components/ui/refresh-icon"; |
| 2 | import { | 3 | import { |
| 3 | questionAttachmentDraftId, | 4 | questionAttachmentDraftId, |
| ... | @@ -850,11 +851,6 @@ import { | ... | @@ -850,11 +851,6 @@ import { |
| 850 | PaperclipIcon, | 851 | PaperclipIcon, |
| 851 | PencilRulerIcon, | 852 | PencilRulerIcon, |
| 852 | PlayIcon, | 853 | PlayIcon, |
| 853 | type LucideIcon, | ||
| 854 | LockIcon, | ||
| 855 | LockOpenIcon, | ||
| 856 | PenLineIcon, | ||
| 857 | SparklesIcon, | ||
| 858 | XIcon, | 854 | XIcon, |
| 859 | } from "lucide-react"; | 855 | } from "lucide-react"; |
| 860 | import { proposedPlanTitle } from "../../proposedPlan"; | 856 | import { proposedPlanTitle } from "../../proposedPlan"; |
| ... | @@ -898,33 +894,6 @@ import type { ReviewCommentContext } from "../../reviewCommentContext"; | ... | @@ -898,33 +894,6 @@ import type { ReviewCommentContext } from "../../reviewCommentContext"; |
| 898 | 894 | ||
| 899 | const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000; | 895 | const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000; |
| 900 | 896 | ||
| 901 | const runtimeModeConfig: Record< | ||
| 902 | RuntimeMode, | ||
| 903 | { label: string; description: string; icon: LucideIcon } | ||
| 904 | > = { | ||
| 905 | "approval-required": { | ||
| 906 | label: "Supervised", | ||
| 907 | description: "Ask before commands and file changes.", | ||
| 908 | icon: LockIcon, | ||
| 909 | }, | ||
| 910 | "auto-accept-edits": { | ||
| 911 | label: "Auto-accept edits", | ||
| 912 | description: "Auto-approve edits, ask before other actions.", | ||
| 913 | icon: PenLineIcon, | ||
| 914 | }, | ||
| 915 | auto: { | ||
| 916 | label: "Auto", | ||
| 917 | description: "Supported providers approve routine actions; others still ask.", | ||
| 918 | icon: SparklesIcon, | ||
| 919 | }, | ||
| 920 | "full-access": { | ||
| 921 | label: "Full access", | ||
| 922 | description: "Allow commands and edits without prompts.", | ||
| 923 | icon: LockOpenIcon, | ||
| 924 | }, | ||
| 925 | }; | ||
| 926 | |||
| 927 | const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; | ||
| 928 | const extendReplacementRangeForTrailingSpace = ( | 897 | const extendReplacementRangeForTrailingSpace = ( |
| 929 | text: string, | 898 | text: string, |
| 930 | rangeEnd: number, | 899 | rangeEnd: number, |
apps/web/src/components/chat/runtimeModeConfig.ts created+30| ... | @@ -0,0 +1,30 @@ | ||
| 1 | import type { RuntimeMode } from "@t3tools/contracts"; | ||
| 2 | import { type LucideIcon, LockIcon, LockOpenIcon, PenLineIcon, SparklesIcon } from "lucide-react"; | ||
| 3 | |||
| 4 | export const runtimeModeConfig: Record< | ||
| 5 | RuntimeMode, | ||
| 6 | { label: string; description: string; icon: LucideIcon } | ||
| 7 | > = { | ||
| 8 | "approval-required": { | ||
| 9 | label: "Supervised", | ||
| 10 | description: "Ask before commands and file changes.", | ||
| 11 | icon: LockIcon, | ||
| 12 | }, | ||
| 13 | "auto-accept-edits": { | ||
| 14 | label: "Auto-accept edits", | ||
| 15 | description: "Auto-approve edits, ask before other actions.", | ||
| 16 | icon: PenLineIcon, | ||
| 17 | }, | ||
| 18 | auto: { | ||
| 19 | label: "Auto", | ||
| 20 | description: "Supported providers approve routine actions; others still ask.", | ||
| 21 | icon: SparklesIcon, | ||
| 22 | }, | ||
| 23 | "full-access": { | ||
| 24 | label: "Full access", | ||
| 25 | description: "Allow commands and edits without prompts.", | ||
| 26 | icon: LockOpenIcon, | ||
| 27 | }, | ||
| 28 | }; | ||
| 29 | |||
| 30 | export const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; | ||
apps/web/src/components/settings/ProjectDefaultsSettings.tsx+64| ... | @@ -19,6 +19,7 @@ import { useEnvironments } from "../../state/environments"; | ... | @@ -19,6 +19,7 @@ import { useEnvironments } from "../../state/environments"; |
| 19 | import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; | 19 | import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; |
| 20 | import { resolveEnvModeLabel } from "../BranchToolbar.logic"; | 20 | import { resolveEnvModeLabel } from "../BranchToolbar.logic"; |
| 21 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; | 21 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; |
| 22 | import { runtimeModeConfig, runtimeModeOptions } from "../chat/runtimeModeConfig"; | ||
| 22 | import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic"; | 23 | import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic"; |
| 23 | import { TraitsPicker } from "../chat/TraitsPicker"; | 24 | import { TraitsPicker } from "../chat/TraitsPicker"; |
| 24 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; | 25 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; |
| ... | @@ -67,6 +68,8 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting | ... | @@ -67,6 +68,8 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting |
| 67 | ); | 68 | ); |
| 68 | const activeEntry = entries.find((entry) => entry.instanceId === selection?.instanceId); | 69 | const activeEntry = entries.find((entry) => entry.instanceId === selection?.instanceId); |
| 69 | const mixedModel = useScopedSettingsMixed(["defaultModelSelection"]); | 70 | const mixedModel = useScopedSettingsMixed(["defaultModelSelection"]); |
| 71 | const mixedPermissions = useScopedSettingsMixed(["defaultRuntimeMode"]); | ||
| 72 | const PermissionIcon = runtimeModeConfig[settings.defaultRuntimeMode].icon; | ||
| 70 | const mixedWorkspace = useScopedSettingsMixed(["defaultThreadEnvMode"]); | 73 | const mixedWorkspace = useScopedSettingsMixed(["defaultThreadEnvMode"]); |
| 71 | const mixedBrowser = useScopedSettingsMixed(["enableAgentBrowserAccess"]); | 74 | const mixedBrowser = useScopedSettingsMixed(["enableAgentBrowserAccess"]); |
| 72 | const mixedAutoPull = useScopedSettingsMixed(["defaultAutoPull"]); | 75 | const mixedAutoPull = useScopedSettingsMixed(["defaultAutoPull"]); |
| ... | @@ -220,6 +223,67 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting | ... | @@ -220,6 +223,67 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting |
| 220 | ) | 223 | ) |
| 221 | } | 224 | } |
| 222 | /> | 225 | /> |
| 226 | <SettingsRow | ||
| 227 | serverScoped | ||
| 228 | settingKeys={["defaultRuntimeMode"]} | ||
| 229 | mixed={mixedPermissions} | ||
| 230 | {...searchableSetting("default-permissions")} | ||
| 231 | description={ | ||
| 232 | isProjectScope | ||
| 233 | ? "Permissions for new threads in this project." | ||
| 234 | : "Default permissions for new threads. Projects can override them." | ||
| 235 | } | ||
| 236 | resetAction={ | ||
| 237 | settings.defaultRuntimeMode !== DEFAULT_SERVER_SETTINGS.defaultRuntimeMode ? ( | ||
| 238 | <SettingResetButton | ||
| 239 | label="default permissions" | ||
| 240 | onClick={() => | ||
| 241 | updateSettings({ | ||
| 242 | defaultRuntimeMode: DEFAULT_SERVER_SETTINGS.defaultRuntimeMode, | ||
| 243 | }) | ||
| 244 | } | ||
| 245 | /> | ||
| 246 | ) : null | ||
| 247 | } | ||
| 248 | control={ | ||
| 249 | <Select | ||
| 250 | value={mixedPermissions ? null : settings.defaultRuntimeMode} | ||
| 251 | onValueChange={(value) => { | ||
| 252 | if (value) updateSettings({ defaultRuntimeMode: value }); | ||
| 253 | }} | ||
| 254 | > | ||
| 255 | <SelectTrigger size="sm" aria-label="Default permissions"> | ||
| 256 | {!mixedPermissions && ( | ||
| 257 | <PermissionIcon className="size-3.5 shrink-0 text-muted-foreground" /> | ||
| 258 | )} | ||
| 259 | <SelectValue> | ||
| 260 | {mixedPermissions | ||
| 261 | ? "Mixed" | ||
| 262 | : runtimeModeConfig[settings.defaultRuntimeMode].label} | ||
| 263 | </SelectValue> | ||
| 264 | </SelectTrigger> | ||
| 265 | <SelectPopup align="end" alignItemWithTrigger={false}> | ||
| 266 | {runtimeModeOptions.map((mode) => { | ||
| 267 | const option = runtimeModeConfig[mode]; | ||
| 268 | const Icon = option.icon; | ||
| 269 | return ( | ||
| 270 | <SelectItem key={mode} value={mode} className="min-w-64 py-2"> | ||
| 271 | <div className="grid gap-0.5"> | ||
| 272 | <span className="inline-flex items-center gap-1.5 font-medium"> | ||
| 273 | <Icon className="size-3.5 shrink-0 text-muted-foreground" /> | ||
| 274 | {option.label} | ||
| 275 | </span> | ||
| 276 | <span className="text-xs leading-4 text-muted-foreground"> | ||
| 277 | {option.description} | ||
| 278 | </span> | ||
| 279 | </div> | ||
| 280 | </SelectItem> | ||
| 281 | ); | ||
| 282 | })} | ||
| 283 | </SelectPopup> | ||
| 284 | </Select> | ||
| 285 | } | ||
| 286 | /> | ||
| 223 | <SettingsRow | 287 | <SettingsRow |
| 224 | serverScoped | 288 | serverScoped |
| 225 | settingKeys={["defaultThreadEnvMode"]} | 289 | settingKeys={["defaultThreadEnvMode"]} |
apps/web/src/components/settings/settingsSearch.ts+9| ... | @@ -108,6 +108,15 @@ export const SETTINGS_SEARCH_ITEMS = [ | ... | @@ -108,6 +108,15 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 108 | scope: "project-defaults", | 108 | scope: "project-defaults", |
| 109 | searchTerms: ["new thread project provider reasoning effort"], | 109 | searchTerms: ["new thread project provider reasoning effort"], |
| 110 | }, | 110 | }, |
| 111 | { | ||
| 112 | id: "default-permissions", | ||
| 113 | title: "Permissions", | ||
| 114 | to: "/settings/general", | ||
| 115 | scope: "project-defaults", | ||
| 116 | searchTerms: [ | ||
| 117 | "new thread default runtime mode supervised approvals auto accept edits full access", | ||
| 118 | ], | ||
| 119 | }, | ||
| 111 | { | 120 | { |
| 112 | id: "color-scheme", | 121 | id: "color-scheme", |
| 113 | title: "Color scheme", | 122 | title: "Color scheme", |
apps/web/src/hooks/useHandleNewThread.test.ts+25| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | import type { RuntimeMode } from "@t3tools/contracts"; | ||
| 2 | 3 | ||
| 3 | const testState = vi.hoisted(() => { | 4 | const testState = vi.hoisted(() => { |
| 4 | let completeProjectFileRead: (value: null) => void = () => undefined; | 5 | let completeProjectFileRead: (value: null) => void = () => undefined; |
| ... | @@ -7,6 +8,7 @@ const testState = vi.hoisted(() => { | ... | @@ -7,6 +8,7 @@ const testState = vi.hoisted(() => { |
| 7 | defaultThreadEnvMode: "local" as "local" | "worktree", | 8 | defaultThreadEnvMode: "local" as "local" | "worktree", |
| 8 | newWorktreesStartFromOrigin: false, | 9 | newWorktreesStartFromOrigin: false, |
| 9 | defaultModelSelection: null, | 10 | defaultModelSelection: null, |
| 11 | defaultRuntimeMode: "full-access" as RuntimeMode, | ||
| 10 | }; | 12 | }; |
| 11 | let storedDraft: { | 13 | let storedDraft: { |
| 12 | readonly draftId: string; | 14 | readonly draftId: string; |
| ... | @@ -55,6 +57,7 @@ const testState = vi.hoisted(() => { | ... | @@ -55,6 +57,7 @@ const testState = vi.hoisted(() => { |
| 55 | defaultThreadEnvMode: workspaceDefaults.envMode, | 57 | defaultThreadEnvMode: workspaceDefaults.envMode, |
| 56 | newWorktreesStartFromOrigin: workspaceDefaults.startFromOrigin, | 58 | newWorktreesStartFromOrigin: workspaceDefaults.startFromOrigin, |
| 57 | defaultModelSelection: null, | 59 | defaultModelSelection: null, |
| 60 | defaultRuntimeMode: "full-access", | ||
| 58 | }; | 61 | }; |
| 59 | router.state.location.href = "/"; | 62 | router.state.location.href = "/"; |
| 60 | router.navigate.mockClear(); | 63 | router.navigate.mockClear(); |
| ... | @@ -184,6 +187,28 @@ describe.each([ | ... | @@ -184,6 +187,28 @@ describe.each([ |
| 184 | }, | 187 | }, |
| 185 | ], | 188 | ], |
| 186 | ])("useNewThreadHandler with a %s draft", (_, draft) => { | 189 | ])("useNewThreadHandler with a %s draft", (_, draft) => { |
| 190 | it.each(["approval-required", "auto-accept-edits", "auto", "full-access"] as const)( | ||
| 191 | "uses the target environment's %s permissions for new threads", | ||
| 192 | async (runtimeMode) => { | ||
| 193 | testState.reset(draft); | ||
| 194 | testState.targetSettings.defaultRuntimeMode = runtimeMode; | ||
| 195 | const projectRef = { | ||
| 196 | environmentId: "environment-ssh", | ||
| 197 | projectId: "project-remote", | ||
| 198 | } as never; | ||
| 199 | const pendingOpen = useNewThreadHandler()(projectRef); | ||
| 200 | testState.completeProjectFileRead(null); | ||
| 201 | const opened = await pendingOpen; | ||
| 202 | |||
| 203 | expect(testState.draftStore.setLogicalProjectDraftThreadId).toHaveBeenCalledWith( | ||
| 204 | "remote-project", | ||
| 205 | projectRef, | ||
| 206 | opened!.draftId, | ||
| 207 | expect.objectContaining({ runtimeMode }), | ||
| 208 | ); | ||
| 209 | }, | ||
| 210 | ); | ||
| 211 | |||
| 187 | it("abandons a delayed draft open when the user navigates elsewhere", async () => { | 212 | it("abandons a delayed draft open when the user navigates elsewhere", async () => { |
| 188 | testState.reset(draft); | 213 | testState.reset(draft); |
| 189 | const openThread = useNewThreadHandler(); | 214 | const openThread = useNewThreadHandler(); |
apps/web/src/hooks/useHandleNewThread.ts+7-16| ... | @@ -4,12 +4,7 @@ import { | ... | @@ -4,12 +4,7 @@ import { |
| 4 | scopeProjectRef, | 4 | scopeProjectRef, |
| 5 | scopeThreadRef, | 5 | scopeThreadRef, |
| 6 | } from "@t3tools/client-runtime/environment"; | 6 | } from "@t3tools/client-runtime/environment"; |
| 7 | import { | 7 | import { DEFAULT_SERVER_SETTINGS, type ScopedProjectRef, type ThreadId } from "@t3tools/contracts"; |
| 8 | DEFAULT_RUNTIME_MODE, | ||
| 9 | DEFAULT_SERVER_SETTINGS, | ||
| 10 | type ScopedProjectRef, | ||
| 11 | type ThreadId, | ||
| 12 | } from "@t3tools/contracts"; | ||
| 13 | import { useParams, useRouter } from "@tanstack/react-router"; | 8 | import { useParams, useRouter } from "@tanstack/react-router"; |
| 14 | import { useCallback, useMemo } from "react"; | 9 | import { useCallback, useMemo } from "react"; |
| 15 | import { | 10 | import { |
| ... | @@ -100,8 +95,8 @@ export function useNewThreadHandler() { | ... | @@ -100,8 +95,8 @@ export function useNewThreadHandler() { |
| 100 | const routeChangedSinceRequest = () => router.state.location.href !== requestingRouteHref; | 95 | const routeChangedSinceRequest = () => router.state.location.href !== requestingRouteHref; |
| 101 | const currentRouteTarget = getCurrentRouteTarget(); | 96 | const currentRouteTarget = getCurrentRouteTarget(); |
| 102 | // A new thread carries the user's working mode from the thread being | 97 | // A new thread carries the user's working mode from the thread being |
| 103 | // viewed. The target project's configured model still wins; runtime and | 98 | // viewed. The target project's configured model still wins; interaction |
| 104 | // interaction modes carry independently. Branch, worktree, and env mode | 99 | // mode carries independently. Permissions, branch, worktree, and env mode |
| 105 | // come from configured defaults unless the caller passes them explicitly. | 100 | // come from configured defaults unless the caller passes them explicitly. |
| 106 | const carrySourceShell = | 101 | const carrySourceShell = |
| 107 | currentRouteTarget?.kind === "server" | 102 | currentRouteTarget?.kind === "server" |
| ... | @@ -124,11 +119,6 @@ export function useNewThreadHandler() { | ... | @@ -124,11 +119,6 @@ export function useNewThreadHandler() { |
| 124 | : null; | 119 | : null; |
| 125 | const carryModelSelection = | 120 | const carryModelSelection = |
| 126 | composerModelSelection ?? carrySourceShell?.modelSelection ?? null; | 121 | composerModelSelection ?? carrySourceShell?.modelSelection ?? null; |
| 127 | const carryRuntimeMode = | ||
| 128 | carrySourceComposer?.runtimeMode ?? | ||
| 129 | carrySourceShell?.runtimeMode ?? | ||
| 130 | carrySourceDraft?.runtimeMode ?? | ||
| 131 | null; | ||
| 132 | const carryInteractionMode = | 122 | const carryInteractionMode = |
| 133 | carrySourceComposer?.interactionMode ?? | 123 | carrySourceComposer?.interactionMode ?? |
| 134 | carrySourceShell?.interactionMode ?? | 124 | carrySourceShell?.interactionMode ?? |
| ... | @@ -147,6 +137,7 @@ export function useNewThreadHandler() { | ... | @@ -147,6 +137,7 @@ export function useNewThreadHandler() { |
| 147 | project, | 137 | project, |
| 148 | ); | 138 | ); |
| 149 | const projectDefaultModelSelection = projectSettings.settings.defaultModelSelection; | 139 | const projectDefaultModelSelection = projectSettings.settings.defaultModelSelection; |
| 140 | const defaultRuntimeMode = projectSettings.settings.defaultRuntimeMode; | ||
| 150 | const projectThreadEnvMode = | 141 | const projectThreadEnvMode = |
| 151 | projectSettings.sources.defaultThreadEnvMode === "project" | 142 | projectSettings.sources.defaultThreadEnvMode === "project" |
| 152 | ? projectSettings.settings.defaultThreadEnvMode | 143 | ? projectSettings.settings.defaultThreadEnvMode |
| ... | @@ -276,7 +267,7 @@ export function useNewThreadHandler() { | ... | @@ -276,7 +267,7 @@ export function useNewThreadHandler() { |
| 276 | if (workspaceContext) { | 267 | if (workspaceContext) { |
| 277 | setDraftThreadContext(emptyStoredDraftThread.draftId, { | 268 | setDraftThreadContext(emptyStoredDraftThread.draftId, { |
| 278 | ...workspaceContext, | 269 | ...workspaceContext, |
| 279 | ...(carryRuntimeMode ? { runtimeMode: carryRuntimeMode } : {}), | 270 | ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), |
| 280 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), | 271 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), |
| 281 | }); | 272 | }); |
| 282 | } | 273 | } |
| ... | @@ -313,7 +304,7 @@ export function useNewThreadHandler() { | ... | @@ -313,7 +304,7 @@ export function useNewThreadHandler() { |
| 313 | { | 304 | { |
| 314 | threadId: emptyStoredDraftThread.threadId, | 305 | threadId: emptyStoredDraftThread.threadId, |
| 315 | ...workspaceContext, | 306 | ...workspaceContext, |
| 316 | ...(carryRuntimeMode ? { runtimeMode: carryRuntimeMode } : {}), | 307 | ...(!isDraftAlreadyOpen ? { runtimeMode: defaultRuntimeMode } : {}), |
| 317 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), | 308 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), |
| 318 | }, | 309 | }, |
| 319 | ); | 310 | ); |
| ... | @@ -426,7 +417,7 @@ export function useNewThreadHandler() { | ... | @@ -426,7 +417,7 @@ export function useNewThreadHandler() { |
| 426 | envMode: initialEnvMode, | 417 | envMode: initialEnvMode, |
| 427 | newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, | 418 | newWorktreesStartFromOrigin: projectSettings.settings.newWorktreesStartFromOrigin, |
| 428 | }), | 419 | }), |
| 429 | runtimeMode: carryRuntimeMode ?? DEFAULT_RUNTIME_MODE, | 420 | runtimeMode: defaultRuntimeMode, |
| 430 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), | 421 | ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), |
| 431 | }); | 422 | }); |
| 432 | applyStickyState(draftId); | 423 | applyStickyState(draftId); |
docs/user/permission-modes.md+4-2| ... | @@ -3,8 +3,10 @@ | ... | @@ -3,8 +3,10 @@ |
| 3 | Permission modes control when an agent needs your approval to act. Choose a mode in the message | 3 | Permission modes control when an agent needs your approval to act. Choose a mode in the message |
| 4 | composer; it applies to that thread. | 4 | composer; it applies to that thread. |
| 5 | 5 | ||
| 6 | New threads start in **Full access** unless you choose another mode before sending. A thread | 6 | Set the default for new threads in **Settings → General → New threads → Permissions**. |
| 7 | created from another thread inherits its mode. | 7 | Projects can override the environment default. New threads use this setting rather than the |
| 8 | mode of the thread you were viewing. The initial default is **Full access**; existing threads | ||
| 9 | and modes you choose in a draft keep their permissions. | ||
| 8 | 10 | ||
| 9 | | Mode | Behavior | | 11 | | Mode | Behavior | |
| 10 | | --------------------- | ------------------------------------------------------------------------------------- | | 12 | | --------------------- | ------------------------------------------------------------------------------------- | |
packages/contracts/src/settings.test.ts+29| ... | @@ -20,6 +20,35 @@ const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); | ... | @@ -20,6 +20,35 @@ const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); |
| 20 | const encodeServerSettings = Schema.encodeSync(ServerSettings); | 20 | const encodeServerSettings = Schema.encodeSync(ServerSettings); |
| 21 | const decodeClaudeSettings = Schema.decodeUnknownSync(ClaudeSettings); | 21 | const decodeClaudeSettings = Schema.decodeUnknownSync(ClaudeSettings); |
| 22 | 22 | ||
| 23 | describe("ServerSettings default permissions", () => { | ||
| 24 | it("keeps full access for settings saved before a default was configured", () => { | ||
| 25 | expect(decodeServerSettings({}).defaultRuntimeMode).toBe("full-access"); | ||
| 26 | expect(DEFAULT_SERVER_SETTINGS.defaultRuntimeMode).toBe("full-access"); | ||
| 27 | }); | ||
| 28 | |||
| 29 | it.each(["approval-required", "auto-accept-edits", "auto", "full-access"])( | ||
| 30 | "round-trips %s as an environment default and project override", | ||
| 31 | (defaultRuntimeMode) => { | ||
| 32 | const input = { | ||
| 33 | defaultRuntimeMode, | ||
| 34 | projectSettingsOverrides: { project: { defaultRuntimeMode } }, | ||
| 35 | }; | ||
| 36 | expect(encodeServerSettings(decodeServerSettings(input))).toMatchObject(input); | ||
| 37 | expect(decodeServerSettingsPatch(input)).toEqual(input); | ||
| 38 | }, | ||
| 39 | ); | ||
| 40 | |||
| 41 | it("rejects unsupported permission defaults", () => { | ||
| 42 | expect(() => decodeServerSettings({ defaultRuntimeMode: "unsupported" })).toThrow(); | ||
| 43 | expect(() => decodeServerSettingsPatch({ defaultRuntimeMode: "unsupported" })).toThrow(); | ||
| 44 | expect(() => | ||
| 45 | decodeServerSettingsPatch({ | ||
| 46 | projectSettingsOverrides: { project: { defaultRuntimeMode: "unsupported" } }, | ||
| 47 | }), | ||
| 48 | ).toThrow(); | ||
| 49 | }); | ||
| 50 | }); | ||
| 51 | |||
| 23 | describe("ServerSettings usage price overrides", () => { | 52 | describe("ServerSettings usage price overrides", () => { |
| 24 | const prices = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 }; | 53 | const prices = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 }; |
| 25 | 54 |
packages/contracts/src/settings.ts+12-1| ... | @@ -18,7 +18,12 @@ import { | ... | @@ -18,7 +18,12 @@ import { |
| 18 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, | 18 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, |
| 19 | ProviderOptionSelections, | 19 | ProviderOptionSelections, |
| 20 | } from "./model.ts"; | 20 | } from "./model.ts"; |
| 21 | import { ModelSelection, ProjectScript } from "./orchestration.ts"; | 21 | import { |
| 22 | DEFAULT_RUNTIME_MODE, | ||
| 23 | ModelSelection, | ||
| 24 | ProjectScript, | ||
| 25 | RuntimeMode, | ||
| 26 | } from "./orchestration.ts"; | ||
| 22 | import { BrowserProfile, BrowserProfileId, DEFAULT_BROWSER_PROFILE_ID } from "./browserProfile.ts"; | 27 | import { BrowserProfile, BrowserProfileId, DEFAULT_BROWSER_PROFILE_ID } from "./browserProfile.ts"; |
| 23 | import { | 28 | import { |
| 24 | DEFAULT_PREVIEW_APPEARANCE, | 29 | DEFAULT_PREVIEW_APPEARANCE, |
| ... | @@ -934,6 +939,7 @@ export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; | ... | @@ -934,6 +939,7 @@ export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; |
| 934 | */ | 939 | */ |
| 935 | export const PROJECT_SCOPED_SERVER_SETTING_KEYS = [ | 940 | export const PROJECT_SCOPED_SERVER_SETTING_KEYS = [ |
| 936 | "defaultModelSelection", | 941 | "defaultModelSelection", |
| 942 | "defaultRuntimeMode", | ||
| 937 | "defaultThreadEnvMode", | 943 | "defaultThreadEnvMode", |
| 938 | "newWorktreesStartFromOrigin", | 944 | "newWorktreesStartFromOrigin", |
| 939 | "defaultAutoPull", | 945 | "defaultAutoPull", |
| ... | @@ -958,6 +964,7 @@ export type ProjectScopedServerSettingKey = (typeof PROJECT_SCOPED_SERVER_SETTIN | ... | @@ -958,6 +964,7 @@ export type ProjectScopedServerSettingKey = (typeof PROJECT_SCOPED_SERVER_SETTIN |
| 958 | */ | 964 | */ |
| 959 | export const ProjectSettingsOverrides = Schema.Struct({ | 965 | export const ProjectSettingsOverrides = Schema.Struct({ |
| 960 | defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), | 966 | defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), |
| 967 | defaultRuntimeMode: Schema.optionalKey(RuntimeMode), | ||
| 961 | defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode), | 968 | defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode), |
| 962 | newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean), | 969 | newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean), |
| 963 | defaultAutoPull: Schema.optionalKey(Schema.Boolean), | 970 | defaultAutoPull: Schema.optionalKey(Schema.Boolean), |
| ... | @@ -1015,6 +1022,9 @@ export const ServerSettings = Schema.Struct({ | ... | @@ -1015,6 +1022,9 @@ export const ServerSettings = Schema.Struct({ |
| 1015 | defaultModelSelection: Schema.NullOr(ModelSelection).pipe( | 1022 | defaultModelSelection: Schema.NullOr(ModelSelection).pipe( |
| 1016 | Schema.withDecodingDefault(Effect.succeed(null)), | 1023 | Schema.withDecodingDefault(Effect.succeed(null)), |
| 1017 | ), | 1024 | ), |
| 1025 | defaultRuntimeMode: RuntimeMode.pipe( | ||
| 1026 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE)), | ||
| 1027 | ), | ||
| 1018 | /** | 1028 | /** |
| 1019 | * Per-project overrides of the keys in `PROJECT_SCOPED_SERVER_SETTING_KEYS`. | 1029 | * Per-project overrides of the keys in `PROJECT_SCOPED_SERVER_SETTING_KEYS`. |
| 1020 | * The source of truth for project settings; `projectAgentBrowserAccessOverrides`, | 1030 | * The source of truth for project settings; `projectAgentBrowserAccessOverrides`, |
| ... | @@ -1328,6 +1338,7 @@ export const ServerSettingsPatch = Schema.Struct({ | ... | @@ -1328,6 +1338,7 @@ export const ServerSettingsPatch = Schema.Struct({ |
| 1328 | Schema.Record(ProjectId, Schema.NullOr(Schema.Boolean)), | 1338 | Schema.Record(ProjectId, Schema.NullOr(Schema.Boolean)), |
| 1329 | ), | 1339 | ), |
| 1330 | defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), | 1340 | defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), |
| 1341 | defaultRuntimeMode: Schema.optionalKey(RuntimeMode), | ||
| 1331 | /** | 1342 | /** |
| 1332 | * Per-project entry replacement: each entry replaces that project's whole | 1343 | * Per-project entry replacement: each entry replaces that project's whole |
| 1333 | * override set and `null` removes it. Clearing one override means resending | 1344 | * override set and `null` removes it. Clearing one override means resending |