diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index a65717054..ff02c78bd 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -534,6 +534,41 @@ function claimInitialSupervisorMessageForPage(projectPath: string) { return true; } +/** + * 历史回读与"尚未落盘的运行时消息"合并。 + * + * 初始需求是**乐观插入**到 messages 的(latch 命中后先插一条 user 消息,再发起回合), + * 而历史回读在 replace 分支里是无条件整体替换 —— 只要回读晚于乐观插入,那条用户消息 + * 就会被冲掉(界面上看不到初始需求,但回合其实已经跑起来了)。 + * 这里把当前 messages 里"运行时拥有、且回读结果里没有"的消息保留在末尾(它们是最新的)。 + */ +function mergeLoadedConversationWithPendingRuntimeMessages( + loaded: ChatMessage[], + current: ChatMessage[], +): ChatMessage[] { + if (current.length === 0) { + return loaded; + } + const loadedIds = new Set( + loaded + .map((message) => message.messageId) + .filter((id): id is string => Boolean(id)), + ); + const loadedTexts = new Set( + loaded.map((message) => `${message.role}\u0000${message.text}`), + ); + const pending = current.filter((message) => { + if (!message.runtimeOwned) { + return false; + } + if (message.messageId) { + return !loadedIds.has(message.messageId); + } + return !loadedTexts.has(`${message.role}\u0000${message.text}`); + }); + return pending.length > 0 ? [...loaded, ...pending] : loaded; +} + export { AuthenticatedClient } from './app/AuthenticatedClient'; export type { PendingCommand } from './app/types'; export { @@ -3475,8 +3510,12 @@ export function App({ setProjectSupervisorResponseStream(null); } setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setMessages(() => { - const nextMessages = conversationMessages; + setMessages((current) => { + // 不能整体替换:乐观插入、尚未落盘的用户消息会被冲掉(初始需求看不到就是这个原因)。 + const nextMessages = mergeLoadedConversationWithPendingRuntimeMessages( + conversationMessages, + current, + ); savedConversationProjectPathRef.current = nextProjectPath; savedConversationCountRef.current = nextMessages.length; latestMessagesRef.current = nextMessages; @@ -3690,7 +3729,12 @@ export function App({ } setProjectSupervisorRuntimeError(runtimeError || resumeError); setMessages((current) => { - const nextConversationMessages = conversationMessages; + // replace 分支同样不能丢掉尚未落盘的运行时消息(初始需求)。 + const nextConversationMessages = + mergeLoadedConversationWithPendingRuntimeMessages( + conversationMessages, + current, + ); const hasOnlyDefaultGreeting = current.length === 1 && current[0]?.role === 'assistant' && diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index a0f965ca2..3358312d0 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -591,15 +591,7 @@ export function ProjectSupervisorView({ : '陶泥儿正在处理'} - {transientReply ? ( -