diff --git a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx index 03c473876..77c4e4194 100644 --- a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx @@ -10,7 +10,6 @@ import { cycleModeOption, getCurrentModeFromConfigOptions, } from "@features/sessions/stores/sessionStore"; -import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { TaskInputEditor } from "@features/task-detail/components/TaskInputEditor"; import { WorkspaceModeSelect } from "@features/task-detail/components/WorkspaceModeSelect"; import { usePreviewConfig } from "@features/task-detail/hooks/usePreviewConfig"; @@ -59,7 +58,6 @@ interface TutorialStepProps { } export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { - const { allowBypassPermissions } = useSettingsStore(); const completeOnboarding = useOnboardingStore( (state) => state.completeOnboarding, ); @@ -204,11 +202,11 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { // Shift+tab mode cycling (only active during explain-mode step) const handleCycleMode = useCallback(() => { - const nextValue = cycleModeOption(modeOption, allowBypassPermissions); + const nextValue = cycleModeOption(modeOption); if (nextValue && modeOption) { setConfigOption(modeOption.id, nextValue); } - }, [modeOption, allowBypassPermissions, setConfigOption]); + }, [modeOption, setConfigOption]); useHotkeys( "shift+tab", diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index 1ef6a2bd5..2e3559173 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -11,7 +11,6 @@ import { usePendingPermissionsForTask, } from "@features/sessions/stores/sessionStore"; import type { Plan } from "@features/sessions/types"; -import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping"; import { Pause, Spinner, Warning } from "@phosphor-icons/react"; import { Box, Button, ContextMenu, Flex, Text } from "@radix-ui/themes"; @@ -92,25 +91,10 @@ export function SessionView({ const { setShowRawLogs } = useSessionViewActions(); const pendingPermissions = usePendingPermissionsForTask(taskId); const modeOption = useModeConfigOptionForTask(taskId); - const { allowBypassPermissions } = useSettingsStore(); - const currentModeId = modeOption?.currentValue; - - useEffect(() => { - if (allowBypassPermissions) return; - const isBypass = - currentModeId === "bypassPermissions" || currentModeId === "full-access"; - if (isBypass && taskId) { - getSessionService().setSessionConfigOptionByCategory( - taskId, - "mode", - "default", - ); - } - }, [allowBypassPermissions, currentModeId, taskId]); const handleModeChange = useCallback(() => { if (!taskId) return; - const nextMode = cycleModeOption(modeOption, allowBypassPermissions); + const nextMode = cycleModeOption(modeOption); if (nextMode) { getSessionService().setSessionConfigOptionByCategory( taskId, @@ -118,7 +102,7 @@ export function SessionView({ nextMode, ); } - }, [taskId, allowBypassPermissions, modeOption]); + }, [taskId, modeOption]); const sessionId = taskId ?? "default"; const setContext = useDraftStore((s) => s.actions.setContext); @@ -147,7 +131,7 @@ export function SessionView({ (e) => { e.preventDefault(); if (!taskId) return; - const nextMode = cycleModeOption(modeOption, allowBypassPermissions); + const nextMode = cycleModeOption(modeOption); if (nextMode) { getSessionService().setSessionConfigOptionByCategory( taskId, @@ -161,14 +145,7 @@ export function SessionView({ enableOnContentEditable: true, enabled: isRunning && !!modeOption && isActiveSession, }, - [ - taskId, - currentModeId, - isRunning, - modeOption, - allowBypassPermissions, - isActiveSession, - ], + [taskId, isRunning, modeOption, isActiveSession], ); const latestPlan = useMemo((): Plan | null => { diff --git a/apps/code/src/renderer/features/sessions/stores/sessionStore.test.ts b/apps/code/src/renderer/features/sessions/stores/sessionStore.test.ts new file mode 100644 index 000000000..81bd0b823 --- /dev/null +++ b/apps/code/src/renderer/features/sessions/stores/sessionStore.test.ts @@ -0,0 +1,43 @@ +import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { describe, expect, it } from "vitest"; +import { cycleModeOption } from "./sessionStore"; + +function createModeOption( + currentValue: string, + values: string[], +): SessionConfigOption { + return { + id: "mode", + name: "Approval Preset", + type: "select", + category: "mode", + currentValue, + options: values.map((value) => ({ + value, + name: value, + })), + } as SessionConfigOption; +} + +describe("cycleModeOption", () => { + it("cycles through auto-accept permissions for claude", () => { + const option = createModeOption("plan", [ + "default", + "acceptEdits", + "plan", + "bypassPermissions", + ]); + + expect(cycleModeOption(option)).toBe("bypassPermissions"); + }); + + it("cycles through full access for codex", () => { + const option = createModeOption("auto", [ + "read-only", + "auto", + "full-access", + ]); + + expect(cycleModeOption(option)).toBe("full-access"); + }); +}); diff --git a/apps/code/src/renderer/features/sessions/stores/sessionStore.ts b/apps/code/src/renderer/features/sessions/stores/sessionStore.ts index 38808cb6c..59610e4a7 100644 --- a/apps/code/src/renderer/features/sessions/stores/sessionStore.ts +++ b/apps/code/src/renderer/features/sessions/stores/sessionStore.ts @@ -146,27 +146,19 @@ export function getConfigOptionByCategory( */ export function cycleModeOption( modeOption: SessionConfigOption | undefined, - allowBypassPermissions: boolean, ): string | undefined { if (!modeOption || modeOption.type !== "select") return undefined; const allOptions = flattenSelectOptions(modeOption.options); - const filteredOptions = allowBypassPermissions - ? allOptions - : allOptions.filter( - (opt) => - opt.value !== "bypassPermissions" && opt.value !== "full-access", - ); + if (allOptions.length === 0) return undefined; - if (filteredOptions.length === 0) return allOptions[0]?.value; - - const currentIndex = filteredOptions.findIndex( + const currentIndex = allOptions.findIndex( (opt) => opt.value === modeOption.currentValue, ); - if (currentIndex === -1) return filteredOptions[0]?.value; + if (currentIndex === -1) return allOptions[0]?.value; - const nextIndex = (currentIndex + 1) % filteredOptions.length; - return filteredOptions[nextIndex]?.value; + const nextIndex = (currentIndex + 1) % allOptions.length; + return allOptions[nextIndex]?.value; } /** diff --git a/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx index 092c7c4a0..7162f105c 100644 --- a/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx @@ -81,14 +81,15 @@ export function ClaudeCodeSettings() { (checked: boolean) => { if (checked) { setShowBypassWarning(true); - } else { - track(ANALYTICS_EVENTS.SETTING_CHANGED, { - setting_name: "allow_bypass_permissions", - new_value: false, - old_value: true, - }); - setAllowBypassPermissions(false); + return; } + + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "allow_bypass_permissions", + new_value: false, + old_value: true, + }); + setAllowBypassPermissions(false); }, [setAllowBypassPermissions], ); diff --git a/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts b/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts index 6e546a8d9..132bc0261 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts @@ -28,6 +28,7 @@ describe("feature settingsStore cloud selections", () => { removeItem.mockResolvedValue(undefined); useSettingsStore.setState({ + allowBypassPermissions: false, lastUsedCloudRepository: null, }); }); @@ -65,4 +66,19 @@ describe("feature settingsStore cloud selections", () => { "posthog/posthog", ); }); + + it("rehydrates the unsafe mode toggle", async () => { + getItem.mockResolvedValue( + JSON.stringify({ + state: { + allowBypassPermissions: true, + }, + version: 0, + }), + ); + + await useSettingsStore.persist.rehydrate(); + + expect(useSettingsStore.getState().allowBypassPermissions).toBe(true); + }); }); diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index 2f84abfb4..f61ac46fc 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -62,7 +62,6 @@ export function TaskInput({ setLastUsedAdapter, lastUsedCloudRepository, setLastUsedCloudRepository, - allowBypassPermissions, setLastUsedEnvironment, getLastUsedEnvironment, defaultInitialTaskMode, @@ -285,11 +284,11 @@ export function TaskInput({ }); const handleCycleMode = useCallback(() => { - const nextValue = cycleModeOption(modeOption, allowBypassPermissions); + const nextValue = cycleModeOption(modeOption); if (nextValue && modeOption) { setConfigOption(modeOption.id, nextValue); } - }, [modeOption, allowBypassPermissions, setConfigOption]); + }, [modeOption, setConfigOption]); // Global shift+tab to cycle mode regardless of focus useHotkeys(