From 2113c3378986c15bde346fda463894d62444ce5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 15 Jul 2026 17:26:40 +0800 Subject: [PATCH] frontend message dont depend on message.id --- packages/shared/src/contracts/editorAgent.ts | 3 +- .../EditorAgentConversationPanelView.test.tsx | 18 ++++------ .../EditorAgentConversationPanelView.tsx | 7 ++-- .../EditorAgentConversation/MessageBubble.tsx | 4 ++- .../useEditorAgentConversation.test.tsx | 15 ++------- .../useEditorAgentConversation.ts | 33 +++++-------------- .../ImageCanvasEditorView.test.tsx | 2 -- 7 files changed, 26 insertions(+), 56 deletions(-) diff --git a/packages/shared/src/contracts/editorAgent.ts b/packages/shared/src/contracts/editorAgent.ts index d5437301b..58a3d536b 100644 --- a/packages/shared/src/contracts/editorAgent.ts +++ b/packages/shared/src/contracts/editorAgent.ts @@ -96,7 +96,8 @@ export interface EditorAgentToolCall { } export interface EditorAgentMessage { - id: number; + // frontend should not depend on this, it is used only for locate tool call + // id: number; role: EditorAgentMessageRole; text: string; attachments: EditorAgentAttachmentRef[]; diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index bb4d6ac83..8ad97a37f 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -71,7 +71,6 @@ function createClient(): EditorAgentConversationClient { title: '角色参考', messages: [ { - id: 1, role: 'assistant', text: '已经看到画布内容', attachments: [], @@ -99,7 +98,6 @@ function createClient(): EditorAgentConversationClient { }, deltaMessages: [ { - id: 2, role: 'assistant', text: '收到,我会参考这张图。', attachments: [], @@ -116,7 +114,6 @@ function createClient(): EditorAgentConversationClient { function createPendingToolCallMessage(): EditorAgentMessage { return { - id: 2, role: 'system', text: 'internal system prompt that must stay hidden', attachments: [], @@ -457,7 +454,6 @@ describe('EditorAgentConversationPanelView', () => { }, deltaMessages: [ { - id: 3, role: 'assistant', text: '我来生成图片。', attachments: [], @@ -554,15 +550,13 @@ describe('EditorAgentConversationPanelView', () => { fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { - expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 2); + expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 0); }); - expect( - within(screen.getByRole('article', { name: 'Agent操作' })).getByText( - '执行中', - ), - ).toBeTruthy(); + expect(screen.getByRole('button', { name: '执行中' })).toBeTruthy(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(screen.queryByRole('button', { name: '取消' })).toBeNull(); + expect(screen.getByRole('button', { name: '取消' }).hasAttribute('disabled')).toBe( + true, + ); await act(async () => { resolveConfirmation({ @@ -626,7 +620,7 @@ describe('EditorAgentConversationPanelView', () => { fireEvent.click(await screen.findByRole('button', { name: '取消' })); expect(await screen.findByText('已取消')).toBeTruthy(); - expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 2); + expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 0); expect( screen.queryByText( 'internal cancelled tool output that must stay hidden', diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx index 31335c96a..3ad8b326b 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx @@ -594,12 +594,13 @@ export function EditorAgentConversationPanelView({ ) : messages.length ? ( <> - {messages.map((message) => ( + {messages.map((message, messageIndex) => ( void; onCancelToolCall: (messageId: number) => void; @@ -46,6 +47,7 @@ type MessageBubbleProps = { export function MessageBubble({ message, + messageIndex, busyAction, onConfirmToolCall, onCancelToolCall, @@ -62,7 +64,7 @@ export function MessageBubble({ ) { return ( { ); }); - it('replaces a lazily reconciled tool message instead of appending a duplicate', async () => { + it('appends a lazily reconciled tool message delta', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { - id: 1, role: 'system', text: 'pending edit', attachments: [], @@ -229,11 +227,8 @@ describe('useEditorAgentConversation', () => { await result.current.sendMessage('继续'); }); - const reconciledMessages = result.current.messages.filter( - (message) => message.id === 1, - ); - expect(reconciledMessages).toHaveLength(1); - expect(reconciledMessages[0]?.toolCall?.images[0]?.imageSrc).toBe( + expect(result.current.messages).toHaveLength(3); + expect(result.current.messages[2]?.toolCall?.images[0]?.imageSrc).toBe( '/generated/result.png', ); }); @@ -340,7 +335,6 @@ describe('useEditorAgentConversation', () => { }, deltaMessages: [ { - id: 1, role: 'system', text: '需要生成一张图', attachments: [], @@ -361,7 +355,6 @@ describe('useEditorAgentConversation', () => { errorMessage: null, } as EditorAgentMessageResponse); vi.mocked(client.confirmToolCall).mockResolvedValue({ - id: 1, role: 'system', text: 'internal completed tool output', attachments: [], @@ -433,7 +426,6 @@ describe('useEditorAgentConversation', () => { it('keeps the action busy until confirmation returns an external job id', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { - id: 0, role: 'system', text: '需要生成一张图', attachments: [], @@ -503,7 +495,6 @@ describe('useEditorAgentConversation', () => { it('cancels a pending tool call and keeps the replacement in the same position', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { - id: 0, role: 'system' as const, text: 'internal pending tool prompt', attachments: [], diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index 8a25f3a81..54a26837f 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts @@ -59,7 +59,7 @@ type UseEditorAgentConversationOptions = { export type EditorAgentToolCallAction = 'confirm' | 'cancel'; export type EditorAgentToolCallActionState = { - messageId: number; + messageIndex: number; action: EditorAgentToolCallAction; } | null; @@ -84,12 +84,10 @@ function isAbortError(error: unknown) { } function createLocalUserMessage(params: { - id: number; text: string; attachments: EditorAgentAttachmentRef[]; }): EditorAgentMessage { return { - id: params.id, role: 'user', text: params.text, attachments: params.attachments, @@ -160,7 +158,6 @@ export function useEditorAgentConversation({ const activeRequestAbortControllerRef = useRef(null); const activeConversationIdRef = useRef(null); const activeToolCallActionRef = useRef(null); - const nextLocalMessageIdRef = useRef(-1); useEffect(() => { activeConversationIdRef.current = activeConversationId; @@ -324,18 +321,7 @@ export function useEditorAgentConversation({ const applyDeltaMessages = useCallback( (deltaMessages: EditorAgentMessage[]) => { setMessages((currentMessages) => { - const nextMessages = [...currentMessages]; - for (const deltaMessage of deltaMessages) { - const existingIndex = nextMessages.findIndex( - (message) => message.id === deltaMessage.id, - ); - if (existingIndex >= 0) { - nextMessages[existingIndex] = deltaMessage; - } else { - nextMessages.push(deltaMessage); - } - } - return nextMessages; + return [...currentMessages, ...deltaMessages]; }); requestCanvasRefreshForMessages(deltaMessages); }, @@ -367,12 +353,9 @@ export function useEditorAgentConversation({ activeRequestAbortControllerRef.current = abortController; setErrorMessage(null); setIsWaiting(true); - const localMessageId = nextLocalMessageIdRef.current; - nextLocalMessageIdRef.current -= 1; setMessages((currentMessages) => [ ...currentMessages, createLocalUserMessage({ - id: localMessageId, text, attachments, }), @@ -432,29 +415,29 @@ export function useEditorAgentConversation({ }, []); const resolveToolCall = useCallback( - async (messageId: number, action: EditorAgentToolCallAction) => { + async (messageIndex: number, action: EditorAgentToolCallAction) => { const conversationId = activeConversationIdRef.current; if (!conversationId || activeToolCallActionRef.current) { return null; } - const nextAction = { messageId, action } as const; + const nextAction = { messageIndex, action } as const; activeToolCallActionRef.current = nextAction; setToolCallAction(nextAction); setErrorMessage(null); try { const updatedMessage = await (action === 'confirm' - ? client.confirmToolCall(conversationId, messageId) - : client.cancelToolCall(conversationId, messageId)); + ? client.confirmToolCall(conversationId, messageIndex) + : client.cancelToolCall(conversationId, messageIndex)); if (activeConversationIdRef.current !== conversationId) { return updatedMessage; } setMessages((currentMessages) => - currentMessages.map((message) => - message.id === updatedMessage.id ? updatedMessage : message, + currentMessages.map((message, index) => + index === messageIndex ? updatedMessage : message, ), ); if (action === 'confirm') { diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index d8620f848..a5d034c42 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -220,7 +220,6 @@ function createEditorAgentDetailWithGeneration( createdAt: '2026-07-03T00:00:00.000Z', messages: [ { - id: 1, role: 'system', text: 'internal completed tool output', attachments: [], @@ -2144,7 +2143,6 @@ describe('ImageCanvasEditorView', () => { createdAt: '2026-07-03T00:00:00.000Z', messages: [ { - id: 0, role: 'system', text: 'internal pending tool prompt', attachments: [],