feat: add shortcuts to rename topic and edit last user message (#9466)

* feat: add rename topic shortcut (Ctrl/Cmd+T)

* fix: add migration for rename topic shortcut

* feat: add shortcut to edit last user message (Ctrl/Cmd+Shift+E)
- 用于在用户提示词生成的响应不符合预期时,便捷地激活提示词编辑,从而配合编辑器编辑支持的Enter键提交绑定来生成新的模型响应
- messages:绑定 useShortcut('edit_last_user_message'),定位最后一条非 clear 的用户消息并发出 EDIT_MESSAGE
- message:监听 EDIT_MESSAGE,调用 startEditing(message.id) 激活内联编辑(沿用现有自动滚动)

* fix: lint errors and sync i18n

* fix(i18n): complete missing translations in ES, PT, EL and FR locales

* disable new shortcuts by default

* show topic tab on rename shortcut

* refactor: use findLast to simplify find last user message

* add esc key to cancel message editing (discard changes)

* fix missing comma

* remove extra linebreak

* fix: version

---------

Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
Murphy 2025-09-05 19:59:59 +08:00 committed by GitHub
parent 6531d40386
commit 60e1f15e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 3216 additions and 5980 deletions

File diff suppressed because one or more lines are too long

View File

@ -175,7 +175,9 @@ const shortcutKeyMap = {
actions: 'settings.shortcuts.actions', actions: 'settings.shortcuts.actions',
clear_shortcut: 'settings.shortcuts.clear_shortcut', clear_shortcut: 'settings.shortcuts.clear_shortcut',
clear_topic: 'settings.shortcuts.clear_topic', clear_topic: 'settings.shortcuts.clear_topic',
rename_topic: 'settings.shortcuts.rename_topic',
copy_last_message: 'settings.shortcuts.copy_last_message', copy_last_message: 'settings.shortcuts.copy_last_message',
edit_last_user_message: 'settings.shortcuts.edit_last_user_message',
enabled: 'settings.shortcuts.enabled', enabled: 'settings.shortcuts.enabled',
exit_fullscreen: 'settings.shortcuts.exit_fullscreen', exit_fullscreen: 'settings.shortcuts.exit_fullscreen',
label: 'settings.shortcuts.label', label: 'settings.shortcuts.label',

View File

@ -3892,12 +3892,14 @@
"clear_shortcut": "Clear Shortcut", "clear_shortcut": "Clear Shortcut",
"clear_topic": "Clear Messages", "clear_topic": "Clear Messages",
"copy_last_message": "Copy Last Message", "copy_last_message": "Copy Last Message",
"edit_last_user_message": "Edit Last User Message",
"enabled": "Enable", "enabled": "Enable",
"exit_fullscreen": "Exit Fullscreen", "exit_fullscreen": "Exit Fullscreen",
"label": "Key", "label": "Key",
"mini_window": "Quick Assistant", "mini_window": "Quick Assistant",
"new_topic": "New Topic", "new_topic": "New Topic",
"press_shortcut": "Press Shortcut", "press_shortcut": "Press Shortcut",
"rename_topic": "Rename Topic",
"reset_defaults": "Reset Defaults", "reset_defaults": "Reset Defaults",
"reset_defaults_confirm": "Are you sure you want to reset all shortcuts?", "reset_defaults_confirm": "Are you sure you want to reset all shortcuts?",
"reset_to_default": "Reset to Default", "reset_to_default": "Reset to Default",

View File

@ -3892,12 +3892,14 @@
"clear_shortcut": "清除快捷键", "clear_shortcut": "清除快捷键",
"clear_topic": "清空消息", "clear_topic": "清空消息",
"copy_last_message": "复制上一条消息", "copy_last_message": "复制上一条消息",
"edit_last_user_message": "编辑最后一条用户消息",
"enabled": "启用", "enabled": "启用",
"exit_fullscreen": "退出全屏", "exit_fullscreen": "退出全屏",
"label": "按键", "label": "按键",
"mini_window": "快捷助手", "mini_window": "快捷助手",
"new_topic": "新建话题", "new_topic": "新建话题",
"press_shortcut": "按下快捷键", "press_shortcut": "按下快捷键",
"rename_topic": "重命名话题",
"reset_defaults": "重置默认快捷键", "reset_defaults": "重置默认快捷键",
"reset_defaults_confirm": "确定要重置所有快捷键吗?", "reset_defaults_confirm": "确定要重置所有快捷键吗?",
"reset_to_default": "重置为默认", "reset_to_default": "重置为默认",

View File

@ -3892,12 +3892,14 @@
"clear_shortcut": "清除快捷鍵", "clear_shortcut": "清除快捷鍵",
"clear_topic": "清除所有訊息", "clear_topic": "清除所有訊息",
"copy_last_message": "複製上一則訊息", "copy_last_message": "複製上一則訊息",
"edit_last_user_message": "編輯最後一則使用者訊息",
"enabled": "啟用", "enabled": "啟用",
"exit_fullscreen": "退出螢幕", "exit_fullscreen": "退出螢幕",
"label": "按鍵", "label": "按鍵",
"mini_window": "快捷助手", "mini_window": "快捷助手",
"new_topic": "新增話題", "new_topic": "新增話題",
"press_shortcut": "按下快捷鍵", "press_shortcut": "按下快捷鍵",
"rename_topic": "重新命名話題",
"reset_defaults": "重設預設快捷鍵", "reset_defaults": "重設預設快捷鍵",
"reset_defaults_confirm": "確定要重設所有快捷鍵嗎?", "reset_defaults_confirm": "確定要重設所有快捷鍵嗎?",
"reset_to_default": "重設為預設", "reset_to_default": "重設為預設",

View File

@ -3888,12 +3888,14 @@
"clear_shortcut": "Καθαρισμός συντομού πλήκτρου", "clear_shortcut": "Καθαρισμός συντομού πλήκτρου",
"clear_topic": "Άδειασμα μηνυμάτων", "clear_topic": "Άδειασμα μηνυμάτων",
"copy_last_message": "Αντιγραφή του τελευταίου μηνύματος", "copy_last_message": "Αντιγραφή του τελευταίου μηνύματος",
"edit_last_user_message": "Επεξεργασία του τελευταίου μηνύματος χρήστη",
"enabled": "ενεργοποίηση", "enabled": "ενεργοποίηση",
"exit_fullscreen": "Έξοδος από πλήρη οθόνη", "exit_fullscreen": "Έξοδος από πλήρη οθόνη",
"label": "Πλήκτρο", "label": "Πλήκτρο",
"mini_window": "Συντομεύστε επιχειρηματικά", "mini_window": "Συντομεύστε επιχειρηματικά",
"new_topic": "Νέο θέμα", "new_topic": "Νέο θέμα",
"press_shortcut": "Πάτησε το συντομού πλήκτρου", "press_shortcut": "Πάτησε το συντομού πλήκτρου",
"rename_topic": "Μετονομασία θέματος",
"reset_defaults": "Επαναφορά στα προεπιλεγμένα συντομού πλήκτρα", "reset_defaults": "Επαναφορά στα προεπιλεγμένα συντομού πλήκτρα",
"reset_defaults_confirm": "Θέλετε να επαναφέρετε όλα τα συντομού πλήκτρα στις προεπιλεγμένες τιμές;", "reset_defaults_confirm": "Θέλετε να επαναφέρετε όλα τα συντομού πλήκτρα στις προεπιλεγμένες τιμές;",
"reset_to_default": "Επαναφορά στις προεπιλεγμένες", "reset_to_default": "Επαναφορά στις προεπιλεγμένες",

View File

@ -3888,12 +3888,14 @@
"clear_shortcut": "Borrar atajo", "clear_shortcut": "Borrar atajo",
"clear_topic": "Vaciar mensaje", "clear_topic": "Vaciar mensaje",
"copy_last_message": "Copiar el último mensaje", "copy_last_message": "Copiar el último mensaje",
"edit_last_user_message": "Editar último mensaje de usuario",
"enabled": "habilitar", "enabled": "habilitar",
"exit_fullscreen": "Salir de pantalla completa", "exit_fullscreen": "Salir de pantalla completa",
"label": "Tecla", "label": "Tecla",
"mini_window": "Asistente rápido", "mini_window": "Asistente rápido",
"new_topic": "Nuevo tema", "new_topic": "Nuevo tema",
"press_shortcut": "Presionar atajo", "press_shortcut": "Presionar atajo",
"rename_topic": "Renombrar tema",
"reset_defaults": "Restablecer atajos predeterminados", "reset_defaults": "Restablecer atajos predeterminados",
"reset_defaults_confirm": "¿Está seguro de querer restablecer todos los atajos?", "reset_defaults_confirm": "¿Está seguro de querer restablecer todos los atajos?",
"reset_to_default": "Restablecer a predeterminado", "reset_to_default": "Restablecer a predeterminado",

View File

@ -3888,12 +3888,14 @@
"clear_shortcut": "Effacer raccourci clavier", "clear_shortcut": "Effacer raccourci clavier",
"clear_topic": "Vider les messages", "clear_topic": "Vider les messages",
"copy_last_message": "Copier le dernier message", "copy_last_message": "Copier le dernier message",
"edit_last_user_message": "Éditer le dernier message utilisateur",
"enabled": "activer", "enabled": "activer",
"exit_fullscreen": "Quitter le plein écran", "exit_fullscreen": "Quitter le plein écran",
"label": "Touche", "label": "Touche",
"mini_window": "Assistant rapide", "mini_window": "Assistant rapide",
"new_topic": "Nouveau sujet", "new_topic": "Nouveau sujet",
"press_shortcut": "Appuyer sur raccourci clavier", "press_shortcut": "Appuyer sur raccourci clavier",
"rename_topic": "Renommer le sujet",
"reset_defaults": "Réinitialiser raccourcis par défaut", "reset_defaults": "Réinitialiser raccourcis par défaut",
"reset_defaults_confirm": "Êtes-vous sûr de vouloir réinitialiser tous les raccourcis clavier ?", "reset_defaults_confirm": "Êtes-vous sûr de vouloir réinitialiser tous les raccourcis clavier ?",
"reset_to_default": "Réinitialiser aux valeurs par défaut", "reset_to_default": "Réinitialiser aux valeurs par défaut",

View File

@ -3892,12 +3892,14 @@
"clear_shortcut": "ショートカットをクリア", "clear_shortcut": "ショートカットをクリア",
"clear_topic": "メッセージを消去", "clear_topic": "メッセージを消去",
"copy_last_message": "最後のメッセージをコピー", "copy_last_message": "最後のメッセージをコピー",
"edit_last_user_message": "最後のユーザーメッセージを編集",
"enabled": "有効化", "enabled": "有効化",
"exit_fullscreen": "フルスクリーンを終了", "exit_fullscreen": "フルスクリーンを終了",
"label": "キー", "label": "キー",
"mini_window": "クイックアシスタント", "mini_window": "クイックアシスタント",
"new_topic": "新しいトピック", "new_topic": "新しいトピック",
"press_shortcut": "ショートカットを押す", "press_shortcut": "ショートカットを押す",
"rename_topic": "トピックの名前を変更",
"reset_defaults": "デフォルトのショートカットをリセット", "reset_defaults": "デフォルトのショートカットをリセット",
"reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?", "reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?",
"reset_to_default": "デフォルトにリセット", "reset_to_default": "デフォルトにリセット",

View File

@ -3888,12 +3888,14 @@
"clear_shortcut": "Limpar atalho", "clear_shortcut": "Limpar atalho",
"clear_topic": "Limpar mensagem", "clear_topic": "Limpar mensagem",
"copy_last_message": "Copiar a última mensagem", "copy_last_message": "Copiar a última mensagem",
"edit_last_user_message": "Editar última mensagem do usuário",
"enabled": "ativar", "enabled": "ativar",
"exit_fullscreen": "Sair da tela cheia", "exit_fullscreen": "Sair da tela cheia",
"label": "Tecla", "label": "Tecla",
"mini_window": "Atalho de assistente", "mini_window": "Atalho de assistente",
"new_topic": "Novo tópico", "new_topic": "Novo tópico",
"press_shortcut": "Pressionar atalho", "press_shortcut": "Pressionar atalho",
"rename_topic": "Renomear tópico",
"reset_defaults": "Redefinir atalhos padrão", "reset_defaults": "Redefinir atalhos padrão",
"reset_defaults_confirm": "Tem certeza de que deseja redefinir todos os atalhos?", "reset_defaults_confirm": "Tem certeza de que deseja redefinir todos os atalhos?",
"reset_to_default": "Redefinir para padrão", "reset_to_default": "Redefinir para padrão",

View File

@ -3892,12 +3892,14 @@
"clear_shortcut": "Очистить сочетание клавиш", "clear_shortcut": "Очистить сочетание клавиш",
"clear_topic": "Очистить все сообщения", "clear_topic": "Очистить все сообщения",
"copy_last_message": "Копировать последнее сообщение", "copy_last_message": "Копировать последнее сообщение",
"enabled": "启用", "edit_last_user_message": "Редактировать последнее сообщение пользователя",
"enabled": "Включить",
"exit_fullscreen": "Выйти из полноэкранного режима", "exit_fullscreen": "Выйти из полноэкранного режима",
"label": "Клавиша", "label": "Клавиша",
"mini_window": "Быстрый помощник", "mini_window": "Быстрый помощник",
"new_topic": "Новый топик", "new_topic": "Новый топик",
"press_shortcut": "Нажмите сочетание клавиш", "press_shortcut": "Нажмите сочетание клавиш",
"rename_topic": "Переименовать топик",
"reset_defaults": "Сбросить настройки по умолчанию", "reset_defaults": "Сбросить настройки по умолчанию",
"reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?", "reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?",
"reset_to_default": "Сбросить настройки по умолчанию", "reset_to_default": "Сбросить настройки по умолчанию",

View File

@ -2,6 +2,7 @@ import { loggerService } from '@logger'
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup' import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { QuickPanelProvider } from '@renderer/components/QuickPanel' import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
@ -9,6 +10,7 @@ import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import { Flex } from 'antd' import { Flex } from 'antd'
@ -16,6 +18,7 @@ import { debounce } from 'lodash'
import { AnimatePresence, motion } from 'motion/react' import { AnimatePresence, motion } from 'motion/react'
import React, { FC, useState } from 'react' import React, { FC, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import ChatNavbar from './ChatNavbar' import ChatNavbar from './ChatNavbar'
@ -34,7 +37,8 @@ interface Props {
} }
const Chat: FC<Props> = (props) => { const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id) const { assistant, updateTopic } = useAssistant(props.assistant.id)
const { t } = useTranslation()
const { topicPosition, messageStyle, messageNavigation } = useSettings() const { topicPosition, messageStyle, messageNavigation } = useSettings()
const { showTopics } = useShowTopics() const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic) const { isMultiSelectMode } = useChatContext(props.activeTopic)
@ -59,6 +63,24 @@ const Chat: FC<Props> = (props) => {
} }
}) })
useShortcut('rename_topic', async () => {
const topic = props.activeTopic
if (!topic) return
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
const name = await PromptPopup.show({
title: t('chat.topics.edit.title'),
message: '',
defaultValue: topic.name || '',
extraNode: <div style={{ color: 'var(--color-text-3)', marginTop: 8 }}>{t('chat.topics.edit.title_tip')}</div>
})
if (name && topic.name !== name) {
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
updateTopic(updatedTopic as Topic)
}
})
const contentSearchFilter: NodeFilter = { const contentSearchFilter: NodeFilter = {
acceptNode(node) { acceptNode(node) {
const container = node.parentElement?.closest('.message-content-container') const container = node.parentElement?.closest('.message-content-container')

View File

@ -70,7 +70,7 @@ const MessageItem: FC<Props> = ({
const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings() const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings()
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic) const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing() const { editingMessageId, startEditing, stopEditing } = useMessageEditing()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const isEditing = editingMessageId === message.id const isEditing = editingMessageId === message.id
@ -148,6 +148,19 @@ const MessageItem: FC<Props> = ({
return () => unsubscribes.forEach((unsub) => unsub()) return () => unsubscribes.forEach((unsub) => unsub())
}, [message.id, messageHighlightHandler]) }, [message.id, messageHighlightHandler])
// Listen for external edit requests and activate editor for this message if it matches
useEffect(() => {
const handleEditRequest = (targetId: string) => {
if (targetId === message.id) {
startEditing(message.id)
}
}
const unsubscribe = EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, handleEditRequest)
return () => {
unsubscribe()
}
}, [message.id, startEditing])
if (message.type === 'clear') { if (message.type === 'clear') {
return ( return (
<NewContextMessage <NewContextMessage

View File

@ -228,6 +228,12 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
return return
} }
if (event.key === 'Escape') {
event.preventDefault()
onCancel()
return
}
// keep the same enter behavior as inputbar // keep the same enter behavior as inputbar
const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing
if (isEnterPressed) { if (isEnterPressed) {

View File

@ -281,6 +281,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
} }
}) })
useShortcut('edit_last_user_message', () => {
const lastUserMessage = messagesRef.current.findLast((m) => m.role === 'user' && m.type !== 'clear')
if (lastUserMessage) {
EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, lastUserMessage.id)
}
})
useEffect(() => { useEffect(() => {
requestAnimationFrame(() => onComponentUpdate?.()) requestAnimationFrame(() => onComponentUpdate?.())
}, [onComponentUpdate]) }, [onComponentUpdate])

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 149, version: 150,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate migrate
}, },

