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,