1- import { useEffect , useState } from "react" ;
1+ import { useEffect , useLayoutEffect , useRef , useState } from "react" ;
2+ import type { CSSProperties } from "react" ;
3+ import { Check , ChevronDown } from "lucide-react" ;
24import { app } from "../lib/bridge" ;
35import { useI18n , useT } from "../lib/i18n" ;
46import { useUpdater } from "../lib/useUpdater" ;
@@ -175,35 +177,23 @@ function ModelsSection({ s, busy, apply, onManageProviders }: SectionProps & { o
175177
176178 < div className = "set-row" >
177179 < label className = "set-label" > { t ( "settings.defaultModel" ) } </ label >
178- < select
179- className = "mem-select set-grow"
180+ < ModelSelect
180181 value = { toRef ( s . defaultModel , s ) }
182+ options = { refs }
181183 disabled = { busy }
182- onChange = { ( e ) => void apply ( ( ) => app . SetDefaultModel ( e . target . value ) ) }
183- >
184- { refs . map ( ( r ) => (
185- < option key = { r } value = { r } >
186- { r }
187- </ option >
188- ) ) }
189- </ select >
184+ onChange = { ( value ) => void apply ( ( ) => app . SetDefaultModel ( value ) ) }
185+ />
190186 </ div >
191187
192188 < div className = "set-row" >
193189 < label className = "set-label" > { t ( "settings.plannerModel" ) } </ label >
194- < select
195- className = "mem-select set-grow"
190+ < ModelSelect
196191 value = { toRef ( s . plannerModel , s ) }
192+ options = { refs }
193+ emptyLabel = { t ( "settings.plannerNone" ) }
197194 disabled = { busy }
198- onChange = { ( e ) => void apply ( ( ) => app . SetPlannerModel ( e . target . value ) ) }
199- >
200- < option value = "" > { t ( "settings.plannerNone" ) } </ option >
201- { refs . map ( ( r ) => (
202- < option key = { r } value = { r } >
203- { r }
204- </ option >
205- ) ) }
206- </ select >
195+ onChange = { ( value ) => void apply ( ( ) => app . SetPlannerModel ( value ) ) }
196+ />
207197 </ div >
208198
209199 < div className = "settings-model-card" >
@@ -233,6 +223,93 @@ function ModelsSection({ s, busy, apply, onManageProviders }: SectionProps & { o
233223 ) ;
234224}
235225
226+ function ModelSelect ( {
227+ value,
228+ options,
229+ emptyLabel,
230+ disabled,
231+ onChange,
232+ } : {
233+ value : string ;
234+ options : string [ ] ;
235+ emptyLabel ?: string ;
236+ disabled : boolean ;
237+ onChange : ( value : string ) => void ;
238+ } ) {
239+ const [ open , setOpen ] = useState ( false ) ;
240+ const [ menuStyle , setMenuStyle ] = useState < CSSProperties > ( { } ) ;
241+ const triggerRef = useRef < HTMLButtonElement | null > ( null ) ;
242+ const selectedLabel = value || emptyLabel || "" ;
243+ const items = emptyLabel ? [ "" , ...options ] : options ;
244+
245+ useLayoutEffect ( ( ) => {
246+ if ( ! open ) return ;
247+ const place = ( ) => {
248+ const rect = triggerRef . current ?. getBoundingClientRect ( ) ;
249+ if ( ! rect ) return ;
250+ const margin = 8 ;
251+ const width = Math . min ( Math . max ( rect . width , 220 ) , window . innerWidth - margin * 2 ) ;
252+ const spaceBelow = window . innerHeight - rect . bottom - margin ;
253+ const spaceAbove = rect . top - margin ;
254+ const openUp = spaceBelow < 180 && spaceAbove > spaceBelow ;
255+ setMenuStyle ( {
256+ left : Math . min ( Math . max ( margin , rect . left ) , window . innerWidth - width - margin ) ,
257+ top : openUp ? undefined : rect . bottom + 4 ,
258+ bottom : openUp ? window . innerHeight - rect . top + 4 : undefined ,
259+ width,
260+ maxHeight : Math . max ( 140 , Math . min ( 280 , openUp ? spaceAbove : spaceBelow ) ) ,
261+ } ) ;
262+ } ;
263+ place ( ) ;
264+ window . addEventListener ( "resize" , place ) ;
265+ window . addEventListener ( "scroll" , place , true ) ;
266+ return ( ) => {
267+ window . removeEventListener ( "resize" , place ) ;
268+ window . removeEventListener ( "scroll" , place , true ) ;
269+ } ;
270+ } , [ open ] ) ;
271+
272+ const choose = ( next : string ) => {
273+ setOpen ( false ) ;
274+ if ( next !== value ) onChange ( next ) ;
275+ } ;
276+
277+ return (
278+ < div className = "model-select set-grow" >
279+ < button
280+ ref = { triggerRef }
281+ className = "model-select__trigger"
282+ type = "button"
283+ disabled = { disabled }
284+ onClick = { ( ) => setOpen ( ( v ) => ! v ) }
285+ >
286+ < span > { selectedLabel } </ span >
287+ < ChevronDown size = { 14 } />
288+ </ button >
289+ { open && (
290+ < >
291+ < div className = "model-select__backdrop" onClick = { ( ) => setOpen ( false ) } />
292+ < div className = "model-select__menu" style = { menuStyle } role = "listbox" >
293+ { items . map ( ( item ) => (
294+ < button
295+ key = { item || "__empty__" }
296+ type = "button"
297+ role = "option"
298+ aria-selected = { item === value }
299+ className = { `model-select__item${ item === value ? " model-select__item--selected" : "" } ` }
300+ onClick = { ( ) => choose ( item ) }
301+ >
302+ < span > { item || emptyLabel } </ span >
303+ { item === value && < Check size = { 13 } /> }
304+ </ button >
305+ ) ) }
306+ </ div >
307+ </ >
308+ ) }
309+ </ div >
310+ ) ;
311+ }
312+
236313function ProvidersSection ( { s, busy, apply } : SectionProps ) {
237314 const t = useT ( ) ;
238315 // The provider backing the default model — can't be deleted (would dangle the
@@ -386,7 +463,19 @@ function ProviderEditor({
386463function KeyField ( { apiKeyEnv, busy, onSet } : { apiKeyEnv : string ; busy : boolean ; onSet : ( v : string ) => Promise < void > } ) {
387464 const t = useT ( ) ;
388465 const [ val , setVal ] = useState ( "" ) ;
466+ const [ saving , setSaving ] = useState ( false ) ;
389467 if ( ! apiKeyEnv ) return null ;
468+ const save = async ( ) => {
469+ const next = val . trim ( ) ;
470+ if ( ! next || saving ) return ;
471+ setSaving ( true ) ;
472+ try {
473+ await onSet ( next ) ;
474+ setVal ( "" ) ;
475+ } finally {
476+ setSaving ( false ) ;
477+ }
478+ } ;
390479 return (
391480 < div className = "set-key" >
392481 < input
@@ -395,14 +484,12 @@ function KeyField({ apiKeyEnv, busy, onSet }: { apiKeyEnv: string; busy: boolean
395484 placeholder = { t ( "settings.setKey" , { env : apiKeyEnv } ) }
396485 value = { val }
397486 onChange = { ( e ) => setVal ( e . target . value ) }
487+ disabled = { busy || saving }
398488 />
399489 < button
400490 className = "btn btn--small"
401- disabled = { busy || ! val . trim ( ) }
402- onClick = { ( ) => {
403- void onSet ( val . trim ( ) ) ;
404- setVal ( "" ) ;
405- } }
491+ disabled = { busy || saving || ! val . trim ( ) }
492+ onClick = { ( ) => void save ( ) }
406493 >
407494 { t ( "settings.saveKey" ) }
408495 </ button >
0 commit comments