summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/components/dialog-select-model.tsx27
-rw-r--r--packages/ui/src/components/list.css38
-rw-r--r--packages/ui/src/components/list.tsx47
3 files changed, 86 insertions, 26 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index cdb299c79..8288a8255 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/packages/app/src/components/dialog-select-model.tsx
@@ -4,6 +4,7 @@ import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
import { Button } from "@opencode-ai/ui/button"
+import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tag } from "@opencode-ai/ui/tag"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
@@ -15,6 +16,7 @@ const ModelList: Component<{
provider?: string
class?: string
onSelect: () => void
+ action?: JSX.Element
}> = (props) => {
const local = useLocal()
const language = useLanguage()
@@ -29,7 +31,7 @@ const ModelList: Component<{
return (
<List
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
- search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
+ search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
@@ -71,6 +73,12 @@ export const ModelSelectorPopover: Component<{
children: JSX.Element
}> = (props) => {
const [open, setOpen] = createSignal(false)
+ const dialog = useDialog()
+
+ const handleManage = () => {
+ setOpen(false)
+ dialog.show(() => <DialogManageModels />)
+ }
const language = useLanguage()
return (
@@ -79,7 +87,22 @@ export const ModelSelectorPopover: Component<{
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
- <ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
+ <ModelList
+ provider={props.provider}
+ onSelect={() => setOpen(false)}
+ class="p-1"
+ action={
+ <IconButton
+ icon="sliders"
+ variant="ghost"
+ iconSize="normal"
+ class="size-6"
+ aria-label="Manage models"
+ title="Manage models"
+ onClick={handleManage}
+ />
+ }
+ />
</Kobalte.Content>
</Kobalte.Portal>
</Kobalte>
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index ee9be422c..95641bb20 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -23,14 +23,46 @@
overflow: hidden;
padding: 0 12px;
- [data-slot="list-search"] {
+ [data-slot="list-search-wrapper"] {
display: flex;
flex-shrink: 0;
- padding: 8px;
align-items: center;
- gap: 12px;
+ gap: 8px;
align-self: stretch;
margin-bottom: 4px;
+ padding-right: 4px;
+
+ > [data-component="icon-button"] {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ background-color: transparent;
+ opacity: 0.5;
+ transition: opacity 0.15s ease;
+
+ &:hover:not(:disabled),
+ &:focus:not(:disabled),
+ &:active:not(:disabled) {
+ background-color: transparent;
+ opacity: 0.7;
+ }
+
+ &:hover:not(:disabled) [data-slot="icon-svg"] {
+ color: var(--icon-hover);
+ }
+
+ &:active:not(:disabled) [data-slot="icon-svg"] {
+ color: var(--icon-active);
+ }
+ }
+ }
+
+ [data-slot="list-search"] {
+ display: flex;
+ flex: 1;
+ padding: 8px;
+ align-items: center;
+ gap: 12px;
border-radius: var(--radius-md);
background: var(--surface-base);
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 066363502..fc9fa5405 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -11,6 +11,7 @@ export interface ListSearchProps {
autofocus?: boolean
hideIcon?: boolean
class?: string
+ action?: JSX.Element
}
export interface ListProps<T> extends FilteredListProps<T> {
@@ -60,6 +61,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
const { filter, grouped, flat, active, setActive, onKeyDown, onInput } = useFilteredList<T>(props)
const searchProps = () => (typeof props.search === "object" ? props.search : {})
+ const searchAction = () => searchProps().action
const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
@@ -198,29 +200,32 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
return (
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
<Show when={!!props.search}>
- <div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}>
- <div data-slot="list-search-container">
- <Show when={!searchProps().hideIcon}>
- <Icon name="magnifying-glass" />
+ <div data-slot="list-search-wrapper">
+ <div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}>
+ <div data-slot="list-search-container">
+ <Show when={!searchProps().hideIcon}>
+ <Icon name="magnifying-glass" />
+ </Show>
+ <TextField
+ autofocus={searchProps().autofocus}
+ variant="ghost"
+ data-slot="list-search-input"
+ type="text"
+ value={internalFilter()}
+ onChange={setInternalFilter}
+ onKeyDown={handleKey}
+ placeholder={searchProps().placeholder}
+ spellcheck={false}
+ autocorrect="off"
+ autocomplete="off"
+ autocapitalize="off"
+ />
+ </div>
+ <Show when={internalFilter()}>
+ <IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
</Show>
- <TextField
- autofocus={searchProps().autofocus}
- variant="ghost"
- data-slot="list-search-input"
- type="text"
- value={internalFilter()}
- onChange={setInternalFilter}
- onKeyDown={handleKey}
- placeholder={searchProps().placeholder}
- spellcheck={false}
- autocorrect="off"
- autocomplete="off"
- autocapitalize="off"
- />
</div>
- <Show when={internalFilter()}>
- <IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
- </Show>
+ {searchAction()}
</div>
</Show>
<div ref={setScrollRef} data-slot="list-scroll">