解耦设计Agent与策划V2状态

为 Design Agent 增加独立 active、瞬态回复和 reasoning 状态

保留策划 V2 会话自身的状态与消息动画链路

统一项目总控轮询使用两条策划 lane 的合并状态

移除 Design Agent 对旧 planning lane ref 的共享依赖
This commit is contained in:
2026-09-15 05:03:38 +00:00
parent 908d248f27
commit 45401de57e
+116 -64
View File
@@ -584,13 +584,15 @@ export function App({
onMakeGameFromApprovedGdd,
}: AppProps = {}) {
const { setTitle: setWindowTitle } = useWindowChrome();
const [planningV2Active, setPlanningV2Active] = useState(planningStartMode);
const planningV2ActiveRef = useRef(planningStartMode);
const [planningV2Active, setPlanningV2Active] = useState(false);
const planningV2ActiveRef = useRef(false);
const [designAgentActive, setDesignAgentActive] = useState(planningStartMode);
const designAgentActiveRef = useRef(planningStartMode);
planningV2ActiveRef.current = planningV2Active;
designAgentActiveRef.current = designAgentActive;
const [designAgentView, setDesignAgentView] = useState<DesignView | null>(
null,
);
const designAgentLaneRef = useRef(planningStartMode);
const designAgentTurnRef = useRef<{
projectPath: string;
clientTurnId: string;
@@ -609,7 +611,9 @@ export function App({
projectSupervisorOnly &&
!supervisorChatOnly &&
!planningStartMode &&
!planningV2Active;
!planningV2Active &&
!designAgentActive;
const planningLaneActive = planningV2Active || designAgentActive;
const [devMode] = useState(() =>
projectSupervisorOnly ? false : isDeveloperMode(),
);
@@ -826,12 +830,16 @@ export function App({
useState('');
const planningV2TransientReplyTargetRef = useRef('');
const planningV2VisibleReplyRef = useRef('');
const [designAgentTransientReply, setDesignAgentTransientReplyVisible] =
useState('');
const designAgentTransientReplyTargetRef = useRef('');
const designAgentVisibleReplyRef = useRef('');
const designAgentPendingViewRef = useRef<{
clientTurnId: string;
projectPath: string;
view: DesignView;
} | null>(null);
const [planningV2Reasoning, setPlanningV2Reasoning] = useState('');
const [designAgentReasoning, setDesignAgentReasoning] = useState('');
const designAgentReasoningTurnRef = useRef<{
projectPath: string;
clientTurnId: string;
@@ -864,6 +872,14 @@ export function App({
}
}
function setDesignAgentTransientReplyTarget(next: string) {
designAgentTransientReplyTargetRef.current = next;
if (!next) {
designAgentVisibleReplyRef.current = '';
setDesignAgentTransientReplyVisible('');
}
}
function designAgentEventSubscriptionReady() {
// 业务动作只读取当前订阅代次;清理与下一次 effect 建立之间不能创建
// 一个没有订阅 effect 接管的悬挂 promise。
@@ -909,12 +925,37 @@ export function App({
return () => window.clearInterval(timer);
}, []);
useEffect(() => {
const timer = window.setInterval(() => {
const target = designAgentTransientReplyTargetRef.current;
setDesignAgentTransientReplyVisible((current) => {
if (!target) {
designAgentVisibleReplyRef.current = '';
return '';
}
const prefix = target.startsWith(current) ? current : '';
if (prefix === target) {
designAgentVisibleReplyRef.current = target;
return target;
}
const remaining = target.length - prefix.length;
const step = remaining > 160 ? 4 : remaining > 48 ? 2 : 1;
const next = target.slice(0, prefix.length + step);
designAgentVisibleReplyRef.current = next;
return next;
});
}, 50);
return () => window.clearInterval(timer);
}, []);
function applyPlanningV2CommandResult(
result: PlanningSessionCommandResultV2,
clientTurnId?: string,
) {
planningV2ActiveRef.current = true;
setPlanningV2Active(true);
designAgentActiveRef.current = false;
setDesignAgentActive(false);
planningV2SessionRef.current = result;
setPlanningV2Session(result);
projectSupervisorSessionIdRef.current = result.session.sessionId;
@@ -1004,13 +1045,15 @@ export function App({
setPlanningV2Session(null);
setPlanGddState(null);
if (planningStartMode) {
planningV2ActiveRef.current = true;
setPlanningV2Active(true);
designAgentLaneRef.current = true;
planningV2ActiveRef.current = false;
setPlanningV2Active(false);
designAgentActiveRef.current = true;
setDesignAgentActive(true);
}
return null;
}
designAgentLaneRef.current = false;
designAgentActiveRef.current = false;
setDesignAgentActive(false);
setDesignAgentView(null);
applyPlanningV2CommandResult(result);
return result;
@@ -1039,10 +1082,9 @@ export function App({
}
function applyDesignView(view: DesignView, projectPath: string) {
designAgentLaneRef.current = true;
designAgentActiveRef.current = true;
setDesignAgentActive(true);
setDesignAgentView(view);
planningV2ActiveRef.current = true;
setPlanningV2Active(true);
setPlanGddError(view.session.lastError);
setChatAgentBusy(view.running);
const conversation = designMessagesToChat(view);
@@ -1056,8 +1098,8 @@ export function App({
const pendingTurnId = designAgentPendingViewRef.current?.clientTurnId;
designAgentPendingViewRef.current = null;
applyDesignView(view, projectPath);
setPlanningV2Reasoning('');
setPlanningV2TransientReplyTarget('');
setDesignAgentReasoning('');
setDesignAgentTransientReplyTarget('');
if (designAgentTurnRef.current?.clientTurnId === pendingTurnId) {
designAgentTurnRef.current = null;
}
@@ -1068,7 +1110,7 @@ export function App({
projectPath: string,
clientTurnId: string,
) {
let target = planningV2TransientReplyTargetRef.current;
let target = designAgentTransientReplyTargetRef.current;
const tracked = designAgentTurnRef.current;
if (!target.trim() && !view.running) {
const latestAssistantText = [...view.messages]
@@ -1076,7 +1118,7 @@ export function App({
.find((message) => message.role !== 'user' && message.text.trim())
?.text.trim();
if (latestAssistantText) {
setPlanningV2TransientReplyTarget(latestAssistantText);
setDesignAgentTransientReplyTarget(latestAssistantText);
target = latestAssistantText;
}
}
@@ -1084,7 +1126,7 @@ export function App({
!view.running &&
tracked?.clientTurnId === clientTurnId &&
target.trim() &&
planningV2VisibleReplyRef.current !== target
designAgentVisibleReplyRef.current !== target
) {
designAgentPendingViewRef.current = {
clientTurnId,
@@ -1102,8 +1144,8 @@ export function App({
if (!pending) {
return;
}
const target = planningV2TransientReplyTargetRef.current;
if (target && planningV2VisibleReplyRef.current !== target) {
const target = designAgentTransientReplyTargetRef.current;
if (target && designAgentVisibleReplyRef.current !== target) {
return;
}
commitDesignAgentView(pending.view, pending.projectPath);
@@ -1155,8 +1197,8 @@ export function App({
await designAgentEventSubscriptionReady();
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setPlanningV2TransientReplyTarget('');
setPlanningV2Reasoning('');
setDesignAgentTransientReplyTarget('');
setDesignAgentReasoning('');
try {
const view = await invoke<DesignView>('continue_design_agent_session', {
projectPath: nextProjectPath,
@@ -1180,7 +1222,7 @@ export function App({
} finally {
if (!designAgentPendingViewRef.current) {
designAgentTurnRef.current = null;
setPlanningV2TransientReplyTarget('');
setDesignAgentTransientReplyTarget('');
}
setChatAgentBusy(false);
}
@@ -1642,10 +1684,11 @@ export function App({
setPlanningV2TransientReplyTarget('');
designAgentPendingViewRef.current = null;
designAgentReasoningTurnRef.current = null;
setPlanningV2Reasoning('');
setPlanningV2Active(planningStartMode);
planningV2ActiveRef.current = planningStartMode;
designAgentLaneRef.current = planningStartMode;
setDesignAgentReasoning('');
setPlanningV2Active(false);
planningV2ActiveRef.current = false;
setDesignAgentActive(planningStartMode);
designAgentActiveRef.current = planningStartMode;
designAgentTurnRef.current = null;
setDesignAgentView(null);
}
@@ -1912,7 +1955,7 @@ export function App({
if (
!canSubscribeTauriEvents() ||
directCodexProductRuntime ||
planningV2Active
planningLaneActive
) {
return;
}
@@ -2015,7 +2058,7 @@ export function App({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
directCodexProductRuntime,
planningV2Active,
planningLaneActive,
refreshManifest,
updateProjectSupervisorResponseStream,
updateProjectSupervisorRuntime,
@@ -2023,7 +2066,7 @@ export function App({
useEffect(() => {
const ready = createDesignAgentEventSubscriptionReady();
if (!canSubscribeTauriEvents() || !planningV2Active) {
if (!canSubscribeTauriEvents() || !designAgentActive) {
resolveDesignAgentEventSubscriptionReady();
return () => {
if (designAgentEventSubscriptionReadyRef.current === ready) {
@@ -2044,7 +2087,7 @@ export function App({
return;
}
if (payload.kind === 'text' && payload.text != null) {
setPlanningV2TransientReplyTarget(payload.text);
setDesignAgentTransientReplyTarget(payload.text);
}
if (payload.reasoningText != null) {
const reasoningTurn = designAgentReasoningTurnRef.current;
@@ -2054,11 +2097,11 @@ export function App({
reasoningTurn.clientTurnId === payload.clientTurnId
) {
reasoningTurn.text = payload.reasoningText;
setPlanningV2Reasoning(payload.reasoningText);
setDesignAgentReasoning(payload.reasoningText);
}
}
if (payload.kind === 'tool' && payload.text) {
setPlanningV2TransientReplyTarget(payload.text);
setDesignAgentTransientReplyTarget(payload.text);
}
if (payload.view) {
applyDesignAgentViewAfterTransient(
@@ -2091,7 +2134,7 @@ export function App({
// applyDesignView 读的是 refs 和当前项目路径,
// 把它写进依赖会在每轮回复时重订事件。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [planningV2Active]);
}, [designAgentActive]);
useEffect(() => {
if (!canSubscribeTauriEvents()) {
@@ -2140,7 +2183,7 @@ export function App({
if (
!invoke ||
directCodexProductRuntime ||
planningV2Active ||
planningLaneActive ||
!nextProjectPath ||
!sessionId ||
!trackedRunId ||
@@ -2236,7 +2279,7 @@ export function App({
}, [
localProject?.projectPath,
directCodexProductRuntime,
planningV2Active,
planningLaneActive,
projectSupervisorSessionId,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
@@ -2251,7 +2294,7 @@ export function App({
if (
!projectSupervisorOnly ||
directCodexProductRuntime ||
planningV2Active ||
planningLaneActive ||
!invoke ||
!nextProjectPath ||
!supervisorRunId
@@ -2332,7 +2375,7 @@ export function App({
}, [
localProject?.projectPath,
directCodexProductRuntime,
planningV2Active,
planningLaneActive,
projectSupervisorOnly,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
@@ -2344,7 +2387,7 @@ export function App({
(!supervisorChatOnly &&
!(
projectSupervisorOnly &&
(directCodexProductRuntime || planningV2Active)
(directCodexProductRuntime || planningLaneActive)
)) ||
!supervisorChatShouldFollowLatestRef.current
) {
@@ -2364,7 +2407,7 @@ export function App({
directCodexTransientReply,
directCodexTransientReplyUpdatedAt,
directCodexProductRuntime,
planningV2Active,
planningLaneActive,
projectSupervisorOnly,
supervisorChatOnly,
]);
@@ -3333,14 +3376,16 @@ export function App({
) {
return;
}
planningV2ActiveRef.current = true;
setPlanningV2Active(true);
planningV2ActiveRef.current = Boolean(planningV2);
setPlanningV2Active(Boolean(planningV2));
if (planningV2) {
designAgentLaneRef.current = false;
designAgentActiveRef.current = false;
setDesignAgentActive(false);
setDesignAgentView(null);
applyPlanningV2CommandResult(planningV2);
} else {
designAgentLaneRef.current = true;
designAgentActiveRef.current = true;
setDesignAgentActive(true);
planningV2SessionRef.current = null;
setPlanningV2Session(null);
setPlanGddState(null);
@@ -6138,7 +6183,6 @@ export function App({
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setPlanningV2TransientReplyTarget('');
setPlanningV2Reasoning('');
try {
const result = currentSessionId
? await invoke<PlanningSessionCommandResultV2>(
@@ -6204,13 +6248,17 @@ export function App({
directPolicyChecked = false,
references,
}: ExecuteChatAgentReplyInput) {
if (planningV2ActiveRef.current || planningStartMode) {
if (
planningV2ActiveRef.current ||
designAgentActiveRef.current ||
planningStartMode
) {
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setProjectSupervisorRuntimeError('请先初始化本地项目');
return;
}
if (designAgentLaneRef.current) {
if (designAgentActiveRef.current) {
await executeDesignAgentTurn(
nextProjectPath,
{ type: 'message', text: prompt },
@@ -7074,7 +7122,7 @@ export function App({
answers: Record<string, string>,
) {
const nextProjectPath = resolveChatProjectPath(localProject);
if (planningV2ActiveRef.current || planningStartMode) {
if (planningV2ActiveRef.current) {
const current = planningV2SessionRef.current;
const currentQuestion = current?.session.currentQuestion;
const answer = Object.values(answers)
@@ -11774,7 +11822,7 @@ export function App({
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
);
const useDesignAgentSurface =
Boolean(designAgentView) || (planningStartMode && !planningV2Session);
Boolean(designAgentView) || (designAgentActive && !planningV2Session);
if (projectSupervisorOnly && supervisorChatOnly) {
const supervisorProjectPath =
@@ -11807,11 +11855,13 @@ export function App({
runtimeConfigOpen={runtimeConfigOpen}
runtimeError={projectSupervisorRuntimeError}
transientReply={
planningV2Active
? planningV2TransientReply
: directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
designAgentActive
? designAgentTransientReply
: planningV2Active
? planningV2TransientReply
: directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
}
hasConversationControls={projectSupervisorHasConversationControls}
hiddenConversationCount={hiddenConversationCount}
@@ -11857,14 +11907,16 @@ export function App({
pendingCommand={directCodexProductRuntime ? pendingCommand : null}
projectPath={localProject?.projectPath ?? projectPath}
transientReply={
planningV2Active
? planningV2TransientReply
: directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
designAgentActive
? designAgentTransientReply
: planningV2Active
? planningV2TransientReply
: directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
}
showDesignReasoning={planningV2Active}
designReasoning={planningV2Reasoning}
showDesignReasoning={designAgentActive}
designReasoning={designAgentReasoning}
designReasoningEntries={
useDesignAgentSurface ? (designAgentView?.reasoningEntries ?? []) : []
}
@@ -11880,7 +11932,7 @@ export function App({
planGddError={planGddError}
onPlanGddRefresh={() => void hydratePlanGddState()}
onPlanGddDecision={decidePlanGdd}
planningLane={planningV2Active}
planningLane={planningLaneActive}
designView={useDesignAgentSurface ? designAgentView : null}
onDesignApprove={
useDesignAgentSurface
@@ -11901,8 +11953,8 @@ export function App({
text: '',
};
designAgentPendingViewRef.current = null;
setPlanningV2TransientReplyTarget('');
setPlanningV2Reasoning('');
setDesignAgentTransientReplyTarget('');
setDesignAgentReasoning('');
setChatAgentBusy(true);
setPlanGddDecisionBusy(true);
void designAgentEventSubscriptionReady()
@@ -11951,7 +12003,7 @@ export function App({
}
if (!designAgentPendingViewRef.current) {
designAgentTurnRef.current = null;
setPlanningV2TransientReplyTarget('');
setDesignAgentTransientReplyTarget('');
}
setChatAgentBusy(false);
setPlanGddDecisionBusy(false);