authorgravatar for me.oski646@gmail.comOskar Sekutowicz <me.oski646@gmail.com> 2026-04-09 20:10:05+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-09 18:10:05+00:00
log7372184d06646dde603e5fd92bc23d3f35ae8d10
tree2e2679de6bf398fac4139df12d8126e043ec796f
parentd9ded65dfef01156056d99dedfe2265d0cb5ea59
signature Signed by PGP key B5690EEEBB952194

fix: map runtime modes to correct permission levels (#1587)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

9 files changed, 189 insertions(+), 105 deletions(-)

apps/server/src/codexAppServerManager.ts+18-12
...@@ -290,20 +290,26 @@ In Default mode, strongly prefer making reasonable assumptions and executing the...@@ -290,20 +290,26 @@ In Default mode, strongly prefer making reasonable assumptions and executing the
290</collaboration_mode>`;290</collaboration_mode>`;
291291
292function mapCodexRuntimeMode(runtimeMode: RuntimeMode): {292function mapCodexRuntimeMode(runtimeMode: RuntimeMode): {
293 readonly approvalPolicy: "on-request" | "never";293 readonly approvalPolicy: "untrusted" | "on-request" | "never";
294 readonly sandbox: "workspace-write" | "danger-full-access";294 readonly sandbox: "read-only" | "workspace-write" | "danger-full-access";
295} {295} {
296 if (runtimeMode === "approval-required") {296 switch (runtimeMode) {
297 return {297 case "approval-required":
298 approvalPolicy: "on-request",298 return {
299 sandbox: "workspace-write",299 approvalPolicy: "untrusted",
300 };300 sandbox: "read-only",
301 };
302 case "auto-accept-edits":
303 return {
304 approvalPolicy: "on-request",
305 sandbox: "workspace-write",
306 };
307 case "full-access":
308 return {
309 approvalPolicy: "never",
310 sandbox: "danger-full-access",
311 };
301 }312 }
302
303 return {
304 approvalPolicy: "never",
305 sandbox: "danger-full-access",
306 };
307}313}
308314
309/**315/**
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+51-44
...@@ -14,6 +14,7 @@ import {...@@ -14,6 +14,7 @@ import {
14 ApprovalRequestId,14 ApprovalRequestId,
15 ProviderItemId,15 ProviderItemId,
16 ProviderRuntimeEvent,16 ProviderRuntimeEvent,
17 type RuntimeMode,
17 ThreadId,18 ThreadId,
18} from "@t3tools/contracts";19} from "@t3tools/contracts";
19import { assert, describe, it } from "@effect/vitest";20import { assert, describe, it } from "@effect/vitest";
...@@ -2496,57 +2497,63 @@ describe("ClaudeAdapterLive", () => {...@@ -2496,57 +2497,63 @@ describe("ClaudeAdapterLive", () => {
2496 );2497 );
2497 });2498 });
24982499
2499 it.effect("restores base permission mode on sendTurn when interactionMode is default", () => {2500 it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([
2500 const harness = makeHarness();2501 { runtimeMode: "full-access", expectedBase: "bypassPermissions" },
2501 return Effect.gen(function* () {2502 { runtimeMode: "approval-required", expectedBase: "default" },
2502 const adapter = yield* ClaudeAdapter;2503 { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" },
2504 ])(
2505 "restores $expectedBase permission mode after plan turn ($runtimeMode)",
2506 ({ runtimeMode, expectedBase }) => {
2507 const harness = makeHarness();
2508 return Effect.gen(function* () {
2509 const adapter = yield* ClaudeAdapter;
25032510
2504 const session = yield* adapter.startSession({2511 const session = yield* adapter.startSession({
2505 threadId: THREAD_ID,2512 threadId: THREAD_ID,
2506 provider: "claudeAgent",2513 provider: "claudeAgent",
2507 runtimeMode: "full-access",2514 runtimeMode,
2508 });2515 });
25092516
2510 // First turn in plan mode2517 // First turn in plan mode
2511 yield* adapter.sendTurn({2518 yield* adapter.sendTurn({
2512 threadId: session.threadId,2519 threadId: session.threadId,
2513 input: "plan this",2520 input: "plan this",
2514 interactionMode: "plan",2521 interactionMode: "plan",
2515 attachments: [],2522 attachments: [],
2516 });2523 });
25172524
2518 // Complete the turn so we can send another2525 // Complete the turn so we can send another
2519 const turnCompletedFiber = yield* Stream.filter(2526 const turnCompletedFiber = yield* Stream.filter(
2520 adapter.streamEvents,2527 adapter.streamEvents,
2521 (event) => event.type === "turn.completed",2528 (event) => event.type === "turn.completed",
2522 ).pipe(Stream.runHead, Effect.forkChild);2529 ).pipe(Stream.runHead, Effect.forkChild);
25232530
2524 harness.query.emit({2531 harness.query.emit({
2525 type: "result",2532 type: "result",
2526 subtype: "success",2533 subtype: "success",
2527 is_error: false,2534 is_error: false,
2528 errors: [],2535 errors: [],
2529 session_id: "sdk-session-plan-restore",2536 session_id: `sdk-session-${runtimeMode}`,
2530 uuid: "result-plan",2537 uuid: `result-${runtimeMode}`,
2531 } as unknown as SDKMessage);2538 } as unknown as SDKMessage);
25322539
2533 yield* Fiber.join(turnCompletedFiber);2540 yield* Fiber.join(turnCompletedFiber);
25342541
2535 // Second turn back to default2542 // Second turn back to default
2536 yield* adapter.sendTurn({2543 yield* adapter.sendTurn({
2537 threadId: session.threadId,2544 threadId: session.threadId,
2538 input: "now do it",2545 input: "now do it",
2539 interactionMode: "default",2546 interactionMode: "default",
2540 attachments: [],2547 attachments: [],
2541 });2548 });
25422549
2543 // First call sets "plan", second call restores "bypassPermissions" (the base for full-access)2550 assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]);
2544 assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", "bypassPermissions"]);2551 }).pipe(
2545 }).pipe(2552 Effect.provideService(Random.Random, makeDeterministicRandomService()),
2546 Effect.provideService(Random.Random, makeDeterministicRandomService()),2553 Effect.provide(harness.layer),
2547 Effect.provide(harness.layer),2554 );
2548 );2555 },
2549 });2556 );
25502557
2551 it.effect("does not call setPermissionMode when interactionMode is absent", () => {2558 it.effect("does not call setPermissionMode when interactionMode is absent", () => {
2552 const harness = makeHarness();2559 const harness = makeHarness();
apps/server/src/provider/Layers/ClaudeAdapter.ts+6-3
...@@ -2693,7 +2693,11 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2693,7 +2693,11 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2693 ? modelSelection.options.thinking2693 ? modelSelection.options.thinking
2694 : undefined;2694 : undefined;
2695 const effectiveEffort = getEffectiveClaudeCodeEffort(effort);2695 const effectiveEffort = getEffectiveClaudeCodeEffort(effort);
2696 const permissionMode = input.runtimeMode === "full-access" ? "bypassPermissions" : undefined;2696 const runtimeModeToPermission: Record<string, PermissionMode> = {
2697 "auto-accept-edits": "acceptEdits",
2698 "full-access": "bypassPermissions",
2699 };
2700 const permissionMode = runtimeModeToPermission[input.runtimeMode];
2697 const settings = {2701 const settings = {
2698 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),2702 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
2699 ...(fastMode ? { fastMode: true } : {}),2703 ...(fastMode ? { fastMode: true } : {}),
...@@ -2881,8 +2885,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2881,8 +2885,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2881 });2885 });
2882 } else if (input.interactionMode === "default") {2886 } else if (input.interactionMode === "default") {
2883 yield* Effect.tryPromise({2887 yield* Effect.tryPromise({
2884 try: () =>2888 try: () => context.query.setPermissionMode(context.basePermissionMode ?? "default"),
2885 context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"),
2886 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),2889 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),
2887 });2890 });
2888 }2891 }
apps/web/src/components/ChatView.browser.tsx+36
...@@ -956,6 +956,16 @@ async function waitForButtonContainingText(text: string): Promise<HTMLButtonElem...@@ -956,6 +956,16 @@ async function waitForButtonContainingText(text: string): Promise<HTMLButtonElem
956 );956 );
957}957}
958958
959async function waitForSelectItemContainingText(text: string): Promise<HTMLElement> {
960 return waitForElement(
961 () =>
962 Array.from(document.querySelectorAll<HTMLElement>('[data-slot="select-item"]')).find((item) =>
963 item.textContent?.includes(text),
964 ) ?? null,
965 `Unable to find select item containing "${text}".`,
966 );
967}
968
959async function expectComposerActionsContained(): Promise<void> {969async function expectComposerActionsContained(): Promise<void> {
960 const footer = await waitForElement(970 const footer = await waitForElement(
961 () => document.querySelector<HTMLElement>('[data-chat-composer-footer="true"]'),971 () => document.querySelector<HTMLElement>('[data-chat-composer-footer="true"]'),
...@@ -2326,6 +2336,32 @@ describe("ChatView timeline estimator parity (full app)", () => {...@@ -2326,6 +2336,32 @@ describe("ChatView timeline estimator parity (full app)", () => {
2326 }2336 }
2327 });2337 });
23282338
2339 it("shows runtime mode descriptions in the desktop composer access select", async () => {
2340 setDraftThreadWithoutWorktree();
2341
2342 const mounted = await mountChatView({
2343 viewport: WIDE_FOOTER_VIEWPORT,
2344 snapshot: createDraftOnlySnapshot(),
2345 });
2346
2347 try {
2348 const runtimeModeSelect = await waitForButtonByText("Full access");
2349 runtimeModeSelect.click();
2350
2351 expect((await waitForSelectItemContainingText("Supervised")).textContent).toContain(
2352 "Ask before commands and file changes",
2353 );
2354
2355 const autoAcceptItem = await waitForSelectItemContainingText("Auto-accept edits");
2356 expect(autoAcceptItem.textContent).toContain("Auto-approve edits");
2357 expect((await waitForSelectItemContainingText("Full access")).textContent).toContain(
2358 "Allow commands and edits without prompts",
2359 );
2360 } finally {
2361 await mounted.cleanup();
2362 }
2363 });
2364
2329 it("keeps removed terminal context pills removed when a new one is added", async () => {2365 it("keeps removed terminal context pills removed when a new one is added", async () => {
2330 const removedLabel = "Terminal 1 lines 1-2";2366 const removedLabel = "Terminal 1 lines 1-2";
2331 const addedLabel = "Terminal 2 lines 9-10";2367 const addedLabel = "Terminal 2 lines 9-10";
apps/web/src/components/ChatView.tsx+61-28
...@@ -115,10 +115,13 @@ import {...@@ -115,10 +115,13 @@ import {
115 ListTodoIcon,115 ListTodoIcon,
116 LockIcon,116 LockIcon,
117 LockOpenIcon,117 LockOpenIcon,
118 type LucideIcon,
119 PenLineIcon,
118 XIcon,120 XIcon,
119} from "lucide-react";121} from "lucide-react";
120import { Button } from "./ui/button";122import { Button } from "./ui/button";
121import { Separator } from "./ui/separator";123import { Separator } from "./ui/separator";
124import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
122import { cn, randomUUID } from "~/lib/utils";125import { cn, randomUUID } from "~/lib/utils";
123import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";126import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
124import { toastManager } from "./ui/toast";127import { toastManager } from "./ui/toast";
...@@ -410,6 +413,29 @@ interface TerminalLaunchContext {...@@ -410,6 +413,29 @@ interface TerminalLaunchContext {
410 worktreePath: string | null;413 worktreePath: string | null;
411}414}
412415
416const runtimeModeConfig: Record<
417 RuntimeMode,
418 { label: string; description: string; icon: LucideIcon }
419> = {
420 "approval-required": {
421 label: "Supervised",
422 description: "Ask before commands and file changes.",
423 icon: LockIcon,
424 },
425 "auto-accept-edits": {
426 label: "Auto-accept edits",
427 description: "Auto-approve edits, ask before other actions.",
428 icon: PenLineIcon,
429 },
430 "full-access": {
431 label: "Full access",
432 description: "Allow commands and edits without prompts.",
433 icon: LockOpenIcon,
434 },
435};
436
437const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[];
438
413type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">;439type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">;
414440
415function useLocalDispatchState(input: {441function useLocalDispatchState(input: {
...@@ -960,6 +986,8 @@ export default function ChatView(props: ChatViewProps) {...@@ -960,6 +986,8 @@ export default function ChatView(props: ChatViewProps) {
960 composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;986 composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;
961 const interactionMode =987 const interactionMode =
962 composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;988 composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
989 const runtimeModeOption = runtimeModeConfig[runtimeMode];
990 const RuntimeModeIcon = runtimeModeOption.icon;
963 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;991 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
964 const canCheckoutPullRequestIntoThread = isLocalDraftThread;992 const canCheckoutPullRequestIntoThread = isLocalDraftThread;
965 const diffOpen = rawSearch.diff === "1";993 const diffOpen = rawSearch.diff === "1";
...@@ -2350,11 +2378,6 @@ export default function ChatView(props: ChatViewProps) {...@@ -2350,11 +2378,6 @@ export default function ChatView(props: ChatViewProps) {
2350 const toggleInteractionMode = useCallback(() => {2378 const toggleInteractionMode = useCallback(() => {
2351 handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan");2379 handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan");
2352 }, [handleInteractionModeChange, interactionMode]);2380 }, [handleInteractionModeChange, interactionMode]);
2353 const toggleRuntimeMode = useCallback(() => {
2354 void handleRuntimeModeChange(
2355 runtimeMode === "full-access" ? "approval-required" : "full-access",
2356 );
2357 }, [handleRuntimeModeChange, runtimeMode]);
2358 const togglePlanSidebar = useCallback(() => {2381 const togglePlanSidebar = useCallback(() => {
2359 setPlanSidebarOpen((open) => {2382 setPlanSidebarOpen((open) => {
2360 if (open) {2383 if (open) {
...@@ -4651,7 +4674,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -4651,7 +4674,7 @@ export default function ChatView(props: ChatViewProps) {
4651 traitsMenuContent={providerTraitsMenuContent}4674 traitsMenuContent={providerTraitsMenuContent}
4652 onToggleInteractionMode={toggleInteractionMode}4675 onToggleInteractionMode={toggleInteractionMode}
4653 onTogglePlanSidebar={togglePlanSidebar}4676 onTogglePlanSidebar={togglePlanSidebar}
4654 onToggleRuntimeMode={toggleRuntimeMode}4677 onRuntimeModeChange={handleRuntimeModeChange}
4655 />4678 />
4656 ) : (4679 ) : (
4657 <>4680 <>
...@@ -4693,29 +4716,39 @@ export default function ChatView(props: ChatViewProps) {...@@ -4693,29 +4716,39 @@ export default function ChatView(props: ChatViewProps) {
4693 className="mx-0.5 hidden h-4 sm:block"4716 className="mx-0.5 hidden h-4 sm:block"
4694 />4717 />
46954718
4696 <Button4719 <Select
4697 variant="ghost"4720 value={runtimeMode}
4698 className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"4721 onValueChange={(value) => handleRuntimeModeChange(value!)}
4699 size="sm"
4700 type="button"
4701 onClick={() =>
4702 void handleRuntimeModeChange(
4703 runtimeMode === "full-access"
4704 ? "approval-required"
4705 : "full-access",
4706 )
4707 }
4708 title={
4709 runtimeMode === "full-access"
4710 ? "Full access — click to require approvals"
4711 : "Approval required — click for full access"
4712 }
4713 >4722 >
4714 {runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}4723 <SelectTrigger
4715 <span className="sr-only sm:not-sr-only">4724 variant="ghost"
4716 {runtimeMode === "full-access" ? "Full access" : "Supervised"}4725 size="sm"
4717 </span>4726 aria-label="Runtime mode"
4718 </Button>4727 title={runtimeModeOption.description}
4728 >
4729 <RuntimeModeIcon className="size-4" />
4730 <SelectValue>{runtimeModeOption.label}</SelectValue>
4731 </SelectTrigger>
4732 <SelectPopup alignItemWithTrigger={false}>
4733 {runtimeModeOptions.map((mode) => {
4734 const option = runtimeModeConfig[mode];
4735 const OptionIcon = option.icon;
4736 return (
4737 <SelectItem key={mode} value={mode} className="min-w-64 py-2">
4738 <div className="grid min-w-0 gap-0.5">
4739 <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
4740 <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />
4741 {option.label}
4742 </span>
4743 <span className="text-muted-foreground text-xs leading-4">
4744 {option.description}
4745 </span>
4746 </div>
4747 </SelectItem>
4748 );
4749 })}
4750 </SelectPopup>
4751 </Select>
47194752
4720 {activePlan || sidebarProposedPlan || planSidebarOpen ? (4753 {activePlan || sidebarProposedPlan || planSidebarOpen ? (
4721 <>4754 <>
apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx+1-1
...@@ -138,7 +138,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str...@@ -138,7 +138,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
138 }138 }
139 onToggleInteractionMode={vi.fn()}139 onToggleInteractionMode={vi.fn()}
140 onTogglePlanSidebar={vi.fn()}140 onTogglePlanSidebar={vi.fn()}
141 onToggleRuntimeMode={vi.fn()}141 onRuntimeModeChange={vi.fn()}
142 />,142 />,
143 { container: host },143 { container: host },
144 );144 );
apps/web/src/components/chat/CompactComposerControlsMenu.tsx+3-2
...@@ -20,7 +20,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls...@@ -20,7 +20,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
20 traitsMenuContent?: ReactNode;20 traitsMenuContent?: ReactNode;
21 onToggleInteractionMode: () => void;21 onToggleInteractionMode: () => void;
22 onTogglePlanSidebar: () => void;22 onTogglePlanSidebar: () => void;
23 onToggleRuntimeMode: () => void;23 onRuntimeModeChange: (mode: RuntimeMode) => void;
24}) {24}) {
25 return (25 return (
26 <Menu>26 <Menu>
...@@ -60,10 +60,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls...@@ -60,10 +60,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
60 value={props.runtimeMode}60 value={props.runtimeMode}
61 onValueChange={(value) => {61 onValueChange={(value) => {
62 if (!value || value === props.runtimeMode) return;62 if (!value || value === props.runtimeMode) return;
63 props.onToggleRuntimeMode();63 props.onRuntimeModeChange(value as RuntimeMode);
64 }}64 }}
65 >65 >
66 <MenuRadioItem value="approval-required">Supervised</MenuRadioItem>66 <MenuRadioItem value="approval-required">Supervised</MenuRadioItem>
67 <MenuRadioItem value="auto-accept-edits">Auto-accept edits</MenuRadioItem>
67 <MenuRadioItem value="full-access">Full access</MenuRadioItem>68 <MenuRadioItem value="full-access">Full access</MenuRadioItem>
68 </MenuRadioGroup>69 </MenuRadioGroup>
69 {props.activePlan ? (70 {props.activePlan ? (
apps/web/src/composerDraftStore.ts+8-14
...@@ -45,6 +45,7 @@ import { UnifiedSettings } from "@t3tools/contracts/settings";...@@ -45,6 +45,7 @@ import { UnifiedSettings } from "@t3tools/contracts/settings";
45export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";45export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";
46const COMPOSER_DRAFT_STORAGE_VERSION = 5;46const COMPOSER_DRAFT_STORAGE_VERSION = 5;
47const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);47const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);
48const isRuntimeMode = Schema.is(RuntimeMode);
48export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type;49export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type;
4950
50export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));51export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));
...@@ -1195,11 +1196,9 @@ function normalizePersistedDraftThreads(...@@ -1195,11 +1196,9 @@ function normalizePersistedDraftThreads(
1195 typeof createdAt === "string" && createdAt.length > 01196 typeof createdAt === "string" && createdAt.length > 0
1196 ? createdAt1197 ? createdAt
1197 : new Date().toISOString(),1198 : new Date().toISOString(),
1198 runtimeMode:1199 runtimeMode: isRuntimeMode(candidateDraftThread.runtimeMode)
1199 candidateDraftThread.runtimeMode === "approval-required" ||1200 ? candidateDraftThread.runtimeMode
1200 candidateDraftThread.runtimeMode === "full-access"1201 : DEFAULT_RUNTIME_MODE,
1201 ? candidateDraftThread.runtimeMode
1202 : DEFAULT_RUNTIME_MODE,
1203 interactionMode:1202 interactionMode:
1204 candidateDraftThread.interactionMode === "plan" ||1203 candidateDraftThread.interactionMode === "plan" ||
1205 candidateDraftThread.interactionMode === "default"1204 candidateDraftThread.interactionMode === "default"
...@@ -1318,11 +1317,9 @@ function normalizePersistedDraftsByThreadId(...@@ -1318,11 +1317,9 @@ function normalizePersistedDraftsByThreadId(
1318 return normalized ? [normalized] : [];1317 return normalized ? [normalized] : [];
1319 })1318 })
1320 : [];1319 : [];
1321 const runtimeMode =1320 const runtimeMode = isRuntimeMode(draftCandidate.runtimeMode)
1322 draftCandidate.runtimeMode === "approval-required" ||1321 ? draftCandidate.runtimeMode
1323 draftCandidate.runtimeMode === "full-access"1322 : null;
1324 ? draftCandidate.runtimeMode
1325 : null;
1326 const interactionMode =1323 const interactionMode =
1327 draftCandidate.interactionMode === "plan" || draftCandidate.interactionMode === "default"1324 draftCandidate.interactionMode === "plan" || draftCandidate.interactionMode === "default"
1328 ? draftCandidate.interactionMode1325 ? draftCandidate.interactionMode
...@@ -2373,10 +2370,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2373,10 +2370,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2373 if (threadKey.length === 0) {2370 if (threadKey.length === 0) {
2374 return;2371 return;
2375 }2372 }
2376 const nextRuntimeMode =2373 const nextRuntimeMode = isRuntimeMode(runtimeMode) ? runtimeMode : null;
2377 runtimeMode === "approval-required" || runtimeMode === "full-access"
2378 ? runtimeMode
2379 : null;
2380 set((state) => {2374 set((state) => {
2381 const existing = state.draftsByThreadKey[threadKey];2375 const existing = state.draftsByThreadKey[threadKey];
2382 if (!existing && nextRuntimeMode === null) {2376 if (!existing && nextRuntimeMode === null) {
packages/contracts/src/orchestration.ts+5-1
...@@ -59,7 +59,11 @@ export type ClaudeModelSelection = typeof ClaudeModelSelection.Type;...@@ -59,7 +59,11 @@ export type ClaudeModelSelection = typeof ClaudeModelSelection.Type;
59export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]);59export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]);
60export type ModelSelection = typeof ModelSelection.Type;60export type ModelSelection = typeof ModelSelection.Type;
6161
62export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]);62export const RuntimeMode = Schema.Literals([
63 "approval-required",
64 "auto-accept-edits",
65 "full-access",
66]);
63export type RuntimeMode = typeof RuntimeMode.Type;67export type RuntimeMode = typeof RuntimeMode.Type;
64export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";68export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
65export const ProviderInteractionMode = Schema.Literals(["default", "plan"]);69export const ProviderInteractionMode = Schema.Literals(["default", "plan"]);