frontend message dont depend on message.id

This commit is contained in:
2026-07-15 17:26:40 +08:00
parent acb8149a12
commit 2113c33789
7 changed files with 26 additions and 56 deletions
+2 -1
View File
@@ -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[];
@@ -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',
@@ -594,12 +594,13 @@ export function EditorAgentConversationPanelView({
</div>
) : messages.length ? (
<>
{messages.map((message) => (
{messages.map((message, messageIndex) => (
<MessageBubble
key={message.id}
key={`${message.createdAt}-${messageIndex}`}
message={message}
messageIndex={messageIndex}
busyAction={
toolCallAction?.messageId === message.id
toolCallAction?.messageIndex === messageIndex
? toolCallAction.action
: null
}
@@ -38,6 +38,7 @@ export function ThinkingBubble() {
type MessageBubbleProps = {
message: EditorAgentMessage;
messageIndex: number;
busyAction: 'confirm' | 'cancel' | null;
onConfirmToolCall: (messageId: number) => 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 (
<PendingToolCall
messageId={message.id}
messageId={messageIndex}
toolCall={message.toolCall}
busyAction={busyAction}
onConfirm={onConfirmToolCall}
@@ -75,7 +75,6 @@ function createClient(): EditorAgentConversationClient {
},
deltaMessages: [
{
id: 1,
role: 'assistant',
text: '我来处理',
attachments: [],
@@ -169,10 +168,9 @@ describe('useEditorAgentConversation', () => {
);
});
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: [],
@@ -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<AbortController | null>(null);
const activeConversationIdRef = useRef<string | null>(null);
const activeToolCallActionRef = useRef<EditorAgentToolCallActionState>(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') {
@@ -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: [],