Skip to content

Commit b7e445b

Browse files
fix(app): make prompt submit and newline rebindable
Register `prompt.submit` (default `enter`) and `prompt.newline` (default `shift+enter`) as web commands so they appear in the existing Settings → Keyboard Shortcuts UI alongside every other rebindable shortcut. `handleKeyDown` in prompt-input.tsx now dispatches via `matchKeybind` against the configured keybinds instead of hardcoded `event.key === "Enter"` checks. Defaults preserve current behavior byte-for-byte. Related: #16226, #11898, #9836
1 parent 33b2795 commit b7e445b

File tree

2 files changed

+51
-6
lines changed

2 files changed

+51
-6
lines changed

packages/app/src/components/prompt-input.tsx

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { Select } from "@opencode-ai/ui/select"
2828
import { useDialog } from "@opencode-ai/ui/context/dialog"
2929
import { ModelSelectorPopover } from "@/components/dialog-select-model"
3030
import { useProviders } from "@/hooks/use-providers"
31-
import { useCommand } from "@/context/command"
31+
import { matchKeybind, parseKeybind, useCommand } from "@/context/command"
32+
import { useSettings } from "@/context/settings"
3233
import { Persist, persisted } from "@/utils/persist"
3334
import { usePermission } from "@/context/permission"
3435
import { useLanguage } from "@/context/language"
@@ -100,6 +101,11 @@ const EXAMPLES = [
100101

101102
const NON_EMPTY_TEXT = /[^\s\u200B]/
102103

104+
const PROMPT_SUBMIT_ID = "prompt.submit"
105+
const PROMPT_NEWLINE_ID = "prompt.newline"
106+
const DEFAULT_PROMPT_SUBMIT_KEYBIND = "enter"
107+
const DEFAULT_PROMPT_NEWLINE_KEYBIND = "shift+enter"
108+
103109
export const PromptInput: Component<PromptInputProps> = (props) => {
104110
const sdk = useSDK()
105111

@@ -112,9 +118,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
112118
const dialog = useDialog()
113119
const providers = useProviders()
114120
const command = useCommand()
121+
const settings = useSettings()
115122
const permission = usePermission()
116123
const language = useLanguage()
117124
const platform = usePlatform()
125+
126+
// Register Enter / Shift+Enter as rebindable commands so they surface in
127+
// the Keyboard Shortcuts settings UI (grouped under "Prompt" via the
128+
// "prompt." id prefix). The actual dispatch stays local in handleKeyDown
129+
// below so the editor always wins over the global keymap.
130+
//
131+
// `disabled: true` excludes these from the global keymap in context/command
132+
// so a focused non-editable target (e.g. a <button>) pressing Enter isn't
133+
// silently swallowed by the global handler. The catalog entries remain
134+
// visible in settings-keybinds.tsx because catalog population doesn't
135+
// filter by disabled.
136+
command.register(() => [
137+
{
138+
id: PROMPT_SUBMIT_ID,
139+
title: language.t("command.prompt.submit"),
140+
keybind: DEFAULT_PROMPT_SUBMIT_KEYBIND,
141+
disabled: true,
142+
},
143+
{
144+
id: PROMPT_NEWLINE_ID,
145+
title: language.t("command.prompt.newline"),
146+
keybind: DEFAULT_PROMPT_NEWLINE_KEYBIND,
147+
disabled: true,
148+
},
149+
])
150+
151+
const submitKeybinds = createMemo(() =>
152+
parseKeybind(settings.keybinds.get(PROMPT_SUBMIT_ID) ?? DEFAULT_PROMPT_SUBMIT_KEYBIND),
153+
)
154+
const newlineKeybinds = createMemo(() =>
155+
parseKeybind(settings.keybinds.get(PROMPT_NEWLINE_ID) ?? DEFAULT_PROMPT_NEWLINE_KEYBIND),
156+
)
118157
const { params, tabs, view } = useSessionLayout()
119158
let editorRef!: HTMLDivElement
120159
let fileInputRef: HTMLInputElement | undefined
@@ -1165,11 +1204,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
11651204
}
11661205
}
11671206

1168-
// Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input
1169-
// and should always insert a newline regardless of composition state
1170-
if (event.key === "Enter" && event.shiftKey) {
1207+
// Handle configured newline keybind BEFORE the IME check - the default
1208+
// (Shift+Enter) is never used for IME input and should always insert a
1209+
// newline regardless of composition state. Users who rebind the newline
1210+
// keybind to unmodified Enter accept that this takes precedence over IME.
1211+
if (matchKeybind(newlineKeybinds(), event)) {
11711212
addPart({ type: "text", content: "\n", start: 0, end: 0 })
11721213
event.preventDefault()
1214+
event.stopPropagation()
11731215
return
11741216
}
11751217

@@ -1232,9 +1274,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12321274
return
12331275
}
12341276

1235-
// Note: Shift+Enter is handled earlier, before IME check
1236-
if (event.key === "Enter" && !event.shiftKey) {
1277+
// Note: the newline keybind is handled earlier, before the IME check.
1278+
if (matchKeybind(submitKeybinds(), event)) {
12371279
event.preventDefault()
1280+
event.stopPropagation()
12381281
if (event.repeat) return
12391282
if (
12401283
working() &&

packages/app/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export const dict = {
7373
"command.model.variant.cycle.description": "Switch to the next effort level",
7474
"command.prompt.mode.shell": "Shell",
7575
"command.prompt.mode.normal": "Prompt",
76+
"command.prompt.submit": "Submit prompt",
77+
"command.prompt.newline": "Insert newline in prompt",
7678
"command.permissions.autoaccept.enable": "Auto-accept permissions",
7779
"command.permissions.autoaccept.disable": "Stop auto-accepting permissions",
7880
"command.workspace.toggle": "Toggle workspaces",

0 commit comments

Comments
 (0)