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 fc0559cd686..395650b13e9 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>() const [query, setQuery] = createSignal("") @@ -204,14 +205,14 @@ export function DialogModel(props: { providerID?: string }) { ) }, }, { - 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 1217bb54ae0..0a9d28602e0 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,9 +4,9 @@ import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { createEffect, createMemo, createSignal, onMount } from "solid-js" import { Locale } from "@/util/locale" -import { Keybind } from "@/util/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" +import { useKeybind } from "../context/keybind" import { DialogSessionRename } from "./dialog-session-rename" import "opentui-spinner/solid" @@ -16,11 +16,10 @@ export function DialogSessionList() { const { theme } = useTheme() const route = useRoute() const sdk = useSDK() + const keybind = useKeybind() const [toDelete, setToDelete] = createSignal() - const deleteKeybind = "ctrl+d" - const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] @@ -40,7 +39,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, @@ -76,7 +75,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) { @@ -90,7 +89,7 @@ export function DialogSessionList() { }, }, { - keybind: Keybind.parse("ctrl+r")[0], + keybind: keybind.all.session_rename?.[0], title: "rename", onTrigger: async (option) => { dialog.replace(() => ) 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 29f2d78dca9..e8664f6289b 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() @@ -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 1e764d66bba..d7b6239c453 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 { onSelect?: (option: DialogSelectOption) => void skipFilter?: boolean keybind?: { - keybind: Keybind.Info + keybind?: Keybind.Info title: string disabled?: boolean onTrigger: (option: DialogSelectOption) => void @@ -161,7 +161,7 @@ export function DialogSelect(props: DialogSelectProps) { } 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) { @@ -183,7 +183,7 @@ export function DialogSelect(props: DialogSelectProps) { } props.ref?.(ref) - const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? []) + const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? []) return ( diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ba9d1973025..d466a6f7bb1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -442,7 +442,11 @@ export namespace Config { session_list: z.string().optional().default("l").describe("List all sessions"), session_timeline: z.string().optional().default("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 69fef28f0d9..59318a31b09 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/plugin/package.json b/packages/plugin/package.json index 1c6b2607ec3..3a3eac4066b 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index cf4211c24c7..c08c9106250 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0a31394ed9c..1135f76d87e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -861,6 +861,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 */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 96ba0720c73..a176c84cb3e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7321,7 +7321,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": {