| author | |
| committer | |
| log | 7372184d06646dde603e5fd92bc23d3f35ae8d10 |
| tree | 2e2679de6bf398fac4139df12d8126e043ec796f |
| parent | d9ded65dfef01156056d99dedfe2265d0cb5ea59 |
| signature | Signed by PGP key B5690EEEBB952194 |
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>`; |
| 291 | 291 | ||
| 292 | function mapCodexRuntimeMode(runtimeMode: RuntimeMode): { | 292 | function 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 | } |
| 308 | 314 | ||
| 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"; |
| 19 | import { assert, describe, it } from "@effect/vitest"; | 20 | import { assert, describe, it } from "@effect/vitest"; |
| ... | @@ -2496,57 +2497,63 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -2496,57 +2497,63 @@ describe("ClaudeAdapterLive", () => { |
| 2496 | ); | 2497 | ); |
| 2497 | }); | 2498 | }); |
| 2498 | 2499 | ||
| 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; | ||
| 2503 | 2510 | ||
| 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 | }); |
| 2509 | 2516 | ||
| 2510 | // First turn in plan mode | 2517 | // 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 | }); |
| 2517 | 2524 | ||
| 2518 | // Complete the turn so we can send another | 2525 | // 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); |
| 2523 | 2530 | ||
| 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); |
| 2532 | 2539 | ||
| 2533 | yield* Fiber.join(turnCompletedFiber); | 2540 | yield* Fiber.join(turnCompletedFiber); |
| 2534 | 2541 | ||
| 2535 | // Second turn back to default | 2542 | // 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 | }); |
| 2542 | 2549 | ||
| 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 | ); |
| 2550 | 2557 | ||
| 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.thinking | 2693 | ? 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 | } |
| 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 | async function expectComposerActionsContained(): Promise<void> { | 969 | async 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 | }); |
| 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 | 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"; |
| 120 | import { Button } from "./ui/button"; | 122 | import { Button } from "./ui/button"; |
| 121 | import { Separator } from "./ui/separator"; | 123 | import { Separator } from "./ui/separator"; |
| 124 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | ||
| 122 | import { cn, randomUUID } from "~/lib/utils"; | 125 | import { cn, randomUUID } from "~/lib/utils"; |
| 123 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 126 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 124 | import { toastManager } from "./ui/toast"; | 127 | import { 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 | } |
| 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 | type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; | 439 | type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; |
| 414 | 440 | ||
| 415 | function useLocalDispatchState(input: { | 441 | function 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 | /> |
| 4695 | 4718 | ||
| 4696 | <Button | 4719 | <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> | ||
| 4719 | 4752 | ||
| 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"; |
| 45 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; | 45 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; |
| 46 | const COMPOSER_DRAFT_STORAGE_VERSION = 5; | 46 | const COMPOSER_DRAFT_STORAGE_VERSION = 5; |
| 47 | const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); | 47 | const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); |
| 48 | const isRuntimeMode = Schema.is(RuntimeMode); | ||
| 48 | export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; | 49 | export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; |
| 49 | 50 | ||
| 50 | export const DraftId = Schema.String.pipe(Schema.brand("DraftId")); | 51 | export 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 > 0 | 1196 | typeof createdAt === "string" && createdAt.length > 0 |
| 1196 | ? createdAt | 1197 | ? 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.interactionMode | 1325 | ? 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; |
| 59 | export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]); | 59 | export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]); |
| 60 | export type ModelSelection = typeof ModelSelection.Type; | 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 | export type RuntimeMode = typeof RuntimeMode.Type; | 67 | export type RuntimeMode = typeof RuntimeMode.Type; |
| 64 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; | 68 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 65 | export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); | 69 | export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); |