fix message id mismatch in frontend
This commit is contained in:
@@ -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[];
|
||||
|
||||
+7
-2
@@ -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',
|
||||
|
||||
+1
-2
@@ -598,9 +598,8 @@ export function EditorAgentConversationPanelView({
|
||||
<MessageBubble
|
||||
key={`${message.createdAt}-${messageIndex}`}
|
||||
message={message}
|
||||
messageIndex={messageIndex}
|
||||
busyAction={
|
||||
toolCallAction?.messageIndex === messageIndex
|
||||
toolCallAction?.messageId === message.id
|
||||
? toolCallAction.action
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ export function ThinkingBubble() {
|
||||
|
||||
type MessageBubbleProps = {
|
||||
message: EditorAgentMessage;
|
||||
messageIndex: number;
|
||||
busyAction: 'confirm' | 'cancel' | null;
|
||||
onConfirmToolCall: (messageId: number) => 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 (
|
||||
<PendingToolCall
|
||||
messageId={messageIndex}
|
||||
messageId={message.id}
|
||||
toolCall={message.toolCall}
|
||||
busyAction={busyAction}
|
||||
onConfirm={onConfirmToolCall}
|
||||
|
||||
+13
-6
@@ -75,6 +75,7 @@ function createClient(): EditorAgentConversationClient {
|
||||
},
|
||||
deltaMessages: [
|
||||
{
|
||||
id: 1,
|
||||
role: 'assistant',
|
||||
text: '我来处理',
|
||||
attachments: [],
|
||||
@@ -171,6 +172,7 @@ describe('useEditorAgentConversation', () => {
|
||||
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<EditorAgentMessage | null> | 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();
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user