diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index b73e874169c..975309f4d2a 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -169,10 +169,13 @@ 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" }, + { id: "options:runtime:auto", title: "Auto" }, { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index e13115d205f..f91c32cb5b7 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -30,6 +30,7 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; +import { ErrorBanner } from "../../components/ErrorBanner"; import { ControlPill, ControlPillMenu } from "../../components/ControlPill"; import { ProviderIcon } from "../../components/ProviderIcon"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; @@ -499,10 +500,13 @@ 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" }, + { id: "options:runtime:auto", title: "Auto" }, { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -806,6 +810,12 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer automatically. ) : null} + {/* ponytail: no reliable client-side model capability registry exists for auto-mode support */} + {props.selectedThread.session?.lastError ? ( + + + + ) : null} { ); }); + it.effect("uses native auto permissions for auto claude sessions", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + runtimeMode: "auto", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.permissionMode, "auto"); + assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("forwards claude effort levels into query options", () => { const harness = makeHarness(); return Effect.gen(function* () { @@ -3461,6 +3480,7 @@ describe("ClaudeAdapterLive", () => { { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, { runtimeMode: "approval-required", expectedBase: "default" }, { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" }, + { runtimeMode: "auto", expectedBase: "auto" }, ])( "restores $expectedBase permission mode after plan turn ($runtimeMode)", ({ runtimeMode, expectedBase }) => { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 74bb11b833c..e620cde1833 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -2980,6 +2980,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/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 93d0ecc8d2b..214c84204b5 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -176,6 +176,51 @@ describe("buildTurnStartParams", () => { }); }); + it("maps runtimeMode 'auto' to the same conservative policy as auto-accept-edits", () => { + const params = Effect.runSync( + buildTurnStartParams({ + threadId: "provider-thread-1", + runtimeMode: "auto", + prompt: "Implement it", + model: "gpt-5.3-codex", + interactionMode: "default", + attachments: [ + { + type: "image", + url: "data:image/png;base64,abc", + }, + ], + }), + ); + + assert.deepStrictEqual(params, { + threadId: "provider-thread-1", + approvalPolicy: "on-request", + sandboxPolicy: { + type: "workspaceWrite", + }, + input: [ + { + type: "text", + text: "Implement it", + }, + { + type: "image", + url: "data:image/png;base64,abc", + }, + ], + model: "gpt-5.3-codex", + collaborationMode: { + mode: "default", + settings: { + model: "gpt-5.3-codex", + reasoning_effort: "medium", + developer_instructions: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, + }, + }, + }); + }); + it("omits collaboration mode when interaction mode is absent", () => { const params = Effect.runSync( buildTurnStartParams({ @@ -642,4 +687,20 @@ describe("buildThreadStartParams visual-plan MCP", () => { }); assert.equal(params.config, undefined); }); + + it("maps runtimeMode 'auto' to the same conservative policy as auto-accept-edits", () => { + const params = buildThreadStartParams({ + cwd: "/tmp/project", + runtimeMode: "auto", + model: undefined, + serviceTier: undefined, + visualPlanMcp: undefined, + }); + + assert.deepStrictEqual(params, { + cwd: "/tmp/project", + approvalPolicy: "on-request", + sandbox: "workspace-write", + }); + }); }); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index d6b62f76074..64ca83a0120 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -320,6 +320,7 @@ function runtimeModeToThreadConfig(input: RuntimeMode): { approvalPolicy: "untrusted", sandbox: "read-only", }; + case "auto": case "auto-accept-edits": return { approvalPolicy: "on-request", @@ -376,6 +377,7 @@ function runtimeModeToTurnSandboxPolicy( return { type: "readOnly", }; + case "auto": case "auto-accept-edits": return { type: "workspaceWrite", diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 711272ffe84..04288dc739c 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -92,6 +92,7 @@ import { LockIcon, LockOpenIcon, PenLineIcon, + SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -132,6 +133,11 @@ const runtimeModeConfig: Record< description: "Auto-approve edits, ask before other actions.", icon: PenLineIcon, }, + auto: { + label: "Auto", + description: "Auto-approves safe actions; asks before risky ones.", + icon: SparklesIcon, + }, "full-access": { label: "Full access", description: "Allow commands and edits without prompts.", diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index fb43335e3ae..ebae48c2740 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -74,6 +74,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls > Supervised Auto-accept edits + Auto Full access {props.showPlanToggle ? ( diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index e910173f842..f1152117e07 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -127,6 +127,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;