refactor(tui): unify command registry and derive slash commands (#9115)

This commit is contained in:
Kit Langton
2026-01-17 21:39:19 -05:00
committed by GitHub
parent 9db54f3987
commit 0f974fb90a
6 changed files with 194 additions and 222 deletions
@@ -16,9 +16,17 @@ import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
type Context = ReturnType<typeof init>
const ctx = createContext<Context>()
export type CommandOption = DialogSelectOption & {
export type Slash = {
name: string
aliases?: string[]
}
export type CommandOption = DialogSelectOption<string> & {
keybind?: keyof KeybindsConfig
suggested?: boolean
slash?: Slash
hidden?: boolean
enabled?: boolean
}
function init() {
@@ -26,27 +34,35 @@ function init() {
const [suspendCount, setSuspendCount] = createSignal(0)
const dialog = useDialog()
const keybind = useKeybind()
const options = createMemo(() => {
const entries = createMemo(() => {
const all = registrations().flatMap((x) => x())
const suggested = all.filter((x) => x.suggested)
return [
...suggested.map((x) => ({
...x,
category: "Suggested",
value: "suggested." + x.value,
})),
...all,
].map((x) => ({
return all.map((x) => ({
...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined,
}))
})
const isEnabled = (option: CommandOption) => option.enabled !== false
const isVisible = (option: CommandOption) => isEnabled(option) && !option.hidden
const visibleOptions = createMemo(() => entries().filter((option) => isVisible(option)))
const suggestedOptions = createMemo(() =>
visibleOptions()
.filter((option) => option.suggested)
.map((option) => ({
...option,
value: `suggested:${option.value}`,
category: "Suggested",
})),
)
const suspended = () => suspendCount() > 0
useKeyboard((evt) => {
if (suspended()) return
if (dialog.stack.length > 0) return
for (const option of options()) {
for (const option of entries()) {
if (!isEnabled(option)) continue
if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault()
option.onSelect?.(dialog)
@@ -56,20 +72,33 @@ function init() {
})
const result = {
trigger(name: string, source?: "prompt") {
for (const option of options()) {
trigger(name: string) {
for (const option of entries()) {
if (option.value === name) {
option.onSelect?.(dialog, source)
if (!isEnabled(option)) return
option.onSelect?.(dialog)
return
}
}
},
slashes() {
return visibleOptions().flatMap((option) => {
const slash = option.slash
if (!slash) return []
return {
display: "/" + slash.name,
description: option.description ?? option.title,
aliases: slash.aliases?.map((alias) => "/" + alias),
onSelect: () => result.trigger(option.value),
}
})
},
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))
},
suspended,
show() {
dialog.replace(() => <DialogCommand options={options()} />)
dialog.replace(() => <DialogCommand options={visibleOptions()} suggestedOptions={suggestedOptions()} />)
},
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
@@ -78,9 +107,6 @@ function init() {
setRegistrations((arr) => arr.filter((x) => x !== results))
})
},
get options() {
return options()
},
}
return result
}
@@ -104,7 +130,7 @@ export function CommandProvider(props: ParentProps) {
if (evt.defaultPrevented) return
if (keybind.match("command_list", evt)) {
evt.preventDefault()
dialog.replace(() => <DialogCommand options={value.options} />)
value.show()
return
}
})
@@ -112,13 +138,11 @@ export function CommandProvider(props: ParentProps) {
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
function DialogCommand(props: { options: CommandOption[] }) {
function DialogCommand(props: { options: CommandOption[]; suggestedOptions: CommandOption[] }) {
let ref: DialogSelectRef<string>
return (
<DialogSelect
ref={(r) => (ref = r)}
title="Commands"
options={props.options.filter((x) => !ref?.filter || !x.value.startsWith("suggested."))}
/>
)
const list = () => {
if (ref?.filter) return props.options
return [...props.suggestedOptions, ...props.options]
}
return <DialogSelect ref={(r) => (ref = r)} title="Commands" options={list()} />
}