@@ -17,6 +17,13 @@ import { Select } from "@opencode-ai/ui/select"
1717import { Tag } from "@opencode-ai/ui/tag"
1818import { getDirectory , getFilename } from "@opencode-ai/util/path"
1919import { useLayout } from "@/context/layout"
20+ import { popularProviders , useProviders } from "@/hooks/use-providers"
21+ import { Dialog } from "@opencode-ai/ui/dialog"
22+ import { List , ListRef } from "@opencode-ai/ui/list"
23+ import { iife } from "@opencode-ai/util/iife"
24+ import { Input } from "@opencode-ai/ui/input"
25+ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
26+ import { IconName } from "@opencode-ai/ui/icons/provider"
2027
2128interface PromptInputProps {
2229 class ?: string
@@ -58,6 +65,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
5865 const local = useLocal ( )
5966 const session = useSession ( )
6067 const layout = useLayout ( )
68+ const providers = useProviders ( )
6169 let editorRef ! : HTMLDivElement
6270
6371 const [ store , setStore ] = createStore < {
@@ -461,60 +469,173 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
461469 < Icon name = "chevron-down" size = "small" />
462470 </ Button >
463471 < Show when = { layout . dialog . opened ( ) === "model" } >
464- < SelectDialog
465- defaultOpen
466- onOpenChange = { ( open ) => {
467- if ( open ) {
468- layout . dialog . open ( "model" )
469- } else {
470- layout . dialog . close ( "model" )
471- }
472- } }
473- title = "Select model"
474- placeholder = "Search models"
475- emptyMessage = "No model results"
476- key = { ( x ) => `${ x . provider . id } :${ x . id } ` }
477- items = { local . model . list ( ) }
478- current = { local . model . current ( ) }
479- filterKeys = { [ "provider.name" , "name" , "id" ] }
480- // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
481- groupBy = { ( x ) => x . provider . name }
482- sortGroupsBy = { ( a , b ) => {
483- const order = [ "opencode" , "anthropic" , "github-copilot" , "openai" , "google" , "openrouter" , "vercel" ]
484- if ( a . category === "Recent" && b . category !== "Recent" ) return - 1
485- if ( b . category === "Recent" && a . category !== "Recent" ) return 1
486- const aProvider = a . items [ 0 ] . provider . id
487- const bProvider = b . items [ 0 ] . provider . id
488- if ( order . includes ( aProvider ) && ! order . includes ( bProvider ) ) return - 1
489- if ( ! order . includes ( aProvider ) && order . includes ( bProvider ) ) return 1
490- return order . indexOf ( aProvider ) - order . indexOf ( bProvider )
491- } }
492- onSelect = { ( x ) =>
493- local . model . set ( x ? { modelID : x . id , providerID : x . provider . id } : undefined , { recent : true } )
494- }
495- actions = {
496- < Button
497- class = "h-7 -my-1 text-14-medium"
498- icon = "plus-small"
499- tabIndex = { - 1 }
500- onClick = { ( ) => layout . dialog . open ( "provider" ) }
472+ < Switch >
473+ < Match when = { providers ( ) . connected ( ) . length > 0 } >
474+ < SelectDialog
475+ defaultOpen
476+ onOpenChange = { ( open ) => {
477+ if ( open ) {
478+ layout . dialog . open ( "model" )
479+ } else {
480+ layout . dialog . close ( "model" )
481+ }
482+ } }
483+ title = "Select model"
484+ placeholder = "Search models"
485+ emptyMessage = "No model results"
486+ key = { ( x ) => `${ x . provider . id } :${ x . id } ` }
487+ items = { local . model . list ( ) }
488+ current = { local . model . current ( ) }
489+ filterKeys = { [ "provider.name" , "name" , "id" ] }
490+ // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
491+ groupBy = { ( x ) => x . provider . name }
492+ sortGroupsBy = { ( a , b ) => {
493+ if ( a . category === "Recent" && b . category !== "Recent" ) return - 1
494+ if ( b . category === "Recent" && a . category !== "Recent" ) return 1
495+ const aProvider = a . items [ 0 ] . provider . id
496+ const bProvider = b . items [ 0 ] . provider . id
497+ if ( popularProviders . includes ( aProvider ) && ! popularProviders . includes ( bProvider ) ) return - 1
498+ if ( ! popularProviders . includes ( aProvider ) && popularProviders . includes ( bProvider ) ) return 1
499+ return popularProviders . indexOf ( aProvider ) - popularProviders . indexOf ( bProvider )
500+ } }
501+ onSelect = { ( x ) =>
502+ local . model . set ( x ? { modelID : x . id , providerID : x . provider . id } : undefined , { recent : true } )
503+ }
504+ actions = {
505+ < Button
506+ class = "h-7 -my-1 text-14-medium"
507+ icon = "plus-small"
508+ tabIndex = { - 1 }
509+ onClick = { ( ) => layout . dialog . open ( "provider" ) }
510+ >
511+ Connect provider
512+ </ Button >
513+ }
501514 >
502- Connect provider
503- </ Button >
504- }
505- >
506- { ( i ) => (
507- < div class = "w-full flex items-center gap-x-2.5" >
508- < span > { i . name } </ span >
509- < Show when = { ! i . cost || i . cost ?. input === 0 } >
510- < Tag > Free</ Tag >
511- </ Show >
512- < Show when = { i . latest } >
513- < Tag > Latest</ Tag >
514- </ Show >
515- </ div >
516- ) }
517- </ SelectDialog >
515+ { ( i ) => (
516+ < div class = "w-full flex items-center gap-x-2.5" >
517+ < span > { i . name } </ span >
518+ < Show when = { ! i . cost || i . cost ?. input === 0 } >
519+ < Tag > Free</ Tag >
520+ </ Show >
521+ < Show when = { i . latest } >
522+ < Tag > Latest</ Tag >
523+ </ Show >
524+ </ div >
525+ ) }
526+ </ SelectDialog >
527+ </ Match >
528+ < Match when = { true } >
529+ { iife ( ( ) => {
530+ let listRef : ListRef | undefined
531+ const handleKey = ( e : KeyboardEvent ) => {
532+ if ( e . key === "Escape" ) return
533+ listRef ?. onKeyDown ( e )
534+ }
535+ return (
536+ < Dialog
537+ modal
538+ defaultOpen
539+ onOpenChange = { ( open ) => {
540+ if ( open ) {
541+ layout . dialog . open ( "model" )
542+ } else {
543+ layout . dialog . close ( "model" )
544+ }
545+ } }
546+ >
547+ < Dialog . Header >
548+ < Dialog . Title > Select model</ Dialog . Title >
549+ < Dialog . CloseButton tabIndex = { - 1 } />
550+ </ Dialog . Header >
551+ < Dialog . Body >
552+ < Input hidden type = "text" class = "opacity-0 size-0" autofocus onKeyDown = { handleKey } />
553+ < div class = "flex flex-col gap-3 px-2.5" >
554+ < div class = "text-14-medium text-text-base px-2.5" > Free models provided by OpenCode</ div >
555+ < List
556+ ref = { ( ref ) => ( listRef = ref ) }
557+ items = { local . model . list ( ) }
558+ current = { local . model . current ( ) }
559+ key = { ( x ) => `${ x . provider . id } :${ x . id } ` }
560+ onSelect = { ( x ) => {
561+ local . model . set ( x ? { modelID : x . id , providerID : x . provider . id } : undefined , {
562+ recent : true ,
563+ } )
564+ layout . dialog . close ( "model" )
565+ } }
566+ >
567+ { ( i ) => (
568+ < div class = "w-full flex items-center gap-x-2.5" >
569+ < span > { i . name } </ span >
570+ < Tag > Free</ Tag >
571+ < Show when = { i . latest } >
572+ < Tag > Latest</ Tag >
573+ </ Show >
574+ </ div >
575+ ) }
576+ </ List >
577+ < div />
578+ < div />
579+ </ div >
580+ < div class = "px-1.5 pb-1.5" >
581+ < div class = "w-full rounded-sm border border-border-weak-base bg-surface-raised-base" >
582+ < div class = "w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-6" >
583+ < div class = "px-2 text-14-medium text-text-base" >
584+ Add more models from popular providers
585+ </ div >
586+ < List
587+ class = "w-full"
588+ key = { ( x ) => x ?. id }
589+ items = { providers ( ) . popular ( ) }
590+ activeIcon = "plus-small"
591+ sortBy = { ( a , b ) => {
592+ if ( popularProviders . includes ( a . id ) && popularProviders . includes ( b . id ) )
593+ return popularProviders . indexOf ( a . id ) - popularProviders . indexOf ( b . id )
594+ return a . name . localeCompare ( b . name )
595+ } }
596+ onSelect = { ( x ) => {
597+ layout . dialog . close ( "model" )
598+ } }
599+ >
600+ { ( i ) => (
601+ < div class = "w-full flex items-center gap-x-4" >
602+ < ProviderIcon
603+ data-slot = "list-item-extra-icon"
604+ id = { i . id as IconName }
605+ // TODO: clean this up after we update icon in models.dev
606+ classList = { {
607+ "text-icon-weak-base" : true ,
608+ "size-4 mx-0.5" : i . id === "opencode" ,
609+ "size-5" : i . id !== "opencode" ,
610+ } }
611+ />
612+ < span > { i . name } </ span >
613+ < Show when = { i . id === "opencode" } >
614+ < Tag > Recommended</ Tag >
615+ </ Show >
616+ < Show when = { i . id === "anthropic" } >
617+ < div class = "text-14-regular text-text-weak" >
618+ Connect with Claude Pro/Max or API key
619+ </ div >
620+ </ Show >
621+ </ div >
622+ ) }
623+ </ List >
624+ < Button variant = "ghost" class = "w-full justify-start" >
625+ < div class = "flex items-center gap-2" >
626+ < Icon name = "plus-small" />
627+ < div class = "text-text-strong" > View all providers</ div >
628+ </ div >
629+ </ Button >
630+ </ div >
631+ </ div >
632+ </ div >
633+ </ Dialog . Body >
634+ </ Dialog >
635+ )
636+ } ) }
637+ </ Match >
638+ </ Switch >
518639 </ Show >
519640 </ div >
520641 < Tooltip
0 commit comments