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
53 changes: 49 additions & 4 deletions apps/mobile/src/app/task/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ import { StopRunButton } from "@/features/tasks/components/StopRunButton";
import { TaskSessionView } from "@/features/tasks/components/TaskSessionView";
import { buildCloudPromptBlocks } from "@/features/tasks/composer/attachments/buildCloudPrompt";
import type { PendingAttachment } from "@/features/tasks/composer/attachments/types";
import {
type ContextWindow,
DEFAULT_CONTEXT_WINDOW,
} from "@/features/tasks/composer/options";
import { QueuedMessagesDock } from "@/features/tasks/composer/QueuedMessagesDock";
import { TaskChatComposer } from "@/features/tasks/composer/TaskChatComposer";
import {
Expand Down Expand Up @@ -215,6 +219,13 @@ export default function TaskDetailScreen() {
)
? requestedComposerReasoning
: DEFAULT_REASONING_EFFORT;
const composerContextWindow: ContextWindow =
(composerConfigMatchesAdapter
? composerConfig?.contextWindow
: undefined) ?? DEFAULT_CONTEXT_WINDOW;
const composerFastMode: boolean =
(composerConfigMatchesAdapter ? composerConfig?.fastMode : undefined) ??
false;

const messagingMode = useMessagingMode(taskId);
const queuedCount = useQueuedCount(taskId);
Expand Down Expand Up @@ -368,6 +379,8 @@ export default function TaskDetailScreen() {
mode: composerMode,
model: composerModel,
reasoning: composerReasoning,
contextWindow: composerContextWindow,
fastMode: composerFastMode,
}),
rtkEnabled: usePreferencesStore.getState().rtkEnabledCloud,
},
Expand Down Expand Up @@ -397,6 +410,8 @@ export default function TaskDetailScreen() {
composerAdapter,
composerModel,
composerReasoning,
composerContextWindow,
composerFastMode,
],
);

Expand Down Expand Up @@ -554,10 +569,14 @@ export default function TaskDetailScreen() {
const handleAdapterChange = useCallback(
(selection: CloudComposerSelection) => {
if (!taskId) return;
setComposerConfig(taskId, selection);
const preferences = usePreferencesStore.getState();
preferences.setLastNewTaskMode(selection.mode);
preferences.setLastUsedReasoningEffort(selection.reasoning);
setComposerConfig(taskId, {
...selection,
contextWindow: DEFAULT_CONTEXT_WINDOW,
fastMode: false,
});
usePreferencesStore
.getState()
.resetLastUsedAgentConfig(selection.mode, selection.reasoning);
},
[taskId, setComposerConfig],
);
Expand Down Expand Up @@ -585,6 +604,24 @@ export default function TaskDetailScreen() {
[taskId, composerAdapter, setComposerConfig, setConfigOption],
);

const handleContextWindowChange = useCallback(
(value: ContextWindow) => {
if (!taskId) return;
setComposerConfig(taskId, { contextWindow: value });
usePreferencesStore.getState().setLastUsedContextWindow(value);
},
[taskId, setComposerConfig],
);

const handleFastModeChange = useCallback(
(value: boolean) => {
if (!taskId) return;
setComposerConfig(taskId, { fastMode: value });
usePreferencesStore.getState().setLastUsedFastMode(value);
},
[taskId, setComposerConfig],
);

const handleStop = useCallback(() => {
if (!taskId) return;
// cancelPrompt returns false on failure — no need to alert,
Expand Down Expand Up @@ -639,6 +676,8 @@ export default function TaskDetailScreen() {
mode: composerMode,
model: composerModel,
reasoning: composerReasoning,
contextWindow: composerContextWindow,
fastMode: composerFastMode,
}),
rtkEnabled: usePreferencesStore.getState().rtkEnabledCloud,
},
Expand Down Expand Up @@ -666,6 +705,8 @@ export default function TaskDetailScreen() {
composerModel,
composerReasoning,
composerMode,
composerContextWindow,
composerFastMode,
]);

