Remove loading state blocks input, Add "Retry failed messages" button (#9513)

* feat(chat/input): allow sending during streaming; remove loading guard

- Inputbar: drop loading check; keep Send clickable; right-click to pause
- Message/MessageMenubar: render menubar even while streaming
- Minor cleanup of unused imports and lints

* feat(chat/menubar): one-click regenerate (remove confirm); add empty-context fallback

- MessageMenubar: remove Popconfirm; click to regenerate
- ApiService: ensure last user message is included if filters empty (avoid messages=[])
- Minor cleanup

* feat(chat/ui): decouple generation from sending; enable actions during streaming

- MessageGroupMenuBar: add “Retry all” (ReloadOutlined) to retry errored/empty/no-block replies; tooltip via i18n
- Context fallback: always include last user message in both messageThunk and ApiService
- i18n: add message.group.retry_failed (zh-CN, en-US, ja-JP, ru-RU, zh-TW)
- Cleanup: remove unused imports; fix lints

* feat(chat/settings): Add confirmation settings for message actions

- Add "Confirm before deleting message" and "Confirm before regenerating message" options to the settings page, allowing users to customize action confirmations.
- Update internationalization files to support multi-language prompt messages.
- Modify the message menu bar to integrate the confirmation logic, enhancing the user experience.

* fix(chat/ui): Apply regeneration confirmation to user messages

Previously, the "Regenerate" action on user messages would trigger immediately, bypassing the `confirmRegenerateMessage` setting. This behavior was inconsistent with the regeneration logic for assistant messages, which correctly showed a confirmation dialog.

This commit wraps the user message's regenerate button in a `Popconfirm` component, conditioned on the `confirmRegenerateMessage` setting. This aligns its behavior with the existing logic for assistant messages.

Now, all regeneration actions are uniformly governed by the user's confirmation preference, creating a more consistent and predictable user experience.

* fix(ui): Only show 'Retry failed' button when errors exist

fix(ui): Conditionally render the 'Retry failed' button

The 'Retry failed messages' button in the message group menu bar was previously always visible, even when no messages had failed.

- The 'Retry failed' button is now conditionally rendered and will only appear if one or more messages in the group meet the failure criteria.

* feat(chat/ui): Add dedicated button to pause message generation

Replaced the undiscoverable right-click-to-pause functionality on the send button with a dedicated, visible "Pause" button. This new button only appears during message generation, making the action intuitive and accessible.

- Removed `onPause` and context menu logic from `SendMessageButton`.
- Added a conditional `CirclePause` button to the `Inputbar` when loading.

* feat(settings/migrate): initialize confirm message flags for legacy users

- Add migration 138 to default confirmDeleteMessage/confirmRegenerateMessage
- No behavior change for fresh installs (uses initialState)

fix(format): correct indentation in MessageMenubar

fix(settings/migrate): fix persistedReducer verison

* fix(ui): resolve React Hook dependency warnings

- Remove unnecessary `topic.prompt` dependencies from Message components
- Remove `loading` dependency from Inputbar useCallback

Resolves ESLint exhaustive-deps warnings and conflicts

---------

Co-authored-by: n2yt584v2t4nh7y <117180266+n2yt584v2t4nh7y@users.noreply.github.com>
This commit is contained in:
2025-09-02 11:24:20 +08:00 committed by GitHub
parent 16b9f49cc8
commit 2480822690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 221 additions and 62 deletions

View File

@ -1320,7 +1320,8 @@
"delete": {
"content": "Deleting a group message will delete the user's question and all assistant's answers",
"title": "Delete Group Message"
}
},
"retry_failed": "Retry failed messages"
},
"ignore": {
"knowledge": {
@ -3347,6 +3348,8 @@
"label": "Grid detail trigger"
},
"input": {
"confirm_delete_message": "Confirm before deleting messages",
"confirm_regenerate_message": "Confirm before regenerating messages",
"enable_quick_triggers": "Enable / and @ triggers",
"paste_long_text_as_file": "Paste long text as file",
"paste_long_text_threshold": "Paste long text length",

View File

@ -1320,7 +1320,8 @@
"delete": {
"content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
"title": "分組メッセージを削除"
}
},
"retry_failed": "エラーになったメッセージを再試行"
},
"ignore": {
"knowledge": {
@ -3347,6 +3348,8 @@
"label": "グリッド詳細トリガー"
},
"input": {
"confirm_delete_message": "メッセージ削除前に確認",
"confirm_regenerate_message": "メッセージ再生成前に確認",
"enable_quick_triggers": "/ と @ を有効にしてクイックメニューを表示します。",
"paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
"paste_long_text_threshold": "長いテキストの長さ",

View File

@ -1320,7 +1320,8 @@
"delete": {
"content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
"title": "Удалить группу сообщений"
}
},
"retry_failed": "Повторить неудавшиеся сообщения"
},
"ignore": {
"knowledge": {
@ -3347,6 +3348,8 @@
"label": "Триггер для отображения подробной информации в сетке"
},
"input": {
"confirm_delete_message": "Подтверждать перед удалением сообщений",
"confirm_regenerate_message": "Подтверждать перед пересозданием сообщений",
"enable_quick_triggers": "Включите / и @, чтобы вызвать быстрое меню.",
"paste_long_text_as_file": "Вставлять длинный текст как файл",
"paste_long_text_threshold": "Длина вставки длинного текста",

View File

@ -1320,7 +1320,8 @@
"delete": {
"content": "删除分组消息会删除用户提问和所有助手的回答",
"title": "删除分组消息"
}
},
"retry_failed": "重试出错的消息"
},
"ignore": {
"knowledge": {
@ -3347,6 +3348,8 @@
"label": "网格详情触发"
},
"input": {
"confirm_delete_message": "删除消息前确认",
"confirm_regenerate_message": "重新生成消息前确认",
"enable_quick_triggers": "启用 / 和 @ 触发快捷菜单",
"paste_long_text_as_file": "长文本粘贴为文件",
"paste_long_text_threshold": "长文本长度",

View File

@ -1320,7 +1320,8 @@
"delete": {
"content": "刪除分組訊息會刪除使用者提問和所有助手的回答",
"title": "刪除分組訊息"
}
},
"retry_failed": "重試出錯的訊息"
},
"ignore": {
"knowledge": {
@ -3347,6 +3348,8 @@
"label": "網格詳細資訊觸發"
},
"input": {
"confirm_delete_message": "刪除訊息前確認",
"confirm_regenerate_message": "重新生成訊息前確認",
"enable_quick_triggers": "啟用 / 和 @ 觸發快捷選單",
"paste_long_text_as_file": "將長文字貼上為檔案",
"paste_long_text_threshold": "長文字長度",

View File

@ -210,7 +210,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
)
const sendMessage = useCallback(async () => {
if (inputEmpty || loading) {
if (inputEmpty) {
return
}
if (checkRateLimit(assistant)) {
@ -258,7 +258,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
logger.warn('Failed to send message:', error as Error)
parent?.recordException(error as Error)
}
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
}, [assistant, dispatch, files, inputEmpty, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
const translate = useCallback(async () => {
if (isTranslating) {
@ -927,6 +927,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
onClick={onNewContext}
/>
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
<SendMessageButton sendMessage={sendMessage} disabled={inputEmpty} />
{loading && (
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2 }}>
@ -934,7 +935,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
</ToolbarButton>
</Tooltip>
)}
{!loading && <SendMessageButton sendMessage={sendMessage} disabled={loading || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>

View File

@ -60,7 +60,6 @@ const MessageItem: FC<Props> = ({
index,
hideMenuBar = false,
isGrouped,
isStreaming = false,
onUpdateUseful,
isGroupContextMessage
}) => {
@ -116,7 +115,7 @@ const MessageItem: FC<Props> = ({
const isLastMessage = index === 0 || !!isGrouped
const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
const showMenubar = !hideMenuBar && !isEditing
const messageHighlightHandler = useCallback(
(highlight: boolean = true) => {

View File

@ -3,13 +3,17 @@ import {
ColumnWidthOutlined,
DeleteOutlined,
FolderOutlined,
NumberOutlined
NumberOutlined,
ReloadOutlined
} from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { Button, Tooltip } from 'antd'
import { FC, memo } from 'react'
import { useTranslation } from 'react-i18next'
@ -36,7 +40,8 @@ const MessageGroupMenuBar: FC<Props> = ({
topic
}) => {
const { t } = useTranslation()
const { deleteGroupMessages } = useMessageOperations(topic)
const { deleteGroupMessages, regenerateAssistantMessage } = useMessageOperations(topic)
const { assistant } = useAssistant(messages[0]?.assistantId)
const handleDeleteGroup = async () => {
const askId = messages[0]?.askId
@ -54,6 +59,39 @@ const MessageGroupMenuBar: FC<Props> = ({
})
}
const isFailedMessage = (m: Message) => {
if (m.role !== 'assistant') return false
const isError = (m.status || '').toLowerCase() === 'error'
const content = getMainTextContent(m)
const noContent = !content || content.trim().length === 0
const noBlocks = !m.blocks || m.blocks.length === 0
return isError || noContent || noBlocks
}
const isTransmittingMessage = (m: Message) => {
if (m.role !== 'assistant') return false
const status = m.status as AssistantMessageStatus
return (
status === AssistantMessageStatus.PROCESSING ||
status === AssistantMessageStatus.PENDING ||
status === AssistantMessageStatus.SEARCHING
)
}
const hasFailedMessages = messages.some((m) => isFailedMessage(m) && !isTransmittingMessage(m))
const handleRetryAll = async () => {
const candidates = messages.filter((m) => isFailedMessage(m) && !isTransmittingMessage(m))
for (const msg of candidates) {
try {
await regenerateAssistantMessage(msg, assistant)
} catch (e) {
// swallow per-item errors to continue others
}
}
}
const multiModelMessageStyleTextByLayout = {
fold: t('message.message.multi_model_style.fold.label'),
vertical: t('message.message.multi_model_style.vertical'),
@ -95,6 +133,17 @@ const MessageGroupMenuBar: FC<Props> = ({
)}
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
</HStack>
{hasFailedMessages && (
<Tooltip title={t('message.group.retry_failed')} mouseEnterDelay={0.6}>
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={handleRetryAll}
style={{ marginRight: 4 }}
/>
</Tooltip>
)}
<Button
type="text"
size="small"

View File

@ -1,4 +1,4 @@
import { InfoCircleOutlined } from '@ant-design/icons'
// import { InfoCircleOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -7,9 +7,9 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isEmbeddingModel, isRerankModel, isVisionModel } from '@renderer/config/models'
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
import { useEnableDeveloperMode, useMessageStyle } from '@renderer/hooks/useSettings'
import { useEnableDeveloperMode, useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
import useTranslate from '@renderer/hooks/useTranslate'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@ -84,7 +84,7 @@ const MessageMenubar: FC<Props> = (props) => {
const { toggleMultiSelectMode } = useChatContext(props.topic)
const [copied, setCopied] = useTemporaryValue(false, 2000)
const [isTranslating, setIsTranslating] = useState(false)
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
// remove confirm for regenerate; tooltip stays simple
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
const { translateLanguages } = useTranslate()
// const assistantModel = assistant?.model
@ -99,8 +99,9 @@ const MessageMenubar: FC<Props> = (props) => {
const { isBubbleStyle } = useMessageStyle()
const { enableDeveloperMode } = useEnableDeveloperMode()
const { confirmDeleteMessage, confirmRegenerateMessage } = useSettings()
const loading = useTopicLoading(topic)
// const loading = useTopicLoading(topic)
const isUserMessage = message.role === 'user'
@ -145,18 +146,15 @@ const MessageMenubar: FC<Props> = (props) => {
)
const onNewBranch = useCallback(async () => {
if (loading) return
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
window.message.success({ content: t('chat.message.new.branch.created'), key: 'new-branch' })
}, [index, t, loading])
}, [index, t])
const handleResendUserMessage = useCallback(
async (messageUpdate?: Message) => {
if (!loading) {
await resendMessage(messageUpdate ?? message, assistant)
}
await resendMessage(messageUpdate ?? message, assistant)
},
[assistant, loading, message, resendMessage]
[assistant, message, resendMessage]
)
const { startEditing } = useMessageEditing()
@ -392,7 +390,6 @@ const MessageMenubar: FC<Props> = (props) => {
const onRegenerate = async (e: React.MouseEvent | undefined) => {
e?.stopPropagation?.()
if (loading) return
// No need to reset or edit the message anymore
// const selectedModel = isGrouped ? model : assistantModel
// const _message = resetAssistantMessage(message, selectedModel)
@ -438,12 +435,11 @@ const MessageMenubar: FC<Props> = (props) => {
const onMentionModel = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation()
if (loading) return
const selectedModel = await SelectModelPopup.show({ model, filter: mentionModelFilter })
if (!selectedModel) return
appendAssistantResponse(message, selectedModel, { ...assistant, model: selectedModel })
},
[appendAssistantResponse, assistant, loading, mentionModelFilter, message, model]
[appendAssistantResponse, assistant, mentionModelFilter, message, model]
)
const onUseful = useCallback(
@ -469,16 +465,32 @@ const MessageMenubar: FC<Props> = (props) => {
{showMessageTokens && <MessageTokens message={message} />}
<MenusBar
className={classNames({ menubar: true, show: isLastMessage, 'user-bubble-style': isUserBubbleStyleMessage })}>
{message.role === 'user' && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
)}
{message.role === 'user' &&
(confirmRegenerateMessage ? (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={() => handleResendUserMessage()}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
) : (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
))}
{message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
@ -492,24 +504,29 @@ const MessageMenubar: FC<Props> = (props) => {
{copied && <Check size={15} color="var(--color-primary)" />}
</ActionButton>
</Tooltip>
{isAssistantMessage && (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
onConfirm={onRegenerate}
onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
<Tooltip
title={t('common.regenerate')}
mouseEnterDelay={0.8}
open={showRegenerateTooltip}
onOpenChange={setShowRegenerateTooltip}>
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
{isAssistantMessage &&
(confirmRegenerateMessage ? (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={onRegenerate}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
) : (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onRegenerate} $softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
)}
))}
{isAssistantMessage && (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
@ -603,15 +620,32 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton>
</Tooltip>
)}
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}>
{confirmDeleteMessage ? (
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<DeleteIcon size={15} />
</Tooltip>
</ActionButton>
</Popconfirm>
) : (
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation()
deleteMessage(message.id, message.traceId, message.model?.name)
}}
$softHoverBg={softHoverBg}>
<Tooltip
title={t('common.delete')}
@ -621,7 +655,7 @@ const MessageMenubar: FC<Props> = (props) => {
<DeleteIcon size={15} />
</Tooltip>
</ActionButton>
</Popconfirm>
)}
{enableDeveloperMode && message.traceId && (
<Tooltip title={t('trace.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={() => handleTraceUserMessage()}>

View File

@ -26,6 +26,8 @@ import {
setCodeShowLineNumbers,
setCodeViewer,
setCodeWrappable,
setConfirmDeleteMessage,
setConfirmRegenerateMessage,
setEnableQuickPanelTriggers,
setFontSize,
setMathEnableSingleDollar,
@ -105,7 +107,9 @@ const SettingsTab: FC<Props> = (props) => {
messageNavigation,
enableQuickPanelTriggers,
showTranslateConfirm,
showMessageOutline
showMessageOutline,
confirmDeleteMessage,
confirmRegenerateMessage
} = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -646,6 +650,24 @@ const SettingsTab: FC<Props> = (props) => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.confirm_delete_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={confirmDeleteMessage}
onChange={(checked) => dispatch(setConfirmDeleteMessage(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.confirm_regenerate_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={confirmRegenerateMessage}
onChange={(checked) => dispatch(setConfirmRegenerateMessage(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language.label')}</SettingRowTitleSmall>
<Selector

View File

@ -462,10 +462,15 @@ export async function fetchChatCompletion({
const filteredMessages4 = filterAdjacentUserMessaegs(filteredMessages3)
const _messages = filterUserRoleStartMessages(
let _messages = filterUserRoleStartMessages(
filterEmptyMessages(filterAfterContextClearMessages(takeRight(filteredMessages4, contextCount + 2))) // 取原来几个provider的最大值
)
// Fallback: ensure at least the last user message is present to avoid empty payloads
if ((!_messages || _messages.length === 0) && lastUserMessage) {
_messages = [lastUserMessage]
}
// FIXME: qwen3即使关闭思考仍然会导致enableReasoning的结果为true
const enableReasoning =
((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) &&

View File

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

View File

@ -2321,6 +2321,18 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'144': (state: RootState) => {
try {
if (state.settings) {
state.settings.confirmDeleteMessage = settingsInitialState.confirmDeleteMessage
state.settings.confirmRegenerateMessage = settingsInitialState.confirmRegenerateMessage
}
return state
} catch (error) {
logger.error('migrate 144 error', error as Error)
return state
}
}
}

View File

@ -122,6 +122,9 @@ export interface SettingsState {
enableTopicNaming: boolean
customCss: string
topicNamingPrompt: string
// 消息操作确认设置
confirmDeleteMessage: boolean
confirmRegenerateMessage: boolean
// Sidebar icons
sidebarIcons: {
visible: SidebarIcon[]
@ -346,6 +349,9 @@ export const initialState: SettingsState = {
enableSpellCheck: false,
spellCheckLanguages: [],
enableQuickPanelTriggers: false,
// 消息操作确认设置
confirmDeleteMessage: true,
confirmRegenerateMessage: true,
// 硬件加速设置
disableHardwareAcceleration: false,
exportMenuOptions: {
@ -770,6 +776,12 @@ const settingsSlice = createSlice({
setEnableQuickPanelTriggers: (state, action: PayloadAction<boolean>) => {
state.enableQuickPanelTriggers = action.payload
},
setConfirmDeleteMessage: (state, action: PayloadAction<boolean>) => {
state.confirmDeleteMessage = action.payload
},
setConfirmRegenerateMessage: (state, action: PayloadAction<boolean>) => {
state.confirmRegenerateMessage = action.payload
},
setDisableHardwareAcceleration: (state, action: PayloadAction<boolean>) => {
state.disableHardwareAcceleration = action.payload
},
@ -949,6 +961,8 @@ export const {
setSpellCheckLanguages,
setExportMenuOptions,
setEnableQuickPanelTriggers,
setConfirmDeleteMessage,
setConfirmRegenerateMessage,
setDisableHardwareAcceleration,
setOpenAISummaryText,
setOpenAIVerbosity,

View File

@ -338,6 +338,15 @@ const fetchAndProcessAssistantResponseImpl = async (
messagesForContext = contextSlice.filter((m) => m && !m.status?.includes('ing'))
}
// Ensure at least the triggering user message is present to avoid empty payloads
if ((!messagesForContext || messagesForContext.length === 0) && userMessageId) {
const stateAfter = getState()
const maybeUserMessage = stateAfter.messages.entities[userMessageId]
if (maybeUserMessage) {
messagesForContext = [maybeUserMessage]
}
}
callbacks = createCallbacks({
blockManager,
dispatch,