mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-09 08:44:45 +08:00
fix: react key warnings from duplicate message IDs + establish jest testing framework (#655)
* fix: resolve issue #588 - react key warnings from duplicate message IDs + establish jest testing framework * Update the makefile and workflow with the js test * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -43,7 +43,7 @@ import {
|
||||
useLastFeedbackMessageId,
|
||||
useLastInterruptMessage,
|
||||
useMessage,
|
||||
useMessageIds,
|
||||
useRenderableMessageIds,
|
||||
useResearchMessage,
|
||||
useStore,
|
||||
} from "~/core/store";
|
||||
@@ -63,7 +63,8 @@ export function MessageListView({
|
||||
) => void;
|
||||
}) {
|
||||
const scrollContainerRef = useRef<ScrollContainerRef>(null);
|
||||
const messageIds = useMessageIds();
|
||||
// Use renderable message IDs to avoid React key warnings from duplicate or non-rendering messages
|
||||
const messageIds = useRenderableMessageIds();
|
||||
const interruptMessage = useLastInterruptMessage();
|
||||
const waitingForFeedbackMessageId = useLastFeedbackMessageId();
|
||||
const responding = useStore((state) => state.responding);
|
||||
|
||||
@@ -46,10 +46,16 @@ export const useStore = create<{
|
||||
openResearchId: null,
|
||||
|
||||
appendMessage(message: Message) {
|
||||
set((state) => ({
|
||||
messageIds: [...state.messageIds, message.id],
|
||||
messages: new Map(state.messages).set(message.id, message),
|
||||
}));
|
||||
set((state) => {
|
||||
// Prevent duplicate message IDs in the array to avoid React key warnings
|
||||
const newMessageIds = state.messageIds.includes(message.id)
|
||||
? state.messageIds
|
||||
: [...state.messageIds, message.id];
|
||||
return {
|
||||
messageIds: newMessageIds,
|
||||
messages: new Map(state.messages).set(message.id, message),
|
||||
};
|
||||
});
|
||||
},
|
||||
updateMessage(message: Message) {
|
||||
set((state) => ({
|
||||
@@ -137,32 +143,48 @@ export async function sendMessage(
|
||||
try {
|
||||
for await (const event of stream) {
|
||||
const { type, data } = event;
|
||||
messageId = data.id;
|
||||
let message: Message | undefined;
|
||||
|
||||
// Handle tool_call_result specially: use the message that contains the tool call
|
||||
if (type === "tool_call_result") {
|
||||
message = findMessageByToolCallId(data.tool_call_id);
|
||||
} else if (!existsMessage(messageId)) {
|
||||
message = {
|
||||
id: messageId,
|
||||
threadId: data.thread_id,
|
||||
agent: data.agent,
|
||||
role: data.role,
|
||||
content: "",
|
||||
contentChunks: [],
|
||||
reasoningContent: "",
|
||||
reasoningContentChunks: [],
|
||||
isStreaming: true,
|
||||
interruptFeedback,
|
||||
};
|
||||
appendMessage(message);
|
||||
if (message) {
|
||||
// Use the found message's ID, not data.id
|
||||
messageId = message.id;
|
||||
} else {
|
||||
// Shouldn't happen, but handle gracefully
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.warn(`Tool call result without matching message: ${data.tool_call_id}`);
|
||||
}
|
||||
continue; // Skip this event
|
||||
}
|
||||
} else {
|
||||
// For other event types, use data.id
|
||||
messageId = data.id;
|
||||
|
||||
if (!existsMessage(messageId)) {
|
||||
message = {
|
||||
id: messageId,
|
||||
threadId: data.thread_id,
|
||||
agent: data.agent,
|
||||
role: data.role,
|
||||
content: "",
|
||||
contentChunks: [],
|
||||
reasoningContent: "",
|
||||
reasoningContentChunks: [],
|
||||
isStreaming: true,
|
||||
interruptFeedback,
|
||||
};
|
||||
appendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
message ??= getMessage(messageId);
|
||||
if (message) {
|
||||
message = mergeMessage(message, event);
|
||||
// Collect pending messages for update, instead of updating immediately.
|
||||
pendingUpdates.set(message.id, message);
|
||||
scheduleUpdate();
|
||||
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -383,6 +405,29 @@ export function useMessageIds() {
|
||||
return useStore(useShallow((state) => state.messageIds));
|
||||
}
|
||||
|
||||
export function useRenderableMessageIds() {
|
||||
return useStore(
|
||||
useShallow((state) => {
|
||||
// Filter to only messages that will actually render in MessageListView
|
||||
// This prevents duplicate keys and React warnings when messages change state
|
||||
return state.messageIds.filter((messageId) => {
|
||||
const message = state.messages.get(messageId);
|
||||
if (!message) return false;
|
||||
|
||||
// Only include messages that match MessageListItem rendering conditions
|
||||
// These are the same conditions checked in MessageListItem component
|
||||
return (
|
||||
message.role === "user" ||
|
||||
message.agent === "coordinator" ||
|
||||
message.agent === "planner" ||
|
||||
message.agent === "podcast" ||
|
||||
state.researchIds.includes(messageId) // startOfResearch condition
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function useLastInterruptMessage() {
|
||||
return useStore(
|
||||
useShallow((state) => {
|
||||
|
||||
Reference in New Issue
Block a user