From 20b7c6f8047a16025be03fbb803633af5d8ed100 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Mon, 6 Jul 2026 17:31:33 -0400 Subject: [PATCH] feat(settings): add per-adapter default model setting Adds a default model preference per adapter (Claude/Codex), mirroring the existing default reasoning effort setting. Each adapter remembers its own default independently so switching adapters no longer silently drops a stored default that belongs to the other. - settingsStore: defaultModels keyed by adapter - usePreviewConfig: applies the per-adapter default when restoring the model - GeneralSettings: two rows (Claude always; Codex when models available) - useAvailableModelOptions: shared fetch+flatten hook Generated-By: PostHog Code Task-Id: 574314ec-1643-4959-ba73-5940abc58aaf --- .../hooks/useAvailableModelOptions.ts | 37 ++++++++++ .../settings/sections/GeneralSettings.tsx | 68 +++++++++++++++++++ .../ui/src/features/settings/settingsStore.ts | 9 +++ .../task-detail/hooks/usePreviewConfig.ts | 15 ++-- 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts diff --git a/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts b/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts new file mode 100644 index 0000000000..e1d0f9f231 --- /dev/null +++ b/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts @@ -0,0 +1,37 @@ +import { useHostTRPC } from "@posthog/host-router/react"; +import { flattenSelectOptions, getCloudUrlFromRegion } from "@posthog/shared"; +import { useAuthStateValue } from "@posthog/ui/features/auth/store"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; + +export interface ModelOption { + value: string; + name: string; +} + +export function useAvailableModelOptions( + adapter: "claude" | "codex", +): ModelOption[] { + const hostTRPC = useHostTRPC(); + const cloudRegion = useAuthStateValue((state) => state.cloudRegion); + const apiHost = useMemo( + () => (cloudRegion ? getCloudUrlFromRegion(cloudRegion) : null), + [cloudRegion], + ); + + const { data: configOptions } = useQuery({ + ...hostTRPC.agent.getPreviewConfigOptions.queryOptions({ + apiHost: apiHost ?? "", + adapter, + }), + enabled: !!apiHost, + }); + + return useMemo(() => { + const modelOpt = configOptions?.find( + (o) => o.category === "model" || o.id === "model", + ); + if (!modelOpt || modelOpt.type !== "select") return []; + return flattenSelectOptions(modelOpt.options); + }, [configOptions]); +} diff --git a/packages/ui/src/features/settings/sections/GeneralSettings.tsx b/packages/ui/src/features/settings/sections/GeneralSettings.tsx index e1563633c4..89f0a59210 100644 --- a/packages/ui/src/features/settings/sections/GeneralSettings.tsx +++ b/packages/ui/src/features/settings/sections/GeneralSettings.tsx @@ -7,11 +7,13 @@ import { COLLAPSE_MODE_OPTIONS, type CollapseMode, } from "@posthog/ui/features/sessions/components/new-thread/conversationThreadConfig"; +import { useAvailableModelOptions } from "@posthog/ui/features/sessions/hooks/useAvailableModelOptions"; import { SettingRow } from "@posthog/ui/features/settings/SettingRow"; import { type AutoConvertLongText, type DefaultInitialTaskMode, type DefaultMessagingMode, + type DefaultModel, type DefaultReasoningEffort, type DiffOpenMode, type SendMessagesWith, @@ -71,6 +73,7 @@ export function GeneralSettings() { autoConvertLongText, defaultInitialTaskMode, defaultMessagingMode, + defaultModels, defaultReasoningEffort, diffOpenMode, sendMessagesWith, @@ -81,6 +84,7 @@ export function GeneralSettings() { setAutoConvertLongText, setDefaultInitialTaskMode, setDefaultMessagingMode, + setDefaultModel, setDefaultReasoningEffort, setDiffOpenMode, setSendMessagesWith, @@ -90,6 +94,9 @@ export function GeneralSettings() { setBrainrotMode, } = useSettingsStore(); + const claudeModelOptions = useAvailableModelOptions("claude"); + const codexModelOptions = useAvailableModelOptions("codex"); + // Appearance handlers const handleThemeChange = useCallback( (value: ThemePreference) => { @@ -164,6 +171,18 @@ export function GeneralSettings() { [defaultReasoningEffort, setDefaultReasoningEffort], ); + const handleDefaultModelChange = useCallback( + (adapter: "claude" | "codex", value: DefaultModel) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "default_model", + new_value: value, + old_value: defaultModels[adapter] ?? "last_used", + }); + setDefaultModel(adapter, value); + }, + [defaultModels, setDefaultModel], + ); + const handleConversationCollapseModeChange = useCallback( (value: CollapseMode) => { track(ANALYTICS_EVENTS.SETTING_CHANGED, { @@ -311,6 +330,55 @@ export function GeneralSettings() { + + + handleDefaultModelChange("claude", value as DefaultModel) + } + size="1" + disabled={claudeModelOptions.length === 0} + > + + + Last used + {claudeModelOptions.map((opt) => ( + + {opt.name} + + ))} + + + + + {codexModelOptions.length > 0 && ( + + + handleDefaultModelChange("codex", value as DefaultModel) + } + size="1" + > + + + Last used + {codexModelOptions.map((opt) => ( + + {opt.name} + + ))} + + + + )} + >; defaultMessagingMode: DefaultMessagingMode; setDefaultMessagingMode: (mode: DefaultMessagingMode) => void; setDefaultRunMode: (mode: DefaultRunMode) => void; @@ -113,6 +116,7 @@ interface SettingsStore { setLastUsedInitialTaskMode: (mode: ExecutionMode) => void; setLastPlanApprovalMode: (mode: ExecutionMode) => void; setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void; + setDefaultModel: (adapter: "claude" | "codex", model: DefaultModel) => void; // Notifications desktopNotifications: boolean; @@ -228,6 +232,7 @@ export const useSettingsStore = create()( lastUsedInitialTaskMode: "plan", lastPlanApprovalMode: null, defaultReasoningEffort: "last_used", + defaultModels: {}, defaultMessagingMode: "queue", setDefaultRunMode: (mode) => set({ defaultRunMode: mode }), setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }), @@ -261,6 +266,10 @@ export const useSettingsStore = create()( setLastPlanApprovalMode: (mode) => set({ lastPlanApprovalMode: mode }), setDefaultReasoningEffort: (effort) => set({ defaultReasoningEffort: effort }), + setDefaultModel: (adapter, model) => + set((state) => ({ + defaultModels: { ...state.defaultModels, [adapter]: model }, + })), setDefaultMessagingMode: (mode) => set({ defaultMessagingMode: mode }), // Notifications diff --git a/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts b/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts index ccde175ece..2f82c7e0fb 100644 --- a/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts +++ b/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts @@ -89,8 +89,10 @@ export function usePreviewConfig( lastUsedInitialTaskMode, defaultReasoningEffort, lastUsedReasoningEffort, + defaultModels, lastUsedModel, } = useSettingsStore.getState(); + const defaultModel = defaultModels[adapter] ?? "last_used"; let initial = deriveInitialConfig( options, @@ -107,19 +109,22 @@ export function usePreviewConfig( // without this the user's last pick (e.g. fable) is lost on every // refetch/remount. Restore it through applyConfigChange so the dependent // effort options are recomputed for the restored model. + // If defaultModel is set (not "last_used"), prefer it over lastUsedModel. + const targetModel = + defaultModel !== "last_used" ? defaultModel : lastUsedModel; const modelOpt = getOptionByCategory(initial, "model"); if ( - lastUsedModel && + targetModel && modelOpt?.type === "select" && - modelOpt.currentValue !== lastUsedModel && - flattenConfigValues(modelOpt).includes(lastUsedModel) + modelOpt.currentValue !== targetModel && + flattenConfigValues(modelOpt).includes(targetModel) ) { initial = applyConfigChange(initial, { adapter, configId: modelOpt.id, - value: lastUsedModel, + value: targetModel, effortOptions: - getReasoningEffortOptions(adapter, lastUsedModel) ?? undefined, + getReasoningEffortOptions(adapter, targetModel) ?? undefined, settings: { defaultInitialTaskMode: "", lastUsedInitialTaskMode: undefined,