diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index ce24198f5e2..dd146948e50 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -198,10 +198,15 @@ export function NewTaskDraftScreen(props: { ? "Approve actions" : flow.runtimeMode === "auto-accept-edits" ? "Auto-accept edits" - : "Full access", + : flow.runtimeMode === "auto" + ? "Auto" + : "Full access", subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, + ...(flow.selectedModelOption?.supportsAutoRuntimeMode + ? [{ id: "options:runtime:auto", title: "Auto" }] + : []), { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -229,7 +234,12 @@ export function NewTaskDraftScreen(props: { }), }, ], - [flow.interactionMode, flow.runtimeMode, providerOptionDescriptors], + [ + flow.interactionMode, + flow.runtimeMode, + flow.selectedModelOption?.supportsAutoRuntimeMode, + providerOptionDescriptors, + ], ); const workspaceMenuActions = useMemo(() => { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 75991cae885..fc29a6d7e69 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -552,10 +552,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ? "Approve actions" : currentRuntimeMode === "auto-accept-edits" ? "Auto-accept edits" - : "Full access", + : currentRuntimeMode === "auto" + ? "Auto" + : "Full access", subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, + ...(currentModelOption?.supportsAutoRuntimeMode + ? [{ id: "options:runtime:auto", title: "Auto" }] + : []), { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -583,7 +588,12 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer }), }, ], - [currentInteractionMode, currentRuntimeMode, providerOptionDescriptors], + [ + currentInteractionMode, + currentModelOption?.supportsAutoRuntimeMode, + currentRuntimeMode, + providerOptionDescriptors, + ], ); // ── Menu handlers ──────────────────────────────────────── diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index e21682414d7..f0e44f6bdf9 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -15,6 +15,7 @@ export type ModelOption = { readonly providerKey: string; readonly providerLabel: string; readonly providerDriver: string; + readonly supportsAutoRuntimeMode: boolean; readonly capabilities: ModelCapabilities | null; readonly selection: ModelSelection; }; @@ -78,6 +79,7 @@ export function buildModelOptions( providerKey: provider.instanceId, providerLabel, providerDriver: provider.driver, + supportsAutoRuntimeMode: provider.supportsAutoRuntimeMode ?? false, capabilities: model.capabilities, selection: normalizeSelectionOptions( { @@ -107,6 +109,7 @@ export function buildModelOptions( providerKey: fallbackModelSelection.instanceId, providerLabel, providerDriver: fallbackModelSelection.instanceId, + supportsAutoRuntimeMode: false, capabilities: null, selection: fallbackModelSelection, }); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 97a93f85829..10e741d0672 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -3431,6 +3431,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model); const runtimeModeToPermission: Record = { "auto-accept-edits": "acceptEdits", + auto: "auto", "full-access": "bypassPermissions", }; const permissionMode = runtimeModeToPermission[input.runtimeMode]; diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 59b7d0464de..edd628e5600 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -47,6 +47,7 @@ const PROVIDER = ProviderDriverKind.make("claudeAgent"); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, + supportsAutoRuntimeMode: true, } as const; const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 99ac498f0c3..8ca5a9ebe89 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -273,6 +273,7 @@ function runtimeModeToThreadConfig(input: RuntimeMode): { sandbox: "read-only", }; case "auto-accept-edits": + case "auto": return { approvalPolicy: "on-request", sandbox: "workspace-write", @@ -311,6 +312,7 @@ function runtimeModeToTurnSandboxPolicy( type: "readOnly", }; case "auto-accept-edits": + case "auto": return { type: "workspaceWrite", }; diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index dfe31ffdc44..2f8a900a4ef 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -57,6 +57,7 @@ export interface ServerProviderPresentation { readonly badgeLabel?: string; readonly showInteractionModeToggle?: boolean; readonly requiresNewThreadForModelChange?: boolean; + readonly supportsAutoRuntimeMode?: boolean; } export type ServerProviderDraft = Omit; @@ -234,6 +235,9 @@ export function buildServerProvider(input: { ...(typeof input.presentation.requiresNewThreadForModelChange === "boolean" ? { requiresNewThreadForModelChange: input.presentation.requiresNewThreadForModelChange } : {}), + ...(typeof input.presentation.supportsAutoRuntimeMode === "boolean" + ? { supportsAutoRuntimeMode: input.presentation.supportsAutoRuntimeMode } + : {}), enabled: input.enabled, installed: input.probe.installed, version: input.probe.version, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 5f9aec837ca..f88fddd2b76 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -101,10 +101,15 @@ import { LockIcon, LockOpenIcon, PenLineIcon, + SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; -import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels"; +import { + getProviderAutoRuntimeModeSupport, + getProviderDisplayName, + getProviderInteractionModeToggle, +} from "../../providerModels"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, @@ -142,6 +147,11 @@ const runtimeModeConfig: Record< description: "Auto-approve edits, ask before other actions.", icon: PenLineIcon, }, + auto: { + label: "Auto", + description: "Run without prompts; a safety classifier reviews actions in the background.", + icon: SparklesIcon, + }, "full-access": { label: "Full access", description: "Allow commands and edits without prompts.", @@ -194,6 +204,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop showInteractionModeToggle: boolean; interactionMode: ProviderInteractionMode; runtimeMode: RuntimeMode; + runtimeModeOptions: ReadonlyArray; showPlanToggle: boolean; planSidebarLabel: string; planSidebarOpen: boolean; @@ -269,7 +280,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop {runtimeModeOption.label} - {runtimeModeOptions.map((mode) => { + {props.runtimeModeOptions.map((mode) => { const option = runtimeModeConfig[mode]; const OptionIcon = option.icon; return ( @@ -817,9 +828,17 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) providerStatuses, selectedProvider, ), + supportsAutoRuntimeMode: getProviderAutoRuntimeModeSupport(providerStatuses, selectedProvider), }), [providerStatuses, selectedProvider], ); + const runtimeModeOptionsForProvider = useMemo( + () => + composerProviderControls.supportsAutoRuntimeMode + ? runtimeModeOptions + : runtimeModeOptions.filter((mode) => mode !== "auto"), + [composerProviderControls.supportsAutoRuntimeMode], + ); const selectedModelSelection = useMemo( () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch), [selectedInstanceId, selectedModel, selectedModelOptionsForDispatch], @@ -2500,6 +2519,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) planSidebarLabel={planSidebarLabel} planSidebarOpen={planSidebarOpen} runtimeMode={runtimeMode} + runtimeModeOptions={runtimeModeOptionsForProvider} showInteractionModeToggle={composerProviderControls.showInteractionModeToggle} traitsMenuContent={providerTraitsMenuContent} onToggleInteractionMode={toggleInteractionMode} @@ -2518,6 +2538,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) showInteractionModeToggle={composerProviderControls.showInteractionModeToggle} interactionMode={interactionMode} runtimeMode={runtimeMode} + runtimeModeOptions={runtimeModeOptionsForProvider} showPlanToggle={showPlanSidebarToggle} planSidebarLabel={planSidebarLabel} planSidebarOpen={planSidebarOpen} diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index f1fbd193a63..fb7cf6fb331 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -12,12 +12,20 @@ import { MenuTrigger, } from "../ui/menu"; +const RUNTIME_MODE_LABELS: Record = { + "approval-required": "Supervised", + "auto-accept-edits": "Auto-accept edits", + auto: "Auto", + "full-access": "Full access", +}; + export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: { activePlan: boolean; interactionMode: ProviderInteractionMode; planSidebarLabel: string; planSidebarOpen: boolean; runtimeMode: RuntimeMode; + runtimeModeOptions: ReadonlyArray; showInteractionModeToggle: boolean; traitsMenuContent?: ReactNode; onToggleInteractionMode: () => void; @@ -69,9 +77,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls props.onRuntimeModeChange(value as RuntimeMode); }} > - Supervised - Auto-accept edits - Full access + {props.runtimeModeOptions.map((mode) => ( + + {RUNTIME_MODE_LABELS[mode]} + + ))} {props.activePlan ? ( <> diff --git a/apps/web/src/providerModels.ts b/apps/web/src/providerModels.ts index aa9afcf31e1..c39e4fa311b 100644 --- a/apps/web/src/providerModels.ts +++ b/apps/web/src/providerModels.ts @@ -53,6 +53,13 @@ export function getProviderInteractionModeToggle( return getProviderSnapshot(providers, provider)?.showInteractionModeToggle ?? true; } +export function getProviderAutoRuntimeModeSupport( + providers: ReadonlyArray, + provider: ProviderDriverKind, +): boolean { + return getProviderSnapshot(providers, provider)?.supportsAutoRuntimeMode ?? false; +} + export function isProviderEnabled( providers: ReadonlyArray, provider: ProviderDriverKind, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 623fed0917b..16e4e53a757 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -117,6 +117,7 @@ export type ModelSelection = typeof ModelSelection.Type; export const RuntimeMode = Schema.Literals([ "approval-required", "auto-accept-edits", + "auto", "full-access", ]); export type RuntimeMode = typeof RuntimeMode.Type; diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index b76ea965afe..d681570a935 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -166,6 +166,7 @@ export const ServerProvider = Schema.Struct({ continuation: Schema.optional(ServerProviderContinuation), showInteractionModeToggle: Schema.optional(Schema.Boolean), requiresNewThreadForModelChange: Schema.optional(Schema.Boolean), + supportsAutoRuntimeMode: Schema.optional(Schema.Boolean), enabled: Schema.Boolean, installed: Schema.Boolean, version: Schema.NullOr(TrimmedNonEmptyString),