diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index b73e87724..386b6cc90 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -229,9 +229,9 @@ import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWo import { buildGameChatProgressEvidence, collectGameChatResultImages, + formatGameChatStageRecord, gameChatFinalReplyMessages, gameChatRuntimeEventMessages, - formatGameChatStageRecord, mergeGameChatFinalReplyMessagesIntoHistory, mergeGameChatRuntimeEventMessagesIntoHistory, SupervisorChatOnlyView, @@ -1074,65 +1074,65 @@ export function App({ }); } - function appendGameChatFinalReplyMessages( - nextProjectPath: string, - runtimeResults: AgentRuntimeResult[], - ) { - if (!gameChatOnly || runtimeResults.length === 0) { - return; - } - // A professional Runtime is only part of the active game-chat turn when - // it was delegated by the current Project Supervisor run. This prevents - // a stale child Runtime (or a different app mode) from leaking into the - // project transcript after a restart. - const supervisorRunId = - projectSupervisorRuntimeRef.current?.runId ?? - runtimeResults.find( - (result) => result.state.agentId === PROJECT_SUPERVISOR_AGENT_ID, - )?.state.runId; - if (!supervisorRunId) { - return; - } - const messages = runtimeResults.flatMap((result) => { - const runtime = agentRuntimeStateFromResult(result); - if ( - runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || - runtime.parentRunId !== supervisorRunId - ) { - return []; + const appendGameChatFinalReplyMessages = useCallback( + (nextProjectPath: string, runtimeResults: AgentRuntimeResult[]) => { + if (!gameChatOnly || runtimeResults.length === 0) { + return; } - return gameChatFinalReplyMessages([result.responseStream]); - }); - if (messages.length === 0) { - return; - } - setMessages((current) => { - const currentMessageIds = new Set( - current - .map((message) => message.messageId?.trim()) - .filter((messageId): messageId is string => Boolean(messageId)), - ); - const missingMessages = messages.filter( - (message) => - message.messageId && !currentMessageIds.has(message.messageId), - ); - if (missingMessages.length === 0) { - return current; + // A professional Runtime is only part of the active game-chat turn when + // it was delegated by the current Project Supervisor run. This prevents + // a stale child Runtime (or a different app mode) from leaking into the + // project transcript after a restart. + const supervisorRunId = + projectSupervisorRuntimeRef.current?.runId ?? + runtimeResults.find( + (result) => result.state.agentId === PROJECT_SUPERVISOR_AGENT_ID, + )?.state.runId; + if (!supervisorRunId) { + return; } - if (savedConversationProjectPathRef.current === nextProjectPath) { - savedConversationCountRef.current = Math.min( - savedConversationCountRef.current, - current.length, + const messages = runtimeResults.flatMap((result) => { + const runtime = agentRuntimeStateFromResult(result); + if ( + runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || + runtime.parentRunId !== supervisorRunId + ) { + return []; + } + return gameChatFinalReplyMessages([result.responseStream]); + }); + if (messages.length === 0) { + return; + } + setMessages((current) => { + const currentMessageIds = new Set( + current + .map((message) => message.messageId?.trim()) + .filter((messageId): messageId is string => Boolean(messageId)), ); - } - const orderedMissingMessages = [...missingMessages].sort( - (left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0), - ); - const nextMessages = [...current, ...orderedMissingMessages]; - latestMessagesRef.current = nextMessages; - return nextMessages; - }); - } + const missingMessages = messages.filter( + (message) => + message.messageId && !currentMessageIds.has(message.messageId), + ); + if (missingMessages.length === 0) { + return current; + } + if (savedConversationProjectPathRef.current === nextProjectPath) { + savedConversationCountRef.current = Math.min( + savedConversationCountRef.current, + current.length, + ); + } + const orderedMissingMessages = [...missingMessages].sort( + (left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0), + ); + const nextMessages = [...current, ...orderedMissingMessages]; + latestMessagesRef.current = nextMessages; + return nextMessages; + }); + }, + [gameChatOnly], + ); function appendGameChatStageRecord( nextProjectPath: string, @@ -1384,7 +1384,12 @@ export function App({ disposed = true; cleanup?.(); }; - }, [updateProjectSupervisorResponseStream, updateProjectSupervisorRuntime]); + }, [ + appendGameChatFinalReplyMessages, + gameChatOnly, + updateProjectSupervisorResponseStream, + updateProjectSupervisorRuntime, + ]); useEffect(() => { const invoke = resolveTauriInvoke(); diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx index e22e0abd7..d3a67f345 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx @@ -13,8 +13,8 @@ import { useEffect, useMemo, useState } from 'react'; import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import type { - AgentRuntimeResponseStream, AgentRuntimeEventRecord, + AgentRuntimeResponseStream, AgentRuntimeState, ChatMessage, LocalPreviewResult, diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 3b1fbba5e..4cd91dd7b 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -14,11 +14,11 @@ import { buildGameChatProgressEvidence, collectGameChatResultImages, collectGameChatRuntimeEvents, + formatGameChatStageRecord, gameChatFinalReplyMessages, gameChatRuntimeEventMessages, - mergeGameChatFinalReplyMessagesIntoHistory, - formatGameChatStageRecord, isGameChatStageRecordMessage, + mergeGameChatFinalReplyMessagesIntoHistory, SupervisorChatOnlyView, } from '../../src/features/project-workspace/SupervisorChatOnlyView'; import {