From e0bb867948251648488c3b04a7ffab04794f494f Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sat, 7 Feb 2026 19:18:39 +0000 Subject: [PATCH] feat(ui): add enter-to-submit toggle for prompt input --- packages/server/src/config/schema.ts | 1 + packages/ui/src/App.tsx | 2 + packages/ui/src/components/prompt-input.tsx | 79 +++++++++++++++++-- packages/ui/src/lib/hooks/use-commands.ts | 13 +++ .../ui/src/lib/i18n/messages/en/commands.ts | 5 ++ .../ui/src/lib/i18n/messages/es/commands.ts | 5 ++ .../ui/src/lib/i18n/messages/fr/commands.ts | 5 ++ .../ui/src/lib/i18n/messages/ja/commands.ts | 5 ++ .../ui/src/lib/i18n/messages/ru/commands.ts | 5 ++ .../src/lib/i18n/messages/zh-Hans/commands.ts | 5 ++ packages/ui/src/stores/preferences.tsx | 10 +++ 11 files changed, 128 insertions(+), 7 deletions(-) diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts index 10b6b325..8da05a62 100644 --- a/packages/server/src/config/schema.ts +++ b/packages/server/src/config/schema.ts @@ -12,6 +12,7 @@ const PreferencesSchema = z.object({ showThinkingBlocks: z.boolean().default(false), thinkingBlocksExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), showTimelineTools: z.boolean().default(true), + promptSubmitOnEnter: z.boolean().default(false), lastUsedBinary: z.string().optional(), locale: z.string().optional(), environmentVariables: z.record(z.string()).default({}), diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 81064972..81f45ae9 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -60,6 +60,7 @@ const App: Component = () => { toggleShowTimelineTools, toggleAutoCleanupBlankSessions, toggleUsageMetrics, + togglePromptSubmitOnEnter, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, @@ -271,6 +272,7 @@ const App: Component = () => { toggleShowThinkingBlocks, toggleShowTimelineTools, toggleUsageMetrics, + togglePromptSubmitOnEnter, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 28e3e172..f5f2d050 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -16,6 +16,7 @@ import { getCommands } from "../stores/commands" import { showAlertDialog } from "../stores/alerts" import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" +import { preferences } from "../stores/preferences" const log = getLogger("actions") @@ -542,13 +543,46 @@ export default function PromptInput(props: PromptInputProps) { } } - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { - e.preventDefault() - if (showPicker()) { - handlePickerClose() + if (e.key === "Enter") { + const isModified = e.metaKey || e.ctrlKey + + // If the picker is open, Enter should select from it. + if (!isModified && showPicker()) { + return + } + + if (submitOnEnter()) { + // Swapped mode: Enter submits, Cmd/Ctrl+Enter inserts a newline. + if (isModified) { + e.preventDefault() + e.stopPropagation() + insertNewlineAtCursor() + return + } + + // Shift+Enter always inserts a newline. + if (e.shiftKey) { + // If the picker is open, avoid selecting an item on Enter. + if (showPicker()) { + e.stopPropagation() + } + return + } + + e.preventDefault() + handleSend() + return + } + + // Default: Cmd/Ctrl+Enter submits. + if (isModified) { + e.preventDefault() + if (showPicker()) { + handlePickerClose() + } + handleSend() + return } - handleSend() - return } if (e.key === "ArrowUp") { @@ -1056,6 +1090,25 @@ export default function PromptInput(props: PromptInputProps) { : { key: "!", text: t("promptInput.hints.shell.enable") } const commandHint = () => ({ key: "/", text: t("promptInput.hints.commands") }) + const submitOnEnter = () => preferences().promptSubmitOnEnter + + function insertNewlineAtCursor() { + const textarea = textareaRef + const current = prompt() + const start = textarea ? textarea.selectionStart : current.length + const end = textarea ? textarea.selectionEnd : current.length + const nextValue = current.substring(0, start) + "\n" + current.substring(end) + const nextCursor = start + 1 + + setPrompt(nextValue) + + setTimeout(() => { + if (!textareaRef) return + textareaRef.focus() + textareaRef.setSelectionRange(nextCursor, nextCursor) + }, 0) + } + const shouldShowOverlay = () => prompt().length === 0 const instance = () => getActiveInstance() @@ -1142,7 +1195,19 @@ export default function PromptInput(props: PromptInputProps) { fallback={ <> - Enter {t("promptInput.overlay.newLine")} • {t("promptInput.overlay.send")} • @ {t("promptInput.overlay.filesAgents")} • ↑↓ {t("promptInput.overlay.history")} + + Enter {t("promptInput.overlay.newLine")} • {t("promptInput.overlay.send")} + + } + > + <> + Enter {t("promptInput.overlay.send")} • {t("promptInput.overlay.newLine")} + + + {" "}• @ {t("promptInput.overlay.filesAgents")} • ↑↓ {t("promptInput.overlay.history")} 0}> {t("promptInput.overlay.attachments", { count: attachments().length })} diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index 0dd480e2..bdd2e8d5 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -31,6 +31,7 @@ export interface UseCommandsOptions { toggleShowTimelineTools: () => void toggleUsageMetrics: () => void toggleAutoCleanupBlankSessions: () => void + togglePromptSubmitOnEnter: () => void setDiffViewMode: (mode: "split" | "unified") => void setToolOutputExpansion: (mode: ExpansionPreference) => void setDiagnosticsExpansion: (mode: ExpansionPreference) => void @@ -423,6 +424,18 @@ export function useCommands(options: UseCommandsOptions) { }, }) + commandRegistry.register({ + id: "prompt-submit-shortcut", + label: () => + options.preferences().promptSubmitOnEnter + ? tGlobal("commands.promptSubmitShortcut.label.swapped") + : tGlobal("commands.promptSubmitShortcut.label.default"), + description: () => tGlobal("commands.promptSubmitShortcut.description"), + category: "Input & Focus", + keywords: () => splitKeywords("commands.promptSubmitShortcut.keywords"), + action: options.togglePromptSubmitOnEnter, + }) + commandRegistry.register({ id: "thinking", label: () => tGlobal(options.preferences().showThinkingBlocks ? "commands.thinkingBlocks.label.hide" : "commands.thinkingBlocks.label.show"), diff --git a/packages/ui/src/lib/i18n/messages/en/commands.ts b/packages/ui/src/lib/i18n/messages/en/commands.ts index 84758e65..66ff78f7 100644 --- a/packages/ui/src/lib/i18n/messages/en/commands.ts +++ b/packages/ui/src/lib/i18n/messages/en/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "Clear the prompt textarea", "commands.clearInput.keywords": "clear, reset", + "commands.promptSubmitShortcut.label.default": "Enter: New Line, Cmd/Ctrl+Enter: Submit Prompt", + "commands.promptSubmitShortcut.label.swapped": "Enter: Submit Prompt, Cmd/Ctrl+Enter: New Line", + "commands.promptSubmitShortcut.description": "Swap Enter and Cmd/Ctrl+Enter behavior in the prompt input", + "commands.promptSubmitShortcut.keywords": "enter, cmd, ctrl, submit, send, newline, shortcut, keybind, prompt", + "commands.thinkingBlocks.label.show": "Show Thinking", "commands.thinkingBlocks.label.hide": "Hide Thinking", "commands.thinkingBlocks.description": "Show or hide AI thinking sections", diff --git a/packages/ui/src/lib/i18n/messages/es/commands.ts b/packages/ui/src/lib/i18n/messages/es/commands.ts index fea31cd1..0bad4e2f 100644 --- a/packages/ui/src/lib/i18n/messages/es/commands.ts +++ b/packages/ui/src/lib/i18n/messages/es/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "Borrar el área de texto del prompt", "commands.clearInput.keywords": "limpiar, reiniciar", + "commands.promptSubmitShortcut.label.default": "Enter: Nueva linea, Cmd/Ctrl+Enter: Enviar prompt", + "commands.promptSubmitShortcut.label.swapped": "Enter: Enviar prompt, Cmd/Ctrl+Enter: Nueva linea", + "commands.promptSubmitShortcut.description": "Intercambiar el comportamiento de Enter y Cmd/Ctrl+Enter en la entrada de prompt", + "commands.promptSubmitShortcut.keywords": "enter, enviar, salto de linea, atajo, teclado, cmd, ctrl, prompt", + "commands.thinkingBlocks.label.show": "Mostrar pensamiento", "commands.thinkingBlocks.label.hide": "Ocultar pensamiento", "commands.thinkingBlocks.description": "Mostrar u ocultar secciones de pensamiento de IA", diff --git a/packages/ui/src/lib/i18n/messages/fr/commands.ts b/packages/ui/src/lib/i18n/messages/fr/commands.ts index 826b8a00..52bdea76 100644 --- a/packages/ui/src/lib/i18n/messages/fr/commands.ts +++ b/packages/ui/src/lib/i18n/messages/fr/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "Effacer la zone de texte du prompt", "commands.clearInput.keywords": "effacer, réinitialiser, prompt", + "commands.promptSubmitShortcut.label.default": "Entree: Nouvelle ligne, Cmd/Ctrl+Entree: Envoyer le prompt", + "commands.promptSubmitShortcut.label.swapped": "Entree: Envoyer le prompt, Cmd/Ctrl+Entree: Nouvelle ligne", + "commands.promptSubmitShortcut.description": "Echanger le comportement de Entree et Cmd/Ctrl+Entree dans la saisie du prompt", + "commands.promptSubmitShortcut.keywords": "entree, envoyer, nouvelle ligne, raccourci, cmd, ctrl, prompt", + "commands.thinkingBlocks.label.show": "Afficher la réflexion", "commands.thinkingBlocks.label.hide": "Masquer la réflexion", "commands.thinkingBlocks.description": "Afficher ou masquer les sections de réflexion de l'IA", diff --git a/packages/ui/src/lib/i18n/messages/ja/commands.ts b/packages/ui/src/lib/i18n/messages/ja/commands.ts index 809c3a03..30a2adc5 100644 --- a/packages/ui/src/lib/i18n/messages/ja/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ja/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "プロンプト入力欄をクリア", "commands.clearInput.keywords": "クリア, リセット, clear, reset", + "commands.promptSubmitShortcut.label.default": "Enter: 改行, Cmd/Ctrl+Enter: プロンプト送信", + "commands.promptSubmitShortcut.label.swapped": "Enter: プロンプト送信, Cmd/Ctrl+Enter: 改行", + "commands.promptSubmitShortcut.description": "プロンプト入力で Enter と Cmd/Ctrl+Enter の動作を入れ替え", + "commands.promptSubmitShortcut.keywords": "enter, 送信, 改行, ショートカット, cmd, ctrl, プロンプト", + "commands.thinkingBlocks.label.show": "思考を表示", "commands.thinkingBlocks.label.hide": "思考を非表示", "commands.thinkingBlocks.description": "AI の思考セクションを表示/非表示", diff --git a/packages/ui/src/lib/i18n/messages/ru/commands.ts b/packages/ui/src/lib/i18n/messages/ru/commands.ts index 1c2ad298..6c3f28ec 100644 --- a/packages/ui/src/lib/i18n/messages/ru/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ru/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "Очистить поле prompt", "commands.clearInput.keywords": "очистить, сброс", + "commands.promptSubmitShortcut.label.default": "Enter: Новая строка, Cmd/Ctrl+Enter: Отправить промпт", + "commands.promptSubmitShortcut.label.swapped": "Enter: Отправить промпт, Cmd/Ctrl+Enter: Новая строка", + "commands.promptSubmitShortcut.description": "Поменять местами Enter и Cmd/Ctrl+Enter в поле ввода промпта", + "commands.promptSubmitShortcut.keywords": "enter, отправить, новая строка, сочетание, cmd, ctrl, промпт", + "commands.thinkingBlocks.label.show": "Показать размышления", "commands.thinkingBlocks.label.hide": "Скрыть размышления", "commands.thinkingBlocks.description": "Показать или скрыть секции размышлений ИИ", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts index 6a6f9351..9c95f63e 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts @@ -82,6 +82,11 @@ export const commandMessages = { "commands.clearInput.description": "清空 prompt 输入框", "commands.clearInput.keywords": "clear, reset, 清空, 重置", + "commands.promptSubmitShortcut.label.default": "Enter: 换行, Cmd/Ctrl+Enter: 提交提示词", + "commands.promptSubmitShortcut.label.swapped": "Enter: 提交提示词, Cmd/Ctrl+Enter: 换行", + "commands.promptSubmitShortcut.description": "在提示词输入框中交换 Enter 与 Cmd/Ctrl+Enter 的行为", + "commands.promptSubmitShortcut.keywords": "enter, 换行, 提交, 发送, 快捷键, cmd, ctrl, 提示词", + "commands.thinkingBlocks.label.show": "显示思考", "commands.thinkingBlocks.label.hide": "隐藏思考", "commands.thinkingBlocks.description": "显示或隐藏 AI 思考部分", diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index f3213c6e..2d28160f 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -36,6 +36,7 @@ export interface Preferences { showThinkingBlocks: boolean thinkingBlocksExpansion: ExpansionPreference showTimelineTools: boolean + promptSubmitOnEnter: boolean lastUsedBinary?: string locale?: string environmentVariables: Record @@ -73,6 +74,7 @@ const defaultPreferences: Preferences = { showThinkingBlocks: false, thinkingBlocksExpansion: "expanded", showTimelineTools: true, + promptSubmitOnEnter: false, environmentVariables: {}, modelRecents: [], modelFavorites: [], @@ -120,6 +122,7 @@ function normalizePreferences(pref?: Partial & { agentModelSelectio showThinkingBlocks: sanitized.showThinkingBlocks ?? defaultPreferences.showThinkingBlocks, thinkingBlocksExpansion: sanitized.thinkingBlocksExpansion ?? defaultPreferences.thinkingBlocksExpansion, showTimelineTools: sanitized.showTimelineTools ?? defaultPreferences.showTimelineTools, + promptSubmitOnEnter: sanitized.promptSubmitOnEnter ?? defaultPreferences.promptSubmitOnEnter, lastUsedBinary: sanitized.lastUsedBinary ?? defaultPreferences.lastUsedBinary, locale: sanitized.locale ?? defaultPreferences.locale, environmentVariables, @@ -381,6 +384,10 @@ function toggleUsageMetrics(): void { updatePreferences({ showUsageMetrics: !preferences().showUsageMetrics }) } +function togglePromptSubmitOnEnter(): void { + updatePreferences({ promptSubmitOnEnter: !preferences().promptSubmitOnEnter }) +} + function toggleAutoCleanupBlankSessions(): void { const nextValue = !preferences().autoCleanupBlankSessions log.info("toggle auto cleanup", { value: nextValue }) @@ -490,6 +497,7 @@ interface ConfigContextValue { toggleShowTimelineTools: typeof toggleShowTimelineTools toggleUsageMetrics: typeof toggleUsageMetrics toggleAutoCleanupBlankSessions: typeof toggleAutoCleanupBlankSessions + togglePromptSubmitOnEnter: typeof togglePromptSubmitOnEnter setDiffViewMode: typeof setDiffViewMode setToolOutputExpansion: typeof setToolOutputExpansion @@ -526,6 +534,7 @@ const configContextValue: ConfigContextValue = { toggleShowTimelineTools, toggleUsageMetrics, toggleAutoCleanupBlankSessions, + togglePromptSubmitOnEnter, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, @@ -585,6 +594,7 @@ export { toggleShowTimelineTools, toggleAutoCleanupBlankSessions, toggleUsageMetrics, + togglePromptSubmitOnEnter, recentFolders, addRecentFolder, removeRecentFolder,