From 05fc2fbb11ca61f4fec631d75dc89dc0cff7ab23 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 21 Oct 2026 14:35:42 -0700 Subject: [PATCH] fix(app): tidy the model picker header, stick group titles and show variants --- .../src/providers/models/select-dialog.css | 70 ++++------ .../src/providers/models/select-dialog.tsx | 126 +++++++++--------- packages/app/src/shell/palette-menu.css | 1 + .../ui/src/forms/text-input/text-input.css | 9 ++ .../ui/src/forms/text-input/text-input.tsx | 6 + 5 files changed, 107 insertions(+), 105 deletions(-) diff --git a/packages/app/src/providers/models/select-dialog.css b/packages/app/src/providers/models/select-dialog.css index 84720dfe9b9b..5bf9c6ee9ddd 100644 --- a/packages/app/src/providers/models/select-dialog.css +++ b/packages/app/src/providers/models/select-dialog.css @@ -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"] { @@ -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"] { @@ -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"] { diff --git a/packages/app/src/providers/models/select-dialog.tsx b/packages/app/src/providers/models/select-dialog.tsx index 913ef4f23ea6..99de94474375 100644 --- a/packages/app/src/providers/models/select-dialog.tsx +++ b/packages/app/src/providers/models/select-dialog.tsx @@ -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" @@ -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 () => { @@ -638,7 +640,6 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState const [staged, setStaged] = createStore>({}) let container: HTMLElement | undefined - let input: HTMLInputElement | undefined let flyoutRef: HTMLDivElement | undefined let timer: ReturnType | undefined onCleanup(() => clearTimeout(timer)) @@ -849,16 +850,16 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState {language.t("model.tag.latest")} - 0 && variant(row.item)}> - {(name) => ( - - {name()} - - )} + {/* The slot keeps its width while hidden, so hovering a row never shifts it. The selected row shows only an explicit variant. */} + 0}> + + {variant(row.item) ?? language.t("common.default")} + @@ -875,59 +876,58 @@ export function DialogSelectModel(props: { provider?: string; model?: ModelState {language.t("dialog.model.select.title")} -
-
input?.focus()}> - - (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} - /> - - - -
- - - - - - -
-
+ } + aria-label={language.t("command.provider.connect")} + onPointerDown={(event: PointerEvent) => event.preventDefault()} + onClick={connect} + /> + + + } + aria-label={language.t("dialog.model.manage")} + onPointerDown={(event: PointerEvent) => event.preventDefault()} + onClick={manage} + /> + + + } + />
, "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. */ @@ -34,6 +36,7 @@ export function TextInput(props: TextInputProps) { "class", "classList", "leadingIcon", + "trailing", "showCopyButton", "showClearButton", "clearIcon", @@ -100,6 +103,9 @@ export function TextInput(props: TextInputProps) {
+ +
{local.trailing}
+
) }