Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -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]);
}
68 changes: 68 additions & 0 deletions packages/ui/src/features/settings/sections/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -71,6 +73,7 @@ export function GeneralSettings() {
autoConvertLongText,
defaultInitialTaskMode,
defaultMessagingMode,
defaultModels,
defaultReasoningEffort,
diffOpenMode,
sendMessagesWith,
Expand All @@ -81,6 +84,7 @@ export function GeneralSettings() {
setAutoConvertLongText,
setDefaultInitialTaskMode,
setDefaultMessagingMode,
setDefaultModel,
setDefaultReasoningEffort,
setDiffOpenMode,
setSendMessagesWith,
Expand All @@ -90,6 +94,9 @@ export function GeneralSettings() {
setBrainrotMode,
} = useSettingsStore();

const claudeModelOptions = useAvailableModelOptions("claude");
const codexModelOptions = useAvailableModelOptions("codex");

// Appearance handlers
const handleThemeChange = useCallback(
(value: ThemePreference) => {
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -311,6 +330,55 @@ export function GeneralSettings() {
</Select.Root>
</SettingRow>

<SettingRow
label="Default Claude model"
description="Choose the default model for new Claude tasks, or remember your last-used model"
>
<Select.Root
value={defaultModels.claude ?? "last_used"}
onValueChange={(value) =>
handleDefaultModelChange("claude", value as DefaultModel)
}
size="1"
disabled={claudeModelOptions.length === 0}
>
<Select.Trigger className="min-w-[140px]" />
<Select.Content>
<Select.Item value="last_used">Last used</Select.Item>
{claudeModelOptions.map((opt) => (
<Select.Item key={opt.value} value={opt.value}>
{opt.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</SettingRow>

{codexModelOptions.length > 0 && (
<SettingRow
label="Default Codex model"
description="Choose the default model for new Codex tasks, or remember your last-used model"
>
<Select.Root
value={defaultModels.codex ?? "last_used"}
onValueChange={(value) =>
handleDefaultModelChange("codex", value as DefaultModel)
}
size="1"
>
<Select.Trigger className="min-w-[140px]" />
<Select.Content>
<Select.Item value="last_used">Last used</Select.Item>
{codexModelOptions.map((opt) => (
<Select.Item key={opt.value} value={opt.value}>
{opt.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</SettingRow>
)}

<SettingRow
label="Default effort level"
description="Choose the default reasoning effort for new tasks, or remember your last-used level"
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/features/settings/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type DefaultReasoningEffort =
| "max"
| "last_used";

export type DefaultModel = string | "last_used";

export type SendMessagesWith = "enter" | "cmd+enter";
export type AutoConvertLongText = "off" | "1000" | "2500" | "5000" | "10000";
export type DiffOpenMode = "auto" | "split" | "same-pane" | "last-active-pane";
Expand Down Expand Up @@ -91,6 +93,7 @@ interface SettingsStore {
// Mode last chosen when approving a plan; pre-selected on the next approval.
lastPlanApprovalMode: ExecutionMode | null;
defaultReasoningEffort: DefaultReasoningEffort;
defaultModels: Partial<Record<"claude" | "codex", DefaultModel>>;
defaultMessagingMode: DefaultMessagingMode;
setDefaultMessagingMode: (mode: DefaultMessagingMode) => void;
setDefaultRunMode: (mode: DefaultRunMode) => void;
Expand All @@ -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;
Expand Down Expand Up @@ -228,6 +232,7 @@ export const useSettingsStore = create<SettingsStore>()(
lastUsedInitialTaskMode: "plan",
lastPlanApprovalMode: null,
defaultReasoningEffort: "last_used",
defaultModels: {},
defaultMessagingMode: "queue",
setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }),
Expand Down Expand Up @@ -261,6 +266,10 @@ export const useSettingsStore = create<SettingsStore>()(
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
Expand Down
15 changes: 10 additions & 5 deletions packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ export function usePreviewConfig(
lastUsedInitialTaskMode,
defaultReasoningEffort,
lastUsedReasoningEffort,
defaultModels,
lastUsedModel,
} = useSettingsStore.getState();
const defaultModel = defaultModels[adapter] ?? "last_used";

let initial = deriveInitialConfig(
options,
Expand All @@ -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,
Expand Down
Loading