Skip to content

Commit 5daaae2

Browse files
authored
feat(mobile): add configurable default effort level (#2406)
1 parent bd32626 commit 5daaae2

5 files changed

Lines changed: 155 additions & 4 deletions

File tree

apps/mobile/src/app/settings/index.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useDismissedReportsStore } from "@/features/inbox/stores/dismissedRepor
88
import { usePushTokenStore } from "@/features/notifications/stores/pushTokenStore";
99
import {
1010
type CompletionSound,
11+
type DefaultReasoningEffort,
1112
type InitialTaskMode,
1213
type ThemePreference,
1314
usePreferencesStore,
@@ -59,6 +60,23 @@ const TASK_MODE_OPTIONS = [
5960
},
6061
] as const;
6162

63+
const REASONING_EFFORT_OPTIONS: ReadonlyArray<{
64+
value: DefaultReasoningEffort;
65+
label: string;
66+
description?: string;
67+
}> = [
68+
{
69+
value: "last_used",
70+
label: "Last used",
71+
description: "Remember the effort level you picked last time",
72+
},
73+
{ value: "low", label: "Low" },
74+
{ value: "medium", label: "Medium" },
75+
{ value: "high", label: "High" },
76+
{ value: "xhigh", label: "Extra High" },
77+
{ value: "max", label: "Max" },
78+
];
79+
6280
function themeLabel(theme: ThemePreference): string {
6381
return THEME_OPTIONS.find((o) => o.value === theme)?.label ?? "Match system";
6482
}
@@ -78,6 +96,13 @@ function taskModeLabel(mode: InitialTaskMode): string {
7896
return TASK_MODE_OPTIONS.find((o) => o.value === mode)?.label ?? "Plan";
7997
}
8098

