diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index 805b096fa..7dc459ef1 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -446,6 +446,69 @@ describe('EditorAgentConversationPanelView', () => { }); }); + it('restores the draft and selected attachments when sending fails', async () => { + const client = createClient(); + vi.mocked(client.sendMessage).mockRejectedValueOnce( + new Error('Network error'), + ); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByText('已经看到画布内容')).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole('button', { name: '添加附件' })); + const attachmentDialog = screen.getByRole('dialog', { + name: '选择图片附件', + }); + fireEvent.click( + within(attachmentDialog).getByRole('checkbox', { + name: '选择画布图片 角色图层', + }), + ); + fireEvent.click( + within(attachmentDialog).getByRole('button', { name: '应用' }), + ); + fireEvent.change(screen.getByLabelText('发送给画布 Agent'), { + target: { value: '失败后恢复这条草稿' }, + }); + fireEvent.click(screen.getByRole('button', { name: '发送' })); + + expect(await screen.findByText('Network error')).toBeTruthy(); + expect( + (screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value, + ).toBe('失败后恢复这条草稿'); + expect(screen.getByText('角色图层')).toBeTruthy(); + expect( + within(screen.getByRole('log', { name: '画布 Agent 消息流' })).queryByText( + '失败后恢复这条草稿', + ), + ).toBeNull(); + }); + it('preserves messages when the panel is collapsed and reopened', async () => { const client = createClient(); vi.mocked(client.sendMessage).mockResolvedValue({ diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx index 84a5371c8..8f233e88c 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx @@ -553,7 +553,7 @@ describe('useEditorAgentConversation', () => { expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); }); - it('handles fetch error as failed stage', async () => { + it('rethrows fetch errors and rolls back the optimistic message', async () => { const client = createClient(); vi.mocked(client.sendMessage).mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => @@ -567,10 +567,13 @@ describe('useEditorAgentConversation', () => { }); await act(async () => { - await expect(result.current.sendMessage('test')).resolves.toBeUndefined(); + await expect(result.current.sendMessage('test')).rejects.toThrow( + 'Network error', + ); }); expect(result.current.errorMessage).toBe('Network error'); + expect(result.current.messages).toHaveLength(0); }); it('aborts the active request and resets state on stopCurrentTurn', async () => { diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index 82a1e250d..19e18d7ac 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts @@ -355,13 +355,14 @@ export function useEditorAgentConversation({ activeRequestAbortControllerRef.current = abortController; setErrorMessage(null); setIsWaiting(true); + const optimisticMessage = createLocalUserMessage({ + id: -1, + text, + attachments, + }); setMessages((currentMessages) => [ ...currentMessages, - createLocalUserMessage({ - id: -1, // does not care - text, - attachments, - }), + optimisticMessage, ]); try { @@ -394,6 +395,10 @@ export function useEditorAgentConversation({ const message = error instanceof Error ? error.message : '发送画布 Agent 消息失败'; setErrorMessage(message); + setMessages((currentMessages) => + currentMessages.filter((message) => message !== optimisticMessage), + ); + throw error; } finally { if (activeRequestAbortControllerRef.current === abortController) { activeRequestAbortControllerRef.current = null;