支持自定义消息发送和消息换行快捷键

This commit is contained in:
xintaofei
2026-03-12 08:53:47 +08:00
parent f9771d6f27
commit d75e0cef48
14 changed files with 126 additions and 6 deletions

View File

@@ -15,6 +15,8 @@ import {
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { Ellipsis, FileSearch, Plus, Send, Square, X } from "lucide-react" import { Ellipsis, FileSearch, Plus, Send, Square, X } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { readFileBase64 } from "@/lib/tauri" import { readFileBase64 } from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener" import { disposeTauriListener } from "@/lib/tauri-listener"
import type { import type {
@@ -261,6 +263,7 @@ export function MessageInput({
isActive = false, isActive = false,
}: MessageInputProps) { }: MessageInputProps) {
const t = useTranslations("Folder.chat.messageInput") const t = useTranslations("Folder.chat.messageInput")
const { shortcuts } = useShortcutSettings()
const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null
const resolvedPlaceholder = placeholder ?? t("askAnything") const resolvedPlaceholder = placeholder ?? t("askAnything")
const [text, setText] = useState(() => { const [text, setText] = useState(() => {
@@ -932,14 +935,26 @@ export function MessageInput({
} }
} }
if (e.key === "Enter" && !e.shiftKey) { if (matchShortcutEvent(e, shortcuts.send_message)) {
e.preventDefault() e.preventDefault()
if (!disabled) handleSend() if (!disabled) handleSend()
} else if (matchShortcutEvent(e, shortcuts.newline_in_message)) {
e.preventDefault()
const textarea = e.currentTarget as HTMLTextAreaElement
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = textarea.value
const newValue = value.substring(0, start) + "\n" + value.substring(end)
setText(newValue)
requestAnimationFrame(() => {
textarea.selectionStart = textarea.selectionEnd = start + 1
})
} }
}, },
[ [
disabled, disabled,
handleSend, handleSend,
shortcuts,
slashMenuOpen, slashMenuOpen,
filteredSlashCommands, filteredSlashCommands,
slashSelectedIndex, slashSelectedIndex,

View File

@@ -4,6 +4,8 @@ import { useState, useRef, useEffect, useCallback } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { MessageCircleQuestion, SendHorizonal } from "lucide-react" import { MessageCircleQuestion, SendHorizonal } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import type { PendingQuestion } from "@/contexts/acp-connections-context" import type { PendingQuestion } from "@/contexts/acp-connections-context"
interface QuestionDialogProps { interface QuestionDialogProps {
@@ -13,6 +15,7 @@ interface QuestionDialogProps {
export function QuestionDialog({ question, onAnswer }: QuestionDialogProps) { export function QuestionDialog({ question, onAnswer }: QuestionDialogProps) {
const t = useTranslations("Folder.chat.questionDialog") const t = useTranslations("Folder.chat.questionDialog")
const { shortcuts } = useShortcutSettings()
const [answer, setAnswer] = useState("") const [answer, setAnswer] = useState("")
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const prevQuestionIdRef = useRef<string | null>(null) const prevQuestionIdRef = useRef<string | null>(null)
@@ -40,12 +43,12 @@ export function QuestionDialog({ question, onAnswer }: QuestionDialogProps) {
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) { if (matchShortcutEvent(e, shortcuts.send_message)) {
e.preventDefault() e.preventDefault()
handleSubmit() handleSubmit()
} }
}, },
[handleSubmit] [handleSubmit, shortcuts]
) )
if (!question) return null if (!question) return null

View File

@@ -8,6 +8,7 @@ import { useIsMac } from "@/hooks/use-is-mac"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings" import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { import {
DEFAULT_SHORTCUTS, DEFAULT_SHORTCUTS,
INPUT_SHORTCUT_IDS,
SHORTCUT_DEFINITIONS, SHORTCUT_DEFINITIONS,
type ShortcutActionId, type ShortcutActionId,
formatShortcutLabel, formatShortcutLabel,
@@ -64,7 +65,8 @@ export function ShortcutSettings() {
return return
} }
const shortcut = shortcutFromKeyboardEvent(event) const allowNoModifier = INPUT_SHORTCUT_IDS.has(recordingAction)
const shortcut = shortcutFromKeyboardEvent(event, allowNoModifier)
if (!shortcut) return if (!shortcut) return
const conflict = SHORTCUT_DEFINITIONS.find( const conflict = SHORTCUT_DEFINITIONS.find(

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "إغلاق جميع تبويبات الملفات", "title": "إغلاق جميع تبويبات الملفات",
"description": "إغلاق جميع تبويبات الملفات في وضع الملفات فقط" "description": "إغلاق جميع تبويبات الملفات في وضع الملفات فقط"
},
"send_message": {
"title": "إرسال الرسالة",
"description": "إرسال الرسالة الحالية في مربع الإدخال"
},
"newline_in_message": {
"title": "سطر جديد في الرسالة",
"description": "إدراج سطر جديد في مربع الإدخال"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "Alle Dateitabs schließen", "title": "Alle Dateitabs schließen",
"description": "Schließt alle Dateitabs nur im Dateimodus" "description": "Schließt alle Dateitabs nur im Dateimodus"
},
"send_message": {
"title": "Nachricht senden",
"description": "Die aktuelle Nachricht im Eingabefeld senden"
},
"newline_in_message": {
"title": "Zeilenumbruch einfügen",
"description": "Einen Zeilenumbruch im Eingabefeld einfügen"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "Close All File Tabs", "title": "Close All File Tabs",
"description": "Close all file tabs in file mode only" "description": "Close all file tabs in file mode only"
},
"send_message": {
"title": "Send Message",
"description": "Send the current message in the input box"
},
"newline_in_message": {
"title": "Newline in Message",
"description": "Insert a newline in the message input box"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "Cerrar todas las pestañas de archivos", "title": "Cerrar todas las pestañas de archivos",
"description": "Cierra todas las pestañas de archivos solo en modo de archivos" "description": "Cierra todas las pestañas de archivos solo en modo de archivos"
},
"send_message": {
"title": "Enviar mensaje",
"description": "Enviar el mensaje actual en el cuadro de entrada"
},
"newline_in_message": {
"title": "Nueva línea en mensaje",
"description": "Insertar una nueva línea en el cuadro de entrada"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "Fermer tous les onglets de fichiers", "title": "Fermer tous les onglets de fichiers",
"description": "Fermer tous les onglets de fichiers uniquement en mode fichiers" "description": "Fermer tous les onglets de fichiers uniquement en mode fichiers"
},
"send_message": {
"title": "Envoyer le message",
"description": "Envoyer le message actuel dans la zone de saisie"
},
"newline_in_message": {
"title": "Retour à la ligne",
"description": "Insérer un retour à la ligne dans la zone de saisie"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "すべてのファイルタブを閉じる", "title": "すべてのファイルタブを閉じる",
"description": "ファイルモードでのみすべてのファイルタブを閉じます" "description": "ファイルモードでのみすべてのファイルタブを閉じます"
},
"send_message": {
"title": "メッセージを送信",
"description": "入力欄のメッセージを送信する"
},
"newline_in_message": {
"title": "メッセージ内で改行",
"description": "入力欄で改行を挿入する"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "모든 파일 탭 닫기", "title": "모든 파일 탭 닫기",
"description": "파일 모드에서만 모든 파일 탭을 닫습니다" "description": "파일 모드에서만 모든 파일 탭을 닫습니다"
},
"send_message": {
"title": "메시지 보내기",
"description": "입력창에서 현재 메시지를 전송"
},
"newline_in_message": {
"title": "메시지 줄바꿈",
"description": "입력창에 줄바꿈을 삽입"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "Fechar todas as abas de arquivo", "title": "Fechar todas as abas de arquivo",
"description": "Fecha todas as abas de arquivo apenas no modo de arquivos" "description": "Fecha todas as abas de arquivo apenas no modo de arquivos"
},
"send_message": {
"title": "Enviar mensagem",
"description": "Enviar a mensagem atual na caixa de entrada"
},
"newline_in_message": {
"title": "Nova linha na mensagem",
"description": "Inserir uma nova linha na caixa de entrada"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "关闭全部文件标签", "title": "关闭全部文件标签",
"description": "仅在文件模式下关闭所有文件标签" "description": "仅在文件模式下关闭所有文件标签"
},
"send_message": {
"title": "发送消息",
"description": "在输入框中发送当前消息"
},
"newline_in_message": {
"title": "消息换行",
"description": "在输入框中插入换行符"
} }
} }
}, },

View File

@@ -183,6 +183,14 @@
"close_all_file_tabs": { "close_all_file_tabs": {
"title": "關閉全部檔案分頁", "title": "關閉全部檔案分頁",
"description": "僅在檔案模式下關閉所有檔案分頁" "description": "僅在檔案模式下關閉所有檔案分頁"
},
"send_message": {
"title": "傳送訊息",
"description": "在輸入框中傳送目前的訊息"
},
"newline_in_message": {
"title": "訊息換行",
"description": "在輸入框中插入換行符"
} }
} }
}, },

