diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a56a00fc355a..1c8e878e9af3 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -67,6 +67,7 @@ export const globalSettingsSchema = z.object({ alwaysAllowFollowupQuestions: z.boolean().optional(), followupAutoApproveTimeoutMs: z.number().optional(), alwaysAllowUpdateTodoList: z.boolean().optional(), + requireCtrlEnterToSend: z.boolean().optional(), allowedCommands: z.array(z.string()).optional(), deniedCommands: z.array(z.string()).optional(), commandExecutionTimeout: z.number().optional(), diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6a7a84b74b94..abf7ce124c98 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1793,6 +1793,7 @@ export class ClineProvider terminalCompressProgressBar, historyPreviewCollapsed, reasoningBlockCollapsed, + requireCtrlEnterToSend, cloudUserInfo, cloudIsAuthenticated, sharingEnabled, @@ -1927,6 +1928,7 @@ export class ClineProvider hasSystemPromptOverride, historyPreviewCollapsed: historyPreviewCollapsed ?? false, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, + requireCtrlEnterToSend: requireCtrlEnterToSend ?? false, cloudUserInfo, cloudIsAuthenticated: cloudIsAuthenticated ?? false, cloudOrganizations, @@ -2142,6 +2144,7 @@ export class ClineProvider maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, + requireCtrlEnterToSend: stateValues.requireCtrlEnterToSend ?? false, cloudUserInfo, cloudIsAuthenticated, sharingEnabled, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index af5f9925c353..179c28fbc15c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1621,6 +1621,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true) // No need to call postStateToWebview here as the UI already updated optimistically break + case "requireCtrlEnterToSend": + await updateGlobalState("requireCtrlEnterToSend", message.bool) + // No need to call postStateToWebview here as the UI already updated optimistically + break case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 66f389f81c10..20503e8a7660 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -126,8 +126,11 @@ export interface ExtensionMessage { | "insertTextIntoTextarea" | "dismissedUpsells" | "organizationSwitchResult" + | "requireCtrlEnterToSend" + | "usagePreviewData" text?: string payload?: any // Add a generic payload for now, can refine later + bool?: boolean action?: | "chatButtonClicked" | "mcpButtonClicked" @@ -288,6 +291,7 @@ export type ExtensionState = Pick< | "openRouterImageGenerationSelectedModel" | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" + | "requireCtrlEnterToSend" > & { version: string clineMessages: ClineMessage[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d43a2fce0434..363feed30f30 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -195,6 +195,7 @@ export interface WebviewMessage { | "profileThresholds" | "setHistoryPreviewCollapsed" | "setReasoningBlockCollapsed" + | "requireCtrlEnterToSend" | "openExternal" | "filterMarketplaceItems" | "marketplaceButtonClicked" diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c7813372fa79..6aa1983db574 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -11,6 +11,7 @@ import { ExtensionMessage } from "@roo/ExtensionMessage" import { vscode } from "@src/utils/vscode" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useAppTranslation } from "@src/i18n/TranslationContext" +import { getSendMessageKeyCombination } from "@src/utils/platform" import { ContextMenuOptionType, getContextMenuOptions, @@ -89,6 +90,7 @@ export const ChatTextArea = forwardRef( clineMessages, commands, cloudUserInfo, + requireCtrlEnterToSend, } = useExtensionState() // Find the ID and display text for the currently selected API configuration. @@ -468,6 +470,11 @@ export const ChatTextArea = forwardRef( } if (event.key === "Enter" && !event.shiftKey && !isComposing) { + // If Ctrl+Enter is required but neither Ctrl nor Meta (Cmd) key is pressed, don't send + if (requireCtrlEnterToSend && !event.ctrlKey && !event.metaKey) { + return + } + event.preventDefault() // Always call onSend - let ChatView handle queueing when disabled @@ -536,6 +543,7 @@ export const ChatTextArea = forwardRef( handleHistoryNavigation, resetHistoryNavigation, commands, + requireCtrlEnterToSend, ], ) @@ -1154,7 +1162,13 @@ export const ChatTextArea = forwardRef( )} - +