frontend message dont depend on message.id
This commit is contained in:
@@ -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[];
|
||||
|
||||
+6
-12
@@ -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',
|
||||
|
||||
+4
-3
@@ -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}
|
||||
|
||||
+3
-12
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user