View File

@@ -10,6 +10,8 @@ export type ShortcutActionId =
| "open_settings" | "open_settings"
| "close_current_tab" | "close_current_tab"
| "close_all_file_tabs" | "close_all_file_tabs"
| "send_message"
| "newline_in_message"
export interface ShortcutDefinition { export interface ShortcutDefinition {
id: ShortcutActionId id: ShortcutActionId
@@ -49,8 +51,20 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{ {
id: "close_all_file_tabs", id: "close_all_file_tabs",
}, },
{
id: "send_message",
},
{
id: "newline_in_message",
},
] ]
/** Actions that allow shortcuts without modifier keys (e.g. plain Enter). */
export const INPUT_SHORTCUT_IDS = new Set<ShortcutActionId>([
"send_message",
"newline_in_message",
])
export type ShortcutSettings = Record<ShortcutActionId, string> export type ShortcutSettings = Record<ShortcutActionId, string>
export const DEFAULT_SHORTCUTS: ShortcutSettings = { export const DEFAULT_SHORTCUTS: ShortcutSettings = {
@@ -65,6 +79,8 @@ export const DEFAULT_SHORTCUTS: ShortcutSettings = {
open_settings: "mod+,", open_settings: "mod+,",
close_current_tab: "mod+w", close_current_tab: "mod+w",
close_all_file_tabs: "mod+shift+w", close_all_file_tabs: "mod+shift+w",
send_message: "enter",
newline_in_message: "shift+enter",
} }
export const SHORTCUTS_STORAGE_KEY = "settings:shortcuts:v1" export const SHORTCUTS_STORAGE_KEY = "settings:shortcuts:v1"
@@ -230,12 +246,16 @@ export function shortcutFromKeyboardEvent(
event: Pick< event: Pick<
KeyboardEvent, KeyboardEvent,
"key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey" "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey"
> >,
/** When true, allow shortcuts without modifier keys (e.g. plain Enter). */
allowNoModifier = false
): string | null { ): string | null {
const keyToken = normalizeKeyToken(event.key) const keyToken = normalizeKeyToken(event.key)
if (!keyToken || MODIFIER_KEY_SET.has(keyToken)) return null if (!keyToken || MODIFIER_KEY_SET.has(keyToken)) return null
if (!event.metaKey && !event.ctrlKey && !event.altKey) return null if (!allowNoModifier && !event.metaKey && !event.ctrlKey && !event.altKey) {
return null
}
const parts: string[] = [] const parts: string[] = []
if (event.metaKey || event.ctrlKey) parts.push("mod") if (event.metaKey || event.ctrlKey) parts.push("mod")