flatten to keybind compatible config (#26421)

This commit is contained in:
Sebastian
2026-05-09 01:29:13 +02:00
committed by GitHub
parent 35deef6175
commit a0fc27e424
38 changed files with 1096 additions and 1518 deletions
@@ -0,0 +1,384 @@
export * as TuiKeybind from "./keybind"
import type { KeyEvent, Renderable } from "@opentui/core"
import type { Binding } from "@opentui/keymap"
import type { BindingCommandMap, BindingConfig, BindingDefaults, BindingValue } from "@opentui/keymap/extras"
import z from "zod"
const KeyStroke = z
.object({
name: z.string(),
ctrl: z.boolean().optional(),
shift: z.boolean().optional(),
meta: z.boolean().optional(),
super: z.boolean().optional(),
hyper: z.boolean().optional(),
})
.strict()
const BindingObject = z
.object({
key: z.union([z.string(), KeyStroke]),
event: z.enum(["press", "release"]).optional(),
preventDefault: z.boolean().optional(),
fallthrough: z.boolean().optional(),
})
.passthrough()
const BindingItem = z.union([z.string(), KeyStroke, BindingObject])
export const BindingValueSchema = z.union([z.literal(false), z.literal("none"), BindingItem, z.array(BindingItem)])
type Definition = {
default: z.input<typeof BindingValueSchema>
description: string
}
const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z"
export const LeaderDefault = "ctrl+x"
const keybind = (value: Definition["default"], description: string): Definition => ({ default: value, description })
const Definitions = {
leader: keybind(LeaderDefault, "Leader key for keybind combinations"),
app_exit: keybind("ctrl+c,ctrl+d,<leader>q", "Exit the application"),
app_debug: keybind("none", "Toggle debug panel"),
app_console: keybind("none", "Toggle console"),
app_heap_snapshot: keybind("none", "Write heap snapshot"),
app_toggle_animations: keybind("none", "Toggle animations"),
app_toggle_file_context: keybind("none", "Toggle file context"),
app_toggle_diffwrap: keybind("none", "Toggle diff wrapping"),
app_toggle_paste_summary: keybind("none", "Toggle paste summary"),
app_toggle_session_directory_filter: keybind("none", "Toggle session directory filtering"),
command_list: keybind("ctrl+p", "List available commands"),
help_show: keybind("none", "Open help dialog"),
docs_open: keybind("none", "Open documentation"),
editor_open: keybind("<leader>e", "Open external editor"),
theme_list: keybind("<leader>t", "List available themes"),
theme_switch_mode: keybind("none", "Switch between light and dark theme mode"),
theme_mode_lock: keybind("none", "Lock or unlock theme mode"),
sidebar_toggle: keybind("<leader>b", "Toggle sidebar"),
scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
status_view: keybind("<leader>s", "View status"),
session_export: keybind("<leader>x", "Export session to editor"),
session_copy: keybind("none", "Copy session transcript"),
session_new: keybind("<leader>n", "Create a new session"),
session_list: keybind("<leader>l", "List all sessions"),
session_timeline: keybind("<leader>g", "Show session timeline"),
session_fork: keybind("none", "Fork session from message"),
session_rename: keybind("ctrl+r", "Rename session"),
session_delete: keybind("ctrl+d", "Delete session"),
session_share: keybind("none", "Share current session"),
session_unshare: keybind("none", "Unshare current session"),
session_interrupt: keybind("escape", "Interrupt current session"),
session_compact: keybind("<leader>c", "Compact the session"),
session_toggle_timestamps: keybind("none", "Toggle message timestamps"),
session_toggle_generic_tool_output: keybind("none", "Toggle generic tool output"),
session_child_first: keybind("<leader>down", "Go to first child session"),
session_child_cycle: keybind("right", "Go to next child session"),
session_child_cycle_reverse: keybind("left", "Go to previous child session"),
session_parent: keybind("up", "Go to parent session"),
stash_delete: keybind("ctrl+d", "Delete stash entry"),
model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"),
model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"),
model_list: keybind("<leader>m", "List available models"),
model_cycle_recent: keybind("f2", "Next recently used model"),
model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"),
model_cycle_favorite: keybind("none", "Next favorite model"),
model_cycle_favorite_reverse: keybind("none", "Previous favorite model"),
mcp_list: keybind("none", "List MCP servers"),
provider_connect: keybind("none", "Connect provider"),
console_org_switch: keybind("none", "Switch console organization"),
agent_list: keybind("<leader>a", "List agents"),
agent_cycle: keybind("tab", "Next agent"),
agent_cycle_reverse: keybind("shift+tab", "Previous agent"),
variant_cycle: keybind("ctrl+t", "Cycle model variants"),
variant_list: keybind("none", "List model variants"),
messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"),
messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"),
messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"),
messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"),
messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"),
messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"),
messages_first: keybind("ctrl+g,home", "Navigate to first message"),
messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"),
messages_next: keybind("none", "Navigate to next message"),
messages_previous: keybind("none", "Navigate to previous message"),
messages_last_user: keybind("none", "Navigate to last user message"),
messages_copy: keybind("<leader>y", "Copy message"),
messages_undo: keybind("<leader>u", "Undo message"),
messages_redo: keybind("<leader>r", "Redo message"),
messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"),
tool_details: keybind("none", "Toggle tool details visibility"),
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
prompt_submit: keybind("none", "Submit prompt"),
prompt_editor_context_clear: keybind("none", "Clear editor context"),
prompt_skills: keybind("none", "Open skill selector"),
prompt_stash: keybind("none", "Stash prompt"),
prompt_stash_pop: keybind("none", "Pop stashed prompt"),
prompt_stash_list: keybind("none", "List stashed prompts"),
workspace_set: keybind("none", "Set workspace"),
input_clear: keybind("ctrl+c", "Clear input field"),
input_paste: keybind({ key: "ctrl+v", preventDefault: false }, "Paste from clipboard"),
input_submit: keybind("return", "Submit input"),
input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"),
input_move_left: keybind("left,ctrl+b", "Move cursor left in input"),
input_move_right: keybind("right,ctrl+f", "Move cursor right in input"),
input_move_up: keybind("up", "Move cursor up in input"),
input_move_down: keybind("down", "Move cursor down in input"),
input_select_left: keybind("shift+left", "Select left in input"),
input_select_right: keybind("shift+right", "Select right in input"),
input_select_up: keybind("shift+up", "Select up in input"),
input_select_down: keybind("shift+down", "Select down in input"),
input_line_home: keybind("ctrl+a", "Move to start of line in input"),
input_line_end: keybind("ctrl+e", "Move to end of line in input"),
input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"),
input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"),
input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"),
input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"),
input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"),
input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"),
input_buffer_home: keybind("home", "Move to start of buffer in input"),
input_buffer_end: keybind("end", "Move to end of buffer in input"),
input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"),
input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"),
input_delete_line: keybind("ctrl+shift+d", "Delete line in input"),
input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"),
input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"),
input_backspace: keybind("backspace,shift+backspace", "Backspace in input"),
input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"),
input_undo: keybind(inputUndoDefault, "Undo in input"),
input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"),
input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"),
input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"),
input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"),
input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"),
input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"),
input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"),
input_select_all: keybind("super+a", "Select all in input"),
history_previous: keybind("up", "Previous history item"),
history_next: keybind("down", "Next history item"),
"dialog.select.prev": keybind("up,ctrl+p", "Move to previous dialog item"),
"dialog.select.next": keybind("down,ctrl+n", "Move to next dialog item"),
"dialog.select.page_up": keybind("pageup", "Move up one page in dialog"),
"dialog.select.page_down": keybind("pagedown", "Move down one page in dialog"),
"dialog.select.home": keybind("home", "Move to first dialog item"),
"dialog.select.end": keybind("end", "Move to last dialog item"),
"dialog.select.submit": keybind("return", "Submit selected dialog item"),
"dialog.mcp.toggle": keybind("space", "Toggle MCP in MCP dialog"),
"prompt.autocomplete.prev": keybind("up,ctrl+p", "Move to previous autocomplete item"),
"prompt.autocomplete.next": keybind("down,ctrl+n", "Move to next autocomplete item"),
"prompt.autocomplete.hide": keybind("escape", "Hide autocomplete"),
"prompt.autocomplete.select": keybind("return", "Select autocomplete item"),
"prompt.autocomplete.complete": keybind("tab", "Complete autocomplete item"),
"permission.prompt.fullscreen": keybind("ctrl+f", "Toggle permission prompt fullscreen"),
"plugins.toggle": keybind("space", "Toggle plugin"),
"dialog.plugins.install": keybind("shift+i", "Install plugin from plugin dialog"),
terminal_suspend: keybind("ctrl+z", "Suspend terminal"),
terminal_title_toggle: keybind("none", "Toggle terminal title"),
tips_toggle: keybind("<leader>h", "Toggle tips on home screen"),
plugin_manager: keybind("none", "Open plugin manager dialog"),
plugin_install: keybind("none", "Install plugin"),
which_key_toggle: keybind("ctrl+alt+k", "Toggle which-key panel"),
which_key_layout_toggle: keybind("ctrl+alt+shift+k", "Switch which-key layout"),
which_key_pending_toggle: keybind("ctrl+alt+shift+p", "Toggle which-key pending preview"),
which_key_group_previous: keybind("ctrl+alt+left,ctrl+alt+[", "Previous which-key group"),
which_key_group_next: keybind("ctrl+alt+right,ctrl+alt+]", "Next which-key group"),
which_key_scroll_up: keybind("ctrl+alt+up,ctrl+alt+p", "Scroll which-key up"),
which_key_scroll_down: keybind("ctrl+alt+down,ctrl+alt+n", "Scroll which-key down"),
which_key_page_up: keybind("ctrl+alt+pageup", "Page which-key up"),
which_key_page_down: keybind("ctrl+alt+pagedown", "Page which-key down"),
which_key_home: keybind("ctrl+alt+home", "Jump to first which-key binding"),
which_key_end: keybind("ctrl+alt+end", "Jump to last which-key binding"),
} satisfies Record<string, Definition>
type KeybindName = keyof typeof Definitions & string
const KeybindShape = Object.fromEntries(
Object.entries(Definitions).map(([name, item]) => [
name,
BindingValueSchema.optional().default(item.default).describe(item.description),
]),
) as Record<KeybindName, z.ZodDefault<z.ZodOptional<typeof BindingValueSchema>>>
const KeybindOverrideShape = Object.fromEntries(
Object.entries(Definitions).map(([name, item]) => [name, BindingValueSchema.optional().describe(item.description)]),
) as Record<KeybindName, z.ZodOptional<typeof BindingValueSchema>>
export const Keybinds = z.strictObject(KeybindShape).describe("TUI keybinding configuration")
export const KeybindOverrides = z.strictObject(KeybindOverrideShape).describe("TUI keybinding overrides")
export const Descriptions = Object.fromEntries(
Object.entries(Definitions).map(([name, item]) => [name, item.description]),
) as Record<KeybindName, string>
export const CommandMap = {
app_exit: "app.exit",
app_debug: "app.debug",
app_console: "app.console",
app_heap_snapshot: "app.heap_snapshot",
app_toggle_animations: "app.toggle.animations",
app_toggle_file_context: "app.toggle.file_context",
app_toggle_diffwrap: "app.toggle.diffwrap",
app_toggle_paste_summary: "app.toggle.paste_summary",
app_toggle_session_directory_filter: "app.toggle.session_directory_filter",
command_list: "command.palette.show",
help_show: "help.show",
docs_open: "docs.open",
editor_open: "prompt.editor",
theme_list: "theme.switch",
theme_switch_mode: "theme.switch_mode",
theme_mode_lock: "theme.mode.lock",
sidebar_toggle: "session.sidebar.toggle",
scrollbar_toggle: "session.toggle.scrollbar",
status_view: "opencode.status",
session_export: "session.export",
session_copy: "session.copy",
session_new: "session.new",
session_list: "session.list",
session_timeline: "session.timeline",
session_fork: "session.fork",
session_rename: "session.rename",
session_delete: "session.delete",
session_share: "session.share",
session_unshare: "session.unshare",
session_interrupt: "session.interrupt",
session_compact: "session.compact",
session_toggle_timestamps: "session.toggle.timestamps",
session_toggle_generic_tool_output: "session.toggle.generic_tool_output",
session_child_first: "session.child.first",
session_child_cycle: "session.child.next",
session_child_cycle_reverse: "session.child.previous",
session_parent: "session.parent",
stash_delete: "stash.delete",
model_provider_list: "model.dialog.provider",
model_favorite_toggle: "model.dialog.favorite",
model_list: "model.list",
model_cycle_recent: "model.cycle_recent",
model_cycle_recent_reverse: "model.cycle_recent_reverse",
model_cycle_favorite: "model.cycle_favorite",
model_cycle_favorite_reverse: "model.cycle_favorite_reverse",
mcp_list: "mcp.list",
provider_connect: "provider.connect",
console_org_switch: "console.org.switch",
agent_list: "agent.list",
agent_cycle: "agent.cycle",
agent_cycle_reverse: "agent.cycle.reverse",
variant_cycle: "variant.cycle",
variant_list: "variant.list",
messages_page_up: "session.page.up",
messages_page_down: "session.page.down",
messages_line_up: "session.line.up",
messages_line_down: "session.line.down",
messages_half_page_up: "session.half.page.up",
messages_half_page_down: "session.half.page.down",
messages_first: "session.first",
messages_last: "session.last",
messages_next: "session.message.next",
messages_previous: "session.message.previous",
messages_last_user: "session.messages_last_user",
messages_copy: "messages.copy",
messages_undo: "session.undo",
messages_redo: "session.redo",
messages_toggle_conceal: "session.toggle.conceal",
tool_details: "session.toggle.actions",
display_thinking: "session.toggle.thinking",
prompt_submit: "prompt.submit",
prompt_editor_context_clear: "prompt.editor_context.clear",
prompt_skills: "prompt.skills",
prompt_stash: "prompt.stash",
prompt_stash_pop: "prompt.stash.pop",
prompt_stash_list: "prompt.stash.list",
workspace_set: "workspace.set",
input_clear: "prompt.clear",
input_paste: "prompt.paste",
input_submit: "input.submit",
input_newline: "input.newline",
input_move_left: "input.move.left",
input_move_right: "input.move.right",
input_move_up: "input.move.up",
input_move_down: "input.move.down",
input_select_left: "input.select.left",
input_select_right: "input.select.right",
input_select_up: "input.select.up",
input_select_down: "input.select.down",
input_line_home: "input.line.home",
input_line_end: "input.line.end",
input_select_line_home: "input.select.line.home",
input_select_line_end: "input.select.line.end",
input_visual_line_home: "input.visual.line.home",
input_visual_line_end: "input.visual.line.end",
input_select_visual_line_home: "input.select.visual.line.home",
input_select_visual_line_end: "input.select.visual.line.end",
input_buffer_home: "input.buffer.home",
input_buffer_end: "input.buffer.end",
input_select_buffer_home: "input.select.buffer.home",
input_select_buffer_end: "input.select.buffer.end",
input_delete_line: "input.delete.line",
input_delete_to_line_end: "input.delete.to.line.end",
input_delete_to_line_start: "input.delete.to.line.start",
input_backspace: "input.backspace",
input_delete: "input.delete",
input_undo: "input.undo",
input_redo: "input.redo",
input_word_forward: "input.word.forward",
input_word_backward: "input.word.backward",
input_select_word_forward: "input.select.word.forward",
input_select_word_backward: "input.select.word.backward",
input_delete_word_forward: "input.delete.word.forward",
input_delete_word_backward: "input.delete.word.backward",
input_select_all: "input.select.all",
history_previous: "prompt.history.previous",
history_next: "prompt.history.next",
terminal_suspend: "terminal.suspend",
terminal_title_toggle: "terminal.title.toggle",
tips_toggle: "tips.toggle",
plugin_manager: "plugins.list",
plugin_install: "plugins.install",
which_key_toggle: "which-key.toggle",
which_key_layout_toggle: "which-key.layout.toggle",
which_key_pending_toggle: "which-key.pending.toggle",
which_key_group_previous: "which-key.group.previous",
which_key_group_next: "which-key.group.next",
which_key_scroll_up: "which-key.scroll.up",
which_key_scroll_down: "which-key.scroll.down",
which_key_page_up: "which-key.page.up",
which_key_page_down: "which-key.page.down",
which_key_home: "which-key.home",
which_key_end: "which-key.end",
} satisfies BindingCommandMap
const CommandDescriptions = Object.fromEntries(
Object.entries(Definitions).map(([name, item]) => [
CommandMap[name as keyof typeof CommandMap] ?? name,
item.description,
]),
) as Record<string, string>
export type Keybinds = z.output<typeof Keybinds>
export type KeybindOverrides = z.output<typeof KeybindOverrides>
export type BindingLookupView = {
readonly bindings: readonly Binding<Renderable, KeyEvent>[]
get(command: string): readonly Binding<Renderable, KeyEvent>[]
has(command: string): boolean
gather(name: string, commands: readonly string[]): readonly Binding<Renderable, KeyEvent>[]
pick(name: string, commands: readonly string[]): Binding<Renderable, KeyEvent>[]
omit(name: string, commands: readonly string[]): Binding<Renderable, KeyEvent>[]
}
export function toBindingConfig(keybinds: Keybinds): BindingConfig<Renderable, KeyEvent> {
return Object.fromEntries(Object.entries(keybinds)) as BindingConfig<Renderable, KeyEvent>
}
export function bindingDefaults(): BindingDefaults<Renderable, KeyEvent> {
return ({ command, binding }) => {
if (binding.desc !== undefined) return
return { desc: CommandDescriptions[command] }
}
}
@@ -1,187 +0,0 @@
import type { KeyEvent, Renderable } from "@opentui/core"
import type { Binding } from "@opentui/keymap"
import type { BindingValue } from "@opentui/keymap/extras"
import { ConfigKeybinds } from "@/config/keybinds"
import { type KeymapConfigInput, type KeymapSection } from "./tui-schema"
type LegacyKeybinds = Partial<ConfigKeybinds.Keybinds>
type SectionsConfig = Record<string, Record<string, BindingValue<Renderable, KeyEvent>>>
const inputCommands = {
input_submit: "input.submit",
input_newline: "input.newline",
input_move_left: "input.move.left",
input_move_right: "input.move.right",
input_move_up: "input.move.up",
input_move_down: "input.move.down",
input_select_left: "input.select.left",
input_select_right: "input.select.right",
input_select_up: "input.select.up",
input_select_down: "input.select.down",
input_line_home: "input.line.home",
input_line_end: "input.line.end",
input_select_line_home: "input.select.line.home",
input_select_line_end: "input.select.line.end",
input_visual_line_home: "input.visual.line.home",
input_visual_line_end: "input.visual.line.end",
input_select_visual_line_home: "input.select.visual.line.home",
input_select_visual_line_end: "input.select.visual.line.end",
input_buffer_home: "input.buffer.home",
input_buffer_end: "input.buffer.end",
input_select_buffer_home: "input.select.buffer.home",
input_select_buffer_end: "input.select.buffer.end",
input_delete_line: "input.delete.line",
input_delete_to_line_end: "input.delete.to.line.end",
input_delete_to_line_start: "input.delete.to.line.start",
input_backspace: "input.backspace",
input_delete: "input.delete",
input_undo: "input.undo",
input_redo: "input.redo",
input_word_forward: "input.word.forward",
input_word_backward: "input.word.backward",
input_select_word_forward: "input.select.word.forward",
input_select_word_backward: "input.select.word.backward",
input_delete_word_forward: "input.delete.word.forward",
input_delete_word_backward: "input.delete.word.backward",
input_select_all: "input.select.all",
} as const satisfies Partial<Record<keyof LegacyKeybinds, string>>
function add(
config: SectionsConfig,
section: KeymapSection,
command: string,
binding: BindingValue<Renderable, KeyEvent> | undefined,
) {
if (binding === undefined) return
config[section] ??= {}
config[section][command] = binding
}
function bindingWith(key: string | undefined, input: Omit<Binding<Renderable, KeyEvent>, "key" | "cmd">) {
if (!key) return undefined
if (key === "none") return "none"
return { ...input, key }
}
function combineBindings(...keys: (string | undefined)[]) {
const result = Array.from(
new Set(
keys.flatMap((key) => {
if (!key || key === "none") return []
return key
.split(",")
.map((part) => part.trim())
.filter((part) => part && part !== "none")
}),
),
)
if (result.length) return result.join(",")
if (keys.some((key) => key === "none")) return "none"
return undefined
}
export function create(keybinds: LegacyKeybinds): KeymapConfigInput {
const config: SectionsConfig = {}
add(config, "global", "command.palette.show", keybinds.command_list)
add(config, "global", "session.list", keybinds.session_list)
add(config, "global", "session.new", keybinds.session_new)
add(config, "global", "model.list", keybinds.model_list)
add(config, "global", "model.cycle_recent", keybinds.model_cycle_recent)
add(config, "global", "model.cycle_recent_reverse", keybinds.model_cycle_recent_reverse)
add(config, "global", "model.cycle_favorite", keybinds.model_cycle_favorite)
add(config, "global", "model.cycle_favorite_reverse", keybinds.model_cycle_favorite_reverse)
add(config, "global", "agent.list", keybinds.agent_list)
add(config, "global", "agent.cycle", keybinds.agent_cycle)
add(config, "global", "agent.cycle.reverse", keybinds.agent_cycle_reverse)
add(config, "global", "variant.cycle", keybinds.variant_cycle)
add(config, "global", "variant.list", keybinds.variant_list)
add(config, "prompt", "prompt.editor", keybinds.editor_open)
add(config, "global", "opencode.status", keybinds.status_view)
add(config, "global", "theme.switch", keybinds.theme_list)
add(config, "global", "app.exit", keybinds.app_exit)
add(config, "global", "terminal.suspend", keybinds.terminal_suspend)
add(config, "global", "terminal.title.toggle", keybinds.terminal_title_toggle)
add(config, "session", "session.share", keybinds.session_share)
add(config, "session", "session.rename", keybinds.session_rename)
add(config, "session", "session.timeline", keybinds.session_timeline)
add(config, "session", "session.fork", keybinds.session_fork)
add(config, "session", "session.compact", keybinds.session_compact)
add(config, "session", "session.unshare", keybinds.session_unshare)
add(config, "session", "session.undo", keybinds.messages_undo)
add(config, "session", "session.redo", keybinds.messages_redo)
add(config, "session", "session.sidebar.toggle", keybinds.sidebar_toggle)
add(config, "session", "session.toggle.conceal", keybinds.messages_toggle_conceal)
add(config, "session", "session.toggle.thinking", keybinds.display_thinking)
add(config, "session", "session.toggle.actions", keybinds.tool_details)
add(config, "session", "session.toggle.scrollbar", keybinds.scrollbar_toggle)
add(config, "session", "session.page.up", keybinds.messages_page_up)
add(config, "session", "session.page.down", keybinds.messages_page_down)
add(config, "session", "session.line.up", keybinds.messages_line_up)
add(config, "session", "session.line.down", keybinds.messages_line_down)
add(config, "session", "session.half.page.up", keybinds.messages_half_page_up)
add(config, "session", "session.half.page.down", keybinds.messages_half_page_down)
add(config, "session", "session.first", keybinds.messages_first)
add(config, "session", "session.last", keybinds.messages_last)
add(config, "session", "session.messages_last_user", keybinds.messages_last_user)
add(config, "session", "session.message.next", keybinds.messages_next)
add(config, "session", "session.message.previous", keybinds.messages_previous)
add(config, "session", "messages.copy", keybinds.messages_copy)
add(config, "session", "session.export", keybinds.session_export)
add(config, "session", "session.child.first", keybinds.session_child_first)
add(config, "session", "session.parent", keybinds.session_parent)
add(config, "session", "session.child.next", keybinds.session_child_cycle)
add(config, "session", "session.child.previous", keybinds.session_child_cycle_reverse)
add(config, "prompt", "session.interrupt", keybinds.session_interrupt)
add(config, "prompt", "prompt.clear", keybinds.input_clear)
add(config, "prompt", "prompt.paste", bindingWith(keybinds.input_paste, { preventDefault: false }))
add(config, "prompt", "prompt.history.previous", keybinds.history_previous)
add(config, "prompt", "prompt.history.next", keybinds.history_next)
add(config, "autocomplete", "prompt.autocomplete.prev", keybinds["prompt.autocomplete.prev"])
add(config, "autocomplete", "prompt.autocomplete.next", keybinds["prompt.autocomplete.next"])
add(config, "autocomplete", "prompt.autocomplete.hide", keybinds["prompt.autocomplete.hide"])
add(config, "autocomplete", "prompt.autocomplete.select", keybinds["prompt.autocomplete.select"])
add(config, "autocomplete", "prompt.autocomplete.complete", keybinds["prompt.autocomplete.complete"])
for (const [legacy, command] of Object.entries(inputCommands) as [keyof typeof inputCommands, string][]) {
add(config, "input", command, keybinds[legacy])
}
add(config, "dialog_select", "dialog.select.prev", keybinds["dialog.select.prev"])
add(config, "dialog_select", "dialog.select.next", keybinds["dialog.select.next"])
add(config, "dialog_select", "dialog.select.page_up", keybinds["dialog.select.page_up"])
add(config, "dialog_select", "dialog.select.page_down", keybinds["dialog.select.page_down"])
add(config, "dialog_select", "dialog.select.home", keybinds["dialog.select.home"])
add(config, "dialog_select", "dialog.select.end", keybinds["dialog.select.end"])
add(config, "dialog_select", "dialog.select.submit", keybinds["dialog.select.submit"])
add(config, "dialog_actions", "dialog.action.delete", combineBindings(keybinds.stash_delete, keybinds.session_delete))
add(config, "dialog_actions", "dialog.action.rename", keybinds.session_rename)
add(
config,
"dialog_actions",
"dialog.action.toggle",
combineBindings(keybinds["dialog.mcp.toggle"], keybinds["plugins.toggle"]),
)
add(config, "model", "model.dialog.provider", keybinds.model_provider_list)
add(config, "model", "model.dialog.favorite", keybinds.model_favorite_toggle)
add(config, "permission", "permission.reject.cancel", keybinds.app_exit)
add(config, "permission", "permission.prompt.escape", keybinds.app_exit)
add(config, "permission", "permission.prompt.fullscreen", keybinds["permission.prompt.fullscreen"])
add(config, "question", "question.reject", keybinds.app_exit)
add(config, "question", "question.edit.clear", keybinds.input_clear)
add(config, "plugins", "plugins.list", keybinds.plugin_manager)
add(config, "plugins", "plugin.dialog.install", keybinds["dialog.plugins.install"])
add(config, "home_tips", "tips.toggle", keybinds.tips_toggle)
return {
...(keybinds.leader && keybinds.leader !== "none" && { leader: keybinds.leader }),
sections: config,
}
}
export * as LegacyKeymapTransform from "./legacy-keymap-transform"
@@ -1,339 +1,12 @@
import z from "zod"
import type { KeyEvent, Renderable } from "@opentui/core"
import type { Binding } from "@opentui/keymap"
import type { ResolvedBindingSections } from "@opentui/keymap/extras"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigKeybinds } from "@/config/keybinds"
import { TuiKeybind } from "./keybind"
const KeybindOverride = z
.object(
Object.fromEntries(Object.keys(ConfigKeybinds.Keybinds.shape).map((key) => [key, z.string().optional()])) as Record<
string,
z.ZodOptional<z.ZodString>
>,
)
.strict()
const KeyStroke = z
.object({
name: z.string(),
ctrl: z.boolean().optional(),
shift: z.boolean().optional(),
meta: z.boolean().optional(),
super: z.boolean().optional(),
hyper: z.boolean().optional(),
})
.strict()
const KeymapBindingObject = z
.object({
key: z.union([z.string(), KeyStroke]),
event: z.enum(["press", "release"]).optional(),
preventDefault: z.boolean().optional(),
fallthrough: z.boolean().optional(),
})
.passthrough()
const KeymapBindingItem = z.union([z.string(), KeyStroke, KeymapBindingObject])
const KeymapBindingValue = z.union([z.literal(false), z.literal("none"), KeymapBindingItem, z.array(KeymapBindingItem)])
const keymapBinding = (value: z.input<typeof KeymapBindingValue> | (() => z.input<typeof KeymapBindingValue>)) =>
KeymapBindingValue.prefault(value)
const keymapSection = <Shape extends z.ZodRawShape>(shape: Shape) => {
const schema = z.object(shape).strict()
return schema.prefault({} as z.input<typeof schema>)
}
const keymapSectionInput = <Shape extends z.ZodRawShape>(shape: Shape) =>
z
.object(
Object.fromEntries(Object.keys(shape).map((key) => [key, KeymapBindingValue.optional()])) as {
[Key in keyof Shape]: z.ZodOptional<typeof KeymapBindingValue>
},
)
.strict()
const GlobalKeymapSection = {
"command.palette.show": keymapBinding("ctrl+p"),
"session.list": keymapBinding("<leader>l"),
"session.new": keymapBinding("<leader>n"),
"model.list": keymapBinding("<leader>m"),
"model.cycle_recent": keymapBinding("f2"),
"model.cycle_recent_reverse": keymapBinding("shift+f2"),
"model.cycle_favorite": keymapBinding("none"),
"model.cycle_favorite_reverse": keymapBinding("none"),
"agent.list": keymapBinding("<leader>a"),
"mcp.list": keymapBinding("none"),
"agent.cycle": keymapBinding("tab"),
"agent.cycle.reverse": keymapBinding("shift+tab"),
"variant.cycle": keymapBinding("ctrl+t"),
"variant.list": keymapBinding("none"),
"provider.connect": keymapBinding("none"),
"console.org.switch": keymapBinding("none"),
"opencode.status": keymapBinding("<leader>s"),
"theme.switch": keymapBinding("<leader>t"),
"theme.switch_mode": keymapBinding("none"),
"theme.mode.lock": keymapBinding("none"),
"help.show": keymapBinding("none"),
"docs.open": keymapBinding("none"),
"app.exit": keymapBinding("ctrl+c,ctrl+d,<leader>q"),
"app.debug": keymapBinding("none"),
"app.console": keymapBinding("none"),
"app.heap_snapshot": keymapBinding("none"),
"app.toggle.animations": keymapBinding("none"),
"app.toggle.file_context": keymapBinding("none"),
"app.toggle.diffwrap": keymapBinding("none"),
"app.toggle.paste_summary": keymapBinding("none"),
"app.toggle.session_directory_filter": keymapBinding("none"),
"terminal.suspend": keymapBinding(() => (process.platform === "win32" ? "none" : "ctrl+z")),
"terminal.title.toggle": keymapBinding("none"),
}
const WhichKeyKeymapSection = {
"tui-which-key.toggle": keymapBinding("ctrl+alt+k"),
"tui-which-key.layout.toggle": keymapBinding("ctrl+alt+shift+k"),
"tui-which-key.pending.toggle": keymapBinding("ctrl+alt+shift+p"),
"tui-which-key.group.previous": keymapBinding("ctrl+alt+left,ctrl+alt+["),
"tui-which-key.group.next": keymapBinding("ctrl+alt+right,ctrl+alt+]"),
"tui-which-key.scroll.up": keymapBinding("ctrl+alt+up,ctrl+alt+p"),
"tui-which-key.scroll.down": keymapBinding("ctrl+alt+down,ctrl+alt+n"),
"tui-which-key.page.up": keymapBinding("ctrl+alt+pageup"),
"tui-which-key.page.down": keymapBinding("ctrl+alt+pagedown"),
"tui-which-key.home": keymapBinding("ctrl+alt+home"),
"tui-which-key.end": keymapBinding("ctrl+alt+end"),
}
const SessionKeymapSection = {
"session.share": keymapBinding("none"),
"session.rename": keymapBinding("ctrl+r"),
"session.timeline": keymapBinding("<leader>g"),
"session.fork": keymapBinding("none"),
"session.compact": keymapBinding("<leader>c"),
"session.unshare": keymapBinding("none"),
"session.undo": keymapBinding("<leader>u"),
"session.redo": keymapBinding("<leader>r"),
"session.sidebar.toggle": keymapBinding("<leader>b"),
"session.toggle.conceal": keymapBinding("<leader>h"),
"session.toggle.timestamps": keymapBinding("none"),
"session.toggle.thinking": keymapBinding("none"),
"session.toggle.actions": keymapBinding("none"),
"session.toggle.scrollbar": keymapBinding("none"),
"session.toggle.generic_tool_output": keymapBinding("none"),
"session.page.up": keymapBinding("pageup,ctrl+alt+b"),
"session.page.down": keymapBinding("pagedown,ctrl+alt+f"),
"session.line.up": keymapBinding("ctrl+alt+y"),
"session.line.down": keymapBinding("ctrl+alt+e"),
"session.half.page.up": keymapBinding("ctrl+alt+u"),
"session.half.page.down": keymapBinding("ctrl+alt+d"),
"session.first": keymapBinding("ctrl+g,home"),
"session.last": keymapBinding("ctrl+alt+g,end"),
"session.messages_last_user": keymapBinding("none"),
"session.message.next": keymapBinding("none"),
"session.message.previous": keymapBinding("none"),
"messages.copy": keymapBinding("<leader>y"),
"session.copy": keymapBinding("none"),
"session.export": keymapBinding("<leader>x"),
"session.child.first": keymapBinding("<leader>down"),
"session.parent": keymapBinding("up"),
"session.child.next": keymapBinding("right"),
"session.child.previous": keymapBinding("left"),
}
const PromptKeymapSection = {
"prompt.submit": keymapBinding("none"),
"prompt.editor": keymapBinding("<leader>e"),
"prompt.editor_context.clear": keymapBinding("none"),
"prompt.skills": keymapBinding("none"),
"prompt.stash": keymapBinding("none"),
"prompt.stash.pop": keymapBinding("none"),
"prompt.stash.list": keymapBinding("none"),
"workspace.set": keymapBinding("none"),
"session.interrupt": keymapBinding("escape"),
"prompt.clear": keymapBinding("ctrl+c"),
"prompt.paste": keymapBinding({ key: "ctrl+v", preventDefault: false }),
"prompt.history.previous": keymapBinding("up"),
"prompt.history.next": keymapBinding("down"),
}
const AutocompleteKeymapSection = {
"prompt.autocomplete.prev": keymapBinding("up,ctrl+p"),
"prompt.autocomplete.next": keymapBinding("down,ctrl+n"),
"prompt.autocomplete.hide": keymapBinding("escape"),
"prompt.autocomplete.select": keymapBinding("return"),
"prompt.autocomplete.complete": keymapBinding("tab"),
}
const InputKeymapSection = {
"input.submit": keymapBinding("return"),
"input.newline": keymapBinding("shift+return,ctrl+return,alt+return,ctrl+j"),
"input.move.left": keymapBinding("left,ctrl+b"),
"input.move.right": keymapBinding("right,ctrl+f"),
"input.move.up": keymapBinding("up"),
"input.move.down": keymapBinding("down"),
"input.select.left": keymapBinding("shift+left"),
"input.select.right": keymapBinding("shift+right"),
"input.select.up": keymapBinding("shift+up"),
"input.select.down": keymapBinding("shift+down"),
"input.line.home": keymapBinding("ctrl+a"),
"input.line.end": keymapBinding("ctrl+e"),
"input.select.line.home": keymapBinding("ctrl+shift+a"),
"input.select.line.end": keymapBinding("ctrl+shift+e"),
"input.visual.line.home": keymapBinding("alt+a"),
"input.visual.line.end": keymapBinding("alt+e"),
"input.select.visual.line.home": keymapBinding("alt+shift+a"),
"input.select.visual.line.end": keymapBinding("alt+shift+e"),
"input.buffer.home": keymapBinding("home"),
"input.buffer.end": keymapBinding("end"),
"input.select.buffer.home": keymapBinding("shift+home"),
"input.select.buffer.end": keymapBinding("shift+end"),
"input.delete.line": keymapBinding("ctrl+shift+d"),
"input.delete.to.line.end": keymapBinding("ctrl+k"),
"input.delete.to.line.start": keymapBinding("ctrl+u"),
"input.backspace": keymapBinding("backspace,shift+backspace"),
"input.delete": keymapBinding("ctrl+d,delete,shift+delete"),
"input.undo": keymapBinding(() => (process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z")),
"input.redo": keymapBinding("ctrl+.,super+shift+z"),
"input.word.forward": keymapBinding("alt+f,alt+right,ctrl+right"),
"input.word.backward": keymapBinding("alt+b,alt+left,ctrl+left"),
"input.select.word.forward": keymapBinding("alt+shift+f,alt+shift+right"),
"input.select.word.backward": keymapBinding("alt+shift+b,alt+shift+left"),
"input.delete.word.forward": keymapBinding("alt+d,alt+delete,ctrl+delete"),
"input.delete.word.backward": keymapBinding("ctrl+w,ctrl+backspace,alt+backspace"),
"input.select.all": keymapBinding("super+a"),
}
const DialogSelectKeymapSection = {
"dialog.select.prev": keymapBinding("up,ctrl+p"),
"dialog.select.next": keymapBinding("down,ctrl+n"),
"dialog.select.page_up": keymapBinding("pageup"),
"dialog.select.page_down": keymapBinding("pagedown"),
"dialog.select.home": keymapBinding("home"),
"dialog.select.end": keymapBinding("end"),
"dialog.select.submit": keymapBinding("return"),
}
const DialogActionsKeymapSection = {
"dialog.action.toggle": keymapBinding("space"),
"dialog.action.delete": keymapBinding("ctrl+d"),
"dialog.action.rename": keymapBinding("ctrl+r"),
}
const ModelKeymapSection = {
"model.dialog.provider": keymapBinding("ctrl+a"),
"model.dialog.favorite": keymapBinding("ctrl+f"),
}
const PermissionKeymapSection = {
"permission.reject.cancel": keymapBinding("ctrl+c,ctrl+d,<leader>q"),
"permission.prompt.escape": keymapBinding("ctrl+c,ctrl+d,<leader>q"),
"permission.prompt.fullscreen": keymapBinding("ctrl+f"),
}
const QuestionKeymapSection = {
"question.reject": keymapBinding("ctrl+c,ctrl+d,<leader>q"),
"question.edit.clear": keymapBinding("ctrl+c"),
}
const PluginsKeymapSection = {
"plugins.list": keymapBinding("none"),
"plugins.install": keymapBinding("none"),
"plugin.dialog.install": keymapBinding("shift+i"),
}
const HomeTipsKeymapSection = {
"tips.toggle": keymapBinding("<leader>h"),
}
const KeymapSectionsShape = {
global: keymapSection(GlobalKeymapSection),
which_key: keymapSection(WhichKeyKeymapSection),
session: keymapSection(SessionKeymapSection),
prompt: keymapSection(PromptKeymapSection),
autocomplete: keymapSection(AutocompleteKeymapSection),
input: keymapSection(InputKeymapSection),
dialog_select: keymapSection(DialogSelectKeymapSection),
dialog_actions: keymapSection(DialogActionsKeymapSection),
model: keymapSection(ModelKeymapSection),
permission: keymapSection(PermissionKeymapSection),
question: keymapSection(QuestionKeymapSection),
plugins: keymapSection(PluginsKeymapSection),
home_tips: keymapSection(HomeTipsKeymapSection),
}
const KeymapSectionsInputShape = {
global: keymapSectionInput(GlobalKeymapSection).optional(),
which_key: keymapSectionInput(WhichKeyKeymapSection).optional(),
session: keymapSectionInput(SessionKeymapSection).optional(),
prompt: keymapSectionInput(PromptKeymapSection).optional(),
autocomplete: keymapSectionInput(AutocompleteKeymapSection).optional(),
input: keymapSectionInput(InputKeymapSection).optional(),
dialog_select: keymapSectionInput(DialogSelectKeymapSection).optional(),
dialog_actions: keymapSectionInput(DialogActionsKeymapSection).optional(),
model: keymapSectionInput(ModelKeymapSection).optional(),
permission: keymapSectionInput(PermissionKeymapSection).optional(),
question: keymapSectionInput(QuestionKeymapSection).optional(),
plugins: keymapSectionInput(PluginsKeymapSection).optional(),
home_tips: keymapSectionInput(HomeTipsKeymapSection).optional(),
}
export const KeymapSections = z.object(KeymapSectionsShape).strict().prefault({})
export type KeymapSections = z.output<typeof KeymapSections>
export type KeymapSection = keyof KeymapSections
export const KeymapSectionNames = Object.keys(KeymapSectionsShape) as KeymapSection[]
export const KeymapLeaderTimeoutDefault = 2000
export type KeymapInfo = {
leader: string
leader_timeout: number
} & ResolvedBindingSections<Renderable, KeyEvent, KeymapSection>
export const KeymapSectionGroups = {
global: "Global",
which_key: "System",
session: "Session",
prompt: "Prompt",
autocomplete: "Autocomplete",
input: "Text Editing",
dialog_select: "Dialog",
dialog_actions: "Dialog",
model: "Model",
permission: "Permission",
question: "Question",
plugins: "Plugins",
home_tips: "Home",
} satisfies Record<KeymapSection, string>
export function keymapBindingDefaults(input: { section: string; binding: Readonly<Binding<Renderable, KeyEvent>> }) {
if (input.binding.group !== undefined) return
if (!Object.hasOwn(KeymapSectionGroups, input.section)) return
return { group: KeymapSectionGroups[input.section as KeymapSection] }
}
export const KeymapConfig = z
.object({
leader: z.string().prefault("ctrl+x"),
leader_timeout: z
.number()
.int()
.positive()
.prefault(KeymapLeaderTimeoutDefault)
.describe("Leader key timeout in milliseconds"),
sections: KeymapSections,
})
.strict()
.describe("TUI keymap configuration")
export type KeymapConfig = z.output<typeof KeymapConfig>
const KeymapSectionsInput = z.object(KeymapSectionsInputShape).strict().optional()
export const KeymapConfigInput = z
.object({
leader: z.string().optional(),
leader_timeout: z.number().int().positive().optional().describe("Leader key timeout in milliseconds"),
sections: KeymapSectionsInput,
})
.strict()
.describe("TUI keymap configuration")
export type KeymapConfigInput = z.output<typeof KeymapConfigInput>
const KeymapLeaderTimeout = z.number().int().positive().describe("Leader key timeout in milliseconds")
export const TuiOptions = z.object({
leader_timeout: KeymapLeaderTimeout.optional(),
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
scroll_acceleration: z
.object({
@@ -352,17 +25,11 @@ export const TuiInfo = z
.object({
$schema: z.string().optional(),
theme: z.string().optional(),
keybinds: KeybindOverride.optional().meta({
deprecated: true,
description: "Use keymap instead. This will be removed in opencode v2.0.",
}),
keymap: KeymapConfigInput.optional(),
keybinds: TuiKeybind.KeybindOverrides.optional(),
plugin: ConfigPlugin.Spec.zod.array().optional(),
plugin_enabled: z.record(z.string(), z.boolean()).optional(),
})
.extend(TuiOptions.shape)
.strict()
export const TuiJsonSchemaInfo = TuiInfo.extend({
keymap: KeymapConfig.optional(),
}).strict()
export const TuiJsonSchemaInfo = TuiInfo
+13 -29
View File
@@ -1,29 +1,26 @@
export * as TuiConfig from "./tui"
import type z from "zod"
import type { KeyEvent, Renderable } from "@opentui/core"
import { resolveBindingSections, type BindingSectionsConfig } from "@opentui/keymap/extras"
import { createBindingLookup } from "@opentui/keymap/extras"
import { mergeDeep, unique } from "remeda"
import { Context, Effect, Fiber, Layer } from "effect"
import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths"
import { migrateTuiConfig } from "./tui-migrate"
import { KeymapConfig, TuiInfo, TuiJsonSchemaInfo } from "./tui-schema"
import { KeymapLeaderTimeoutDefault, TuiInfo, TuiJsonSchemaInfo } from "./tui-schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import { isRecord } from "@/util/record"
import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CurrentWorkingDirectory } from "./cwd"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigKeybinds } from "@/config/keybinds"
import { TuiKeybind } from "./keybind"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { ConfigVariable } from "@/config/variable"
import { Npm } from "@opencode-ai/core/npm"
import { LegacyKeymapTransform } from "./legacy-keymap-transform"
import { KeymapSectionNames, keymapBindingDefaults, type KeymapInfo, type KeymapSection } from "./tui-schema"
const log = Log.create({ service: "tui.config" })
@@ -36,9 +33,9 @@ type Acc = {
plugin_origins: ConfigPlugin.Origin[]
}
export type Resolved = Omit<Info, "keybinds" | "keymap"> & {
keybinds: ConfigKeybinds.Keybinds
keymap: KeymapInfo
export type Resolved = Omit<Info, "keybinds" | "leader_timeout"> & {
keybinds: TuiKeybind.BindingLookupView
leader_timeout: number
// Internal resolved plugin list used by runtime loading.
plugin_origins?: ConfigPlugin.Origin[]
}
@@ -186,31 +183,18 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
keybinds.terminal_suspend = "none"
keybinds.input_undo ??= unique([
"ctrl+z",
...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","),
...String(TuiKeybind.Keybinds.shape.input_undo.parse(undefined)).split(","),
]).join(",")
}
const parsedKeybinds = ConfigKeybinds.Keybinds.parse(keybinds)
const keymapInput = acc.result.keymap ?? LegacyKeymapTransform.create(acc.result.keybinds ?? {})
const keymapConfig = KeymapConfig.parse(keymapInput)
const keymap = {
leader: !keymapConfig.leader || keymapConfig.leader === "none" ? "ctrl+x" : keymapConfig.leader,
leader_timeout: keymapConfig.leader_timeout,
...resolveBindingSections<Renderable, KeyEvent, BindingSectionsConfig<Renderable, KeyEvent>, KeymapSection>(
keymapConfig.sections,
{
sections: KeymapSectionNames,
bindingDefaults: keymapBindingDefaults,
},
),
}
const parsedKeybinds = TuiKeybind.Keybinds.parse(keybinds)
const result: Resolved = {
...acc.result,
keybinds: parsedKeybinds,
keybinds: createBindingLookup(TuiKeybind.toBindingConfig(parsedKeybinds), {
commandMap: TuiKeybind.CommandMap,
bindingDefaults: TuiKeybind.bindingDefaults(),
}),
leader_timeout: acc.result.leader_timeout ?? KeymapLeaderTimeoutDefault,
plugin_origins: acc.plugin_origins.length ? acc.plugin_origins : undefined,
// `keybinds` is deprecated and will be removed in opencode v2.0. Keep it
// only as the legacy fallback; once `keymap` is configured, ignore
// `keybinds` for keymap resolution.
keymap,
}
return {