From 9856acb8c3d6c50371236bbb6e5c7bed60d5f5d3 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 19:18:34 +0800 Subject: [PATCH] fix message id mismatch in frontend --- packages/shared/src/contracts/editorAgent.ts | 5 +++-- .../EditorAgentConversationPanelView.test.tsx | 9 +++++++-- .../EditorAgentConversationPanelView.tsx | 3 +-- .../EditorAgentConversation/MessageBubble.tsx | 4 +--- .../useEditorAgentConversation.test.tsx | 19 +++++++++++++------ .../useEditorAgentConversation.ts | 17 ++++++++++------- .../ImageCanvasEditorView.test.tsx | 6 ++++-- 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/shared/src/contracts/editorAgent.ts b/packages/shared/src/contracts/editorAgent.ts index 58a3d536b..0a98ac9d3 100644 --- a/packages/shared/src/contracts/editorAgent.ts +++ b/packages/shared/src/contracts/editorAgent.ts @@ -96,8 +96,9 @@ export interface EditorAgentToolCall { } export interface EditorAgentMessage { - // frontend should not depend on this, it is used only for locate tool call - // id: number; + // Frontend must not use this to organize messages. + // It is an opaque backend locator for pending tool-call operations. + 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 8ad97a37f..805b096fa 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -71,6 +71,7 @@ function createClient(): EditorAgentConversationClient { title: '角色参考', messages: [ { + id: 0, role: 'assistant', text: '已经看到画布内容', attachments: [], @@ -98,6 +99,7 @@ function createClient(): EditorAgentConversationClient { }, deltaMessages: [ { + id: 1, role: 'assistant', text: '收到,我会参考这张图。', attachments: [], @@ -114,6 +116,7 @@ function createClient(): EditorAgentConversationClient { function createPendingToolCallMessage(): EditorAgentMessage { return { + id: 2, role: 'system', text: 'internal system prompt that must stay hidden', attachments: [], @@ -454,6 +457,7 @@ describe('EditorAgentConversationPanelView', () => { }, deltaMessages: [ { + id: 1, role: 'assistant', text: '我来生成图片。', attachments: [], @@ -550,7 +554,7 @@ describe('EditorAgentConversationPanelView', () => { fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { - expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 0); + expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 2); }); expect(screen.getByRole('button', { name: '执行中' })).toBeTruthy(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); @@ -561,6 +565,7 @@ describe('EditorAgentConversationPanelView', () => { await act(async () => { resolveConfirmation({ ...pendingMessage, + id: 2, text: 'internal completed tool output that must stay hidden', toolCall: { ...pendingMessage.toolCall!, @@ -620,7 +625,7 @@ describe('EditorAgentConversationPanelView', () => { fireEvent.click(await screen.findByRole('button', { name: '取消' })); expect(await screen.findByText('已取消')).toBeTruthy(); - expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 0); + expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 2); 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 3ad8b326b..0c116ec52 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx @@ -598,9 +598,8 @@ export function EditorAgentConversationPanelView({ void; onCancelToolCall: (messageId: number) => void; @@ -47,7 +46,6 @@ type MessageBubbleProps = { export function MessageBubble({ message, - messageIndex, busyAction, onConfirmToolCall, onCancelToolCall, @@ -64,7 +62,7 @@ export function MessageBubble({ ) { return ( { it('appends a lazily reconciled tool message delta', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { + id: 0, role: 'system', text: 'pending edit', attachments: [], @@ -335,6 +337,7 @@ describe('useEditorAgentConversation', () => { }, deltaMessages: [ { + id: 2, role: 'system', text: '需要生成一张图', attachments: [], @@ -355,6 +358,7 @@ describe('useEditorAgentConversation', () => { errorMessage: null, } as EditorAgentMessageResponse); vi.mocked(client.confirmToolCall).mockResolvedValue({ + id: 2, role: 'system', text: 'internal completed tool output', attachments: [], @@ -404,14 +408,15 @@ describe('useEditorAgentConversation', () => { const systemMsg = result.current.messages[1]; expect(systemMsg?.role).toBe('system'); + expect(systemMsg?.id).toBe(2); expect(systemMsg?.toolCall?.externalJobId).toBeUndefined(); const messageCount = result.current.messages.length; await act(async () => { - await result.current.confirmToolCall(1); + await result.current.confirmToolCall(systemMsg!.id); }); - expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 1); + expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 2); expect(result.current.messages).toHaveLength(messageCount); expect(result.current.messages[1]?.toolCall?.externalJobId).toBe( 'task-edit-1', @@ -426,6 +431,7 @@ describe('useEditorAgentConversation', () => { it('keeps the action busy until confirmation returns an external job id', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { + id: 7, role: 'system', text: '需要生成一张图', attachments: [], @@ -476,10 +482,10 @@ describe('useEditorAgentConversation', () => { let confirmationPromise: Promise | undefined; act(() => { - confirmationPromise = result.current.confirmToolCall(0); + confirmationPromise = result.current.confirmToolCall(7); }); await waitFor(() => expect(resolveConfirmation).toBeTypeOf('function')); - expect(client.confirmToolCall).toHaveBeenCalledTimes(1); + expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 7); await act(async () => { resolveConfirmation?.(completedMessage); @@ -495,6 +501,7 @@ describe('useEditorAgentConversation', () => { it('cancels a pending tool call and keeps the replacement in the same position', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { + id: 9, role: 'system' as const, text: 'internal pending tool prompt', attachments: [], @@ -537,10 +544,10 @@ describe('useEditorAgentConversation', () => { expect(result.current.messages[0]?.toolCall?.status).toBe('not_completed'); }); await act(async () => { - await result.current.cancelToolCall(0); + await result.current.cancelToolCall(9); }); - expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 0); + expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 9); expect(result.current.messages).toHaveLength(1); expect(result.current.messages[0]?.toolCall?.status).toBe('cancelled'); expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index 54a26837f..82a1e250d 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 = { - messageIndex: number; + messageId: number; action: EditorAgentToolCallAction; } | null; @@ -84,10 +84,12 @@ 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, @@ -356,6 +358,7 @@ export function useEditorAgentConversation({ setMessages((currentMessages) => [ ...currentMessages, createLocalUserMessage({ + id: -1, // does not care text, attachments, }), @@ -415,29 +418,29 @@ export function useEditorAgentConversation({ }, []); const resolveToolCall = useCallback( - async (messageIndex: number, action: EditorAgentToolCallAction) => { + async (messageId: number, action: EditorAgentToolCallAction) => { const conversationId = activeConversationIdRef.current; if (!conversationId || activeToolCallActionRef.current) { return null; } - const nextAction = { messageIndex, action } as const; + const nextAction = { messageId, action } as const; activeToolCallActionRef.current = nextAction; setToolCallAction(nextAction); setErrorMessage(null); try { const updatedMessage = await (action === 'confirm' - ? client.confirmToolCall(conversationId, messageIndex) - : client.cancelToolCall(conversationId, messageIndex)); + ? client.confirmToolCall(conversationId, messageId) + : client.cancelToolCall(conversationId, messageId)); if (activeConversationIdRef.current !== conversationId) { return updatedMessage; } setMessages((currentMessages) => - currentMessages.map((message, index) => - index === messageIndex ? updatedMessage : message, + currentMessages.map((message) => + message.id === messageId ? updatedMessage : message, ), ); if (action === 'confirm') { diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index a5d034c42..aa712cc09 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -220,6 +220,7 @@ function createEditorAgentDetailWithGeneration( createdAt: '2026-07-03T00:00:00.000Z', messages: [ { + id: 0, role: 'system', text: 'internal completed tool output', attachments: [], @@ -2143,6 +2144,7 @@ describe('ImageCanvasEditorView', () => { createdAt: '2026-07-03T00:00:00.000Z', messages: [ { + id: 3, role: 'system', text: 'internal pending tool prompt', attachments: [], @@ -2165,7 +2167,7 @@ describe('ImageCanvasEditorView', () => { confirmEditorAgentToolCallMock.mockImplementation(async () => { showGeneratedAsset = true; return { - id: 0, + id: 3, role: 'system', text: 'internal completed tool output', attachments: [], @@ -2242,7 +2244,7 @@ describe('ImageCanvasEditorView', () => { await waitFor(() => { expect(confirmEditorAgentToolCallMock).toHaveBeenCalledWith( 'editor-agent-conv-test', - 0, + 3, ); expect(loadEditorProjectMock).toHaveBeenCalledWith( 'editor-project-default',