diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index f24759d9e..21ff93a40 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -815,67 +815,69 @@ export function App({ [gameChatOnly], ); - function updateProjectSupervisorResponseStream( - incoming: AgentRuntimeResponseStream | null | undefined, - runtime: AgentRuntimeState, - ) { - let nextStream = mergeProjectSupervisorResponseStream( - projectSupervisorResponseStreamRef.current, - incoming, - runtime, - ); - const candidateStream = nextStream; - if (candidateStream?.status === 'ready' && gameChatOnly) { - const text = candidateStream.accumulatedText.trim(); - const responseKey = projectSupervisorResponseStreamIdentity( - candidateStream, + const updateProjectSupervisorResponseStream = useCallback( + ( + incoming: AgentRuntimeResponseStream | null | undefined, + runtime: AgentRuntimeState, + ) => { + let nextStream = mergeProjectSupervisorResponseStream( + projectSupervisorResponseStreamRef.current, + incoming, + runtime, ); - const messageId = `runtime-response:${responseKey}`; - const alreadyCommitted = - gameChatCommittedResponseStreamKeysRef.current.has(responseKey) || + const candidateStream = nextStream; + if (candidateStream?.status === 'ready' && gameChatOnly) { + const text = candidateStream.accumulatedText.trim(); + const responseKey = + projectSupervisorResponseStreamIdentity(candidateStream); + const messageId = `runtime-response:${responseKey}`; + const alreadyCommitted = + gameChatCommittedResponseStreamKeysRef.current.has(responseKey) || + latestMessagesRef.current.some( + (message) => message.messageId === messageId, + ); + if (text && !alreadyCommitted) { + gameChatCommittedResponseStreamKeysRef.current.add(responseKey); + const nextMessage: ChatMessage = { + role: 'assistant', + text: candidateStream.accumulatedText, + messageId, + agentId: PROJECT_SUPERVISOR_AGENT_ID, + updatedAt: candidateStream.updatedAt, + runtimeOwned: true, + }; + setMessages((current) => { + if (current.some((message) => message.messageId === messageId)) { + latestMessagesRef.current = current; + return current; + } + const nextMessages = [...current, nextMessage]; + latestMessagesRef.current = nextMessages; + return nextMessages; + }); + } + // Ready text is now a normal chat message; transientReply must not show + // the same response a second time while polling/event delivery catches up. + nextStream = null; + } else if ( + candidateStream && latestMessagesRef.current.some( - (message) => message.messageId === messageId, - ); - if (text && !alreadyCommitted) { - gameChatCommittedResponseStreamKeysRef.current.add(responseKey); - const nextMessage: ChatMessage = { - role: 'assistant', - text: candidateStream.accumulatedText, - messageId, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - updatedAt: candidateStream.updatedAt, - runtimeOwned: true, - }; - setMessages((current) => { - if (current.some((message) => message.messageId === messageId)) { - latestMessagesRef.current = current; - return current; - } - const nextMessages = [...current, nextMessage]; - latestMessagesRef.current = nextMessages; - return nextMessages; - }); + (message) => + message.runtimeOwned && + message.role === 'assistant' && + message.text === candidateStream.accumulatedText && + typeof message.updatedAt === 'number' && + message.updatedAt >= candidateStream.startedAt, + ) + ) { + // Keep the existing supervisor-chat behavior for restored history. + nextStream = null; } - // Ready text is now a normal chat message; transientReply must not show - // the same response a second time while polling/event delivery catches up. - nextStream = null; - } else if ( - candidateStream && - latestMessagesRef.current.some( - (message) => - message.runtimeOwned && - message.role === 'assistant' && - message.text === candidateStream.accumulatedText && - typeof message.updatedAt === 'number' && - message.updatedAt >= candidateStream.startedAt, - ) - ) { - // Keep the existing supervisor-chat behavior for restored history. - nextStream = null; - } - projectSupervisorResponseStreamRef.current = nextStream; - setProjectSupervisorResponseStream(nextStream); - } + projectSupervisorResponseStreamRef.current = nextStream; + setProjectSupervisorResponseStream(nextStream); + }, + [gameChatOnly], + ); function resetProjectSupervisorState() { projectSupervisorHistoryLoadVersionRef.current += 1; @@ -1155,7 +1157,7 @@ export function App({ disposed = true; cleanup?.(); }; - }, [updateProjectSupervisorRuntime]); + }, [updateProjectSupervisorResponseStream, updateProjectSupervisorRuntime]); useEffect(() => { const invoke = resolveTauriInvoke(); diff --git a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts index 7757782ff..746c6c2a5 100644 --- a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts +++ b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts @@ -8,12 +8,12 @@ import type { } from '../src/app/types'; import { formatAgentRuntimeEvent, + mergeGameChatRuntimeResponseMessagesIntoHistory, projectRuntimeVisibleCurrentWork, projectRuntimeVisibleError, projectSupervisorChatRuntimeStatus, projectSupervisorResponseStreamIdentity, projectSupervisorVisibleConversationText, - mergeGameChatRuntimeResponseMessagesIntoHistory, submitProjectSupervisorRuntimeTask, } from '../src/features/agent-runtime/model';