diff --git a/apps/mobile/src/app/settings/index.tsx b/apps/mobile/src/app/settings/index.tsx index 076a2b91a..021538953 100644 --- a/apps/mobile/src/app/settings/index.tsx +++ b/apps/mobile/src/app/settings/index.tsx @@ -8,6 +8,7 @@ import { useDismissedReportsStore } from "@/features/inbox/stores/dismissedRepor import { usePushTokenStore } from "@/features/notifications/stores/pushTokenStore"; import { type CompletionSound, + type DefaultReasoningEffort, type InitialTaskMode, type ThemePreference, usePreferencesStore, @@ -59,6 +60,23 @@ const TASK_MODE_OPTIONS = [ }, ] as const; +const REASONING_EFFORT_OPTIONS: ReadonlyArray<{ + value: DefaultReasoningEffort; + label: string; + description?: string; +}> = [ + { + value: "last_used", + label: "Last used", + description: "Remember the effort level you picked last time", + }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + { value: "xhigh", label: "Extra High" }, + { value: "max", label: "Max" }, +]; + function themeLabel(theme: ThemePreference): string { return THEME_OPTIONS.find((o) => o.value === theme)?.label ?? "Match system"; } @@ -78,6 +96,13 @@ function taskModeLabel(mode: InitialTaskMode): string { return TASK_MODE_OPTIONS.find((o) => o.value === mode)?.label ?? "Plan"; } +function reasoningEffortLabel(effort: DefaultReasoningEffort): string { + return ( + REASONING_EFFORT_OPTIONS.find((o) => o.value === effort)?.label ?? + "Last used" + ); +} + export default function SettingsScreen() { const themeColors = useThemeColors(); const { insets, bottom } = useScreenInsets(); @@ -113,6 +138,12 @@ export default function SettingsScreen() { const setDefaultInitialTaskMode = usePreferencesStore( (s) => s.setDefaultInitialTaskMode, ); + const defaultReasoningEffort = usePreferencesStore( + (s) => s.defaultReasoningEffort, + ); + const setDefaultReasoningEffort = usePreferencesStore( + (s) => s.setDefaultReasoningEffort, + ); const decidedCount = useDismissedReportsStore( (s) => s.dismissedIds.length + s.acceptedIds.length, ); @@ -122,6 +153,8 @@ export default function SettingsScreen() { const [soundSheetOpen, setSoundSheetOpen] = useState(false); const [volumeSheetOpen, setVolumeSheetOpen] = useState(false); const [taskModeSheetOpen, setTaskModeSheetOpen] = useState(false); + const [reasoningEffortSheetOpen, setReasoningEffortSheetOpen] = + useState(false); const [projectSheetOpen, setProjectSheetOpen] = useState(false); // The selected project's name. Prefer the names fetched for the scoped teams @@ -272,7 +305,6 @@ export default function SettingsScreen() { label="Initial task mode" description="What mode new tasks start in" onPress={() => setTaskModeSheetOpen(true)} - showDivider={false} rightSlot={ <> @@ -282,6 +314,20 @@ export default function SettingsScreen() { } /> + setReasoningEffortSheetOpen(true)} + showDivider={false} + rightSlot={ + <> + + {reasoningEffortLabel(defaultReasoningEffort)} + + + + } + /> {/* Integrations */} @@ -508,6 +554,21 @@ export default function SettingsScreen() { }))} /> + + setDefaultReasoningEffort(value as DefaultReasoningEffort) + } + onClose={() => setReasoningEffortSheetOpen(false)} + options={REASONING_EFFORT_OPTIONS.map((option) => ({ + value: option.value, + label: option.label, + description: option.description, + }))} + /> + {}); + usePreferencesStore.getState().setLastUsedReasoningEffort(value); }, [taskId, setComposerConfig, setConfigOption], ); diff --git a/apps/mobile/src/app/task/index.tsx b/apps/mobile/src/app/task/index.tsx index 62aab442e..70513b3fe 100644 --- a/apps/mobile/src/app/task/index.tsx +++ b/apps/mobile/src/app/task/index.tsx @@ -180,8 +180,16 @@ export default function NewTaskScreen() { return DEFAULT_EXECUTION_MODE; }); const [model, setModel] = useState(DEFAULT_MODEL); - const [reasoning, setReasoning] = - useState(DEFAULT_REASONING); + const [reasoning, setReasoning] = useState(() => { + const prefs = usePreferencesStore.getState(); + const isValidReasoning = (v: string): v is ReasoningEffort => + REASONING_LEVELS.some((r) => r.value === v); + const desired = + prefs.defaultReasoningEffort === "last_used" + ? prefs.lastUsedReasoningEffort + : prefs.defaultReasoningEffort; + return isValidReasoning(desired) ? desired : DEFAULT_REASONING; + }); const [creating, setCreating] = useState(false); const [repoSheetOpen, setRepoSheetOpen] = useState(false); const [modeSheetOpen, setModeSheetOpen] = useState(false); @@ -681,7 +689,11 @@ export default function NewTaskScreen() { open={reasoningSheetOpen} title="Reasoning" value={reasoning} - onChange={(value) => setReasoning(value as ReasoningEffort)} + onChange={(value) => { + const next = value as ReasoningEffort; + setReasoning(next); + usePreferencesStore.getState().setLastUsedReasoningEffort(next); + }} onClose={() => setReasoningSheetOpen(false)} options={REASONING_LEVELS.map((reasoningLevel) => ({ value: reasoningLevel.value, diff --git a/apps/mobile/src/features/preferences/stores/preferencesStore.test.ts b/apps/mobile/src/features/preferences/stores/preferencesStore.test.ts new file mode 100644 index 000000000..cdd907188 --- /dev/null +++ b/apps/mobile/src/features/preferences/stores/preferencesStore.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { usePreferencesStore } from "./preferencesStore"; + +const INITIAL_STATE = usePreferencesStore.getState(); + +beforeEach(() => { + // Reset to the store's defined defaults between tests so persisted state from + // earlier cases doesn't leak in. + usePreferencesStore.setState(INITIAL_STATE, true); +}); + +describe("preferencesStore reasoning effort", () => { + it("defaults defaultReasoningEffort to last_used", () => { + expect(usePreferencesStore.getState().defaultReasoningEffort).toBe( + "last_used", + ); + }); + + it("defaults lastUsedReasoningEffort to high", () => { + expect(usePreferencesStore.getState().lastUsedReasoningEffort).toBe("high"); + }); + + it.each(["low", "medium", "high", "xhigh", "max", "last_used"] as const)( + "updates defaultReasoningEffort to %s via setter", + (effort) => { + usePreferencesStore.getState().setDefaultReasoningEffort(effort); + expect(usePreferencesStore.getState().defaultReasoningEffort).toBe( + effort, + ); + }, + ); + + it.each(["low", "medium", "high", "xhigh", "max"] as const)( + "updates lastUsedReasoningEffort to %s via setter", + (effort) => { + usePreferencesStore.getState().setLastUsedReasoningEffort(effort); + expect(usePreferencesStore.getState().lastUsedReasoningEffort).toBe( + effort, + ); + }, + ); + + it("keeps lastUsedReasoningEffort independent of defaultReasoningEffort", () => { + usePreferencesStore.getState().setDefaultReasoningEffort("low"); + usePreferencesStore.getState().setLastUsedReasoningEffort("max"); + + const state = usePreferencesStore.getState(); + expect(state.defaultReasoningEffort).toBe("low"); + expect(state.lastUsedReasoningEffort).toBe("max"); + }); +}); diff --git a/apps/mobile/src/features/preferences/stores/preferencesStore.ts b/apps/mobile/src/features/preferences/stores/preferencesStore.ts index bdacce031..440c82435 100644 --- a/apps/mobile/src/features/preferences/stores/preferencesStore.ts +++ b/apps/mobile/src/features/preferences/stores/preferencesStore.ts @@ -15,6 +15,14 @@ export type CompletionSound = export type InitialTaskMode = "plan" | "last_used"; +export type DefaultReasoningEffort = + | "low" + | "medium" + | "high" + | "xhigh" + | "max" + | "last_used"; + interface PreferencesState { pingsEnabled: boolean; setPingsEnabled: (enabled: boolean) => void; @@ -35,6 +43,13 @@ interface PreferencesState { * `defaultInitialTaskMode === "last_used"` can pre-fill it next time. */ lastNewTaskMode: string; setLastNewTaskMode: (mode: string) => void; + + defaultReasoningEffort: DefaultReasoningEffort; + setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void; + /** Most recent reasoning effort the user picked. Persisted so + * `defaultReasoningEffort === "last_used"` can pre-fill it next time. */ + lastUsedReasoningEffort: string; + setLastUsedReasoningEffort: (effort: string) => void; } export const usePreferencesStore = create()( @@ -62,6 +77,13 @@ export const usePreferencesStore = create()( set({ defaultInitialTaskMode: mode }), lastNewTaskMode: "plan", setLastNewTaskMode: (mode) => set({ lastNewTaskMode: mode }), + + defaultReasoningEffort: "last_used", + setDefaultReasoningEffort: (effort) => + set({ defaultReasoningEffort: effort }), + lastUsedReasoningEffort: "high", + setLastUsedReasoningEffort: (effort) => + set({ lastUsedReasoningEffort: effort }), }), { name: "posthog-preferences", @@ -74,6 +96,8 @@ export const usePreferencesStore = create()( completionVolume: state.completionVolume, defaultInitialTaskMode: state.defaultInitialTaskMode, lastNewTaskMode: state.lastNewTaskMode, + defaultReasoningEffort: state.defaultReasoningEffort, + lastUsedReasoningEffort: state.lastUsedReasoningEffort, }), }, ),