Przeglądaj źródła

feat(tui): make dialog keybinds configurable (#6143) (#6144)

Cas 1 miesiąc temu
rodzic
commit
76a79284d2

+ 4 - 3
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) => {

+ 6 - 7
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} />)

+ 4 - 3
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) {

+ 3 - 3
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}>

+ 5 - 1
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"),

+ 4 - 3
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")

+ 16 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -966,6 +966,22 @@ export type KeybindsConfig = {
    * Rename session
    */
   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
    */

+ 21 - 1
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": {