| ... | ... | @@ -107,7 +107,15 @@ import { |
| 107 | 107 | CheckIcon, |
| 108 | 108 | } from "lucide-react"; |
| 109 | 109 | import { Button } from "./ui/button"; |
| 110 | | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; |
| 110 | import { |
| 111 | Select, |
| 112 | SelectGroup, |
| 113 | SelectGroupLabel, |
| 114 | SelectItem, |
| 115 | SelectPopup, |
| 116 | SelectTrigger, |
| 117 | SelectValue, |
| 118 | } from "./ui/select"; |
| 111 | 119 | import { Separator } from "./ui/separator"; |
| 112 | 120 | import { Group, GroupSeparator } from "./ui/group"; |
| 113 | 121 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu"; |
| ... | ... | @@ -2265,13 +2273,22 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2265 | 2273 | }, |
| 2266 | 2274 | [activeThread, scheduleComposerFocus, selectedProvider], |
| 2267 | 2275 | ); |
| 2268 | | const onProviderSelect = useCallback( |
| 2269 | | (provider: ProviderKind) => { |
| 2276 | const onProviderModelSelect = useCallback( |
| 2277 | (provider: ProviderKind, model: ModelSlug) => { |
| 2278 | const api = readNativeApi(); |
| 2270 | 2279 | if (!activeThread) return; |
| 2271 | 2280 | setSelectedProviderByThread((existing) => ({ |
| 2272 | 2281 | ...existing, |
| 2273 | 2282 | [activeThread.id]: provider, |
| 2274 | 2283 | })); |
| 2284 | if (api) { |
| 2285 | void api.orchestration.dispatchCommand({ |
| 2286 | type: "thread.meta.update", |
| 2287 | commandId: newCommandId(), |
| 2288 | threadId: activeThread.id, |
| 2289 | model: resolveModelSlugForProvider(provider, model), |
| 2290 | }); |
| 2291 | } |
| 2275 | 2292 | scheduleComposerFocus(); |
| 2276 | 2293 | }, |
| 2277 | 2294 | [activeThread, scheduleComposerFocus], |
| ... | ... | @@ -2712,17 +2729,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2712 | 2729 | {/* Bottom toolbar */} |
| 2713 | 2730 | <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3"> |
| 2714 | 2731 | <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible"> |
| 2715 | | {/* Model picker */} |
| 2716 | | <ProviderPicker provider={selectedProvider} onProviderChange={onProviderSelect} /> |
| 2717 | | |
| 2718 | | {/* Divider */} |
| 2719 | | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> |
| 2720 | | |
| 2721 | | {/* Model picker */} |
| 2722 | | <ModelPicker |
| 2732 | {/* Provider/model picker */} |
| 2733 | <ProviderModelPicker |
| 2734 | provider={selectedProvider} |
| 2723 | 2735 | model={selectedModel} |
| 2724 | | options={modelOptions} |
| 2725 | | onModelChange={onModelSelect} |
| 2736 | onProviderModelChange={onProviderModelSelect} |
| 2726 | 2737 | /> |
| 2727 | 2738 | |
| 2728 | 2739 | {/* Divider */} |
| ... | ... | @@ -3866,54 +3877,65 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 3866 | 3877 | ); |
| 3867 | 3878 | }); |
| 3868 | 3879 | |
| 3869 | | const ModelPicker = memo(function ModelPicker(props: { |
| 3880 | const PROVIDER_MODEL_GROUPS: ReadonlyArray<{ |
| 3881 | readonly provider: ProviderKind; |
| 3882 | readonly label: string; |
| 3883 | readonly options: ReadonlyArray<{ readonly slug: ModelSlug; readonly name: string }>; |
| 3884 | }> = PROVIDER_OPTIONS.filter((option) => option.available).map((option) => ({ |
| 3885 | provider: option.value, |
| 3886 | label: option.label, |
| 3887 | options: getModelOptions(option.value), |
| 3888 | })); |
| 3889 | |
| 3890 | const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 3891 | provider: ProviderKind; |
| 3870 | 3892 | model: ModelSlug; |
| 3871 | | options: ReadonlyArray<{ readonly slug: ModelSlug; readonly name: string }>; |
| 3872 | | onModelChange: (model: ModelSlug) => void; |
| 3893 | onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; |
| 3873 | 3894 | }) { |
| 3874 | | return ( |
| 3875 | | <Select |
| 3876 | | items={props.options.map((option) => ({ label: option.name, value: option.slug }))} |
| 3877 | | value={props.model} |
| 3878 | | onValueChange={(value) => (value ? props.onModelChange(value) : undefined)} |
| 3879 | | > |
| 3880 | | <SelectTrigger size="sm" variant="ghost"> |
| 3881 | | <SelectValue /> |
| 3882 | | </SelectTrigger> |
| 3883 | | <SelectPopup alignItemWithTrigger={false}> |
| 3884 | | {props.options.map(({ slug, name }) => ( |
| 3885 | | <SelectItem key={slug} value={slug}> |
| 3886 | | {name} |
| 3887 | | </SelectItem> |
| 3888 | | ))} |
| 3889 | | </SelectPopup> |
| 3890 | | </Select> |
| 3895 | const items = PROVIDER_MODEL_GROUPS.flatMap(({ provider, label, options }) => |
| 3896 | options.map(({ slug, name }) => ({ |
| 3897 | label: `${label} · ${name}`, |
| 3898 | value: `${provider}:${slug}`, |
| 3899 | })), |
| 3891 | 3900 | ); |
| 3892 | | }); |
| 3893 | 3901 | |
| 3894 | | const ProviderPicker = memo(function ProviderPicker(props: { |
| 3895 | | provider: ProviderKind; |
| 3896 | | onProviderChange: (provider: ProviderKind) => void; |
| 3897 | | }) { |
| 3898 | 3902 | return ( |
| 3899 | 3903 | <Select |
| 3900 | | items={PROVIDER_OPTIONS.filter((option) => option.available).map((option) => ({ |
| 3901 | | label: option.label, |
| 3902 | | value: option.value, |
| 3903 | | }))} |
| 3904 | | value={props.provider} |
| 3904 | items={items} |
| 3905 | value={`${props.provider}:${props.model}`} |
| 3905 | 3906 | onValueChange={(value) => |
| 3906 | | value === "codex" || value === "claudeCode" ? props.onProviderChange(value) : undefined |
| 3907 | value |
| 3908 | ? (() => { |
| 3909 | if (value.startsWith("codex:")) { |
| 3910 | props.onProviderModelChange( |
| 3911 | "codex", |
| 3912 | resolveModelSlugForProvider("codex", value.slice("codex:".length)), |
| 3913 | ); |
| 3914 | return; |
| 3915 | } |
| 3916 | if (value.startsWith("claudeCode:")) { |
| 3917 | props.onProviderModelChange( |
| 3918 | "claudeCode", |
| 3919 | resolveModelSlugForProvider("claudeCode", value.slice("claudeCode:".length)), |
| 3920 | ); |
| 3921 | } |
| 3922 | })() |
| 3923 | : undefined |
| 3907 | 3924 | } |
| 3908 | 3925 | > |
| 3909 | 3926 | <SelectTrigger size="sm" variant="ghost"> |
| 3910 | 3927 | <SelectValue /> |
| 3911 | 3928 | </SelectTrigger> |
| 3912 | 3929 | <SelectPopup alignItemWithTrigger={false}> |
| 3913 | | {PROVIDER_OPTIONS.filter((option) => option.available).map(({ value, label }) => ( |
| 3914 | | <SelectItem key={value} value={value}> |
| 3915 | | {label} |
| 3916 | | </SelectItem> |
| 3930 | {PROVIDER_MODEL_GROUPS.map(({ provider, label, options }) => ( |
| 3931 | <SelectGroup key={provider}> |
| 3932 | <SelectGroupLabel>{label}</SelectGroupLabel> |
| 3933 | {options.map(({ slug, name }) => ( |
| 3934 | <SelectItem key={`${provider}:${slug}`} value={`${provider}:${slug}`}> |
| 3935 | {name} |
| 3936 | </SelectItem> |
| 3937 | ))} |
| 3938 | </SelectGroup> |
| 3917 | 3939 | ))} |
| 3918 | 3940 | </SelectPopup> |
| 3919 | 3941 | </Select> |