策划区合成一个面:阶段进度当标题栏,审批卡头去掉重复的状态与指纹
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / Frontend tests (pull_request) Successful in 4m5s
Project CI / Native shell tests (pull_request) Successful in 15m25s

阶段进度条和审批卡原本各带一圈边框叠在一起。「立项策划 / 待审批」在进度条和卡头
各画一遍,「v1」在进度条、卡片 meta 行和批准按钮上出现三次,卡头还露一截 12 位的
指纹——那是开发者核对用的,用户不需要。

新增 PlanGddSurface 做外壳:边框和圆角只画在它上面,进度条成为标题栏,审批卡成为
正文,卡头只剩游戏标题和一句话。meta 行整行删掉:版本在标题栏和按钮上都有,决定
数就是下面那张清单本身,指纹整条搬进「查看 GDD 正文」弹层底部做追溯。两个挂载点
改成只挂这一个组件,可见性判据抽成两个小函数供外壳和两块子件共用,不再各自拼装。

做游戏工作台原先给卡片设的高度上限和内部滚动挪到外壳为 flex 子件、卡片为可滚行,
行为不变。

新增一条 appSurface 用例(A/B:换回旧卡片即红):状态与版本只在标题栏;卡内没有
「立项策划」「待审批」「版本 v1」「指纹」;弹层底部有完整版本与指纹。