// Clear retrying once the agent finishes a turn or the run terminates.
Expand Down Expand Up @@ -877,10 +918,14 @@ export default function TaskDetailScreen() {
mode={composerMode}
model={composerModel}
reasoning={composerReasoning}
contextWindow={composerContextWindow}
fastMode={composerFastMode}
onAdapterChange={handleAdapterChange}
onModeChange={handleModeChange}
onModelChange={handleModelChange}
onReasoningChange={handleReasoningChange}
onContextWindowChange={handleContextWindowChange}
onFastModeChange={handleFastModeChange}
messagingMode={messagingMode}
queuedCount={queuedCount}
onToggleMessagingMode={toggleMessagingMode}
Expand Down
59 changes: 52 additions & 7 deletions apps/mobile/src/app/task/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
KIMI_MODEL_FLAG,
type SupportedReasoningEffort,
serializeCloudPrompt,
supports1MContext,
supportsFastMode,
} from "@posthog/shared";
import { LinearGradient } from "expo-linear-gradient";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
Expand Down Expand Up @@ -55,6 +57,8 @@ import {
import type { PendingAttachment } from "@/features/tasks/composer/attachments/types";
import { DotBackground } from "@/features/tasks/composer/DotBackground";
import {
type ContextWindow,
DEFAULT_CONTEXT_WINDOW,
filterKimiModelConfigOptions,
getMobileExecutionModes,
getModelConfigOption,
Expand Down Expand Up @@ -199,6 +203,12 @@ export default function NewTaskScreen() {
? desired
: DEFAULT_REASONING_EFFORT;
});
const [contextWindow, setContextWindow] = useState<ContextWindow>(
() => usePreferencesStore.getState().lastUsedContextWindow,
);
const [fastMode, setFastMode] = useState<boolean>(
() => usePreferencesStore.getState().lastUsedFastMode,
);

useEffect(() => {
if (!hasLiveConfig) return;
Expand Down Expand Up @@ -346,7 +356,14 @@ export default function NewTaskScreen() {
// user picked here, so the task detail screen reflects them and every
// subsequent run (resume-after-terminal) reuses the selected mode rather
// than falling back to the default plan mode.
setComposerConfig(task.id, { adapter, mode, model, reasoning });
setComposerConfig(task.id, {
adapter,
mode,
model,
reasoning,
contextWindow,
fastMode,
});

const pendingUserMessage =
attachments.length > 0
Expand All @@ -357,7 +374,14 @@ export default function NewTaskScreen() {

await client.runTaskInCloud(task.id, undefined, {
pendingUserMessage,
...buildCloudTaskRunConfig({ adapter, mode, model, reasoning }),
...buildCloudTaskRunConfig({
adapter,
mode,
model,
reasoning,
contextWindow,
fastMode,
}),
autoPublish: usePreferencesStore.getState().autoPublishCloudRuns,
rtkEnabled: usePreferencesStore.getState().rtkEnabledCloud,
...(signalReport
Expand All @@ -384,6 +408,8 @@ export default function NewTaskScreen() {
model,
prompt,
reasoning,
contextWindow,
fastMode,
router,
selection,
signalReport,
Expand All @@ -410,6 +436,8 @@ export default function NewTaskScreen() {
runtimeAdapter: adapter,
model,
reasoningEffort: showReasoningPill ? reasoning : null,
contextWindow: supports1MContext(model) ? contextWindow : null,
fastMode: supportsFastMode(model) ? fastMode : null,
});

if (isLoading && hasGithubIntegration === null) {
Expand Down Expand Up @@ -591,17 +619,22 @@ export default function NewTaskScreen() {
mode={mode}
model={model}
reasoning={reasoning}
contextWindow={contextWindow}
fastMode={fastMode}
configOptions={configOptions}
onAdapterChange={(next) => {
setAdapter(next.adapter);
setMode(next.mode);
setModel(next.model);
setReasoning(next.reasoning);
const preferences = usePreferencesStore.getState();
preferences.setLastNewTaskMode(next.mode);
preferences.setLastUsedReasoningEffort(
next.reasoning,
);
setContextWindow(DEFAULT_CONTEXT_WINDOW);
setFastMode(false);
usePreferencesStore
.getState()
.resetLastUsedAgentConfig(
next.mode,
next.reasoning,
);
}}
onModeChange={(next) => {
setMode(next);
Expand All @@ -616,6 +649,18 @@ export default function NewTaskScreen() {
.getState()
.setLastUsedReasoningEffort(next);
}}
onContextWindowChange={(next) => {
setContextWindow(next);
usePreferencesStore
.getState()
.setLastUsedContextWindow(next);
}}
onFastModeChange={(next) => {
setFastMode(next);
usePreferencesStore
.getState()
.setLastUsedFastMode(next);
}}
/>
</ScrollView>
{/* Right-edge fade hints that more pills exist when the row
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,63 @@ describe("preferencesStore reasoning effort", () => {
});
});

describe("preferencesStore agent config", () => {
it("defaults context window to 1m and fast mode off", () => {
const state = usePreferencesStore.getState();
expect(state.lastUsedContextWindow).toBe("1m");
expect(state.lastUsedFastMode).toBe(false);
});

it.each(["200k", "1m"] as const)(
"updates lastUsedContextWindow to %s via setter",
(value) => {
usePreferencesStore.getState().setLastUsedContextWindow(value);
expect(usePreferencesStore.getState().lastUsedContextWindow).toBe(value);
},
);

it.each([true, false])(
"updates lastUsedFastMode to %s via setter",
(enabled) => {
usePreferencesStore.getState().setLastUsedFastMode(enabled);
expect(usePreferencesStore.getState().lastUsedFastMode).toBe(enabled);
},
);

it("resets the last-used agent config to a harness's mode and effort", () => {
usePreferencesStore.getState().setLastUsedContextWindow("200k");
usePreferencesStore.getState().setLastUsedFastMode(true);

usePreferencesStore.getState().resetLastUsedAgentConfig("auto", "medium");

const state = usePreferencesStore.getState();
expect(state.lastNewTaskMode).toBe("auto");
expect(state.lastUsedReasoningEffort).toBe("medium");
expect(state.lastUsedContextWindow).toBe("1m");
expect(state.lastUsedFastMode).toBe(false);
});

it("resets the stale agent selection when migrating a pre-versioned install", () => {
const migrate = usePreferencesStore.persist.getOptions().migrate;
const migrated = migrate?.(
{
theme: "dark",
lastUsedReasoningEffort: "ultracode",
defaultReasoningEffort: "low",
},
0,
) as Record<string, unknown>;

expect(migrated).toMatchObject({
theme: "dark",
defaultReasoningEffort: "low",
lastUsedReasoningEffort: "high",
lastUsedContextWindow: "1m",
lastUsedFastMode: false,
});
});
});

describe("preferencesStore scale sound with task length", () => {
it("defaults to false", () => {
expect(usePreferencesStore.getState().scaleSoundWithTaskLength).toBe(false);
Expand Down
45 changes: 45 additions & 0 deletions apps/mobile/src/features/preferences/stores/preferencesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import { Platform } from "react-native";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import {
type ContextWindow,
DEFAULT_CONTEXT_WINDOW,
} from "@/features/tasks/composer/options";

export type ThemePreference = "light" | "dark" | "system";

Expand Down Expand Up @@ -76,6 +80,16 @@ interface PreferencesState {
lastUsedReasoningEffort: string;
setLastUsedReasoningEffort: (effort: string) => void;

/** Most recent context window + fast mode picked in the merged agent config
* control. Persisted so the next new-task composer restores them. */
lastUsedContextWindow: ContextWindow;
setLastUsedContextWindow: (contextWindow: ContextWindow) => void;
lastUsedFastMode: boolean;
setLastUsedFastMode: (enabled: boolean) => void;
/** Resets the last-used agent selection to a new harness's mode/effort with
* default context window and fast mode off, in one write. */
resetLastUsedAgentConfig: (mode: string, reasoning: string) => void;

autoPublishCloudRuns: boolean;
setAutoPublishCloudRuns: (enabled: boolean) => void;

Expand Down Expand Up @@ -122,6 +136,19 @@ export const usePreferencesStore = create<PreferencesState>()(
setLastUsedReasoningEffort: (effort) =>
set({ lastUsedReasoningEffort: effort }),

lastUsedContextWindow: DEFAULT_CONTEXT_WINDOW,
setLastUsedContextWindow: (contextWindow) =>
set({ lastUsedContextWindow: contextWindow }),
lastUsedFastMode: false,
setLastUsedFastMode: (enabled) => set({ lastUsedFastMode: enabled }),
resetLastUsedAgentConfig: (mode, reasoning) =>
set({
lastNewTaskMode: mode,
lastUsedReasoningEffort: reasoning,
lastUsedContextWindow: DEFAULT_CONTEXT_WINDOW,
lastUsedFastMode: false,
}),

autoPublishCloudRuns: true,
setAutoPublishCloudRuns: (enabled) =>
set({ autoPublishCloudRuns: enabled }),
Expand All @@ -132,6 +159,22 @@ export const usePreferencesStore = create<PreferencesState>()(
{
name: "posthog-preferences",
storage: createJSONStorage(() => AsyncStorage),
// Bumped to 1 when the merged agent config control shipped: existing
// installs drop their stale last-used agent selection so they land on the
// new preset defaults instead of a model/effort pair off the new scale.
version: 1,
migrate: (persisted, fromVersion) => {
const state = (persisted ?? {}) as Partial<PreferencesState>;
if (fromVersion < 1) {
return {
...state,
lastUsedReasoningEffort: "high",
lastUsedContextWindow: DEFAULT_CONTEXT_WINDOW,
lastUsedFastMode: false,
} as PreferencesState;
}
return state as PreferencesState;
},
partialize: (state) => ({
pingsEnabled: state.pingsEnabled,
pushNotificationsEnabled: state.pushNotificationsEnabled,
Expand All @@ -144,6 +187,8 @@ export const usePreferencesStore = create<PreferencesState>()(
lastNewTaskMode: state.lastNewTaskMode,
defaultReasoningEffort: state.defaultReasoningEffort,
lastUsedReasoningEffort: state.lastUsedReasoningEffort,
lastUsedContextWindow: state.lastUsedContextWindow,
lastUsedFastMode: state.lastUsedFastMode,
autoPublishCloudRuns: state.autoPublishCloudRuns,
rtkEnabledCloud: state.rtkEnabledCloud,
}),
Expand Down
Loading
Loading