Skip to content

Commit 71ac954

Browse files
committed
new: Ctrl+Enter to send message to chat draft
1 parent 5a3f911 commit 71ac954

File tree

9 files changed

+61
-1
lines changed

9 files changed

+61
-1
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const globalSettingsSchema = z.object({
6767
alwaysAllowFollowupQuestions: z.boolean().optional(),
6868
followupAutoApproveTimeoutMs: z.number().optional(),
6969
alwaysAllowUpdateTodoList: z.boolean().optional(),
70+
requireCtrlEnterToSend: z.boolean().optional(),
7071
allowedCommands: z.array(z.string()).optional(),
7172
deniedCommands: z.array(z.string()).optional(),
7273
commandExecutionTimeout: z.number().optional(),

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ export const webviewMessageHandler = async (
577577
await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
578578
await provider.postStateToWebview()
579579
break
580+
case "requireCtrlEnterToSend":
581+
await updateGlobalState("requireCtrlEnterToSend", message.bool ?? false)
582+
await provider.postStateToWebview()
583+
break
580584
case "askResponse":
581585
provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
582586
break
@@ -1454,6 +1458,10 @@ export const webviewMessageHandler = async (
14541458
Terminal.setTerminalZdotdir(message.bool)
14551459
}
14561460
break
1461+
case "requireCtrlEnterToSend":
1462+
await updateGlobalState("requireCtrlEnterToSend", message.bool)
1463+
await provider.postStateToWebview()
1464+
break
14571465
case "terminalCompressProgressBar":
14581466
await updateGlobalState("terminalCompressProgressBar", message.bool)
14591467
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export type ExtensionState = Pick<
232232
| "alwaysAllowFollowupQuestions"
233233
| "alwaysAllowExecute"
234234
| "alwaysAllowUpdateTodoList"
235+
| "requireCtrlEnterToSend"
235236
| "followupAutoApproveTimeoutMs"
236237
| "allowedCommands"
237238
| "deniedCommands"

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface WebviewMessage {
4848
| "alwaysAllowExecute"
4949
| "alwaysAllowFollowupQuestions"
5050
| "alwaysAllowUpdateTodoList"
51+
| "requireCtrlEnterToSend"
5152
| "followupAutoApproveTimeoutMs"
5253
| "webviewDidLaunch"
5354
| "newTask"

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
8989
clineMessages,
9090
commands,
9191
cloudUserInfo,
92+
requireCtrlEnterToSend,
9293
} = useExtensionState()
9394

9495
// Find the ID and display text for the currently selected API configuration.
@@ -468,6 +469,11 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
468469
}
469470

470471
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
472+
// If Ctrl+Enter is required but Ctrl key is not pressed, don't send
473+
if (requireCtrlEnterToSend && !event.ctrlKey) {
474+
return
475+
}
476+
471477
event.preventDefault()
472478

473479
// Always call onSend - let ChatView handle queueing when disabled
@@ -536,6 +542,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
536542
handleHistoryNavigation,
537543
resetHistoryNavigation,
538544
commands,
545+
requireCtrlEnterToSend,
539546
],
540547
)
541548

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
195195
openRouterImageApiKey,
196196
openRouterImageGenerationSelectedModel,
197197
reasoningBlockCollapsed,
198+
requireCtrlEnterToSend,
198199
} = cachedState
199200

200201
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -384,6 +385,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
384385
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
385386
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
386387
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
388+
vscode.postMessage({ type: "requireCtrlEnterToSend", bool: requireCtrlEnterToSend ?? false })
387389
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
388390
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
389391
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
@@ -782,6 +784,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
782784
{activeTab === "ui" && (
783785
<UISettings
784786
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
787+
requireCtrlEnterToSend={requireCtrlEnterToSend ?? false}
785788
setCachedStateField={setCachedStateField}
786789
/>
787790
)}

webview-ui/src/components/settings/UISettings.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
1111

1212
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
1313
reasoningBlockCollapsed: boolean
14+
requireCtrlEnterToSend?: boolean
1415
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
1516
}
1617

17-
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
18+
export const UISettings = ({
19+
reasoningBlockCollapsed,
20+
requireCtrlEnterToSend,
21+
setCachedStateField,
22+
...props
23+
}: UISettingsProps) => {
1824
const { t } = useAppTranslation()
1925

2026
const handleReasoningBlockCollapsedChange = (value: boolean) => {
@@ -26,6 +32,15 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
2632
})
2733
}
2834

35+
const handleRequireCtrlEnterToSendChange = (value: boolean) => {
36+
setCachedStateField("requireCtrlEnterToSend", value)
37+
38+
// Track telemetry event
39+
telemetryClient.capture("ui_settings_ctrl_enter_changed", {
40+
enabled: value,
41+
})
42+
}
43+
2944
return (
3045
<div {...props}>
3146
<SectionHeader>
@@ -49,6 +64,19 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
4964
{t("settings:ui.collapseThinking.description")}
5065
</div>
5166
</div>
67+
68+
{/* Require Ctrl+Enter to Send Setting */}
69+
<div className="flex flex-col gap-1">
70+
<VSCodeCheckbox
71+
checked={requireCtrlEnterToSend ?? false}
72+
onChange={(e: any) => handleRequireCtrlEnterToSendChange(e.target.checked)}
73+
data-testid="ctrl-enter-checkbox">
74+
<span className="font-medium">{t("settings:ui.requireCtrlEnterToSend.label")}</span>
75+
</VSCodeCheckbox>
76+
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
77+
{t("settings:ui.requireCtrlEnterToSend.description")}
78+
</div>
79+
</div>
5280
</div>
5381
</Section>
5482
</div>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export interface ExtensionStateContextType extends ExtensionState {
158158
setMaxDiagnosticMessages: (value: number) => void
159159
includeTaskHistoryInEnhance?: boolean
160160
setIncludeTaskHistoryInEnhance: (value: boolean) => void
161+
requireCtrlEnterToSend?: boolean
162+
setRequireCtrlEnterToSend: (value: boolean) => void
161163
}
162164

163165
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -264,6 +266,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
264266
alwaysAllowUpdateTodoList: true,
265267
includeDiagnosticMessages: true,
266268
maxDiagnosticMessages: 50,
269+
requireCtrlEnterToSend: false,
267270
openRouterImageApiKey: "",
268271
openRouterImageGenerationSelectedModel: "",
269272
})
@@ -559,6 +562,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
559562
},
560563
includeTaskHistoryInEnhance,
561564
setIncludeTaskHistoryInEnhance,
565+
requireCtrlEnterToSend: state.requireCtrlEnterToSend,
566+
setRequireCtrlEnterToSend: (value) => {
567+
setState((prevState) => ({ ...prevState, requireCtrlEnterToSend: value }))
568+
},
562569
}
563570

564571
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
"collapseThinking": {
4343
"label": "Collapse Thinking messages by default",
4444
"description": "When enabled, thinking blocks will be collapsed by default until you interact with them"
45+
},
46+
"requireCtrlEnterToSend": {
47+
"label": "Require Ctrl+Enter to send messages",
48+
"description": "When enabled, you must press Ctrl+Enter to send messages instead of just Enter"
4549
}
4650
},
4751
"prompts": {

0 commit comments

Comments
 (0)