立项策划链路底部改成窄条,只留澄清卡和失败恢复,做游戏链路面板原样保留

做方案链路的底部挂的是 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 05:37:45 +00:00
parent 1337ce03e1
commit 0638c1b5b9
5 changed files with 414 additions and 20 deletions
@@ -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<string>;
onUserInput: (
request: AgentRuntimeUserInputRequest,
responseId: string,
answers: Record<string, string>,
) => void | Promise<void>;
};
/**
* 立项策划链路下替代 `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 (
<section
className="agent-runtime-status planning-lane-runtime-strip"
aria-label="立项策划运行状态"
>
{errorDetail ? (
<small className="project-runtime-error" role="alert">
{errorDetail}
</small>
) : null}
{showRecovery && runtime ? (
<div
className="project-runtime-recovery"
aria-label={
needsReconciliation ? '立项策划待核对恢复' : '立项策划失败恢复'
}
>
<span>
{needsReconciliation ? (
<>
<small></small>
</>
) : (
<>
<small></small>
</>
)}
</span>
<button
type="button"
disabled={controlBusy || retrySubmitting || retryAccepted}
onClick={() => {
setRetryFeedback(
needsReconciliation
? '正在结束待核对的旧任务…'
: '正在重新启动策划…',
);
setRetrySubmitting(true);
void onSupervisorRetry(runtime)
.then((message) => {
setRetryAccepted(true);
setRetryFeedback(message);
})
.catch((retryError) => {
setRetryAccepted(false);
setRetryFeedback(
projectRuntimeVisibleError(
retryError instanceof Error
? retryError.message
: String(retryError),
'项目总控 Agent',
true,
),
);
})
.finally(() => setRetrySubmitting(false));
}}
>
{retrySubmitting
? needsReconciliation
? '正在结束旧任务…'
: '正在重新启动…'
: retryAccepted
? needsReconciliation
? '旧任务结束请求已受理'
: '重试已受理'
: needsReconciliation
? '已核对,结束旧任务'
: '重新启动策划'}
</button>
{retryFeedback ? (
<small className="project-runtime-retry-feedback" role="status">
{retryFeedback}
</small>
) : null}
</div>
) : null}
{userInputRequest ? (
<AgentRuntimeUserInputCard
key={`${userInputRequest.requestId}:${userInputRequest.responseId ?? 'pending'}`}
request={userInputRequest}
controlBusy={controlBusy}
onSubmit={onUserInput}
/>
) : null}
</section>
);
}
@@ -29,6 +29,8 @@ import {
pendingCommandDetail,
pendingCommandTitle,
} from './pendingCommandPresentation';
import { isPlanningLaneRuntime } from './planningLane';
import { PlanningLaneRuntimeStrip } from './PlanningLaneRuntimeStrip';
import { resolvePendingCommandProjectPath } from './projectCommandPolicy';
type RuntimePanelProps = ComponentProps<typeof ProjectSupervisorRuntimePanel>;
@@ -100,9 +102,7 @@ export function ProjectSupervisorView({
<div className="project-supervisor-conversation">
<PlanGddStageProgress
state={planGddState}
active={
runtimePanelProps.runtime?.source === 'project-supervisor-plan'
}
active={isPlanningLaneRuntime(runtimePanelProps.runtime)}
/>
<GddApprovalCard
state={planGddState}
@@ -149,7 +149,18 @@ export function ProjectSupervisorView({
</p>
) : null}
</div>
{directCodex ? null : (
{directCodex ? null : isPlanningLaneRuntime(
runtimePanelProps.runtime,
) ? (
<PlanningLaneRuntimeStrip
runtime={runtimePanelProps.runtime}
error={runtimePanelProps.error}
controlBusy={runtimePanelProps.controlBusy}
readOnly={runtimePanelProps.readOnly}
onSupervisorRetry={runtimePanelProps.onSupervisorRetry}
onUserInput={runtimePanelProps.onUserInput}
/>
) : (
<ProjectSupervisorRuntimePanel
{...runtimePanelProps}
planGddAwaitingDecision={Boolean(planGddState?.pendingApproval)}
@@ -52,6 +52,8 @@ import {
pendingCommandDetail,
pendingCommandTitle,
} from './pendingCommandPresentation';
import { isPlanningLaneRuntime } from './planningLane';
import { PlanningLaneRuntimeStrip } from './PlanningLaneRuntimeStrip';
import { resolvePendingCommandProjectPath } from './projectCommandPolicy';
type ProjectWorkspaceChatPaneProps = {
@@ -365,9 +367,7 @@ export function ProjectWorkspaceChatPane({
</header>
<PlanGddStageProgress
state={planGddState}
active={
projectSupervisorRuntime?.source === 'project-supervisor-plan'
}
active={isPlanningLaneRuntime(projectSupervisorRuntime)}
/>
<GddApprovalCard
state={planGddState}
@@ -861,19 +861,29 @@ export function ProjectWorkspaceChatPane({
</p>
) : null}
</div>
<ProjectSupervisorRuntimePanel
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={chatAgentBusy}
planGddAwaitingDecision={Boolean(planGddState?.pendingApproval)}
professionalResultsByAgentId={professionalAgentResultsById}
onToolAction={handleProjectSupervisorToolAction}
onSupervisorRetry={handleProjectSupervisorRetry}
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
onProfessionalRetry={handleProjectProfessionalAgentRetry}
onUserInput={handleProjectSupervisorUserInput}
/>
{isPlanningLaneRuntime(projectSupervisorRuntime) ? (
<PlanningLaneRuntimeStrip
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
controlBusy={chatAgentBusy}
onSupervisorRetry={handleProjectSupervisorRetry}
onUserInput={handleProjectSupervisorUserInput}
/>
) : (
<ProjectSupervisorRuntimePanel
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={chatAgentBusy}
planGddAwaitingDecision={Boolean(planGddState?.pendingApproval)}
professionalResultsByAgentId={professionalAgentResultsById}
onToolAction={handleProjectSupervisorToolAction}
onSupervisorRetry={handleProjectSupervisorRetry}
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
onProfessionalRetry={handleProjectProfessionalAgentRetry}
onUserInput={handleProjectSupervisorUserInput}
/>
)}
{pendingCommand ? (
<div className="pending-command">
<span>
@@ -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;
}
@@ -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<typeof createProjectSupervisorRuntimeHarness>,
) {
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
return await screen.findByLabelText('立项策划阶段进度');
}
type PlanGddStateView = ReturnType<typeof createPlanGddStateView>;
/** 策划中、尚未提交 GDD 的状态:没有待审批,也没有可展示的 GDD。 */
function draftPlanGddState(
sessionOverrides: Partial<NonNullable<PlanGddStateView['session']>> = {},
) {
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();
});
}