summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-24 13:33:45 -0600
committerAdam <[email protected]>2026-01-24 15:01:05 -0600
commit7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0 (patch)
tree266685dc54106783e9d450d208bb8f7711111328
parentb951187a6e4e78d577f0aac2a9533b381fa719a8 (diff)
downloadopencode-7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0.tar.gz
opencode-7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0.zip
fix(app): model selector ux
-rw-r--r--packages/app/src/components/dialog-select-model.tsx92
1 files changed, 85 insertions, 7 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index 5569d7780..4d2646e8f 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/packages/app/src/components/dialog-select-model.tsx
@@ -1,5 +1,6 @@
import { Popover as Kobalte } from "@kobalte/core/popover"
-import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
+import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js"
+import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
@@ -92,26 +93,103 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
- const [open, setOpen] = createSignal(false)
+ const [store, setStore] = createStore<{
+ open: boolean
+ dismiss: "escape" | "outside" | null
+ trigger?: HTMLElement
+ content?: HTMLElement
+ }>({
+ open: false,
+ dismiss: null,
+ trigger: undefined,
+ content: undefined,
+ })
const dialog = useDialog()
const handleManage = () => {
- setOpen(false)
+ setStore("open", false)
dialog.show(() => <DialogManageModels />)
}
const language = useLanguage()
+ createEffect(() => {
+ if (!store.open) return
+
+ const inside = (node: Node | null | undefined) => {
+ if (!node) return false
+ const el = store.content
+ if (el && el.contains(node)) return true
+ const anchor = store.trigger
+ if (anchor && anchor.contains(node)) return true
+ return false
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== "Escape") return
+ setStore("dismiss", "escape")
+ setStore("open", false)
+ event.preventDefault()
+ event.stopPropagation()
+ }
+
+ const onPointerDown = (event: PointerEvent) => {
+ const target = event.target
+ if (!(target instanceof Node)) return
+ if (inside(target)) return
+ setStore("dismiss", "outside")
+ setStore("open", false)
+ }
+
+ const onFocusIn = (event: FocusEvent) => {
+ if (!store.content) return
+ const target = event.target
+ if (!(target instanceof Node)) return
+ if (inside(target)) return
+ setStore("dismiss", "outside")
+ setStore("open", false)
+ }
+
+ window.addEventListener("keydown", onKeyDown, true)
+ window.addEventListener("pointerdown", onPointerDown, true)
+ window.addEventListener("focusin", onFocusIn, true)
+
+ onCleanup(() => {
+ window.removeEventListener("keydown", onKeyDown, true)
+ window.removeEventListener("pointerdown", onPointerDown, true)
+ window.removeEventListener("focusin", onFocusIn, true)
+ })
+ })
+
return (
- <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
- <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
+ <Kobalte
+ open={store.open}
+ onOpenChange={(next) => {
+ if (next) setStore("dismiss", null)
+ setStore("open", next)
+ }}
+ placement="top-start"
+ gutter={8}
+ >
+ <Kobalte.Trigger
+ ref={(el) => setStore("trigger", el)}
+ as={props.triggerAs ?? "div"}
+ {...(props.triggerProps as any)}
+ >
{props.children}
</Kobalte.Trigger>
<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.Content
+ ref={(el) => setStore("content", el)}
+ 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"
+ onCloseAutoFocus={(event) => {
+ if (store.dismiss === "outside") event.preventDefault()
+ setStore("dismiss", null)
+ }}
+ >
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
<ModelList
provider={props.provider}
- onSelect={() => setOpen(false)}
+ onSelect={() => setStore("open", false)}
class="p-1"
action={
<IconButton