Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 28 additions & 42 deletions packages/app/src/providers/models/select-dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,47 +43,9 @@
}

.model-dialog {
[data-slot="model-dialog-search"] {
flex-shrink: 0;
padding: 6px;
}

[data-slot="model-dialog-search-box"] {
display: flex;
align-items: center;
gap: 8px;
height: 32px;
padding-inline: 12px 4px;
border-radius: 6px;
background: color-mix(in srgb, var(--v2-background-bg-layer-02) 60%, transparent);
color: var(--v2-icon-icon-muted);

&:hover,
&:focus-within {
background: var(--v2-background-bg-layer-02);
}

input {
flex: 1;
min-width: 0;
height: 100%;
border: 0;
outline: 0;
background: transparent;
color: var(--v2-text-text-base);
font-size: 14px;
font-weight: 440;
letter-spacing: -0.04px;
appearance: none;
}

input::placeholder {
color: var(--v2-text-text-faint);
}

input::-webkit-search-cancel-button {
display: none;
}
/* Arrow keys scroll the active row clear of the sticky group title. */
.scroll-view__viewport {
scroll-padding-top: 28px;
}

[data-slot="model-dialog-results"] {
Expand All @@ -100,18 +62,38 @@
}

[data-slot="model-dialog-group-title"] {
position: sticky;
top: 0;
z-index: 1;
display: flex;
flex-shrink: 0;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 12px;
margin-inline: -6px;
padding: 0 18px;
background: var(--v2-background-bg-base);
container-type: scroll-state;
color: var(--v2-text-text-faint);
font-size: 13px;
font-weight: 530;
line-height: var(--line-height-compact);
letter-spacing: -0.25px;
user-select: none;

&::after {
position: absolute;
inset: auto 0 0;
border-bottom: 0.5px solid var(--v2-border-border-base);
content: "";
opacity: 0;
}

@container scroll-state(stuck: top) {
&::after {
opacity: 1;
}
}
}

[data-slot="model-dialog-row"] {
Expand Down Expand Up @@ -141,6 +123,10 @@

[data-slot="model-dialog-variant"] {
color: var(--v2-text-text-muted);

&[data-default] {
color: var(--v2-text-text-faint);
}
}

[data-slot="model-dialog-hints"] {
Expand Down
126 changes: 63 additions & 63 deletions packages/app/src/providers/models/select-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Button } from "@opencode/ui/button"
import { Badge } from "@opencode/ui/badge"
import { Dialog, DialogBody, DialogTitle } from "@opencode/ui/dialog"
import { Icon } from "@opencode/ui/icon"
import { IconButton } from "@opencode/ui/icon-button"
import { Keybind } from "@opencode/ui/keybind"
import { ScrollView } from "@opencode/ui/scroll-view"
import { Tooltip } from "@opencode/ui/tooltip"
Expand All @@ -45,6 +46,7 @@ import {
import { consoleProviderName } from "@/providers/catalog/console"
import "@/settings/settings.css"
import "@/shell/commands/dialog.css"
import "@/shell/palette-menu.css"
import "./select-dialog.css"

const MobilePanelDrawer = lazy(async () => {
Expand Down Expand Up @@ -638,7 +640,6 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState
const [staged, setStaged] = createStore<Record<string, string | null>>({})

let container: HTMLElement | undefined
let input: HTMLInputElement | undefined
let flyoutRef: HTMLDivElement | undefined
let timer: ReturnType<typeof setTimeout> | undefined
onCleanup(() => clearTimeout(timer))
Expand Down Expand Up @@ -849,16 +850,16 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState
<Show when={row.item.latest}>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
</Show>
<Show when={variantsOf(row.item).length > 0 && variant(row.item)}>
{(name) => (
<span
data-slot="model-dialog-variant"
class="w-[72px] shrink-0 truncate text-end capitalize"
classList={{ invisible: !selected() && !active() }}
>
{name()}
</span>
)}
{/* The slot keeps its width while hidden, so hovering a row never shifts it. The selected row shows only an explicit variant. */}
<Show when={variantsOf(row.item).length > 0}>
<span
data-slot="model-dialog-variant"
data-default={variant(row.item) ? undefined : ""}
class="w-[72px] shrink-0 truncate text-end capitalize"
classList={{ invisible: !active() && !(selected() && variant(row.item)) }}
>
{variant(row.item) ?? language.t("common.default")}
</span>
</Show>
<span class="flex size-4 shrink-0 items-center justify-center">
<Show when={selected()}>
Expand All @@ -875,59 +876,58 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</div>
<DialogBody class="command-palette-body">
<div data-slot="model-dialog-search">
<div data-slot="model-dialog-search-box" onClick={() => input?.focus()}>
<Icon name="magnifying-glass" size="small" class="shrink-0" />
<input
ref={(element) => (input = element)}
type="search"
value={store.search}
autofocus
placeholder={language.t("dialog.model.search.placeholder")}
aria-label={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
onInput={(event) => setSearch(event.currentTarget.value)}
onKeyDown={onKeyDown}
/>
<Show when={store.search.trim()}>
<button
type="button"
class="-me-1 flex size-5 shrink-0 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-faint transition-colors hover:text-v2-icon-icon-base [&>*]:block"
onPointerDown={(event) => event.preventDefault()}
onClick={() => setSearch("")}
aria-label={language.t("common.clear")}
>
<Icon name="circle-xmark" />
</button>
</Show>
<div class="flex shrink-0 items-center gap-0">
<Tooltip placement="top" gutter={4} value={language.t("command.provider.connect")} ignoreSafeArea={false}>
<button
type="button"
class="flex size-7 shrink-0 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-muted transition-colors hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-icon-icon-base focus-visible:outline-none"
aria-label={language.t("command.provider.connect")}
onPointerDown={(event) => event.preventDefault()}
onClick={connect}
>
<Icon name="plus" size="small" />
</button>
</Tooltip>
<Tooltip placement="top" gutter={4} value={language.t("dialog.model.manage")} ignoreSafeArea={false}>
<button
type="button"
class="flex size-7 shrink-0 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-muted transition-colors hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-icon-icon-base focus-visible:outline-none"
aria-label={language.t("dialog.model.manage")}
onPointerDown={(event) => event.preventDefault()}
onClick={manage}
<div class="palette-menu-search">
<TextInput
type="search"
appearance="large"
value={store.search}
autofocus
placeholder={language.t("dialog.model.search.placeholder")}
aria-label={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
leadingIcon={<Icon name="magnifying-glass" />}
onInput={(event) => setSearch(event.currentTarget.value)}
onKeyDown={onKeyDown}
trailing={
<>
<Show when={store.search.trim()}>
<IconButton
variant="ghost-muted"
icon={<Icon name="circle-xmark" />}
aria-label={language.t("common.clear")}
onPointerDown={(event: PointerEvent) => event.preventDefault()}
onClick={() => setSearch("")}
/>
</Show>
<Tooltip
placement="top"
gutter={4}
value={language.t("command.provider.connect")}
ignoreSafeArea={false}
>
<Icon name="outline-sliders" size="small" />
</button>
</Tooltip>
</div>
</div>
<IconButton
variant="ghost-muted"
icon={<Icon name="plus" />}
aria-label={language.t("command.provider.connect")}
onPointerDown={(event: PointerEvent) => event.preventDefault()}
onClick={connect}
/>
</Tooltip>
<Tooltip placement="top" gutter={4} value={language.t("dialog.model.manage")} ignoreSafeArea={false}>
<IconButton
variant="ghost-muted"
icon={<Icon name="outline-sliders" />}
aria-label={language.t("dialog.model.manage")}
onPointerDown={(event: PointerEvent) => event.preventDefault()}
onClick={manage}
/>
</Tooltip>
</>
}
/>
</div>
<ScrollView
class="command-palette-scroll"
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/shell/palette-menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.palette-menu-search [data-component="text-input-v2"] {
width: 100%;
height: 36px;
padding-inline-end: 12px;
border-radius: 6px;
outline: 0;
background: color-mix(in srgb, var(--v2-background-bg-layer-02) 60%, transparent);
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/forms/text-input/text-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@
margin-inline-end: -8px;
}

/* Pulls the buttons' padding out so their glyphs sit at the same inset as the leading icon. */
[data-component="text-input-v2"] [data-slot="text-input-v2-trailing"] {
display: flex;
flex: none;
align-items: center;
gap: 4px;
margin-inline-end: -4px;
}

[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"] {
color: var(--v2-state-fg-danger);
caret-color: var(--v2-state-fg-danger);
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/forms/text-input/text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "./text-input.css"
export interface TextInputProps extends Omit<ComponentProps<"input">, "type"> {
/** Icon or adornment shown before the field value. */
leadingIcon?: JSX.Element
/** Actions after the field value, such as icon buttons. */
trailing?: JSX.Element
/** Show the trailing copy action. */
showCopyButton?: boolean
/** Show the trailing clear action. */
Expand Down Expand Up @@ -34,6 +36,7 @@ export function TextInput(props: TextInputProps) {
"class",
"classList",
"leadingIcon",
"trailing",
"showCopyButton",
"showClearButton",
"clearIcon",
Expand Down Expand Up @@ -100,6 +103,9 @@ export function TextInput(props: TextInputProps) {
<Icon name={local.showClearButton ? (local.clearIcon ?? "xmark-small") : "copy"} />
</button>
</Show>
<Show when={local.trailing}>
<div data-slot="text-input-v2-trailing">{local.trailing}</div>
</Show>
</div>
)
}
Loading