Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion apps/mobile/src/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
}
Expand All @@ -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();
Expand Down Expand Up @@ -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,
);
Expand All @@ -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
Expand Down Expand Up @@ -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={
<>
<Text className="text-[14px] text-gray-11">
Expand All @@ -282,6 +314,20 @@ export default function SettingsScreen() {
</>
}
/>
<SettingsRow
label="Default effort level"
description="Reasoning effort to pre-fill on new tasks"
onPress={() => setReasoningEffortSheetOpen(true)}
showDivider={false}
rightSlot={
<>
<Text className="text-[14px] text-gray-11">
{reasoningEffortLabel(defaultReasoningEffort)}
</Text>
<CaretRight size={14} color={themeColors.gray[10]} />
</>
}
/>
</SettingsSection>

{/* Integrations */}
Expand Down Expand Up @@ -508,6 +554,21 @@ export default function SettingsScreen() {
}))}
/>

<SelectSheet
open={reasoningEffortSheetOpen}
title="Default effort level"
value={defaultReasoningEffort}
onChange={(value) =>
setDefaultReasoningEffort(value as DefaultReasoningEffort)
}
onClose={() => setReasoningEffortSheetOpen(false)}
options={REASONING_EFFORT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
description: option.description,
}))}
/>

<SelectSheet
open={projectSheetOpen}
title="Active project"
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/app/task/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
import { FloatingBackButton } from "@/components/FloatingBackButton";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
import { getTask, runTaskInCloud } from "@/features/tasks/api";
import { FloatingTaskHeader } from "@/features/tasks/components/FloatingTaskHeader";
import { PrDiffStatsBadge } from "@/features/tasks/components/PrDiffStatsBadge";
Expand Down Expand Up @@ -307,6 +308,7 @@ export default function TaskDetailScreen() {
if (!taskId) return;
setComposerConfig(taskId, { reasoning: value });
setConfigOption(taskId, "effort", value).catch(() => {});
usePreferencesStore.getState().setLastUsedReasoningEffort(value);
},
[taskId, setComposerConfig, setConfigOption],
);
Expand Down
18 changes: 15 additions & 3 deletions apps/mobile/src/app/task/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,16 @@ export default function NewTaskScreen() {
return DEFAULT_EXECUTION_MODE;
});
const [model, setModel] = useState<string>(DEFAULT_MODEL);
const [reasoning, setReasoning] =
useState<ReasoningEffort>(DEFAULT_REASONING);
const [reasoning, setReasoning] = useState<ReasoningEffort>(() => {
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);
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});
24 changes: 24 additions & 0 deletions apps/mobile/src/features/preferences/stores/preferencesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PreferencesState>()(
Expand Down Expand Up @@ -62,6 +77,13 @@ export const usePreferencesStore = create<PreferencesState>()(
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",
Expand All @@ -74,6 +96,8 @@ export const usePreferencesStore = create<PreferencesState>()(
completionVolume: state.completionVolume,
defaultInitialTaskMode: state.defaultInitialTaskMode,
lastNewTaskMode: state.lastNewTaskMode,
defaultReasoningEffort: state.defaultReasoningEffort,
lastUsedReasoningEffort: state.lastUsedReasoningEffort,
}),
},
),
Expand Down
Loading