Unified · Split
| @@ -290,20 +290,26 @@ In Default mode, strongly prefer making reasonable assumptions and executing the · 289 unmodified lines | ||
| 290 | 290 | </collaboration_mode>`; |
| 291 | 291 | |
| 292 | 292 | function mapCodexRuntimeMode(runtimeMode: RuntimeMode): { |
| 293 | − readonly approvalPolicy: "on-request" | "never"; | |
| 294 | − readonly sandbox: "workspace-write" | "danger-full-access"; | |
| 293 | + readonly approvalPolicy: "untrusted" | "on-request" | "never"; | |
| 294 | + readonly sandbox: "read-only" | "workspace-write" | "danger-full-access"; | |
| 295 | 295 | } { |
| 296 | − if (runtimeMode === "approval-required") { | |
| 297 | − return { | |
| 298 | − approvalPolicy: "on-request", | |
| 299 | − sandbox: "workspace-write", | |
| 300 | − }; | |
| 296 | + switch (runtimeMode) { | |
| 297 | + case "approval-required": | |
| 298 | + return { | |
| 299 | + approvalPolicy: "untrusted", | |
| 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 | } |
| 308 | 314 | |
| 309 | 315 | /** |
| @@ -14,6 +14,7 @@ import { · 13 unmodified lines | ||
| 14 | 14 | ApprovalRequestId, |
| 15 | 15 | ProviderItemId, |
| 16 | 16 | ProviderRuntimeEvent, |
| 17 | + type RuntimeMode, | |
| 17 | 18 | ThreadId, |
| 18 | 19 | } from "@t3tools/contracts"; |
| 19 | 20 | import { assert, describe, it } from "@effect/vitest"; |
| @@ -2496,57 +2497,63 @@ describe("ClaudeAdapterLive", () => { · 2476 unmodified lines | ||
| 2496 | 2497 | ); |
| 2497 | 2498 | }); |
| 2498 | 2499 | |
| 2499 | − it.effect("restores base permission mode on sendTurn when interactionMode is default", () => { | |
| 2500 | − const harness = makeHarness(); | |
| 2501 | − return Effect.gen(function* () { | |
| 2502 | − const adapter = yield* ClaudeAdapter; | |
| 2500 | + it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([ | |
| 2501 | + { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, | |
| 2502 | + { runtimeMode: "approval-required", expectedBase: "default" }, | |
| 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; | |
| 2503 | 2510 | |
| 2504 | − const session = yield* adapter.startSession({ | |
| 2505 | − threadId: THREAD_ID, | |
| 2506 | − provider: "claudeAgent", | |
| 2507 | − runtimeMode: "full-access", | |
| 2508 | − }); | |
| 2511 | + const session = yield* adapter.startSession({ | |
| 2512 | + threadId: THREAD_ID, | |
| 2513 | + provider: "claudeAgent", | |
| 2514 | + runtimeMode, | |
| 2515 | + }); | |
| 2509 | 2516 | |
| 2510 | − // First turn in plan mode | |
| 2511 | − yield* adapter.sendTurn({ | |
| 2512 | − threadId: session.threadId, | |
| 2513 | − input: "plan this", | |
| 2514 | − interactionMode: "plan", | |
| 2515 | − attachments: [], | |
| 2516 | − }); | |
| 2517 | + // First turn in plan mode | |
| 2518 | + yield* adapter.sendTurn({ | |
| 2519 | + threadId: session.threadId, | |
| 2520 | + input: "plan this", | |
| 2521 | + interactionMode: "plan", | |
| 2522 | + attachments: [], | |
| 2523 | + }); | |
| 2517 | 2524 | |
| 2518 | − // Complete the turn so we can send another | |
| 2519 | − const turnCompletedFiber = yield* Stream.filter( | |
| 2520 | − adapter.streamEvents, | |
| 2521 | − (event) => event.type === "turn.completed", | |
| 2522 | − ).pipe(Stream.runHead, Effect.forkChild); | |
| 2525 | + // Complete the turn so we can send another | |
| 2526 | + const turnCompletedFiber = yield* Stream.filter( | |
| 2527 | + adapter.streamEvents, | |
| 2528 | + (event) => event.type === "turn.completed", | |
| 2529 | + ).pipe(Stream.runHead, Effect.forkChild); | |
| 2523 | 2530 | |
| 2524 | − harness.query.emit({ | |
| 2525 | − type: "result", | |
| 2526 | − subtype: "success", | |
| 2527 | − is_error: false, | |
| 2528 | − errors: [], | |
| 2529 | − session_id: "sdk-session-plan-restore", | |
| 2530 | − uuid: "result-plan", | |
| 2531 | − } as unknown as SDKMessage); | |
| 2531 | + harness.query.emit({ | |
| 2532 | + type: "result", | |
| 2533 | + subtype: "success", | |
| 2534 | + is_error: false, | |
| 2535 | + errors: [], | |
| 2536 | + session_id: `sdk-session-${runtimeMode}`, | |
| 2537 | + uuid: `result-${runtimeMode}`, | |
| 2538 | + } as unknown as SDKMessage); | |
| 2532 | 2539 | |
| 2533 | − yield* Fiber.join(turnCompletedFiber); | |
| 2540 | + yield* Fiber.join(turnCompletedFiber); | |
| 2534 | 2541 | |
| 2535 | − // Second turn back to default | |
| 2536 | − yield* adapter.sendTurn({ | |
| 2537 | − threadId: session.threadId, | |
| 2538 | − input: "now do it", | |
| 2539 | − interactionMode: "default", | |
| 2540 | − attachments: [], | |
| 2541 | − }); | |
| 2542 | + // Second turn back to default | |
| 2543 | + yield* adapter.sendTurn({ | |
| 2544 | + threadId: session.threadId, | |
| 2545 | + input: "now do it", | |
| 2546 | + interactionMode: "default", | |
| 2547 | + attachments: [], | |
| 2548 | + }); | |
| 2542 | 2549 | |
| 2543 | − // First call sets "plan", second call restores "bypassPermissions" (the base for full-access) | |
| 2544 | − assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", "bypassPermissions"]); | |
| 2545 | − }).pipe( | |
| 2546 | − Effect.provideService(Random.Random, makeDeterministicRandomService()), | |
| 2547 | − Effect.provide(harness.layer), | |
| 2548 | − ); | |
| 2549 | − }); | |
| 2550 | + assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]); | |
| 2551 | + }).pipe( | |
| 2552 | + Effect.provideService(Random.Random, makeDeterministicRandomService()), | |
| 2553 | + Effect.provide(harness.layer), | |
| 2554 | + ); | |
| 2555 | + }, | |
| 2556 | + ); | |
| 2550 | 2557 | |
| 2551 | 2558 | it.effect("does not call setPermissionMode when interactionMode is absent", () => { |
| 2552 | 2559 | const harness = makeHarness(); |
| @@ -2693,7 +2693,11 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( · 2692 unmodified lines | ||
| 2693 | 2693 | ? modelSelection.options.thinking |
| 2694 | 2694 | : undefined; |
| 2695 | 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 | 2701 | const settings = { |
| 2698 | 2702 | ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), |
| 2699 | 2703 | ...(fastMode ? { fastMode: true } : {}), |
| @@ -2881,8 +2885,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( · 181 unmodified lines | ||
| 2881 | 2885 | }); |
| 2882 | 2886 | } else if (input.interactionMode === "default") { |
| 2883 | 2887 | yield* Effect.tryPromise({ |
| 2884 | − try: () => | |
| 2885 | − context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"), | |
| 2888 | + try: () => context.query.setPermissionMode(context.basePermissionMode ?? "default"), | |
| 2886 | 2889 | catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), |
| 2887 | 2890 | }); |
| 2888 | 2891 | } |
| @@ -956,6 +956,16 @@ async function waitForButtonContainingText(text: string): Promise<HTMLButtonElem · 955 unmodified lines | ||
| 956 | 956 | ); |
| 957 | 957 | } |
| 958 | 958 | |
| 959 | +async 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 | + | |
| 959 | 969 | async function expectComposerActionsContained(): Promise<void> { |
| 960 | 970 | const footer = await waitForElement( |
| 961 | 971 | () => document.querySelector<HTMLElement>('[data-chat-composer-footer="true"]'), |
| @@ -2326,6 +2336,32 @@ describe("ChatView timeline estimator parity (full app)", () => { · 1364 unmodified lines | ||
| 2326 | 2336 | } |
| 2327 | 2337 | }); |
| 2328 | 2338 | |
| 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 | 2365 | it("keeps removed terminal context pills removed when a new one is added", async () => { |
| 2330 | 2366 | const removedLabel = "Terminal 1 lines 1-2"; |
| 2331 | 2367 | const addedLabel = "Terminal 2 lines 9-10"; |
| @@ -115,10 +115,13 @@ import { · 114 unmodified lines | ||
| 115 | 115 | ListTodoIcon, |
| 116 | 116 | LockIcon, |
| 117 | 117 | LockOpenIcon, |
| 118 | + type LucideIcon, | |
| 119 | + PenLineIcon, | |
| 118 | 120 | XIcon, |
| 119 | 121 | } from "lucide-react"; |
| 120 | 122 | import { Button } from "./ui/button"; |
| 121 | 123 | import { Separator } from "./ui/separator"; |
| 124 | +import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | |
| 122 | 125 | import { cn, randomUUID } from "~/lib/utils"; |
| 123 | 126 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 124 | 127 | import { toastManager } from "./ui/toast"; |
| @@ -410,6 +413,29 @@ interface TerminalLaunchContext { · 285 unmodified lines | ||
| 410 | 413 | worktreePath: string | null; |
| 411 | 414 | } |
| 412 | 415 | |
| 416 | +const 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 | + | |
| 437 | +const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; | |
| 438 | + | |
| 413 | 439 | type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; |
| 414 | 440 | |
| 415 | 441 | function useLocalDispatchState(input: { |
| @@ -960,6 +986,8 @@ export default function ChatView(props: ChatViewProps) { · 544 unmodified lines | ||
| 960 | 986 | composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; |
| 961 | 987 | const interactionMode = |
| 962 | 988 | composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; |
| 989 | + const runtimeModeOption = runtimeModeConfig[runtimeMode]; | |
| 990 | + const RuntimeModeIcon = runtimeModeOption.icon; | |
| 963 | 991 | const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; |
| 964 | 992 | const canCheckoutPullRequestIntoThread = isLocalDraftThread; |
| 965 | 993 | const diffOpen = rawSearch.diff === "1"; |
| @@ -2350,11 +2378,6 @@ export default function ChatView(props: ChatViewProps) { · 1384 unmodified lines | ||
| 2350 | 2378 | const toggleInteractionMode = useCallback(() => { |
| 2351 | 2379 | handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); |
| 2352 | 2380 | }, [handleInteractionModeChange, interactionMode]); |
| 2353 | − const toggleRuntimeMode = useCallback(() => { | |
| 2354 | − void handleRuntimeModeChange( | |
| 2355 | − runtimeMode === "full-access" ? "approval-required" : "full-access", | |
| 2356 | − ); | |
| 2357 | − }, [handleRuntimeModeChange, runtimeMode]); | |
| 2358 | 2381 | const togglePlanSidebar = useCallback(() => { |
| 2359 | 2382 | setPlanSidebarOpen((open) => { |
| 2360 | 2383 | if (open) { |
| @@ -4651,7 +4674,7 @@ export default function ChatView(props: ChatViewProps) { · 2290 unmodified lines | ||
| 4651 | 4674 | traitsMenuContent={providerTraitsMenuContent} |
| 4652 | 4675 | onToggleInteractionMode={toggleInteractionMode} |
| 4653 | 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) { · 35 unmodified lines | ||
| 4693 | 4716 | className="mx-0.5 hidden h-4 sm:block" |
| 4694 | 4717 | /> |
| 4695 | 4718 | |
| 4696 | − <Button | |
| 4697 | − variant="ghost" | |
| 4698 | − className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3" | |
| 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 | − } | |
| 4719 | + <Select | |
| 4720 | + value={runtimeMode} | |
| 4721 | + onValueChange={(value) => handleRuntimeModeChange(value!)} | |
| 4713 | 4722 | > |
| 4714 | − {runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />} | |
| 4715 | − <span className="sr-only sm:not-sr-only"> | |
| 4716 | − {runtimeMode === "full-access" ? "Full access" : "Supervised"} | |
| 4717 | − </span> | |
| 4718 | − </Button> | |
| 4723 | + <SelectTrigger | |
| 4724 | + variant="ghost" | |
| 4725 | + size="sm" | |
| 4726 | + aria-label="Runtime mode" | |
| 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> | |
| 4719 | 4752 | |
| 4720 | 4753 | {activePlan || sidebarProposedPlan || planSidebarOpen ? ( |
| 4721 | 4754 | <> |
| @@ -138,7 +138,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str · 137 unmodified lines | ||
| 138 | 138 | } |
| 139 | 139 | onToggleInteractionMode={vi.fn()} |
| 140 | 140 | onTogglePlanSidebar={vi.fn()} |
| 141 | − onToggleRuntimeMode={vi.fn()} | |
| 141 | + onRuntimeModeChange={vi.fn()} | |
| 142 | 142 | />, |
| 143 | 143 | { container: host }, |
| 144 | 144 | ); |
| @@ -20,7 +20,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls · 19 unmodified lines | ||
| 20 | 20 | traitsMenuContent?: ReactNode; |
| 21 | 21 | onToggleInteractionMode: () => void; |
| 22 | 22 | onTogglePlanSidebar: () => void; |
| 23 | − onToggleRuntimeMode: () => void; | |
| 23 | + onRuntimeModeChange: (mode: RuntimeMode) => void; | |
| 24 | 24 | }) { |
| 25 | 25 | return ( |
| 26 | 26 | <Menu> |
| @@ -60,10 +60,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls · 33 unmodified lines | ||
| 60 | 60 | value={props.runtimeMode} |
| 61 | 61 | onValueChange={(value) => { |
| 62 | 62 | if (!value || value === props.runtimeMode) return; |
| 63 | − props.onToggleRuntimeMode(); | |
| 63 | + props.onRuntimeModeChange(value as RuntimeMode); | |
| 64 | 64 | }} |
| 65 | 65 | > |
| 66 | 66 | <MenuRadioItem value="approval-required">Supervised</MenuRadioItem> |
| 67 | + <MenuRadioItem value="auto-accept-edits">Auto-accept edits</MenuRadioItem> | |
| 67 | 68 | <MenuRadioItem value="full-access">Full access</MenuRadioItem> |
| 68 | 69 | </MenuRadioGroup> |
| 69 | 70 | {props.activePlan ? ( |
| @@ -45,6 +45,7 @@ import { UnifiedSettings } from "@t3tools/contracts/settings"; · 44 unmodified lines | ||
| 45 | 45 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; |
| 46 | 46 | const COMPOSER_DRAFT_STORAGE_VERSION = 5; |
| 47 | 47 | const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); |
| 48 | +const isRuntimeMode = Schema.is(RuntimeMode); | |
| 48 | 49 | export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; |
| 49 | 50 | |
| 50 | 51 | export const DraftId = Schema.String.pipe(Schema.brand("DraftId")); |
| @@ -1195,11 +1196,9 @@ function normalizePersistedDraftThreads( · 1144 unmodified lines | ||
| 1195 | 1196 | typeof createdAt === "string" && createdAt.length > 0 |
| 1196 | 1197 | ? createdAt |
| 1197 | 1198 | : new Date().toISOString(), |
| 1198 | − runtimeMode: | |
| 1199 | − candidateDraftThread.runtimeMode === "approval-required" || | |
| 1200 | − candidateDraftThread.runtimeMode === "full-access" | |
| 1201 | − ? candidateDraftThread.runtimeMode | |
| 1202 | − : DEFAULT_RUNTIME_MODE, | |
| 1199 | + runtimeMode: isRuntimeMode(candidateDraftThread.runtimeMode) | |
| 1200 | + ? candidateDraftThread.runtimeMode | |
| 1201 | + : DEFAULT_RUNTIME_MODE, | |
| 1203 | 1202 | interactionMode: |
| 1204 | 1203 | candidateDraftThread.interactionMode === "plan" || |
| 1205 | 1204 | candidateDraftThread.interactionMode === "default" |
| @@ -1318,11 +1317,9 @@ function normalizePersistedDraftsByThreadId( · 112 unmodified lines | ||
| 1318 | 1317 | return normalized ? [normalized] : []; |
| 1319 | 1318 | }) |
| 1320 | 1319 | : []; |
| 1321 | − const runtimeMode = | |
| 1322 | − draftCandidate.runtimeMode === "approval-required" || | |
| 1323 | − draftCandidate.runtimeMode === "full-access" | |
| 1324 | − ? draftCandidate.runtimeMode | |
| 1325 | − : null; | |
| 1320 | + const runtimeMode = isRuntimeMode(draftCandidate.runtimeMode) | |
| 1321 | + ? draftCandidate.runtimeMode | |
| 1322 | + : null; | |
| 1326 | 1323 | const interactionMode = |
| 1327 | 1324 | draftCandidate.interactionMode === "plan" || draftCandidate.interactionMode === "default" |
| 1328 | 1325 | ? draftCandidate.interactionMode |
| @@ -2373,10 +2370,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()( · 1044 unmodified lines | ||
| 2373 | 2370 | if (threadKey.length === 0) { |
| 2374 | 2371 | return; |
| 2375 | 2372 | } |
| 2376 | − const nextRuntimeMode = | |
| 2377 | − runtimeMode === "approval-required" || runtimeMode === "full-access" | |
| 2378 | − ? runtimeMode | |
| 2379 | − : null; | |
| 2373 | + const nextRuntimeMode = isRuntimeMode(runtimeMode) ? runtimeMode : null; | |
| 2380 | 2374 | set((state) => { |
| 2381 | 2375 | const existing = state.draftsByThreadKey[threadKey]; |
| 2382 | 2376 | if (!existing && nextRuntimeMode === null) { |
| @@ -59,7 +59,11 @@ export type ClaudeModelSelection = typeof ClaudeModelSelection.Type; · 58 unmodified lines | ||
| 59 | 59 | export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]); |
| 60 | 60 | export type ModelSelection = typeof ModelSelection.Type; |
| 61 | 61 | |
| 62 | −export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); | |
| 62 | +export const RuntimeMode = Schema.Literals([ | |
| 63 | + "approval-required", | |
| 64 | + "auto-accept-edits", | |
| 65 | + "full-access", | |
| 66 | +]); | |
| 63 | 67 | export type RuntimeMode = typeof RuntimeMode.Type; |
| 64 | 68 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 65 | 69 | export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); |