From 0638c1b5b941c20a70ef5d73e3389470ee51f42f Mon Sep 17 00:00:00 2001 From: Linghong Date: Sun, 23 Aug 2026 05:37:45 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AB=8B=E9=A1=B9=E7=AD=96=E5=88=92=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E5=BA=95=E9=83=A8=E6=94=B9=E6=88=90=E7=AA=84=E6=9D=A1?= =?UTF-8?q?=EF=BC=8C=E5=8F=AA=E7=95=99=E6=BE=84=E6=B8=85=E5=8D=A1=E5=92=8C?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=81=A2=E5=A4=8D=EF=BC=8C=E5=81=9A=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E9=93=BE=E8=B7=AF=E9=9D=A2=E6=9D=BF=E5=8E=9F=E6=A0=B7?= =?UTF-8?q?=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 做方案链路的底部挂的是 ProjectSupervisorRuntimePanel——一块为做游戏链路设计的 面板:十几个专业 Agent、多步计划、逐 Agent 重试。套到策划链路上,子 Agent 永远只 有 project-planning 一个,计划永远一两步,「专业 Agent 协作:1」永远是 1。它把 D11 的「总控 + 委派子 Run」拓扑整个漏给了用户:currentAction 原文(「等待 project-planning 提交 GDD」)、计划 1/2 进度条、五段式紧凑进度、一张把策划子 Run 当成「专业 Agent」的卡,外加同一个状态在顶部 strip / overview / 子 Agent 卡三处各 画一遍。用户的心智模型是在跟一个策划聊天,不是看总控调度。 新组件 PlanningLaneRuntimeStrip 只画真正需要用户动手的两样:澄清问答卡(复用 AgentRuntimeUserInputCard,零改动)和失败 / 待核对后的恢复入口(逻辑照搬原面板, 文案改成「重新启动策划」)。App 层操作错误那一行保留,那是原面板里唯一面向用户 的文字。其余时候返回 null,不占一行——状态由顶部的 PlanGddStageProgress 承担。 原面板在 waiting-for-user-input 却读不到 userInputRequest 时画的「待回答问题未能 读取」一句不搬:策划链路里这个组合出现在子 Run 退出到父 Run 醒来之间的瞬时窗口, 以及审批等待本身,两种都不是读取失败。 切换判据是 run 的 source === 'project-supervisor-plan',抽成 isPlanningLaneRuntime 放到独立模块(react-refresh 不让组件文件导出函数),两个挂载点 ProjectSupervisorView / ProjectWorkspaceChatPane 和顶部 strip 的 active 判定统一 用它。判据故意不看 planGddState:做游戏链路在策划批准后照样带着一份 approved 状 态,但它的总控 run 是 autonomous 源,必须继续拿完整面板。source 非 plan 时面板一 行未动,既有的「keeps Project Supervisor plan progress compact」用例仍在守门。 新增三条 appSurface 用例,都做过 A/B(强制回旧面板即三条全红): - 策划进行中:底部不存在面板、overview、紧凑进度、子 Agent 列表,且 currentAction 原文与「计划 N/M」都不出现。 - 澄清等待:问答卡仍在窄条里。 - run 失败:窄条给出「重新启动策划」,错误文字保留。 Co-Authored-By: Claude Opus 5 --- .../PlanningLaneRuntimeStrip.tsx | 184 ++++++++++++++++++ .../ProjectSupervisorView.tsx | 19 +- .../ProjectWorkspaceChatPane.tsx | 42 ++-- .../project-workspace/planningLane.ts | 15 ++ .../tests/appSurface/plan-gdd.suite.ts | 174 +++++++++++++++++ 5 files changed, 414 insertions(+), 20 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/PlanningLaneRuntimeStrip.tsx create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/planningLane.ts diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/PlanningLaneRuntimeStrip.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/PlanningLaneRuntimeStrip.tsx new file mode 100644 index 000000000..8f89c10a3 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/PlanningLaneRuntimeStrip.tsx @@ -0,0 +1,184 @@ +import { useEffect, useState } from 'react'; + +import type { + AgentRuntimeState, + AgentRuntimeUserInputRequest, +} from '../../app/types'; +import { + agentRuntimeCanRetry, + AgentRuntimeUserInputCard, + projectRuntimeVisibleError, +} from '../agent-runtime'; + +type PlanningLaneRuntimeStripProps = { + runtime: AgentRuntimeState | null; + error: string; + controlBusy: boolean; + readOnly?: boolean; + onSupervisorRetry: (runtime: AgentRuntimeState) => Promise; + onUserInput: ( + request: AgentRuntimeUserInputRequest, + responseId: string, + answers: Record, + ) => void | Promise; +}; + +/** + * 立项策划链路下替代 `ProjectSupervisorRuntimePanel` 的窄条。 + * + * 完整面板是为做游戏链路设计的:十几个专业 Agent、多步计划、逐 Agent 重试。套到 + * 策划链路上,子 Agent 永远只有 `project-planning` 一个,计划永远一两步,「专业 + * Agent 协作:1」永远是 1——它把 D11 的「总控 + 委派子 Run」拓扑整个漏给了用户,而 + * 用户的心智模型是在跟一个策划聊天。状态本身由顶部的 `PlanGddStageProgress` 承担。 + * + * 这里只画真正需要用户动手的两样:澄清问答卡,以及失败后的恢复入口。其余时候 + * 返回 null,不占一行。 + * + * 完整面板在 `waiting-for-user-input` 却读不到 `userInputRequest` 时会画一句 + * 「待回答问题未能读取」。策划链路里这个组合出现在子 Run 退出到父 Run 醒来之间的 + * 瞬时窗口,以及审批等待(交互面是审批卡)——两种都不是读取失败,所以这里不画。 + */ +export function PlanningLaneRuntimeStrip({ + runtime, + error, + controlBusy, + readOnly = false, + onSupervisorRetry, + onUserInput, +}: PlanningLaneRuntimeStripProps) { + const [retrySubmitting, setRetrySubmitting] = useState(false); + const [retryAccepted, setRetryAccepted] = useState(false); + const [retryFeedback, setRetryFeedback] = useState(''); + useEffect(() => { + setRetrySubmitting(false); + setRetryAccepted(false); + if (runtime?.status === 'failed' || runtime?.phase === 'failed') { + setRetryFeedback(''); + } + }, [runtime?.phase, runtime?.runId, runtime?.status]); + + const userInputRequest = readOnly + ? null + : (runtime?.userInputRequest ?? null); + const needsReconciliation = Boolean( + runtime && + (runtime.status === 'needs-reconciliation' || + runtime.phase === 'needs-reconciliation'), + ); + const stopped = Boolean( + runtime && + (runtime.status === 'failed' || + runtime.phase === 'failed' || + ((runtime.status === 'cancelled' || runtime.phase === 'cancelled') && + (runtime.taskQueue?.pending ?? 0) === 0)), + ); + const canRetry = Boolean( + !readOnly && + runtime && + !runtime.pendingToolAction && + stopped && + agentRuntimeCanRetry(runtime.status), + ); + const showRecovery = + !readOnly && runtime && (needsReconciliation || canRetry); + // 与完整面板同源:App 层的操作错误(如「请先回答当前的澄清问题」)优先,其次是 + // run 自己记下的失败原因。这是原面板里唯一真正面向用户的一行文字,照搬。 + const rawErrorDetail = error || runtime?.error || ''; + const errorDetail = rawErrorDetail + ? projectRuntimeVisibleError(rawErrorDetail, '项目总控 Agent', true) + : ''; + + if (!userInputRequest && !showRecovery && !errorDetail) { + return null; + } + + return ( +
+ {errorDetail ? ( + + {errorDetail} + + ) : null} + {showRecovery && runtime ? ( +
+ + {needsReconciliation ? ( + <> + 本轮工具动作的结果不确定,需要先结束旧任务。 + 不会直接重试,避免重复执行未核对的动作。 + + ) : ( + <> + 本轮策划已停止。 + 在当前项目重新启动策划,不会新建项目。 + + )} + + + {retryFeedback ? ( + + {retryFeedback} + + ) : null} +
+ ) : null} + {userInputRequest ? ( + + ) : null} +
+ ); +} 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 e7851d9f5..4e221e470 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 @@ -29,6 +29,8 @@ import { pendingCommandDetail, pendingCommandTitle, } from './pendingCommandPresentation'; +import { isPlanningLaneRuntime } from './planningLane'; +import { PlanningLaneRuntimeStrip } from './PlanningLaneRuntimeStrip'; import { resolvePendingCommandProjectPath } from './projectCommandPolicy'; type RuntimePanelProps = ComponentProps; @@ -100,9 +102,7 @@ export function ProjectSupervisorView({
) : null}
- {directCodex ? null : ( + {directCodex ? null : isPlanningLaneRuntime( + runtimePanelProps.runtime, + ) ? ( + + ) : ( ) : null} - + {isPlanningLaneRuntime(projectSupervisorRuntime) ? ( + + ) : ( + + )} {pendingCommand ? (
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/planningLane.ts b/apps/ai-game-creator-shell/src/features/project-workspace/planningLane.ts new file mode 100644 index 000000000..f069c02b5 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/planningLane.ts @@ -0,0 +1,15 @@ +import type { AgentRuntimeState } from '../../app/types'; + +export const PROJECT_SUPERVISOR_PLAN_SOURCE = 'project-supervisor-plan'; + +/** + * 当前总控 run 是否属于立项策划链路。 + * + * 判据是 run 的 `source`,不是 `planGddState`:做游戏链路在策划批准之后照样带着 + * 一份 approved 的策划状态,但它的总控 run 是 autonomous 源,必须继续拿完整面板。 + */ +export function isPlanningLaneRuntime( + runtime: AgentRuntimeState | null | undefined, +) { + return runtime?.source === PROJECT_SUPERVISOR_PLAN_SOURCE; +} diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index 881bc3577..339287dc2 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts @@ -1,4 +1,5 @@ import { + agentRuntimeUserInputRequest, createPlanGddStateView, createProjectSupervisorRuntimeHarness, expect, @@ -23,6 +24,49 @@ async function mountApprovalCard( return await screen.findByLabelText('GDD 审批卡'); } +async function mountPlanningSurface( + harness: ReturnType, +) { + window.__TAURI__ = { + core: { invoke: harness.invoke }, + event: { listen: harness.listen }, + }; + renderAppAt('/'); + await openMainProject(harness.projectPath); + return await screen.findByLabelText('立项策划阶段进度'); +} + +type PlanGddStateView = ReturnType; + +/** 策划中、尚未提交 GDD 的状态:没有待审批,也没有可展示的 GDD。 */ +function draftPlanGddState( + sessionOverrides: Partial> = {}, +) { + const base = createPlanGddStateView(); + return createPlanGddStateView({ + state: 'draft', + versions: [], + displayGdd: null, + pendingApproval: null, + approvedGddRef: null, + session: base.session + ? { ...base.session, phase: 'collecting', ...sessionOverrides } + : null, + }); +} + +/** + * 完整总控面板在策划链路下漏给用户的几块:面板本体、currentAction/计划进度、 + * 五段式紧凑进度、子 Agent 列表。策划链路的断言统一从这里查。 + */ +function expectSupervisorRuntimePanelAbsent() { + expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull(); + expect(screen.queryByLabelText('当前工作状态')).toBeNull(); + expect(screen.queryByLabelText('项目总控 Agent 进度')).toBeNull(); + expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull(); + expect(screen.queryByText(/专业 Agent 协作:/)).toBeNull(); +} + function openReviseDialog() { fireEvent.click(screen.getByRole('button', { name: '修改' })); return screen.getByRole('dialog'); @@ -231,4 +275,134 @@ export function registerPlanGddApprovalTests() { expect(screen.queryByLabelText('立项策划 Agent待确认动作')).toBeNull(); expect(screen.getByRole('button', { name: '批准 v1' })).not.toBeNull(); }); + + it('keeps the supervisor runtime panel off the planning lane while the planner is working', async () => { + // 完整面板是给做游戏链路的:十几个专业 Agent、多步计划、逐 Agent 重试。策划链路 + // 只有一个 project-planning 子 Run、一两步计划,面板画出来的全是 D11 拓扑的内部 + // 记账(currentAction 原文、计划 1/2、「专业 Agent 协作:1」、子 Agent 卡)。 + // 状态由顶部的阶段进度承担;底部在策划正常进行时什么都不该画。 + const supervisorRunId = 'plan-root-collecting-run'; + const harness = createProjectSupervisorRuntimeHarness({ + expectedRunProfile: 'standard', + initialRuntime: { + runId: supervisorRunId, + source: 'project-supervisor-plan', + runProfile: 'standard', + status: 'running', + phase: 'waiting-for-delegate-receipts', + currentTask: '剧情向恋爱养成游戏', + currentAction: '等待 project-planning 提交 GDD', + waitingOn: '策划子 Run 提交 Fast GDD', + nextStep: '读取 GDD 并进入审批', + plan: ['委派策划子 Run', '读取 GDD 并审批'], + planSteps: [ + { step: '委派策划子 Run', status: 'active' }, + { step: '读取 GDD 并审批', status: 'pending' }, + ], + activePlanStepIndex: 0, + updatedAt: 7000, + }, + runtimeMapLoader: async () => [ + harness.runtimeState({ + agentId: 'project-planning', + taskId: 'project-planning', + sessionId: 'agent-session-project-planning', + runId: 'delegated-delegation-collecting', + source: 'agent-delegate', + parentAgentId: 'project-supervisor', + parentRunId: supervisorRunId, + delegationId: 'delegation-collecting', + runProfile: 'standard', + status: 'running', + phase: 'running', + currentTask: '完成立项策划并给出 Fast GDD', + currentAction: '整理第 1 轮澄清答案', + updatedAt: 7000, + }), + ], + }); + harness.setPlanGddState(draftPlanGddState()); + const progress = await mountPlanningSurface(harness); + + expect(within(progress).getByText('策划中')).not.toBeNull(); + expectSupervisorRuntimePanelAbsent(); + expect(screen.queryByLabelText('立项策划运行状态')).toBeNull(); + expect(screen.queryByText('等待 project-planning 提交 GDD')).toBeNull(); + expect(screen.queryByText('整理第 1 轮澄清答案')).toBeNull(); + expect(screen.queryByText(/计划 \d+\/\d+/)).toBeNull(); + }); + + it('still surfaces the clarification card on the planning lane', async () => { + // 澄清卡是策划链路唯一需要用户动手的交互面之一,瘦身不能把它一起收掉。 + const supervisorRunId = 'plan-root-clarifying-run'; + const request = agentRuntimeUserInputRequest({ + agentId: 'project-supervisor', + sessionId: 'supervisor-session-active', + runId: supervisorRunId, + requestId: 'request-plan-round-1', + actionId: 'action-plan-round-1', + }); + const harness = createProjectSupervisorRuntimeHarness({ + expectedRunProfile: 'standard', + initialRuntime: { + runId: supervisorRunId, + source: 'project-supervisor-plan', + runProfile: 'standard', + status: 'waiting-for-user-input', + phase: 'waiting-for-user-input', + currentTask: '剧情向恋爱养成游戏', + currentAction: '等待用户回答第 1 轮澄清', + userInputRequest: request, + updatedAt: 7100, + }, + }); + harness.setPlanGddState( + draftPlanGddState({ + clarificationRound: 0, + awaitingAnswerFor: { + delegationId: 'delegation-0001', + requestId: 'request-plan-round-1', + questionId: 'visual_direction', + round: 0, + }, + }), + ); + await mountPlanningSurface(harness); + + const strip = await screen.findByLabelText('立项策划运行状态'); + expect(within(strip).getByLabelText('Needs input')).not.toBeNull(); + expect( + within(strip).getByText('首版角色规范图采用哪种美术方向?'), + ).not.toBeNull(); + expectSupervisorRuntimePanelAbsent(); + expect(screen.queryByText('等待用户回答第 1 轮澄清')).toBeNull(); + }); + + it('offers a restart on the planning lane once the planning run has failed', async () => { + // 失败是另一处需要用户动手的时刻。恢复入口跟着搬进窄条,而不是随面板一起消失。 + const harness = createProjectSupervisorRuntimeHarness({ + expectedRunProfile: 'standard', + initialRuntime: { + runId: 'plan-root-failed-run', + source: 'project-supervisor-plan', + runProfile: 'standard', + status: 'failed', + phase: 'failed', + currentTask: '剧情向恋爱养成游戏', + currentAction: '策划子 Run 退出', + error: '项目总控 Agent Codex 执行失败,请查看运行详情后重试', + updatedAt: 7200, + }, + }); + harness.setPlanGddState(draftPlanGddState()); + await mountPlanningSurface(harness); + + const recovery = await screen.findByLabelText('立项策划失败恢复'); + expect( + within(recovery).getByRole('button', { name: '重新启动策划' }), + ).not.toBeNull(); + expect(screen.getByRole('alert').textContent).toContain('执行失败'); + expectSupervisorRuntimePanelAbsent(); + expect(screen.queryByText('策划子 Run 退出')).toBeNull(); + }); }