From 2dec4c178c3a124161490ca7236e308627eff15a Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 12 Mar 2026 18:09:48 -0400 Subject: [PATCH 1/6] fix: respect user keybinding configuration for input_submit/input_newline Make hardcoded default keybindings conditional so user configuration takes precedence. User bindings now come first in the array, and defaults are only added for actions the user hasn't configured. This fixes the issue where configuring: input_newline: return input_submit: ctrl+return,alt+return,shift+return Would fail because hardcoded defaults always matched first. Changes: - Check if user has configured submit/newline actions - Only add defaults for unconfigured actions - Put user bindings first so they take precedence --- .../cmd/tui/component/textarea-keybindings.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts index 36ab03de545c..d7df599ed7d9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts +++ b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts @@ -64,10 +64,25 @@ export function useTextareaKeybindings() { return createMemo(() => { const keybinds = keybind.all - return [ - { name: "return", action: "submit" }, - { name: "return", meta: true, action: "newline" }, - ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)), - ] satisfies KeyBinding[] + // Get user-defined bindings first + const userBindings = TEXTAREA_ACTIONS.flatMap((action) => + mapTextareaKeybindings(keybinds, action), + ) + + // Check if user has configured each action + const hasSubmitBinding = userBindings.some((b) => b.action === "submit") + const hasNewlineBinding = userBindings.some((b) => b.action === "newline") + + // Build defaults array, only adding defaults for unconfigured actions + const defaults: KeyBinding[] = [] + if (!hasSubmitBinding) { + defaults.push({ name: "return", action: "submit" }) + } + if (!hasNewlineBinding) { + defaults.push({ name: "return", meta: true, action: "newline" }) + } + + // User bindings come first so they take precedence over defaults + return [...userBindings, ...defaults] satisfies KeyBinding[] }) } From 1b5d18f1f700c721471744ea5c4733d1b343229d Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Fri, 13 Mar 2026 21:25:39 -0400 Subject: [PATCH 2/6] Fix: Allow submit keybinding to work with slash command autocomplete --- .../src/cli/cmd/tui/component/prompt/autocomplete.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3240afab326a..c412e11f0f3a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -566,7 +566,13 @@ export function Autocomplete(props: { } if (name === "return") { select() - e.preventDefault() + // After selection, autocomplete is hidden. If the key that triggered + // this is also the submit keybinding, we need to let the event + // propagate so the prompt can handle submission. + // Only prevent default if autocomplete is still visible (selection failed). + if (store.visible) { + e.preventDefault() + } return } if (name === "tab") { From 53ed71c2a76947c34bad841f57208ddc4b1ef63d Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 13 Apr 2026 12:14:56 -0400 Subject: [PATCH 3/6] fix: consume enter when prompt autocomplete selects --- .../src/cli/cmd/tui/component/prompt/autocomplete.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index c794d53ebd8b..2118fe98e1bc 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -569,13 +569,7 @@ export function Autocomplete(props: { } if (name === "return") { select() - // After selection, autocomplete is hidden. If the key that triggered - // this is also the submit keybinding, we need to let the event - // propagate so the prompt can handle submission. - // Only prevent default if autocomplete is still visible (selection failed). - if (store.visible) { - e.preventDefault() - } + e.preventDefault() return } if (name === "tab") { From e302ba98a02797222e6e4addea5fc5896d8d8374 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 16 Apr 2026 10:19:09 -0400 Subject: [PATCH 4/6] fix: honor configured submit key in autocomplete --- .../cmd/tui/component/prompt/autocomplete.tsx | 7 +++- .../test/cli/tui/autocomplete.test.ts | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/cli/tui/autocomplete.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 2118fe98e1bc..5440c82c7b15 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -6,6 +6,7 @@ import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Sh import { createStore } from "solid-js/store" import { useSDK } from "@tui/context/sdk" import { useSync } from "@tui/context/sync" +import { useKeybind } from "@tui/context/keybind" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" import { useTheme, selectedForeground } from "@tui/context/theme" @@ -65,6 +66,9 @@ export type AutocompleteOption = { path?: string } +export function accept(match: (key: string, evt: KeyEvent) => boolean, e: KeyEvent) { + return match("input_submit", e) +} export function Autocomplete(props: { value: string sessionID?: string @@ -79,6 +83,7 @@ export function Autocomplete(props: { }) { const sdk = useSDK() const sync = useSync() + const keybind = useKeybind() const command = useCommandDialog() const { theme } = useTheme() const dimensions = useTerminalDimensions() @@ -567,7 +572,7 @@ export function Autocomplete(props: { e.preventDefault() return } - if (name === "return") { + if (accept(keybind.match, e)) { select() e.preventDefault() return diff --git a/packages/opencode/test/cli/tui/autocomplete.test.ts b/packages/opencode/test/cli/tui/autocomplete.test.ts new file mode 100644 index 000000000000..02e6eb447315 --- /dev/null +++ b/packages/opencode/test/cli/tui/autocomplete.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test" +import type { KeyEvent } from "@opentui/core" +import { accept } from "../../../src/cli/cmd/tui/component/prompt/autocomplete" +import { Keybind } from "../../../src/util/keybind" + +function match(cfg: string) { + return (key: string, evt: KeyEvent) => { + if (key !== "input_submit") return false + const info = Keybind.fromParsedKey(evt) + return Keybind.parse(cfg).some((item) => Keybind.match(item, info)) + } +} + +function key(evt: Partial): KeyEvent { + return { + name: evt.name ?? "return", + ctrl: evt.ctrl ?? false, + meta: evt.meta ?? false, + shift: evt.shift ?? false, + super: evt.super ?? false, + sequence: evt.sequence ?? "", + code: evt.code, + option: evt.option, + raw: evt.raw, + capsLock: evt.capsLock, + numLock: evt.numLock, + } as KeyEvent +} + +describe("autocomplete accept", () => { + test("uses configured submit binding", () => { + expect(accept(match("ctrl+return,alt+return,shift+return"), key({ ctrl: true }))).toBe(true) + expect(accept(match("ctrl+return,alt+return,shift+return"), key({}))).toBe(false) + }) + + test("allows plain return when configured", () => { + expect(accept(match("return"), key({}))).toBe(true) + expect(accept(match("return"), key({ ctrl: true }))).toBe(false) + }) +}) From 0d10e9b710b6aa17cd61c8dd11b0c12d356d719f Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 16 Apr 2026 11:06:35 -0400 Subject: [PATCH 5/6] chore: inline autocomplete submit match --- .../cmd/tui/component/prompt/autocomplete.tsx | 5 +-- .../test/cli/tui/autocomplete.test.ts | 40 ------------------- 2 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 packages/opencode/test/cli/tui/autocomplete.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 5440c82c7b15..9be4668f806f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -66,9 +66,6 @@ export type AutocompleteOption = { path?: string } -export function accept(match: (key: string, evt: KeyEvent) => boolean, e: KeyEvent) { - return match("input_submit", e) -} export function Autocomplete(props: { value: string sessionID?: string @@ -572,7 +569,7 @@ export function Autocomplete(props: { e.preventDefault() return } - if (accept(keybind.match, e)) { + if (keybind.match("input_submit", e)) { select() e.preventDefault() return diff --git a/packages/opencode/test/cli/tui/autocomplete.test.ts b/packages/opencode/test/cli/tui/autocomplete.test.ts deleted file mode 100644 index 02e6eb447315..000000000000 --- a/packages/opencode/test/cli/tui/autocomplete.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { KeyEvent } from "@opentui/core" -import { accept } from "../../../src/cli/cmd/tui/component/prompt/autocomplete" -import { Keybind } from "../../../src/util/keybind" - -function match(cfg: string) { - return (key: string, evt: KeyEvent) => { - if (key !== "input_submit") return false - const info = Keybind.fromParsedKey(evt) - return Keybind.parse(cfg).some((item) => Keybind.match(item, info)) - } -} - -function key(evt: Partial): KeyEvent { - return { - name: evt.name ?? "return", - ctrl: evt.ctrl ?? false, - meta: evt.meta ?? false, - shift: evt.shift ?? false, - super: evt.super ?? false, - sequence: evt.sequence ?? "", - code: evt.code, - option: evt.option, - raw: evt.raw, - capsLock: evt.capsLock, - numLock: evt.numLock, - } as KeyEvent -} - -describe("autocomplete accept", () => { - test("uses configured submit binding", () => { - expect(accept(match("ctrl+return,alt+return,shift+return"), key({ ctrl: true }))).toBe(true) - expect(accept(match("ctrl+return,alt+return,shift+return"), key({}))).toBe(false) - }) - - test("allows plain return when configured", () => { - expect(accept(match("return"), key({}))).toBe(true) - expect(accept(match("return"), key({ ctrl: true }))).toBe(false) - }) -}) From 4a2460195c60a72618fe7715ab6070889e0858eb Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 16 Apr 2026 11:17:43 -0400 Subject: [PATCH 6/6] chore: inline textarea binding checks --- .../src/cli/cmd/tui/component/textarea-keybindings.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts index d7df599ed7d9..c2598097a5bb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts +++ b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts @@ -69,16 +69,12 @@ export function useTextareaKeybindings() { mapTextareaKeybindings(keybinds, action), ) - // Check if user has configured each action - const hasSubmitBinding = userBindings.some((b) => b.action === "submit") - const hasNewlineBinding = userBindings.some((b) => b.action === "newline") - // Build defaults array, only adding defaults for unconfigured actions const defaults: KeyBinding[] = [] - if (!hasSubmitBinding) { + if (!userBindings.some((b) => b.action === "submit")) { defaults.push({ name: "return", action: "submit" }) } - if (!hasNewlineBinding) { + if (!userBindings.some((b) => b.action === "newline")) { defaults.push({ name: "return", meta: true, action: "newline" }) }