View File

@ -2404,6 +2404,16 @@ const migrateConfig = {
logger.error('migrate 149 error', error as Error) logger.error('migrate 149 error', error as Error)
return state return state
} }
},
'150': (state: RootState) => {
try {
addShortcuts(state, ['rename_topic'], 'new_topic')
addShortcuts(state, ['edit_last_user_message'], 'copy_last_message')
return state
} catch (error) {
logger.error('migrate 150 error', error as Error)
return state
}
} }
} }

View File

@ -53,6 +53,13 @@ const initialState: ShortcutsState = {
enabled: true, enabled: true,
system: false system: false
}, },
{
key: 'rename_topic',
shortcut: ['CommandOrControl', 'T'],
editable: true,
enabled: false,
system: false
},
{ {
key: 'toggle_show_assistants', key: 'toggle_show_assistants',
shortcut: ['CommandOrControl', '['], shortcut: ['CommandOrControl', '['],
@ -75,6 +82,13 @@ const initialState: ShortcutsState = {
enabled: false, enabled: false,
system: false system: false
}, },
{
key: 'edit_last_user_message',
shortcut: ['CommandOrControl', 'Shift', 'E'],
editable: true,
enabled: false,
system: false
},
{ {
key: 'search_message_in_chat', key: 'search_message_in_chat',
shortcut: ['CommandOrControl', 'F'], shortcut: ['CommandOrControl', 'F'],