authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 10:07:10-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log5707a6ee1dc470198ecb01a0b6181c5082c7a81f
tree9e3099ff0427533ef4cf79b559fffa4f5e8c9abf
parent291411a45e29f49c5e47dc15cb6b199f08634b32

Use grouped provider/model select in chat composer

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 70 insertions(+), 48 deletions(-)

apps/web/src/components/ChatView.tsx+70-48
......@@ -107,7 +107,15 @@ import {
107107 CheckIcon,
108108} from "lucide-react";
109109import { Button } from "./ui/button";
110import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
110import {
111 Select,
112 SelectGroup,
113 SelectGroupLabel,
114 SelectItem,
115 SelectPopup,
116 SelectTrigger,
117 SelectValue,
118} from "./ui/select";
111119import { Separator } from "./ui/separator";
112120import { Group, GroupSeparator } from "./ui/group";
113121import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu";
......@@ -2265,13 +2273,22 @@ export default function ChatView({ threadId }: ChatViewProps) {
22652273 },
22662274 [activeThread, scheduleComposerFocus, selectedProvider],
22672275 );
2268 const onProviderSelect = useCallback(
2269 (provider: ProviderKind) => {
2276 const onProviderModelSelect = useCallback(
2277 (provider: ProviderKind, model: ModelSlug) => {
2278 const api = readNativeApi();
22702279 if (!activeThread) return;
22712280 setSelectedProviderByThread((existing) => ({
22722281 ...existing,
22732282 [activeThread.id]: provider,
22742283 }));
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 }
22752292 scheduleComposerFocus();
22762293 },
22772294 [activeThread, scheduleComposerFocus],
......@@ -2712,17 +2729,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
27122729 {/* Bottom toolbar */}
27132730 <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">
27142731 <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}
27232735 model={selectedModel}
2724 options={modelOptions}
2725 onModelChange={onModelSelect}
2736 onProviderModelChange={onProviderModelSelect}
27262737 />
27272738
27282739 {/* Divider */}
......@@ -3866,54 +3877,65 @@ const MessagesTimeline = memo(function MessagesTimeline({
38663877 );
38673878});
38683879
3869const ModelPicker = memo(function ModelPicker(props: {
3880const 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
3890const ProviderModelPicker = memo(function ProviderModelPicker(props: {
3891 provider: ProviderKind;
38703892 model: ModelSlug;
3871 options: ReadonlyArray<{ readonly slug: ModelSlug; readonly name: string }>;
3872 onModelChange: (model: ModelSlug) => void;
3893 onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void;
38733894}) {
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 })),
38913900 );
3892});
38933901
3894const ProviderPicker = memo(function ProviderPicker(props: {
3895 provider: ProviderKind;
3896 onProviderChange: (provider: ProviderKind) => void;
3897}) {
38983902 return (
38993903 <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}`}
39053906 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
39073924 }
39083925 >
39093926 <SelectTrigger size="sm" variant="ghost">
39103927 <SelectValue />
39113928 </SelectTrigger>
39123929 <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>
39173939 ))}
39183940 </SelectPopup>
39193941 </Select>