feat(models): add refresh button to model selector bar for 9router

This commit is contained in:
Zac Gaetano 2026-06-02 12:28:52 -04:00
parent e7514c08cc
commit df6345c30a

View file

@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { ChevronDown, Globe } from 'lucide-react'; import { ChevronDown, Globe, RefreshCw } from 'lucide-react';
import type { LLMProvider } from '../../../../types/app'; import type { LLMProvider } from '../../../../types/app';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
@ -19,6 +19,7 @@ interface ModelSelectorBarProps {
codexModelOptions: ModelOption[]; codexModelOptions: ModelOption[];
geminiModelOptions: ModelOption[]; geminiModelOptions: ModelOption[];
cursorModelOptions: ModelOption[]; cursorModelOptions: ModelOption[];
onRefreshModels?: () => Promise<void>;
} }
function useCurrentModel( function useCurrentModel(
@ -57,9 +58,11 @@ export default function ModelSelectorBar({
codexModelOptions, codexModelOptions,
geminiModelOptions, geminiModelOptions,
cursorModelOptions, cursorModelOptions,
onRefreshModels,
}: ModelSelectorBarProps) { }: ModelSelectorBarProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [isRefreshing, setIsRefreshing] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null); const searchRef = useRef<HTMLInputElement>(null);
@ -101,6 +104,16 @@ export default function ModelSelectorBar({
setSearch(''); setSearch('');
}; };
const handleRefresh = async () => {
if (!onRefreshModels || isRefreshing) return;
setIsRefreshing(true);
try {
await onRefreshModels();
} finally {
setIsRefreshing(false);
}
};
return ( return (
<div className="relative z-40 flex-shrink-0 flex items-center justify-between px-4 py-1.5 border-b border-border/40 bg-muted/20 backdrop-blur-sm"> <div className="relative z-40 flex-shrink-0 flex items-center justify-between px-4 py-1.5 border-b border-border/40 bg-muted/20 backdrop-blur-sm">
{/* Left: provider label */} {/* Left: provider label */}
@ -109,59 +122,73 @@ export default function ModelSelectorBar({
<span className="capitalize">{provider}</span> <span className="capitalize">{provider}</span>
</div> </div>
{/* Right: model picker */} {/* Right: model picker + refresh */}
<div className="relative" ref={dropdownRef}> <div className="flex items-center gap-1.5">
<button {provider === 'claude' && onRefreshModels && (
type="button" <button
onClick={() => setOpen((o) => !o)} type="button"
className="flex items-center gap-1.5 rounded-md border border-border/50 bg-background/60 px-2.5 py-1 text-xs text-foreground hover:bg-accent hover:border-border transition-all" onClick={handleRefresh}
> disabled={isRefreshing}
<Globe className="h-3 w-3 text-muted-foreground" /> title="Refresh model list from 9router"
<span className="max-w-[180px] truncate">{label}</span> className="rounded-md border border-border/50 bg-background/60 p-1 text-muted-foreground hover:bg-accent hover:text-foreground hover:border-border transition-all disabled:opacity-50"
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform duration-150 ${open ? 'rotate-180' : ''}`} /> >
</button> <RefreshCw className={`h-3 w-3 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
{open && (
<div className="absolute right-0 top-full mt-1.5 z-[100] w-64 rounded-xl border border-border/60 bg-card shadow-2xl overflow-hidden">
<div className="p-2 border-b border-border/40">
<input
ref={searchRef}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
placeholder="Search models..."
className="w-full rounded-lg bg-muted/50 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 outline-none border border-border/40 focus:border-border"
/>
</div>
<div className="max-h-64 overflow-y-auto py-1">
{filtered.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">No models found</p>
) : (
filtered.map((opt) => (
<button
key={opt.value}
type="button"
onMouseDown={(e) => {
// Fire on mousedown so selection wins before any blur/outside-click handler
e.preventDefault();
e.stopPropagation();
handleSelect(opt.value);
}}
className={`w-full text-left px-3 py-1.5 text-xs transition-colors hover:bg-accent/60 cursor-pointer ${
opt.value === value ? 'text-primary font-medium' : 'text-foreground'
}`}
>
<span className="truncate block">{opt.label}</span>
{opt.value === value && (
<span className="text-[10px] text-muted-foreground block">{opt.value}</span>
)}
</button>
))
)}
</div>
</div>
)} )}
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1.5 rounded-md border border-border/50 bg-background/60 px-2.5 py-1 text-xs text-foreground hover:bg-accent hover:border-border transition-all"
>
<Globe className="h-3 w-3 text-muted-foreground" />
<span className="max-w-[180px] truncate">{label}</span>
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform duration-150 ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="absolute right-0 top-full mt-1.5 z-[100] w-64 rounded-xl border border-border/60 bg-card shadow-2xl overflow-hidden">
<div className="p-2 border-b border-border/40">
<input
ref={searchRef}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
placeholder="Search models..."
className="w-full rounded-lg bg-muted/50 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 outline-none border border-border/40 focus:border-border"
/>
</div>
<div className="max-h-64 overflow-y-auto py-1">
{filtered.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">No models found</p>
) : (
filtered.map((opt) => (
<button
key={opt.value}
type="button"
onMouseDown={(e) => {
// Fire on mousedown so selection wins before any blur/outside-click handler
e.preventDefault();
e.stopPropagation();
handleSelect(opt.value);
}}
className={`w-full text-left px-3 py-1.5 text-xs transition-colors hover:bg-accent/60 cursor-pointer ${
opt.value === value ? 'text-primary font-medium' : 'text-foreground'
}`}
>
<span className="truncate block">{opt.label}</span>
{opt.value === value && (
<span className="text-[10px] text-muted-foreground block">{opt.value}</span>
)}
</button>
))
)}
</div>
</div>
)}
</div>
</div> </div>
</div> </div>
); );