summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorCas <[email protected]>2026-01-15 13:39:52 +0800
committerGitHub <[email protected]>2026-01-14 23:39:52 -0600
commit76a79284d2dfa89323f48c805e5e85d7c77ae1d1 (patch)
treedb825c04234f124420fa52849bff7787b9c9eaa2
parent99a1e73fa1bd5c92c02abd8a20b0e274d5b0d214 (diff)
downloadopencode-76a79284d2dfa89323f48c805e5e85d7c77ae1d1.tar.gz
opencode-76a79284d2dfa89323f48c805e5e85d7c77ae1d1.zip
feat(tui): make dialog keybinds configurable (#6143) (#6144)
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx7
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx13
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx7
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx6
-rw-r--r--packages/opencode/src/config/config.ts6
-rw-r--r--packages/opencode/src/util/keybind.ts7
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts16
-rw-r--r--packages/sdk/openapi.json22
8 files changed, 63 insertions, 21 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index 71a7d22b8..bcbbe6928 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -5,7 +5,7 @@ import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
-import { Keybind } from "@/util/keybind"
+import { useKeybind } from "../context/keybind"
import * as fuzzysort from "fuzzysort"
export function useConnected() {
@@ -19,6 +19,7 @@ export function DialogModel(props: { providerID?: string }) {
const local = useLocal()
const sync = useSync()
const dialog = useDialog()
+ const keybind = useKeybind()
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
const [query, setQuery] = createSignal("")
@@ -207,14 +208,14 @@ export function DialogModel(props: { providerID?: string }) {
<DialogSelect
keybind={[
{
- keybind: Keybind.parse("ctrl+a")[0],
+ keybind: keybind.all.model_provider_list?.[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)
},
},
{
- keybind: Keybind.parse("ctrl+f")[0],
+ keybind: keybind.all.model_favorite_toggle?.[0],
title: "Favorite",
disabled: !connected(),
onTrigger: (option) => {
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index 07de4d472..85c174c1d 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
@@ -4,7 +4,7 @@ import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
import { Locale } from "@/util/locale"
-import { Keybind } from "@/util/keybind"
+import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
@@ -14,9 +14,10 @@ import "opentui-spinner/solid"
export function DialogSessionList() {
const dialog = useDialog()
+ const route = useRoute()
const sync = useSync()
+ const keybind = useKeybind()
const { theme } = useTheme()
- const route = useRoute()
const sdk = useSDK()
const kv = useKV()
@@ -29,8 +30,6 @@ export function DialogSessionList() {
return result.data ?? []
})
- const deleteKeybind = "ctrl+d"
-
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
@@ -52,7 +51,7 @@ export function DialogSessionList() {
const status = sync.data.session_status?.[x.id]
const isWorking = status?.type === "busy"
return {
- title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
+ title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
@@ -89,7 +88,7 @@ export function DialogSessionList() {
}}
keybind={[
{
- keybind: Keybind.parse(deleteKeybind)[0],
+ keybind: keybind.all.session_delete?.[0],
title: "delete",
onTrigger: async (option) => {
if (toDelete() === option.value) {
@@ -103,7 +102,7 @@ export function DialogSessionList() {
},
},
{
- keybind: Keybind.parse("ctrl+r")[0],
+ keybind: keybind.all.session_rename?.[0],
title: "rename",
onTrigger: async (option) => {
dialog.replace(() => <DialogSessionRename session={option.value} />)
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx
index 29f2d78dc..e8664f628 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx
@@ -2,8 +2,8 @@ import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createSignal } from "solid-js"
import { Locale } from "@/util/locale"
-import { Keybind } from "@/util/keybind"
import { useTheme } from "../context/theme"
+import { useKeybind } from "../context/keybind"
import { usePromptStash, type StashEntry } from "./prompt/stash"
function getRelativeTime(timestamp: number): string {
@@ -30,6 +30,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
const dialog = useDialog()
const stash = usePromptStash()
const { theme } = useTheme()
+ const keybind = useKeybind()
const [toDelete, setToDelete] = createSignal<number>()
@@ -41,7 +42,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
const isDeleting = toDelete() === index
const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1
return {
- title: isDeleting ? "Press ctrl+d again to confirm" : getStashPreview(entry.input),
+ title: isDeleting ? `Press ${keybind.print("stash_delete")} again to confirm` : getStashPreview(entry.input),
bg: isDeleting ? theme.error : undefined,
value: index,
description: getRelativeTime(entry.timestamp),
@@ -69,7 +70,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
}}
keybind={[
{
- keybind: Keybind.parse("ctrl+d")[0],
+ keybind: keybind.all.stash_delete?.[0],
title: "delete",
onTrigger: (option) => {
if (toDelete() === option.value) {
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index 98adcdeb1..a0ab462a5 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -21,7 +21,7 @@ export interface DialogSelectProps<T> {
onSelect?: (option: DialogSelectOption<T>) => void
skipFilter?: boolean
keybind?: {
- keybind: Keybind.Info
+ keybind?: Keybind.Info
title: string
disabled?: boolean
onTrigger: (option: DialogSelectOption<T>) => void
@@ -166,7 +166,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
}
for (const item of props.keybind ?? []) {
- if (item.disabled) continue
+ if (item.disabled || !item.keybind) continue
if (Keybind.match(item.keybind, keybind.parse(evt))) {
const s = selected()
if (s) {
@@ -188,7 +188,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
}
props.ref?.(ref)
- const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
+ const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? [])
return (
<box gap={1} paddingBottom={1}>
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index bf4a6035b..f77fb854b 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -621,7 +621,11 @@ export namespace Config {
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
session_fork: z.string().optional().default("none").describe("Fork session from message"),
- session_rename: z.string().optional().default("none").describe("Rename session"),
+ session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
+ session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
+ stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
+ model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
+ model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
session_share: z.string().optional().default("none").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts
index 69fef28f0..59318a31b 100644
--- a/packages/opencode/src/util/keybind.ts
+++ b/packages/opencode/src/util/keybind.ts
@@ -10,8 +10,8 @@ export namespace Keybind {
leader: boolean // our custom field
}
- export function match(a: Info, b: Info): boolean {
- // Normalize super field (undefined and false are equivalent)
+ export function match(a: Info | undefined, b: Info): boolean {
+ if (!a) return false
const normalizedA = { ...a, super: a.super ?? false }
const normalizedB = { ...b, super: b.super ?? false }
return isDeepEqual(normalizedA, normalizedB)
@@ -32,7 +32,8 @@ export namespace Keybind {
}
}
- export function toString(info: Info): string {
+ export function toString(info: Info | undefined): string {
+ if (!info) return ""
const parts: string[] = []
if (info.ctrl) parts.push("ctrl")
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 2bb3a6002..e810e0bf5 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -967,6 +967,22 @@ export type KeybindsConfig = {
*/
session_rename?: string
/**
+ * Delete session
+ */
+ session_delete?: string
+ /**
+ * Delete stash entry
+ */
+ stash_delete?: string
+ /**
+ * Open provider list from model dialog
+ */
+ model_provider_list?: string
+ /**
+ * Toggle model favorite status
+ */
+ model_favorite_toggle?: string
+ /**
* Share current session
*/
session_share?: string
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 107d461f3..0c2e640fc 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -8168,7 +8168,27 @@
},
"session_rename": {
"description": "Rename session",
- "default": "none",
+ "default": "ctrl+r",
+ "type": "string"
+ },
+ "session_delete": {
+ "description": "Delete session",
+ "default": "ctrl+d",
+ "type": "string"
+ },
+ "stash_delete": {
+ "description": "Delete stash entry",
+ "default": "ctrl+d",
+ "type": "string"
+ },
+ "model_provider_list": {
+ "description": "Open provider list from model dialog",
+ "default": "ctrl+a",
+ "type": "string"
+ },
+ "model_favorite_toggle": {
+ "description": "Toggle model favorite status",
+ "default": "ctrl+f",
"type": "string"
},
"session_share": {