| ... | @@ -107,7 +107,15 @@ import { | ... | @@ -107,7 +107,15 @@ import { |
| 107 | CheckIcon, | 107 | CheckIcon, |
| 108 | } from "lucide-react"; | 108 | } from "lucide-react"; |
| 109 | import { Button } from "./ui/button"; | 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 | import { Separator } from "./ui/separator"; | 119 | import { Separator } from "./ui/separator"; |
| 112 | import { Group, GroupSeparator } from "./ui/group"; | 120 | import { Group, GroupSeparator } from "./ui/group"; |
| 113 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu"; | 121 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu"; |
| ... | @@ -2265,13 +2273,22 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2265,13 +2273,22 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2265 | }, | 2273 | }, |
| 2266 | [activeThread, scheduleComposerFocus, selectedProvider], | 2274 | [activeThread, scheduleComposerFocus, selectedProvider], |
| 2267 | ); | 2275 | ); |
| 2268 | const onProviderSelect = useCallback( | 2276 | const onProviderModelSelect = useCallback( |
| 2269 | (provider: ProviderKind) => { | 2277 | (provider: ProviderKind, model: ModelSlug) => { |
| | 2278 | const api = readNativeApi(); |
| 2270 | if (!activeThread) return; | 2279 | if (!activeThread) return; |
| 2271 | setSelectedProviderByThread((existing) => ({ | 2280 | setSelectedProviderByThread((existing) => ({ |
| 2272 | ...existing, | 2281 | ...existing, |
| 2273 | [activeThread.id]: provider, | 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 | scheduleComposerFocus(); | 2292 | scheduleComposerFocus(); |
| 2276 | }, | 2293 | }, |
| 2277 | [activeThread, scheduleComposerFocus], | 2294 | [activeThread, scheduleComposerFocus], |
| ... | @@ -2712,17 +2729,11 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2712,17 +2729,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2712 | {/* Bottom toolbar */} | 2729 | {/* Bottom toolbar */} |
| 2713 | <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"> | 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 | <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"> | 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 */} | 2732 | {/* Provider/model picker */} |
| 2716 | <ProviderPicker provider={selectedProvider} onProviderChange={onProviderSelect} /> | 2733 | <ProviderModelPicker |
| 2717 | | 2734 | provider={selectedProvider} |
| 2718 | {/* Divider */} | | |
| 2719 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> | | |
| 2720 | | | |
| 2721 | {/* Model picker */} | | |
| 2722 | <ModelPicker | | |
| 2723 | model={selectedModel} | 2735 | model={selectedModel} |
| 2724 | options={modelOptions} | 2736 | onProviderModelChange={onProviderModelSelect} |
| 2725 | onModelChange={onModelSelect} | | |
| 2726 | /> | 2737 | /> |
| 2727 | | 2738 | |
| 2728 | {/* Divider */} | 2739 | {/* Divider */} |
| ... | @@ -3866,54 +3877,65 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -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 | model: ModelSlug; | 3892 | model: ModelSlug; |
| 3871 | options: ReadonlyArray<{ readonly slug: ModelSlug; readonly name: string }>; | 3893 | onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; |
| 3872 | onModelChange: (model: ModelSlug) => void; | | |
| 3873 | }) { | 3894 | }) { |
| 3874 | return ( | 3895 | const items = PROVIDER_MODEL_GROUPS.flatMap(({ provider, label, options }) => |
| 3875 | <Select | 3896 | options.map(({ slug, name }) => ({ |
| 3876 | items={props.options.map((option) => ({ label: option.name, value: option.slug }))} | 3897 | label: `${label} · ${name}`, |
| 3877 | value={props.model} | 3898 | value: `${provider}:${slug}`, |
| 3878 | onValueChange={(value) => (value ? props.onModelChange(value) : undefined)} | 3899 | })), |
| 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> | | |
| 3891 | ); | 3900 | ); |
| 3892 | }); | | |
| 3893 | | 3901 | |
| 3894 | const ProviderPicker = memo(function ProviderPicker(props: { | | |
| 3895 | provider: ProviderKind; | | |
| 3896 | onProviderChange: (provider: ProviderKind) => void; | | |
| 3897 | }) { | | |
| 3898 | return ( | 3902 | return ( |
| 3899 | <Select | 3903 | <Select |
| 3900 | items={PROVIDER_OPTIONS.filter((option) => option.available).map((option) => ({ | 3904 | items={items} |
| 3901 | label: option.label, | 3905 | value={`${props.provider}:${props.model}`} |
| 3902 | value: option.value, | | |
| 3903 | }))} | | |
| 3904 | value={props.provider} | | |
| 3905 | onValueChange={(value) => | 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 | <SelectTrigger size="sm" variant="ghost"> | 3926 | <SelectTrigger size="sm" variant="ghost"> |
| 3910 | <SelectValue /> | 3927 | <SelectValue /> |
| 3911 | </SelectTrigger> | 3928 | </SelectTrigger> |
| 3912 | <SelectPopup alignItemWithTrigger={false}> | 3929 | <SelectPopup alignItemWithTrigger={false}> |
| 3913 | {PROVIDER_OPTIONS.filter((option) => option.available).map(({ value, label }) => ( | 3930 | {PROVIDER_MODEL_GROUPS.map(({ provider, label, options }) => ( |
| 3914 | <SelectItem key={value} value={value}> | 3931 | <SelectGroup key={provider}> |
| 3915 | {label} | 3932 | <SelectGroupLabel>{label}</SelectGroupLabel> |
| 3916 | </SelectItem> | 3933 | {options.map(({ slug, name }) => ( |
| | 3934 | <SelectItem key={`${provider}:${slug}`} value={`${provider}:${slug}`}> |
| | 3935 | {name} |
| | 3936 | </SelectItem> |
| | 3937 | ))} |
| | 3938 | </SelectGroup> |
| 3917 | ))} | 3939 | ))} |
| 3918 | </SelectPopup> | 3940 | </SelectPopup> |
| 3919 | </Select> | 3941 | </Select> |