99+
function reasoningEffortLabel(effort: DefaultReasoningEffort): string {
100+
return (
101+
REASONING_EFFORT_OPTIONS.find((o) => o.value === effort)?.label ??
102+
"Last used"
103+
);
104+
}
105+
81106
export default function SettingsScreen() {
82107
const themeColors = useThemeColors();
83108
const { insets, bottom } = useScreenInsets();
@@ -113,6 +138,12 @@ export default function SettingsScreen() {
113138
const setDefaultInitialTaskMode = usePreferencesStore(
114139
(s) => s.setDefaultInitialTaskMode,
115140
);
141+
const defaultReasoningEffort = usePreferencesStore(
142+
(s) => s.defaultReasoningEffort,
143+
);
144+
const setDefaultReasoningEffort = usePreferencesStore(
145+
(s) => s.setDefaultReasoningEffort,
146+
);
116147
const decidedCount = useDismissedReportsStore(
117148
(s) => s.dismissedIds.length + s.acceptedIds.length,
118149
);
@@ -122,6 +153,8 @@ export default function SettingsScreen() {
122153
const [soundSheetOpen, setSoundSheetOpen] = useState(false);
123154
const [volumeSheetOpen, setVolumeSheetOpen] = useState(false);
124155
const [taskModeSheetOpen, setTaskModeSheetOpen] = useState(false);
156+
const [reasoningEffortSheetOpen, setReasoningEffortSheetOpen] =
157+
useState(false);
125158
const [projectSheetOpen, setProjectSheetOpen] = useState(false);
126159

127160
// The selected project's name. Prefer the names fetched for the scoped teams
@@ -272,7 +305,6 @@ export default function SettingsScreen() {
272305
label="Initial task mode"
273306
description="What mode new tasks start in"
274307
onPress={() => setTaskModeSheetOpen(true)}
275-
showDivider={false}
276308
rightSlot={
277309
<>
278310
<Text className="text-[14px] text-gray-11">
@@ -282,6 +314,20 @@ export default function SettingsScreen() {
282314
</>
283315
}
284316
/>
317+
<SettingsRow
318+
label="Default effort level"
319+
description="Reasoning effort to pre-fill on new tasks"
320+
onPress={() => setReasoningEffortSheetOpen(true)}
321+
showDivider={false}
322+
rightSlot={
323+
<>
324+
<Text className="text-[14px] text-gray-11">
325+
{reasoningEffortLabel(defaultReasoningEffort)}
326+
</Text>
327+
<CaretRight size={14} color={themeColors.gray[10]} />
328+
</>
329+
}
330+
/>
285331
</SettingsSection>
286332

287333
{/* Integrations */}
@@ -508,6 +554,21 @@ export default function SettingsScreen() {
508554
}))}
509555
/>
510556

557+
<SelectSheet
558+
open={reasoningEffortSheetOpen}
559+
title="Default effort level"
560+
value={defaultReasoningEffort}
561+
onChange={(value) =>
562+
setDefaultReasoningEffort(value as DefaultReasoningEffort)
563+
}
564+
onClose={() => setReasoningEffortSheetOpen(false)}
565+
options={REASONING_EFFORT_OPTIONS.map((option) => ({
566+
value: option.value,
567+
label: option.label,
568+
description: option.description,
569+
}))}
570+
/>
571+
511572
<SelectSheet
512573
open={projectSheetOpen}
513574
title="Active project"

apps/mobile/src/app/task/[id].tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
1414
import Animated, { useAnimatedStyle } from "react-native-reanimated";
1515
import { FloatingBackButton } from "@/components/FloatingBackButton";
16+
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
1617
import { getTask, runTaskInCloud } from "@/features/tasks/api";
1718
import { FloatingTaskHeader } from "@/features/tasks/components/FloatingTaskHeader";
1819
import { PrDiffStatsBadge } from "@/features/tasks/components/PrDiffStatsBadge";
@@ -307,6 +308,7 @@ export default function TaskDetailScreen() {
307308
if (!taskId) return;
308309
setComposerConfig(taskId, { reasoning: value });
309310
setConfigOption(taskId, "effort", value).catch(() => {});
311+
usePreferencesStore.getState().setLastUsedReasoningEffort(value);
310312
},
311313
[taskId, setComposerConfig, setConfigOption],
312314
);

apps/mobile/src/app/task/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,16 @@ export default function NewTaskScreen() {
180180
return DEFAULT_EXECUTION_MODE;
181181
});
182182
const [model, setModel] = useState<string>(DEFAULT_MODEL);
183-
const [reasoning, setReasoning] =
184-
useState<ReasoningEffort>(DEFAULT_REASONING);
183+
const [reasoning, setReasoning] = useState<ReasoningEffort>(() => {
184+
const prefs = usePreferencesStore.getState();
185+
const isValidReasoning = (v: string): v is ReasoningEffort =>
186+
REASONING_LEVELS.some((r) => r.value === v);
187+
const desired =
188+
prefs.defaultReasoningEffort === "last_used"
189+
? prefs.lastUsedReasoningEffort
190+
: prefs.defaultReasoningEffort;
191+
return isValidReasoning(desired) ? desired : DEFAULT_REASONING;
192+
});
185193
const [creating, setCreating] = useState(false);
186194
const [repoSheetOpen, setRepoSheetOpen] = useState(false);
187195
const [modeSheetOpen, setModeSheetOpen] = useState(false);
@@ -681,7 +689,11 @@ export default function NewTaskScreen() {
681689
open={reasoningSheetOpen}
682690
title="Reasoning"
683691
value={reasoning}
684-
onChange={(value) => setReasoning(value as ReasoningEffort)}
692+
onChange={(value) => {
693+
const next = value as ReasoningEffort;
694+
setReasoning(next);
695+
usePreferencesStore.getState().setLastUsedReasoningEffort(next);
696+
}}
685697
onClose={() => setReasoningSheetOpen(false)}
686698
options={REASONING_LEVELS.map((reasoningLevel) => ({
687699
value: reasoningLevel.value,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
3+
import { usePreferencesStore } from "./preferencesStore";
4+
5+
const INITIAL_STATE = usePreferencesStore.getState();
6+
7+
beforeEach(() => {
8+
// Reset to the store's defined defaults between tests so persisted state from
9+
// earlier cases doesn't leak in.
10+
usePreferencesStore.setState(INITIAL_STATE, true);
11+
});
12+
13+
describe("preferencesStore reasoning effort", () => {
14+
it("defaults defaultReasoningEffort to last_used", () => {
15+
expect(usePreferencesStore.getState().defaultReasoningEffort).toBe(
16+
"last_used",
17+
);
18+
});
19+
20+
it("defaults lastUsedReasoningEffort to high", () => {
21+
expect(usePreferencesStore.getState().lastUsedReasoningEffort).toBe("high");
22+
});
23+
24+
it.each(["low", "medium", "high", "xhigh", "max", "last_used"] as const)(
25+
"updates defaultReasoningEffort to %s via setter",
26+
(effort) => {
27+
usePreferencesStore.getState().setDefaultReasoningEffort(effort);
28+
expect(usePreferencesStore.getState().defaultReasoningEffort).toBe(
29+
effort,
30+
);
31+
},
32+
);
33+
34+
it.each(["low", "medium", "high", "xhigh", "max"] as const)(
35+
"updates lastUsedReasoningEffort to %s via setter",
36+
(effort) => {
37+
usePreferencesStore.getState().setLastUsedReasoningEffort(effort);
38+
expect(usePreferencesStore.getState().lastUsedReasoningEffort).toBe(
39+
effort,
40+
);
41+
},
42+
);
43+
44+
it("keeps lastUsedReasoningEffort independent of defaultReasoningEffort", () => {
45+
usePreferencesStore.getState().setDefaultReasoningEffort("low");
46+
usePreferencesStore.getState().setLastUsedReasoningEffort("max");
47+
48+
const state = usePreferencesStore.getState();
49+
expect(state.defaultReasoningEffort).toBe("low");
50+
expect(state.lastUsedReasoningEffort).toBe("max");
51+
});
52+
});

apps/mobile/src/features/preferences/stores/preferencesStore.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export type CompletionSound =
1515

1616
export type InitialTaskMode = "plan" | "last_used";
1717

18+
export type DefaultReasoningEffort =
19+
| "low"
20+
| "medium"
21+
| "high"
22+
| "xhigh"
23+
| "max"
24+
| "last_used";
25+
1826
interface PreferencesState {
1927
pingsEnabled: boolean;
2028
setPingsEnabled: (enabled: boolean) => void;
@@ -35,6 +43,13 @@ interface PreferencesState {
3543
* `defaultInitialTaskMode === "last_used"` can pre-fill it next time. */
3644
lastNewTaskMode: string;
3745
setLastNewTaskMode: (mode: string) => void;
46+
47+
defaultReasoningEffort: DefaultReasoningEffort;
48+
setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void;
49+
/** Most recent reasoning effort the user picked. Persisted so
50+
* `defaultReasoningEffort === "last_used"` can pre-fill it next time. */
51+
lastUsedReasoningEffort: string;
52+
setLastUsedReasoningEffort: (effort: string) => void;
3853
}
3954

4055
export const usePreferencesStore = create<PreferencesState>()(
@@ -62,6 +77,13 @@ export const usePreferencesStore = create<PreferencesState>()(
6277
set({ defaultInitialTaskMode: mode }),
6378
lastNewTaskMode: "plan",
6479
setLastNewTaskMode: (mode) => set({ lastNewTaskMode: mode }),
80+
81+
defaultReasoningEffort: "last_used",
82+
setDefaultReasoningEffort: (effort) =>
83+
set({ defaultReasoningEffort: effort }),
84+
lastUsedReasoningEffort: "high",
85+
setLastUsedReasoningEffort: (effort) =>
86+
set({ lastUsedReasoningEffort: effort }),
6587
}),
6688
{
6789
name: "posthog-preferences",
@@ -74,6 +96,8 @@ export const usePreferencesStore = create<PreferencesState>()(
7496
completionVolume: state.completionVolume,
7597
defaultInitialTaskMode: state.defaultInitialTaskMode,
7698
lastNewTaskMode: state.lastNewTaskMode,
99+
defaultReasoningEffort: state.defaultReasoningEffort,
100+
lastUsedReasoningEffort: state.lastUsedReasoningEffort,
77101
}),
78102
},
79103
),

0 commit comments

Comments
 (0)