From d9ec9647a9935ceda29a72d440b9fb1765479b18 Mon Sep 17 00:00:00 2001 From: oski646 Date: Mon, 30 Mar 2026 21:24:48 +0200 Subject: [PATCH 1/5] fix: map runtime modes to correct permission levels and add auto-accept-edits mode Sessions launched without --dangerously-skip-permissions crashed when switching away from full-access because the fallback always tried to restore bypassPermissions. Map each runtime mode to its proper Codex approval policy / sandbox pair and Claude SDK permission mode, and expose the new auto-accept-edits option in the UI. Fixes #1437 Fixes #1241 --- apps/server/src/codexAppServerManager.ts | 30 +++--- .../src/provider/Layers/ClaudeAdapter.test.ts | 93 ++++++++++--------- .../src/provider/Layers/ClaudeAdapter.ts | 9 +- apps/web/src/components/ChatView.tsx | 51 ++++++---- .../CompactComposerControlsMenu.browser.tsx | 2 +- .../chat/CompactComposerControlsMenu.tsx | 5 +- packages/contracts/src/orchestration.ts | 6 +- 7 files changed, 114 insertions(+), 82 deletions(-) diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 3145038647..bea28168a6 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -290,20 +290,26 @@ In Default mode, strongly prefer making reasonable assumptions and executing the `; function mapCodexRuntimeMode(runtimeMode: RuntimeMode): { - readonly approvalPolicy: "on-request" | "never"; - readonly sandbox: "workspace-write" | "danger-full-access"; + readonly approvalPolicy: "untrusted" | "on-request" | "never"; + readonly sandbox: "read-only" | "workspace-write" | "danger-full-access"; } { - if (runtimeMode === "approval-required") { - return { - approvalPolicy: "on-request", - sandbox: "workspace-write", - }; + switch (runtimeMode) { + case "approval-required": + return { + approvalPolicy: "untrusted", + sandbox: "read-only", + }; + case "auto-accept-edits": + return { + approvalPolicy: "on-request", + sandbox: "workspace-write", + }; + case "full-access": + return { + approvalPolicy: "never", + sandbox: "danger-full-access", + }; } - - return { - approvalPolicy: "never", - sandbox: "danger-full-access", - }; } /** diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index d064a8239f..65eeef9797 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -2493,57 +2493,62 @@ describe("ClaudeAdapterLive", () => { ); }); - it.effect("restores base permission mode on sendTurn when interactionMode is default", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; + for (const [runtimeMode, expectedBase] of [ + ["full-access", "bypassPermissions"], + ["approval-required", "default"], + ["auto-accept-edits", "acceptEdits"], + ] as const) { + it.effect(`restores ${expectedBase} permission mode after plan turn (${runtimeMode})`, () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeAgent", - runtimeMode: "full-access", - }); + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeAgent", + runtimeMode, + }); - // First turn in plan mode - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "plan this", - interactionMode: "plan", - attachments: [], - }); + // First turn in plan mode + yield* adapter.sendTurn({ + threadId: session.threadId, + input: "plan this", + interactionMode: "plan", + attachments: [], + }); - // Complete the turn so we can send another - const turnCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); + // Complete the turn so we can send another + const turnCompletedFiber = yield* Stream.filter( + adapter.streamEvents, + (event) => event.type === "turn.completed", + ).pipe(Stream.runHead, Effect.forkChild); - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-plan-restore", - uuid: "result-plan", - } as unknown as SDKMessage); + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: `sdk-session-${runtimeMode}`, + uuid: `result-${runtimeMode}`, + } as unknown as SDKMessage); - yield* Fiber.join(turnCompletedFiber); + yield* Fiber.join(turnCompletedFiber); - // Second turn back to default - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "now do it", - interactionMode: "default", - attachments: [], - }); + // Second turn back to default + yield* adapter.sendTurn({ + threadId: session.threadId, + input: "now do it", + interactionMode: "default", + attachments: [], + }); - // First call sets "plan", second call restores "bypassPermissions" (the base for full-access) - assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", "bypassPermissions"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); + assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + } it.effect("does not call setPermissionMode when interactionMode is absent", () => { const harness = makeHarness(); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 6b50bd4fbb..6cbf090d32 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -2693,7 +2693,11 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ? modelSelection.options.thinking : undefined; const effectiveEffort = getEffectiveClaudeCodeEffort(effort); - const permissionMode = input.runtimeMode === "full-access" ? "bypassPermissions" : undefined; + const runtimeModeToPermission: Record = { + "auto-accept-edits": "acceptEdits", + "full-access": "bypassPermissions", + }; + const permissionMode = runtimeModeToPermission[input.runtimeMode]; const settings = { ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), @@ -2881,8 +2885,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( }); } else if (input.interactionMode === "default") { yield* Effect.tryPromise({ - try: () => - context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"), + try: () => context.query.setPermissionMode(context.basePermissionMode ?? "default"), catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), }); } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index fbd332354a..68efba00b8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -94,6 +94,7 @@ import { ListTodoIcon, LockIcon, LockOpenIcon, + PenLineIcon, XIcon, } from "lucide-react"; import { Button } from "./ui/button"; @@ -245,6 +246,30 @@ interface PendingPullRequestSetupRequest { scriptId: string; } +const runtimeModeConfig: Record< + RuntimeMode, + { label: string; title: string; icon: React.ReactNode; next: RuntimeMode } +> = { + "approval-required": { + label: "Supervised", + title: "Supervised - click for auto-accept edits", + icon: , + next: "auto-accept-edits", + }, + "auto-accept-edits": { + label: "Auto-accept edits", + title: "Auto-accept edits - click for full access", + icon: , + next: "full-access", + }, + "full-access": { + label: "Full access", + title: "Full access - click for supervised", + icon: , + next: "approval-required", + }, +}; + export default function ChatView({ threadId }: ChatViewProps) { const threads = useStore((store) => store.threads); const projects = useStore((store) => store.projects); @@ -1665,10 +1690,8 @@ export default function ChatView({ threadId }: ChatViewProps) { const toggleInteractionMode = useCallback(() => { handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); }, [handleInteractionModeChange, interactionMode]); - const toggleRuntimeMode = useCallback(() => { - void handleRuntimeModeChange( - runtimeMode === "full-access" ? "approval-required" : "full-access", - ); + const cycleRuntimeMode = useCallback(() => { + void handleRuntimeModeChange(runtimeModeConfig[runtimeMode].next); }, [handleRuntimeModeChange, runtimeMode]); const togglePlanSidebar = useCallback(() => { setPlanSidebarOpen((open) => { @@ -3907,7 +3930,7 @@ export default function ChatView({ threadId }: ChatViewProps) { traitsMenuContent={providerTraitsMenuContent} onToggleInteractionMode={toggleInteractionMode} onTogglePlanSidebar={togglePlanSidebar} - onToggleRuntimeMode={toggleRuntimeMode} + onRuntimeModeChange={handleRuntimeModeChange} /> ) : ( <> @@ -3954,22 +3977,12 @@ export default function ChatView({ threadId }: ChatViewProps) { className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3" size="sm" type="button" - onClick={() => - void handleRuntimeModeChange( - runtimeMode === "full-access" - ? "approval-required" - : "full-access", - ) - } - title={ - runtimeMode === "full-access" - ? "Full access — click to require approvals" - : "Approval required — click for full access" - } + onClick={cycleRuntimeMode} + title={runtimeModeConfig[runtimeMode].title} > - {runtimeMode === "full-access" ? : } + {runtimeModeConfig[runtimeMode].icon} - {runtimeMode === "full-access" ? "Full access" : "Supervised"} + {runtimeModeConfig[runtimeMode].label} diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx index eee6f885e9..a17651e657 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx @@ -130,7 +130,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str } onToggleInteractionMode={vi.fn()} onTogglePlanSidebar={vi.fn()} - onToggleRuntimeMode={vi.fn()} + onRuntimeModeChange={vi.fn()} />, { container: host }, ); diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index db38ed8c1e..84e0953440 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -20,7 +20,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls traitsMenuContent?: ReactNode; onToggleInteractionMode: () => void; onTogglePlanSidebar: () => void; - onToggleRuntimeMode: () => void; + onRuntimeModeChange: (mode: RuntimeMode) => void; }) { return ( @@ -60,10 +60,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls value={props.runtimeMode} onValueChange={(value) => { if (!value || value === props.runtimeMode) return; - props.onToggleRuntimeMode(); + props.onRuntimeModeChange(value as RuntimeMode); }} > Supervised + Auto-accept edits Full access {props.activePlan ? ( diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index a780a55c78..a954d1e2dc 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -62,7 +62,11 @@ export type ClaudeModelSelection = typeof ClaudeModelSelection.Type; export const ModelSelection = Schema.Union([CodexModelSelection, ClaudeModelSelection]); export type ModelSelection = typeof ModelSelection.Type; -export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); +export const RuntimeMode = Schema.Literals([ + "approval-required", + "auto-accept-edits", + "full-access", +]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); From 7c6a95812601d4e37dc3df1cf9ea56f3dcfbec84 Mon Sep 17 00:00:00 2001 From: oski646 Date: Mon, 30 Mar 2026 21:50:54 +0200 Subject: [PATCH 2/5] fix: stop silently discarding auto-accept-edits runtime mode setRuntimeMode had an explicit allowlist that missed "auto-accept-edits", converting it to null. Replace with nullish coalescing since TypeScript already constrains the parameter to RuntimeMode | null | undefined. --- apps/web/src/composerDraftStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 17b06e7bd1..df057e74c2 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -1801,8 +1801,7 @@ export const useComposerDraftStore = create()( if (threadId.length === 0) { return; } - const nextRuntimeMode = - runtimeMode === "approval-required" || runtimeMode === "full-access" ? runtimeMode : null; + const nextRuntimeMode = runtimeMode ?? null; set((state) => { const existing = state.draftsByThreadId[threadId]; if (!existing && nextRuntimeMode === null) { From 7a3d964d49a59de03ab0dff4071e72cf4fd3a332 Mon Sep 17 00:00:00 2001 From: oski646 Date: Tue, 31 Mar 2026 21:57:50 +0200 Subject: [PATCH 3/5] Fix composer runtime mode hydration --- apps/web/src/composerDraftStore.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 2dd64adf7a..07c4fff282 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -34,6 +34,7 @@ import { UnifiedSettings } from "@t3tools/contracts/settings"; export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; const COMPOSER_DRAFT_STORAGE_VERSION = 3; const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); +const isRuntimeMode = Schema.is(RuntimeMode); export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; @@ -781,11 +782,9 @@ function normalizePersistedDraftThreads( typeof createdAt === "string" && createdAt.length > 0 ? createdAt : new Date().toISOString(), - runtimeMode: - candidateDraftThread.runtimeMode === "approval-required" || - candidateDraftThread.runtimeMode === "full-access" - ? candidateDraftThread.runtimeMode - : DEFAULT_RUNTIME_MODE, + runtimeMode: isRuntimeMode(candidateDraftThread.runtimeMode) + ? candidateDraftThread.runtimeMode + : DEFAULT_RUNTIME_MODE, interactionMode: candidateDraftThread.interactionMode === "plan" || candidateDraftThread.interactionMode === "default" @@ -866,11 +865,9 @@ function normalizePersistedDraftsByThreadId( return normalized ? [normalized] : []; }) : []; - const runtimeMode = - draftCandidate.runtimeMode === "approval-required" || - draftCandidate.runtimeMode === "full-access" - ? draftCandidate.runtimeMode - : null; + const runtimeMode = isRuntimeMode(draftCandidate.runtimeMode) + ? draftCandidate.runtimeMode + : null; const interactionMode = draftCandidate.interactionMode === "plan" || draftCandidate.interactionMode === "default" ? draftCandidate.interactionMode From 215e57f9266588940d245f000f960121d3f0c1dd Mon Sep 17 00:00:00 2001 From: oski646 Date: Wed, 1 Apr 2026 11:13:38 +0200 Subject: [PATCH 4/5] Refactor permission mode test to use it.effect.each --- .../src/provider/Layers/ClaudeAdapter.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 65eeef9797..a7652afb25 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -14,6 +14,7 @@ import { ApprovalRequestId, ProviderItemId, ProviderRuntimeEvent, + type RuntimeMode, ThreadId, } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; @@ -2493,12 +2494,13 @@ describe("ClaudeAdapterLive", () => { ); }); - for (const [runtimeMode, expectedBase] of [ - ["full-access", "bypassPermissions"], - ["approval-required", "default"], - ["auto-accept-edits", "acceptEdits"], - ] as const) { - it.effect(`restores ${expectedBase} permission mode after plan turn (${runtimeMode})`, () => { + it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([ + { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, + { runtimeMode: "approval-required", expectedBase: "default" }, + { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" }, + ])( + "restores $expectedBase permission mode after plan turn ($runtimeMode)", + ({ runtimeMode, expectedBase }) => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; @@ -2547,8 +2549,8 @@ describe("ClaudeAdapterLive", () => { Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); - }); - } + }, + ); it.effect("does not call setPermissionMode when interactionMode is absent", () => { const harness = makeHarness(); From 394c195f0c947935422853088115111a88cca988 Mon Sep 17 00:00:00 2001 From: oski646 Date: Mon, 6 Apr 2026 21:16:12 +0200 Subject: [PATCH 5/5] Use a select for runtime mode choices --- apps/web/src/components/ChatView.browser.tsx | 36 ++++++++++ apps/web/src/components/ChatView.tsx | 72 +++++++++++++------- 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 24099fc7f6..ea7c0f489e 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -844,6 +844,16 @@ async function waitForButtonContainingText(text: string): Promise { + return waitForElement( + () => + Array.from(document.querySelectorAll('[data-slot="select-item"]')).find((item) => + item.textContent?.includes(text), + ) ?? null, + `Unable to find select item containing "${text}".`, + ); +} + async function expectComposerActionsContained(): Promise { const footer = await waitForElement( () => document.querySelector('[data-chat-composer-footer="true"]'), @@ -2166,6 +2176,32 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("shows runtime mode descriptions in the desktop composer access select", async () => { + setDraftThreadWithoutWorktree(); + + const mounted = await mountChatView({ + viewport: WIDE_FOOTER_VIEWPORT, + snapshot: createDraftOnlySnapshot(), + }); + + try { + const runtimeModeSelect = await waitForButtonByText("Full access"); + runtimeModeSelect.click(); + + expect((await waitForSelectItemContainingText("Supervised")).textContent).toContain( + "Ask before commands and file changes", + ); + + const autoAcceptItem = await waitForSelectItemContainingText("Auto-accept edits"); + expect(autoAcceptItem.textContent).toContain("Auto-approve edits"); + expect((await waitForSelectItemContainingText("Full access")).textContent).toContain( + "Allow commands and edits without prompts", + ); + } finally { + await mounted.cleanup(); + } + }); + it("keeps removed terminal context pills removed when a new one is added", async () => { const removedLabel = "Terminal 1 lines 1-2"; const addedLabel = "Terminal 2 lines 9-10"; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 6dba43c99f..d93ec7f4d2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -100,11 +100,13 @@ import { ListTodoIcon, LockIcon, LockOpenIcon, + type LucideIcon, PenLineIcon, XIcon, } from "lucide-react"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; +import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; import { cn, randomUUID } from "~/lib/utils"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { toastManager } from "./ui/toast"; @@ -341,28 +343,27 @@ interface TerminalLaunchContext { const runtimeModeConfig: Record< RuntimeMode, - { label: string; title: string; icon: React.ReactNode; next: RuntimeMode } + { label: string; description: string; icon: LucideIcon } > = { "approval-required": { label: "Supervised", - title: "Supervised - click for auto-accept edits", - icon: , - next: "auto-accept-edits", + description: "Ask before commands and file changes.", + icon: LockIcon, }, "auto-accept-edits": { label: "Auto-accept edits", - title: "Auto-accept edits - click for full access", - icon: , - next: "full-access", + description: "Auto-approve edits, ask before other actions.", + icon: PenLineIcon, }, "full-access": { label: "Full access", - title: "Full access - click for supervised", - icon: , - next: "approval-required", + description: "Allow commands and edits without prompts.", + icon: LockOpenIcon, }, }; +const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; + type PersistentTerminalLaunchContext = Pick; function useLocalDispatchState(input: { @@ -865,6 +866,8 @@ export default function ChatView({ threadId }: ChatViewProps) { composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; const interactionMode = composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; + const runtimeModeOption = runtimeModeConfig[runtimeMode]; + const RuntimeModeIcon = runtimeModeOption.icon; const isServerThread = serverThread !== undefined; const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; const canCheckoutPullRequestIntoThread = isLocalDraftThread; @@ -2016,9 +2019,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const toggleInteractionMode = useCallback(() => { handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); }, [handleInteractionModeChange, interactionMode]); - const cycleRuntimeMode = useCallback(() => { - void handleRuntimeModeChange(runtimeModeConfig[runtimeMode].next); - }, [handleRuntimeModeChange, runtimeMode]); const togglePlanSidebar = useCallback(() => { setPlanSidebarOpen((open) => { if (open) { @@ -4322,19 +4322,41 @@ export default function ChatView({ threadId }: ChatViewProps) { className="mx-0.5 hidden h-4 sm:block" /> - + + + {runtimeModeOption.label} + + + {runtimeModeOptions.map((mode) => { + const option = runtimeModeConfig[mode]; + const OptionIcon = option.icon; + return ( + +
+ + + {option.label} + + + {option.description} + +
+
+ ); + })} +
+ {activePlan || sidebarProposedPlan || planSidebarOpen ? ( <>