styles.css 在基线就不符合 prettier,本次只改相关规则,不整文件重排。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 14:47:01 +00:00
parent e57d81454f
commit 1335b64d6b
5 changed files with 130 additions and 61 deletions
@@ -25,6 +25,60 @@ const stateLabels: Record<PlanGddStateViewV1['state'], string> = {
rejected: '已退回',
};
function stageProgressVisible(
state: PlanGddStateViewV1 | null,
active: boolean,
) {
return Boolean(state && (active || state.state !== 'not_started'));
}
function approvalCardVisible(state: PlanGddStateViewV1 | null) {
return Boolean(
state?.displayGdd && (state.pendingApproval || state.recoveryPending),
);
}
/**
* 策划区的外壳:阶段进度是它的标题栏,审批卡是它的正文。
*
* 两者原本各画一个带边框的盒子,叠在一起时「立项策划 / 状态」在标题栏和卡头各出现
* 一次。合成一个面之后卡头只剩游戏标题和一句话,状态与版本只在标题栏画一遍。两个
* 挂载点都从这里进,不再各自拼装。
*/
export function PlanGddSurface({
state,
active = false,
busy,
error,
onRefresh,
onDecision,
}: GddApprovalCardProps & { active?: boolean }) {
const showProgress = stageProgressVisible(state, active);
const showCard = approvalCardVisible(state);
if (!showProgress && !showCard) {
return null;
}
return (
<section
className={`plan-gdd-surface${showCard ? ' plan-gdd-surface--with-card' : ''}`}
aria-label="立项策划"
>
{showProgress ? (
<PlanGddStageProgress state={state} active={active} />
) : null}
{showCard ? (
<GddApprovalCard
state={state}
busy={busy}
error={error}
onRefresh={onRefresh}
onDecision={onDecision}
/>
) : null}
</section>
);
}
export function PlanGddStageProgress({
state,
active = false,
@@ -32,7 +86,7 @@ export function PlanGddStageProgress({
state: PlanGddStateViewV1 | null;
active?: boolean;
}) {
if (!state || (!active && state.state === 'not_started')) {
if (!state || !stageProgressVisible(state, active)) {
return null;
}
const latestVersion =
@@ -156,10 +210,7 @@ export function GddApprovalCard({
const [comment, setComment] = useState('');
const [gddDetailsOpen, setGddDetailsOpen] = useState(false);
if (
!state?.displayGdd ||
(!state.pendingApproval && !state.recoveryPending)
) {
if (!state?.displayGdd || !approvalCardVisible(state)) {
return null;
}
@@ -187,22 +238,10 @@ export function GddApprovalCard({
return (
<section className="gdd-approval-card" aria-label="GDD 审批卡">
<header className="gdd-approval-card__header">
<div>
<span className="gdd-approval-card__eyebrow">立项策划</span>
<h2>{gdd.game.title || `Fast GDD v${gdd.version}`}</h2>
<p>{gdd.game.oneLiner}</p>
</div>
<span className="gdd-approval-card__state">
{stateLabels[state.state]}
</span>
<h2>{gdd.game.title || `Fast GDD v${gdd.version}`}</h2>
<p>{gdd.game.oneLiner}</p>
</header>
<div className="gdd-approval-card__meta">
<span>{`版本 v${gdd.version}`}</span>
<span>{`指纹 ${gdd.fingerprint.slice(0, 12)}…`}</span>
<span>{`${gdd.decisions.length} 项决定`}</span>
</div>
{gdd.decisions.length > 0 ? (
<div className="gdd-approval-card__decisions" aria-label="决定状态">
{gdd.decisions.map((decision) => (
@@ -349,6 +388,12 @@ export function GddApprovalCard({
<p>{section.content}</p>
</article>
))}
<small
className="gdd-approval-card__details-trace"
aria-label="GDD 版本与指纹"
>
{`Fast GDD v${gdd.version} · ${gdd.fingerprint}`}
</small>
<div className="gdd-approval-card__dialog-actions">
<button type="button" onClick={() => setGddDetailsOpen(false)}>
关闭
@@ -25,7 +25,7 @@ import {
} from '../agent-runtime';
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
import { taskStatusLabels } from '../project-summary/projectSummary';
import { GddApprovalCard, PlanGddStageProgress } from './GddApprovalCard';
import { PlanGddSurface } from './GddApprovalCard';
import {
pendingCommandDetail,
pendingCommandTitle,
@@ -105,12 +105,9 @@ export function ProjectSupervisorView({
aria-label={directCodex ? '陶泥儿项目对话' : '项目总控对话'}
>
<div className="project-supervisor-conversation">
<PlanGddStageProgress
<PlanGddSurface
state={planGddState}
active={isPlanningLaneRuntime(runtimePanelProps.runtime)}
/>
<GddApprovalCard
state={planGddState}
busy={planGddHydrateBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
@@ -47,7 +47,7 @@ import {
taskGroupLabels,
taskStatusLabels,
} from '../project-summary/projectSummary';
import { GddApprovalCard, PlanGddStageProgress } from './GddApprovalCard';
import { PlanGddSurface } from './GddApprovalCard';
import {
pendingCommandDetail,
pendingCommandTitle,
@@ -365,12 +365,9 @@ export function ProjectWorkspaceChatPane({
</button>
</div>
</header>
<PlanGddStageProgress
<PlanGddSurface
state={planGddState}
active={isPlanningLaneRuntime(projectSupervisorRuntime)}
/>
<GddApprovalCard
state={planGddState}
busy={planGddHydrateBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
+30 -32
View File
@@ -3448,21 +3448,32 @@ textarea {
gap: 8px;
}
/* 策划区是一个面:阶段进度是标题栏,审批卡是正文。边框和圆角只画在外壳上,里面两块
不再各自带框。 */
.plan-gdd-surface {
display: grid;
min-width: 0;
border: 1px solid #cfd7e6;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.gdd-approval-card {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #cfd7e6;
border-radius: 10px;
padding: 16px 18px 18px;
background: #f8fbff;
}
.plan-gdd-surface--with-card .plan-gdd-stage-progress {
border-bottom: 1px solid #dbe4f1;
}
.plan-gdd-stage-progress {
display: grid;
gap: 6px;
padding: 10px 12px;
border: 1px solid #dbe4f1;
border-radius: 8px;
background: #fff;
color: #526173;
font-size: 12px;
@@ -3489,15 +3500,9 @@ textarea {
}
.gdd-approval-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.gdd-approval-card__eyebrow {
color: #526173;
font-size: 12px;
display: grid;
gap: 5px;
min-width: 0;
}
.gdd-approval-card h2,
@@ -3507,31 +3512,18 @@ textarea {
}
.gdd-approval-card h2 {
margin-top: 3px;
font-size: 18px;
}
.gdd-approval-card__header p {
margin-top: 5px;
color: #526173;
overflow-wrap: anywhere;
}
.gdd-approval-card__state {
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 999px;
color: #0c4a6e;
background: #dff3ff;
font-size: 12px;
}
.gdd-approval-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
color: #526173;
font-size: 12px;
.gdd-approval-card__details-trace {
color: #6b7a8c;
font-size: 11px;
overflow-wrap: anywhere;
}
.gdd-approval-card__details-trigger {
@@ -6529,8 +6521,14 @@ iframe.preview-frame {
/* 审批卡是这一列里唯一没有天花板的成员:Fast GDD 的决定项越多它越高,能把消息列表和运行
时面板一起挤出可视区。和 `.agent-runtime-status` 一样给它自己的上限加内部滚动,而不是
让它去挤别人。 */
.game-workbench-chat .gdd-approval-card {
.game-workbench-chat .plan-gdd-surface {
flex: 0 1 auto;
min-height: 0;
grid-template-rows: auto minmax(0, 1fr);
}
.game-workbench-chat .gdd-approval-card {
min-height: 0;
max-height: clamp(160px, 34dvh, 380px);
overflow-y: auto;
overscroll-behavior: contain;
@@ -276,6 +276,38 @@ export function registerPlanGddApprovalTests() {
expect(screen.getByRole('button', { name: '批准 v1' })).not.toBeNull();
});
it('paints the planning state once in the stage strip and keeps the fingerprint inside the GDD dialog', async () => {
// 阶段进度和审批卡曾各自带框叠在一起,「立项策划 / 待审批」在标题栏和卡头各画一遍,
// 卡头还露一截指纹。合成一个面之后:状态与版本只在标题栏,卡头只剩游戏标题和一句
// 话,指纹整条放进正文弹层做追溯。
const harness = createProjectSupervisorRuntimeHarness();
const view = createPlanGddStateView();
harness.setPlanGddState(view);
const card = await mountApprovalCard(harness);
const surface = screen.getByLabelText('立项策划');
const strip = within(surface).getByLabelText('立项策划阶段进度');
expect(within(strip).getByText('待审批')).not.toBeNull();
expect(within(strip).getByText('当前版本:v1')).not.toBeNull();
expect(surface.contains(card)).toBe(true);
expect(within(card).queryByText('立项策划')).toBeNull();
expect(within(card).queryByText('待审批')).toBeNull();
expect(within(card).queryByText(/^版本 v1$/)).toBeNull();
expect(within(card).queryByText(/指纹/)).toBeNull();
expect(within(card).getByRole('heading', { level: 2 }).textContent).toBe(
view.displayGdd?.game.title,
);
fireEvent.click(
within(card).getByRole('button', { name: '查看 GDD 正文' }),
);
const dialog = screen.getByRole('dialog');
expect(within(dialog).getByLabelText('GDD 版本与指纹').textContent).toBe(
`Fast GDD v1 · ${view.displayGdd?.fingerprint}`,
);
});
it('keeps the supervisor runtime panel off the planning lane while the planner is working', async () => {
// 完整面板是给做游戏链路的:十几个专业 Agent、多步计划、逐 Agent 重试。策划链路
// 只有一个 project-planning 子 Run、一两步计划,面板画出来的全是 D11 拓扑的内部