diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index cd6dae9eb..b5ac0dc9f 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -33,6 +33,7 @@ import type { ChatMessage, DesignAgentInput, DesignClarificationRequest, + DesignEvent, DesignView, InitLocalProjectResult, LauncherImportedAttachment, @@ -102,6 +103,10 @@ import { setAgcPluginProjectPath, startAvailableAgcPlugin, } from './services/pluginHost'; +import { + canSubscribeTauriEvents, + subscribeTauriEvent, +} from './services/tauriEventSubscription'; import type { HomeCreationType } from './view/home'; import { type ProjectAgentResultSummary, @@ -243,6 +248,20 @@ export function App({ const designAgentEventSubscriptionReadyRef = useRef | null>( null, ); + const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>( + null, + ); + /** + * 本轮策划回合的思考归属。 + * + * 与 `designAgentTurnRef` 分开:那个记录回合本身,回合结束就清空;这个只用来判定 + * 「这条 reasoning 事件是不是本轮的」,回合结束后仍保留,最后一条思考不会在视图 + * 落盘前的空隙里被丢掉。切项目时随其它聊天状态一起清空。 + */ + const designAgentReasoningTurnRef = useRef<{ + projectPath: string; + clientTurnId: string; + } | null>(null); // 项目开发工作台的普通项目固定使用 DirectProject,立项策划项目走策划聊天;两条 // 链路各自独立,不通过兼容分支互相承载。 const directProjectMode = !planningV2Active; @@ -419,6 +438,22 @@ export function App({ return designAgentEventSubscriptionReadyRef.current ?? Promise.resolve(); } + function createDesignAgentEventSubscriptionReady() { + if (!designAgentEventSubscriptionReadyRef.current) { + designAgentEventSubscriptionReadyRef.current = new Promise( + (resolve) => { + designAgentEventSubscriptionResolveRef.current = resolve; + }, + ); + } + return designAgentEventSubscriptionReadyRef.current; + } + + function resolveDesignAgentEventSubscriptionReady() { + designAgentEventSubscriptionResolveRef.current?.(); + designAgentEventSubscriptionResolveRef.current = null; + } + useEffect(() => { const timer = window.setInterval(() => { const target = planningV2TransientReplyTargetRef.current; @@ -442,6 +477,83 @@ export function App({ return () => window.clearInterval(timer); }, []); + /** + * 策划 Agent 的实时事件流:本轮流式正文、思考过程和回合中途的视图都靠它推给界面。 + * + * 订阅建立是异步的,而回合由一个 invoke 发起;`designAgentEventSubscriptionReady()` + * 让回合等监听器挂好再开始,避免开头几个事件丢掉。事件只认当前项目;有在跑的回合时 + * 还要认本轮 `clientTurnId`,迟到的上一轮事件不会画到这一轮上。 + */ + useEffect(() => { + const ready = createDesignAgentEventSubscriptionReady(); + if (!canSubscribeTauriEvents() || !planningV2Active) { + resolveDesignAgentEventSubscriptionReady(); + return () => { + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + } + let cleanup: (() => void) | null = null; + let disposed = false; + void subscribeTauriEvent('design-agent-update', (event) => { + const payload = event.payload; + const tracked = designAgentTurnRef.current; + if ( + payload.projectPath !== localProjectPathRef.current || + (tracked && payload.clientTurnId !== tracked.clientTurnId) + ) { + return; + } + if ( + (payload.kind === 'text' || payload.kind === 'tool') && + payload.text + ) { + setPlanningV2TransientReplyTarget(payload.text); + } + if (payload.reasoningText != null) { + const reasoningTurn = designAgentReasoningTurnRef.current; + if ( + reasoningTurn?.projectPath === payload.projectPath && + reasoningTurn.clientTurnId === payload.clientTurnId + ) { + setPlanningV2Reasoning(payload.reasoningText); + } + } + if (payload.view) { + applyDesignAgentViewAfterTransient( + payload.view, + payload.projectPath, + payload.clientTurnId, + ); + } + }) + .then((unlisten) => { + resolveDesignAgentEventSubscriptionReady(); + if (disposed) { + unlisten(); + return; + } + cleanup = unlisten; + }) + .catch(() => { + resolveDesignAgentEventSubscriptionReady(); + }); + return () => { + disposed = true; + cleanup?.(); + resolveDesignAgentEventSubscriptionReady(); + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + // 订阅只跟着「是否走策划 Agent」这条链路;回调里的项目/回合判据都走 refs, + // 把它们写进依赖会在每轮回复时重订事件。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [planningV2Active]); + function applyPlanningV2CommandResult( result: PlanningSessionCommandResultV2, clientTurnId?: string, @@ -712,6 +824,10 @@ export function App({ projectPath: nextProjectPath, clientTurnId, }; + designAgentReasoningTurnRef.current = { + projectPath: nextProjectPath, + clientTurnId, + }; designAgentPendingViewRef.current = null; await designAgentEventSubscriptionReady(); setChatAgentBusy(true); @@ -1582,6 +1698,7 @@ export function App({ planningV2ActiveRef.current = planningStartMode; designAgentLaneRef.current = planningStartMode; designAgentTurnRef.current = null; + designAgentReasoningTurnRef.current = null; setDesignAgentView(null); } @@ -2689,6 +2806,10 @@ export function App({ projectPath: nextProjectPath, clientTurnId, }; + designAgentReasoningTurnRef.current = { + projectPath: nextProjectPath, + clientTurnId, + }; designAgentPendingViewRef.current = null; setPlanningV2TransientReplyTarget(''); setPlanningV2Reasoning('');