Skip to content

Commit 3458f70

Browse files
lmtr0hannesrudolph
authored andcommitted
new: Ctrl+Enter to send message to chat draft
1 parent 7320d79 commit 3458f70

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
@@ -82,6 +82,7 @@ export const globalSettingsSchema = z.object({
8282
alwaysAllowFollowupQuestions: z.boolean().optional(),
8383
followupAutoApproveTimeoutMs: z.number().optional(),
8484
alwaysAllowUpdateTodoList: z.boolean().optional(),
85+
requireCtrlEnterToSend: z.boolean().optional(),
8586
allowedCommands: z.array(z.string()).optional(),
8687
deniedCommands: z.array(z.string()).optional(),
8788
commandExecutionTimeout: z.number().optional(),

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,10 @@ export const webviewMessageHandler = async (
578578
await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
579579
await provider.postStateToWebview()
580580
break
581+
case "requireCtrlEnterToSend":
582+
await updateGlobalState("requireCtrlEnterToSend", message.bool ?? false)
583+
await provider.postStateToWebview()
584+
break
581585
case "askResponse":
582586
provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
583587
break
@@ -1509,6 +1513,10 @@ export const webviewMessageHandler = async (
15091513
Terminal.setTerminalZdotdir(message.bool)
15101514
}
15111515
break
1516+
case "requireCtrlEnterToSend":
1517+
await updateGlobalState("requireCtrlEnterToSend", message.bool)
1518+
await provider.postStateToWebview()
1519+
break
15121520
case "terminalCompressProgressBar":
15131521
await updateGlobalState("terminalCompressProgressBar", message.bool)
15141522
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export type ExtensionState = Pick<
238238
| "alwaysAllowFollowupQuestions"
239239
| "alwaysAllowExecute"
240240
| "alwaysAllowUpdateTodoList"
241+
| "requireCtrlEnterToSend"
241242
| "followupAutoApproveTimeoutMs"
242243
| "allowedCommands"
243244
| "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
@@ -198,6 +198,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
198198
reasoningBlockCollapsed,
199199
includeCurrentTime,
200200
includeCurrentCost,
201+
requireCtrlEnterToSend,
201202
} = cachedState
202203

203204
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -390,6 +391,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
390391
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
391392
vscode.postMessage({ type: "includeCurrentTime", bool: includeCurrentTime ?? true })
392393
vscode.postMessage({ type: "includeCurrentCost", bool: includeCurrentCost ?? true })
394+
vscode.postMessage({ type: "requireCtrlEnterToSend", bool: requireCtrlEnterToSend ?? false })
393395
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
394396
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
395397
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
@@ -791,6 +793,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
791793
{activeTab === "ui" && (
792794
<UISettings
793795
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
796+
requireCtrlEnterToSend={requireCtrlEnterToSend ?? false}
794797
setCachedStateField={setCachedStateField}
795798
/>
796799
)}

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
@@ -165,6 +165,8 @@ export interface ExtensionStateContextType extends ExtensionState {
165165
setIncludeCurrentTime: (value: boolean) => void
166166
includeCurrentCost?: boolean
167167
setIncludeCurrentCost: (value: boolean) => void
168+
requireCtrlEnterToSend?: boolean
169+
setRequireCtrlEnterToSend: (value: boolean) => void
168170
}
169171

170172
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -272,6 +274,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
272274
alwaysAllowUpdateTodoList: true,
273275
includeDiagnosticMessages: true,
274276
maxDiagnosticMessages: 50,
277+
requireCtrlEnterToSend: false,
275278
openRouterImageApiKey: "",
276279
openRouterImageGenerationSelectedModel: "",
277280
includeCurrentTime: true,
@@ -596,6 +599,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
596599
setIncludeCurrentTime,
597600
includeCurrentCost,
598601
setIncludeCurrentCost,
602+
requireCtrlEnterToSend: state.requireCtrlEnterToSend,
603+
setRequireCtrlEnterToSend: (value) => {
604+
setState((prevState) => ({ ...prevState, requireCtrlEnterToSend: value }))
605+
},
599606
}
600607

601608
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)