feat: configurable send shortcut (Enter vs Ctrl+Enter)

Add toggle in chat input to switch between Enter-to-send (default)
and Ctrl+Enter-to-send modes. Preference persists in localStorage.
Keyboard shortcuts modal reflects the current setting.
This commit is contained in:
Nicolas Varrot
2026-02-14 05:54:11 +00:00
parent 9f2e8ee9fe
commit 1c564d57b5
4 changed files with 58 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useCallback, lazy, Suspense } from 'react';
import { Send, Square, Paperclip, X, FileText, Eye, EyeOff } from 'lucide-react';
import { useT } from '../hooks/useLocale';
import { useSendShortcut } from '../hooks/useSendShortcut';
const ReactMarkdown = lazy(() => import('react-markdown'));
const remarkGfm = import('remark-gfm').then(m => m.default);
@@ -87,6 +88,7 @@ function formatSize(bytes: number): string {
export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey }: Props) {
const t = useT();
const { sendOnEnter, toggle: toggleSendShortcut } = useSendShortcut();
const [text, setText] = useState('');
const [files, setFiles] = useState<FileAttachment[]>([]);
const [isDragOver, setIsDragOver] = useState(false);
@@ -179,9 +181,20 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
if (e.key === 'Enter') {
if (sendOnEnter) {
// Enter sends, Shift+Enter for newline
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
handleSubmit();
}
} else {
// Ctrl+Enter (or Cmd+Enter on Mac) sends, Enter for newline
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
handleSubmit();
}
}
}
};
@@ -305,6 +318,14 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
rows={1}
className="flex-1 bg-transparent resize-none rounded-2xl border border-pc-border bg-pc-input/35 px-4 py-3 text-sm text-pc-text placeholder:text-pc-text-muted outline-none transition-all max-h-[200px]"
/>
{/* Send shortcut toggle */}
<button
onClick={toggleSendShortcut}
className="hidden sm:flex shrink-0 h-7 rounded-xl border border-pc-border bg-pc-elevated/30 px-2 items-center gap-1 text-[10px] text-pc-text-muted hover:text-pc-text-secondary hover:bg-[var(--pc-hover)] transition-colors"
title={`${t('settings.sendShortcut')}: ${sendOnEnter ? t('settings.sendEnter') : t('settings.sendCtrlEnter')}`}
>
<span>{sendOnEnter ? '↵' : (navigator.userAgent.includes('Mac') ? '⌘↵' : 'Ctrl↵')}</span>
</button>
{isGenerating ? (
<button
onClick={onAbort}

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { X, Keyboard } from 'lucide-react';
import { useT } from '../hooks/useLocale';
import { useSendShortcut } from '../hooks/useSendShortcut';
interface Props {
open: boolean;
@@ -37,6 +38,7 @@ const mod = isMac ? '⌘' : 'Ctrl';
export function KeyboardShortcuts({ open, onClose }: Props) {
const t = useT();
const { sendOnEnter } = useSendShortcut();
useEffect(() => {
if (!open) return;
@@ -79,11 +81,11 @@ export function KeyboardShortcuts({ open, onClose }: Props) {
<div className="pb-3">
<SectionTitle>{t('shortcuts.chatSection')}</SectionTitle>
<ShortcutRow
keys={<Kbd>Enter</Kbd>}
keys={sendOnEnter ? <Kbd>Enter</Kbd> : <><Kbd>{mod}</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></>}
label={t('shortcuts.send')}
/>
<ShortcutRow
keys={<><Kbd>Shift</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></>}
keys={sendOnEnter ? <><Kbd>Shift</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></> : <Kbd>Enter</Kbd>}
label={t('shortcuts.newline')}
/>
<ShortcutRow

View File

@@ -0,0 +1,21 @@
import { useState, useCallback } from 'react';
const STORAGE_KEY = 'pinchchat-send-on-enter';
/** Hook to manage the send shortcut preference (Enter vs Ctrl+Enter). */
export function useSendShortcut() {
const [sendOnEnter, setSendOnEnter] = useState(() => {
const stored = localStorage.getItem(STORAGE_KEY);
return stored === null ? true : stored === 'true';
});
const toggle = useCallback(() => {
setSendOnEnter(prev => {
const next = !prev;
localStorage.setItem(STORAGE_KEY, String(next));
return next;
});
}, []);
return { sendOnEnter, toggle };
}

View File

@@ -137,6 +137,11 @@ const en = {
'search.prev': 'Previous match',
'search.next': 'Next match',
'shortcuts.searchMessages': 'Search messages',
// Send shortcut setting
'settings.sendShortcut': 'Send with',
'settings.sendEnter': 'Enter',
'settings.sendCtrlEnter': 'Ctrl+Enter',
} as const;
const fr: Record<keyof typeof en, string> = {
@@ -255,6 +260,10 @@ const fr: Record<keyof typeof en, string> = {
'search.prev': 'Résultat précédent',
'search.next': 'Résultat suivant',
'shortcuts.searchMessages': 'Rechercher dans les messages',
'settings.sendShortcut': 'Envoyer avec',
'settings.sendEnter': 'Entrée',
'settings.sendCtrlEnter': 'Ctrl+Entrée',
};
export type TranslationKey = keyof typeof en;