Skip to content

Commit cafb9f0

Browse files
feat(tui): make dialog keybinds configurable (anomalyco#6143) (anomalyco#6144)
1 parent cb1eeac commit cafb9f0

8 files changed

Lines changed: 63 additions & 21 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
55
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
66
import { useDialog } from "@tui/ui/dialog"
77
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
8-
import { Keybind } from "@/util/keybind"
8+
import { useKeybind } from "../context/keybind"
99
import * as fuzzysort from "fuzzysort"
1010

1111
export function useConnected() {
@@ -19,6 +19,7 @@ export function DialogModel(props: { providerID?: string }) {
1919
const local = useLocal()
2020
const sync = useSync()
2121
const dialog = useDialog()
22+
const keybind = useKeybind()
2223
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
2324
const [query, setQuery] = createSignal("")
2425

@@ -207,14 +208,14 @@ export function DialogModel(props: { providerID?: string }) {
207208
<DialogSelect
208209
keybind={[
209210
{
210-
keybind: Keybind.parse("ctrl+a")[0],
211+
keybind: keybind.all.model_provider_list?.[0],
211212
title: connected() ? "Connect provider" : "View all providers",
212213
onTrigger() {
213214
dialog.replace(() => <DialogProvider />)
214215
},
215216
},
216217
{
217-
keybind: Keybind.parse("ctrl+f")[0],
218+
keybind: keybind.all.model_favorite_toggle?.[0],
218219
title: "Favorite",
219220
disabled: !connected(),
220221
onTrigger: (option) => {

packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useRoute } from "@tui/context/route"
44
import { useSync } from "@tui/context/sync"
55
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
66
import { Locale } from "@/util/locale"
7-
import { Keybind } from "@/util/keybind"
7+
import { useKeybind } from "../context/keybind"
88
import { useTheme } from "../context/theme"
99
import { useSDK } from "../context/sdk"
1010
import { DialogSessionRename } from "./dialog-session-rename"
@@ -14,9 +14,10 @@ import "opentui-spinner/solid"
1414

1515
export function DialogSessionList() {
1616
const dialog = useDialog()
17+
const route = useRoute()
1718
const sync = useSync()
19+
const keybind = useKeybind()
1820
const { theme } = useTheme()
19-
const route = useRoute()
2021
const sdk = useSDK()
2122
const kv = useKV()
2223

@@ -29,8 +30,6 @@ export function DialogSessionList() {
2930
return result.data ?? []
3031
})
3132

32-
const deleteKeybind = "ctrl+d"
33-
3433
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
3534

3635
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
@@ -52,7 +51,7 @@ export function DialogSessionList() {
5251
const status = sync.data.session_status?.[x.id]
5352
const isWorking = status?.type === "busy"
5453
return {
55-
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
54+
title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
5655
bg: isDeleting ? theme.error : undefined,
5756
value: x.id,
5857
category,
@@ -89,7 +88,7 @@ export function DialogSessionList() {
8988
}}
9089
keybind={[
9190
{
92-
keybind: Keybind.parse(deleteKeybind)[0],
91+
keybind: keybind.all.session_delete?.[0],
9392
title: "delete",
9493
onTrigger: async (option) => {
9594
if (toDelete() === option.value) {
@@ -103,7 +102,7 @@ export function DialogSessionList() {
103102
},
104103
},
105104
{
106-
keybind: Keybind.parse("ctrl+r")[0],
105+
keybind: keybind.all.session_rename?.[0],
107106
title: "rename",
108107
onTrigger: async (option) => {
109108
dialog.replace(() => <DialogSessionRename session={option.value} />)

packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useDialog } from "@tui/ui/dialog"
22
import { DialogSelect } from "@tui/ui/dialog-select"
33
import { createMemo, createSignal } from "solid-js"
44
import { Locale } from "@/util/locale"
5-
import { Keybind } from "@/util/keybind"
65
import { useTheme } from "../context/theme"
6+
import { useKeybind } from "../context/keybind"
77
import { usePromptStash, type StashEntry } from "./prompt/stash"
88

99
function getRelativeTime(timestamp: number): string {
@@ -30,6 +30,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
3030
const dialog = useDialog()
3131
const stash = usePromptStash()
3232
const { theme } = useTheme()
33+
const keybind = useKeybind()
3334

3435
const [toDelete, setToDelete] = createSignal<number>()
3536

@@ -41,7 +42,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
4142
const isDeleting = toDelete() === index
4243
const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1
4344
return {
44-
title: isDeleting ? "Press ctrl+d again to confirm" : getStashPreview(entry.input),
45+
title: isDeleting ? `Press ${keybind.print("stash_delete")} again to confirm` : getStashPreview(entry.input),
4546
bg: isDeleting ? theme.error : undefined,
4647
value: index,
4748
description: getRelativeTime(entry.timestamp),
@@ -69,7 +70,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
6970
}}
7071
keybind={[
7172
{
72-
keybind: Keybind.parse("ctrl+d")[0],
73+
keybind: keybind.all.stash_delete?.[0],
7374
title: "delete",
7475
onTrigger: (option) => {
7576
if (toDelete() === option.value) {

packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface DialogSelectProps<T> {
2121
onSelect?: (option: DialogSelectOption<T>) => void
2222
skipFilter?: boolean
2323
keybind?: {
24-
keybind: Keybind.Info
24+
keybind?: Keybind.Info
2525
title: string
2626
disabled?: boolean
2727
onTrigger: (option: DialogSelectOption<T>) => void
@@ -166,7 +166,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
166166
}
167167

168168
for (const item of props.keybind ?? []) {
169-
if (item.disabled) continue
169+
if (item.disabled || !item.keybind) continue
170170
if (Keybind.match(item.keybind, keybind.parse(evt))) {
171171
const s = selected()
172172
if (s) {
@@ -188,7 +188,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
188188
}
189189
props.ref?.(ref)
190190

191-
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
191+
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? [])
192192

193193
return (
194194
<box gap={1} paddingBottom={1}>

packages/opencode/src/config/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,11 @@ export namespace Config {
621621
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
622622
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
623623
session_fork: z.string().optional().default("none").describe("Fork session from message"),
624-
session_rename: z.string().optional().default("none").describe("Rename session"),
624+
session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
625+
session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
626+
stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
627+
model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
628+
model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
625629
session_share: z.string().optional().default("none").describe("Share current session"),
626630
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
627631
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),

packages/opencode/src/util/keybind.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export namespace Keybind {
1010
leader: boolean // our custom field
1111
}
1212

13-
export function match(a: Info, b: Info): boolean {
14-
// Normalize super field (undefined and false are equivalent)
13+
export function match(a: Info | undefined, b: Info): boolean {
14+
if (!a) return false
1515
const normalizedA = { ...a, super: a.super ?? false }
1616
const normalizedB = { ...b, super: b.super ?? false }
1717
return isDeepEqual(normalizedA, normalizedB)
@@ -32,7 +32,8 @@ export namespace Keybind {
3232
}
3333
}
3434

35-
export function toString(info: Info): string {
35+
export function toString(info: Info | undefined): string {
36+
if (!info) return ""
3637
const parts: string[] = []
3738

3839
if (info.ctrl) parts.push("ctrl")

packages/sdk/js/src/v2/gen/types.gen.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,22 @@ export type KeybindsConfig = {
966966
* Rename session
967967
*/
968968
session_rename?: string
969+
/**
970+
* Delete session
971+
*/
972+
session_delete?: string
973+
/**
974+
* Delete stash entry
975+
*/
976+
stash_delete?: string
977+
/**
978+
* Open provider list from model dialog
979+
*/
980+
model_provider_list?: string
981+
/**
982+
* Toggle model favorite status
983+
*/
984+
model_favorite_toggle?: string
969985
/**
970986
* Share current session
971987
*/

packages/sdk/openapi.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8168,7 +8168,27 @@
81688168
},
81698169
"session_rename": {
81708170
"description": "Rename session",
8171-
"default": "none",
8171+
"default": "ctrl+r",
8172+
"type": "string"
8173+
},
8174+
"session_delete": {
8175+
"description": "Delete session",
8176+
"default": "ctrl+d",
8177+
"type": "string"
8178+
},
8179+
"stash_delete": {
8180+
"description": "Delete stash entry",
8181+
"default": "ctrl+d",
8182+
"type": "string"
8183+
},
8184+
"model_provider_list": {
8185+
"description": "Open provider list from model dialog",
8186+
"default": "ctrl+a",
8187+
"type": "string"
8188+
},
8189+
"model_favorite_toggle": {
8190+
"description": "Toggle model favorite status",
8191+
"default": "ctrl+f",
81728192
"type": "string"
81738193
},
81748194
"session_share": {

0 commit comments

Comments
 (0)