From 340285575b510f4ab466fa80d0de1cf20b4e048a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:10:40 -0600 Subject: [PATCH] chore: cleanup --- .../app/src/components/settings-general.tsx | 22 +++++++- .../app/src/components/settings-keybinds.tsx | 38 ++++++++++--- packages/app/src/context/command.tsx | 56 +++++++++++++++++-- packages/ui/src/components/select.tsx | 44 ++++++++++++++- 4 files changed, 142 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 15dc98bfb..10f4facef 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/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) diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 811b34f9b..7bc9b1fd7 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -124,13 +124,23 @@ export const SettingsKeybinds: Component = () => { const out = new Map() 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 | 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) }) } } diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 7986e7509..1e71801bd 100644 --- a/packages/app/src/context/command.tsx +++ b/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[]>([]) const [suspendCount, setSuspendCount] = createSignal(0) + const [catalog, setCatalog, _, catalogReady] = persisted( + Persist.global("command.catalog.v1"), + createStore>({}), + ) + 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() 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() }, diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index e60fcbee1..245a36d38 100644 --- a/packages/ui/src/components/select.tsx +++ b/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 = Omit>, "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(props: SelectProps & 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(props: SelectProps & ButtonProps) { )} itemComponent={(itemProps) => ( move(itemProps.item.rawValue)} + onPointerMove={() => move(itemProps.item.rawValue)} > {local.children @@ -79,6 +114,11 @@ export function Select(props: SelectProps & ButtonProps) { )} onChange={(v) => { local.onSelect?.(v ?? undefined) + stop() + }} + onOpenChange={(open) => { + local.onOpenChange?.(open) + if (!open) stop() }} >