Adam 1 месяц назад
Родитель
Сommit
340285575b

+ 21 - 1
packages/app/src/components/settings-general.tsx

@@ -77,7 +77,15 @@ export const SettingsGeneral: Component = () => {
               current={themeOptions().find((o) => o.id === theme.themeId())}
               value={(o) => o.id}
               label={(o) => o.name}
-              onSelect={(option) => option && theme.setTheme(option.id)}
+              onSelect={(option) => {
+                if (!option) return
+                theme.setTheme(option.id)
+              }}
+              onHighlight={(option) => {
+                if (!option) return
+                theme.previewTheme(option.id)
+                return () => theme.cancelPreview()
+              }}
               variant="secondary"
               size="small"
             />
@@ -135,6 +143,10 @@ export const SettingsGeneral: Component = () => {
               current={soundOptions.find((o) => o.id === settings.sounds.agent())}
               value={(o) => o.id}
               label={(o) => o.label}
+              onHighlight={(option) => {
+                if (!option) return
+                playSound(option.src)
+              }}
               onSelect={(option) => {
                 if (!option) return
                 settings.sounds.setAgent(option.id)
@@ -151,6 +163,10 @@ export const SettingsGeneral: Component = () => {
               current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
               value={(o) => o.id}
               label={(o) => o.label}
+              onHighlight={(option) => {
+                if (!option) return
+                playSound(option.src)
+              }}
               onSelect={(option) => {
                 if (!option) return
                 settings.sounds.setPermissions(option.id)
@@ -167,6 +183,10 @@ export const SettingsGeneral: Component = () => {
               current={soundOptions.find((o) => o.id === settings.sounds.errors())}
               value={(o) => o.id}
               label={(o) => o.label}
+              onHighlight={(option) => {
+                if (!option) return
+                playSound(option.src)
+              }}
               onSelect={(option) => {
                 if (!option) return
                 settings.sounds.setErrors(option.id)

+ 29 - 9
packages/app/src/components/settings-keybinds.tsx

@@ -124,13 +124,23 @@ export const SettingsKeybinds: Component = () => {
     const out = new Map<string, KeybindMeta>()
     out.set(PALETTE_ID, { title: "Command palette", group: "General" })
 
+    for (const opt of command.catalog) {
+      if (opt.id.startsWith("suggested.")) continue
+      out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
+    }
+
     for (const opt of command.options) {
       if (opt.id.startsWith("suggested.")) continue
+      out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
+    }
 
-      out.set(opt.id, {
-        title: opt.title,
-        group: groupFor(opt.id),
-      })
+    const keybinds = settings.current.keybinds as Record<string, string | undefined> | undefined
+    if (keybinds) {
+      for (const [id, value] of Object.entries(keybinds)) {
+        if (typeof value !== "string") continue
+        if (out.has(id)) continue
+        out.set(id, { title: id, group: groupFor(id) })
+      }
     }
 
     return out
@@ -181,11 +191,21 @@ export const SettingsKeybinds: Component = () => {
       add(sig, { id: PALETTE_ID, title: "Command palette" })
     }
 
-    for (const opt of command.options) {
-      if (opt.id.startsWith("suggested.")) continue
-      if (!opt.keybind) continue
-      for (const sig of signatures(opt.keybind)) {
-        add(sig, { id: opt.id, title: opt.title })
+    const valueFor = (id: string) => {
+      const custom = settings.keybinds.get(id)
+      if (typeof custom === "string") return custom
+
+      const live = command.options.find((x) => x.id === id)
+      if (live?.keybind) return live.keybind
+
+      const meta = command.catalog.find((x) => x.id === id)
+      return meta?.keybind
+    }
+
+    for (const id of list().keys()) {
+      if (id === PALETTE_ID) continue
+      for (const sig of signatures(valueFor(id))) {
+        add(sig, { id, title: title(id) })
       }
     }
 

+ 50 - 6
packages/app/src/context/command.tsx

@@ -1,7 +1,9 @@
-import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
+import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
+import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useSettings } from "@/context/settings"
+import { Persist, persisted } from "@/utils/persist"
 
 const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
 
@@ -44,6 +46,14 @@ export interface CommandOption {
   onHighlight?: () => (() => void) | void
 }
 
+export type CommandCatalogItem = {
+  title: string
+  description?: string
+  category?: string
+  keybind?: KeybindConfig
+  slash?: string
+}
+
 export function parseKeybind(config: string): Keybind[] {
   if (!config || config === "none") return []
 
@@ -148,6 +158,11 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
     const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
     const [suspendCount, setSuspendCount] = createSignal(0)
 
+    const [catalog, setCatalog, _, catalogReady] = persisted(
+      Persist.global("command.catalog.v1"),
+      createStore<Record<string, CommandCatalogItem>>({}),
+    )
+
     const bind = (id: string, def: KeybindConfig | undefined) => {
       const custom = settings.keybinds.get(actionId(id))
       const config = custom ?? def
@@ -155,7 +170,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
       return config
     }
 
-    const options = createMemo(() => {
+    const registered = createMemo(() => {
       const seen = new Set<string>()
       const all: CommandOption[] = []
 
@@ -167,7 +182,28 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
         }
       }
 
-      const resolved = all.map((opt) => ({
+      return all
+    })
+
+    createEffect(() => {
+      if (!catalogReady()) return
+
+      for (const opt of registered()) {
+        const id = actionId(opt.id)
+        setCatalog(id, {
+          title: opt.title,
+          description: opt.description,
+          category: opt.category,
+          keybind: opt.keybind,
+          slash: opt.slash,
+        })
+      }
+    })
+
+    const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
+
+    const options = createMemo(() => {
+      const resolved = registered().map((opt) => ({
         ...opt,
         keybind: bind(opt.id, opt.keybind),
       }))
@@ -246,15 +282,23 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
           return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
         }
 
-        const option = options().find((x) => x.id === id || x.id === SUGGESTED_PREFIX + id)
-        if (!option?.keybind) return ""
-        return formatKeybind(option.keybind)
+        const base = actionId(id)
+        const option = options().find((x) => actionId(x.id) === base)
+        if (option?.keybind) return formatKeybind(option.keybind)
+
+        const meta = catalog[base]
+        const config = bind(base, meta?.keybind)
+        if (!config) return ""
+        return formatKeybind(config)
       },
       show: showPalette,
       keybinds(enabled: boolean) {
         setSuspendCount((count) => count + (enabled ? -1 : 1))
       },
       suspended,
+      get catalog() {
+        return catalogOptions()
+      },
       get options() {
         return options()
       },

+ 42 - 2
packages/ui/src/components/select.tsx

@@ -1,5 +1,5 @@
 import { Select as Kobalte } from "@kobalte/core/select"
-import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
+import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
 import { pipe, groupBy, entries, map } from "remeda"
 import { Button, ButtonProps } from "./button"
 import { Icon } from "./icon"
@@ -12,6 +12,7 @@ export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "
   label?: (x: T) => string
   groupBy?: (x: T) => string
   onSelect?: (value: T | undefined) => void
+  onHighlight?: (value: T | undefined) => (() => void) | void
   class?: ComponentProps<"div">["class"]
   classList?: ComponentProps<"div">["classList"]
   children?: (item: T | undefined) => JSX.Element
@@ -28,8 +29,40 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
     "label",
     "groupBy",
     "onSelect",
+    "onHighlight",
+    "onOpenChange",
     "children",
   ])
+
+  const state = {
+    key: undefined as string | undefined,
+    cleanup: undefined as (() => void) | void,
+  }
+
+  const stop = () => {
+    state.cleanup?.()
+    state.cleanup = undefined
+    state.key = undefined
+  }
+
+  const keyFor = (item: T) => (local.value ? local.value(item) : (item as string))
+
+  const move = (item: T | undefined) => {
+    if (!local.onHighlight) return
+    if (!item) {
+      stop()
+      return
+    }
+
+    const key = keyFor(item)
+    if (state.key === key) return
+    state.cleanup?.()
+    state.cleanup = local.onHighlight(item)
+    state.key = key
+  }
+
+  onCleanup(stop)
+
   const grouped = createMemo(() => {
     const result = pipe(
       local.options,
@@ -58,12 +91,14 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
       )}
       itemComponent={(itemProps) => (
         <Kobalte.Item
+          {...itemProps}
           data-slot="select-select-item"
           classList={{
             ...(local.classList ?? {}),
             [local.class ?? ""]: !!local.class,
           }}
-          {...itemProps}
+          onPointerEnter={() => move(itemProps.item.rawValue)}
+          onPointerMove={() => move(itemProps.item.rawValue)}
         >
           <Kobalte.ItemLabel data-slot="select-select-item-label">
             {local.children
@@ -79,6 +114,11 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
       )}
       onChange={(v) => {
         local.onSelect?.(v ?? undefined)
+        stop()
+      }}
+      onOpenChange={(open) => {
+        local.onOpenChange?.(open)
+        if (!open) stop()
       }}
     >
       <Kobalte.Trigger