完成M1D-2入口分流与阶段进度
Project CI / Repository checks (pull_request) Successful in 1m28s
Project CI / Frontend tests (pull_request) Failing after 2m4s
Project CI / Native shell tests (pull_request) Failing after 2m58s
Project CI / Backend tests (pull_request) Successful in 3m45s

游戏新项目默认进入立项策划并保留直接开建路径

项目总控页面挂载GDD审批卡与阶段进度

同步策划展示名称、定向测试与开发日志
This commit is contained in:
2026-08-18 09:11:38 +00:00
parent 9bad7121b5
commit bf2185fba0
15 changed files with 296 additions and 24 deletions
+30 -7
View File
@@ -33,6 +33,7 @@ import {
CONVERSATION_INITIAL_VISIBLE_COUNT,
CONVERSATION_VISIBLE_STEP,
PROJECT_SUPERVISOR_AGENT_ID,
PROJECT_SUPERVISOR_PLAN_SOURCE,
seedManifest,
} from './app/constants';
import { useEscapeToClose } from './app/dialogs';
@@ -6008,12 +6009,31 @@ export function App({
if (!sessionId || localProjectPathRef.current !== nextProjectPath) {
return;
}
const submissionRunProfile =
workspaceProjectKind === 'godot' ||
(supervisorChatOnly && !gameChatOnly)
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
const planningEntry =
!gameChatOnly &&
workspaceProjectKind === 'web' &&
runtimeAtSubmission?.source === PROJECT_SUPERVISOR_PLAN_SOURCE;
const submissionRunProfile = planningEntry
? 'standard'
: workspaceProjectKind === 'godot' ||
(supervisorChatOnly && !gameChatOnly)
? 'standard'
: 'autonomous-game-build';
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
const submissionSource = planningEntry
? PROJECT_SUPERVISOR_PLAN_SOURCE
: gameChatOnly
? 'project-supervisor-game-chat'
: 'project-supervisor-gui';
if (
planningEntry &&
runtimeAtSubmission &&
runtimeAtSubmission.source === PROJECT_SUPERVISOR_PLAN_SOURCE &&
!isAgentRuntimeTerminalState(runtimeAtSubmission)
) {
setProjectSupervisorRuntimeError('请先完成当前立项策划步骤');
return;
}
const steerRuntime = gameChatOnly
? matchingAgentRuntimeForSteer(
[runtimeAtSubmission],
@@ -6077,9 +6097,7 @@ export function App({
prompt,
runtime: runtimeAtSubmission,
runProfile: submissionRunProfile,
source: gameChatOnly
? 'project-supervisor-game-chat'
: 'project-supervisor-gui',
source: submissionSource,
});
const runtimeResult = submission.runtimeResult;
const acceptedRunId = submission.acceptedRunId.trim();
@@ -11324,6 +11342,11 @@ export function App({
visibleMessages={visibleMessages}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
workspaceStatus={workspaceStatus}
planGddState={planGddState}
planGddHydrateBusy={planGddHydrateBusy || planGddDecisionBusy}
planGddError={planGddError}
onPlanGddRefresh={() => void hydratePlanGddState()}
onPlanGddDecision={decidePlanGdd}
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
@@ -12,6 +12,7 @@ export const CONVERSATION_INITIAL_VISIBLE_COUNT = 20;
export const CONVERSATION_VISIBLE_STEP = 20;
export const AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD = 48;
export const PROJECT_SUPERVISOR_AGENT_ID = 'project-supervisor';
export const PROJECT_SUPERVISOR_PLAN_SOURCE = 'project-supervisor-plan';
export const launcherNotifications: Array<{
label: string;
detail: string;
@@ -52,6 +52,8 @@ export type LauncherImportedAttachment = {
export type LocalProjectKind = 'web' | 'godot';
export type ProjectStartMode = 'planning' | 'direct-build';
export type LauncherProjectContext = {
projectPath: string;
projectName: string;
@@ -76,6 +78,7 @@ export type PendingNonEmptyProject =
kind: 'home-create';
projectPath: string;
mode: HomeAgentMode;
startMode: ProjectStartMode;
prompt: string;
attachments: HomeAttachmentDraft[];
}
@@ -1537,6 +1537,9 @@ export function projectSupervisorCollaboratingAgentRuntimes(
}
export function projectProfessionalAgentLabel(agentId: string) {
if (agentId === 'project-planning') {
return '立项策划 Agent';
}
if (agentId === 'design-foundation') {
return '玩法策划 Agent';
}
@@ -63,6 +63,7 @@ export function WorkspaceLauncherShell({
setAgentResults: setActiveProjectAgentResults,
resetLauncherHomeDraft,
createHomeDraft,
createHomeDraftDirectBuild,
createHomeDraftAutomatically,
openProject,
} = homeProject;
@@ -260,6 +261,7 @@ export function WorkspaceLauncherShell({
homeAgentModeItems={homeAgentModeItems}
recentProjectRows={recentProjectRows}
onCreateDraft={createHomeDraft}
onCreateDraftDirectBuild={createHomeDraftDirectBuild}
onCreateDraftAutomatically={createHomeDraftAutomatically}
onProjectsOpen={() => setLauncherView('projects')}
onProjectOpen={(path) => {
@@ -10,7 +10,10 @@ import type {
GameCreationAppManifest,
GameCreationAppPreviewState,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { seedManifest } from '../../app/constants';
import {
PROJECT_SUPERVISOR_PLAN_SOURCE,
seedManifest,
} from '../../app/constants';
import { useEscapeToClose } from '../../app/dialogs';
import { resolveTauriInvoke } from '../../app/tauri';
import type {
@@ -20,6 +23,7 @@ import type {
LocalGameProjectRevisionStatus,
LocalProjectDirectoryStatus,
PendingNonEmptyProject,
ProjectStartMode,
TauriInvoke,
UploadLocalAssetResult,
} from '../../app/types';
@@ -168,6 +172,7 @@ export function useHomeProjectCreation({
mode: HomeAgentMode,
prompt: string,
attachments: HomeAttachmentDraft[],
startMode: ProjectStartMode,
) {
const importedAttachments = await importHomeAttachments(
invoke,
@@ -187,7 +192,10 @@ export function useHomeProjectCreation({
sessionId,
prompt: buildHomeConversationContent(mode, prompt, attachments),
runtime: null,
runProfile: 'autonomous-game-build',
runProfile:
startMode === 'planning' ? 'standard' : 'autonomous-game-build',
source:
startMode === 'planning' ? PROJECT_SUPERVISOR_PLAN_SOURCE : undefined,
});
enterProjectDevelopment({
projectPath: result.projectPath,
@@ -218,6 +226,7 @@ export function useHomeProjectCreation({
mode: HomeAgentMode,
prompt: string,
attachments: HomeAttachmentDraft[],
startMode: ProjectStartMode,
skipNonEmptyCheck = false,
) {
const trimmedProjectPath = validateProjectPath(nextProjectPath);
@@ -241,6 +250,7 @@ export function useHomeProjectCreation({
kind: 'home-create',
projectPath: trimmedProjectPath,
mode,
startMode,
prompt,
attachments,
});
@@ -263,6 +273,7 @@ export function useHomeProjectCreation({
mode,
prompt,
attachments,
startMode,
);
setStatus('已创建项目并交给项目总控 Agent');
} catch (error) {
@@ -285,6 +296,7 @@ export function useHomeProjectCreation({
mode: HomeAgentMode,
prompt: string,
attachments: HomeAttachmentDraft[],
startMode: ProjectStartMode,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
@@ -292,7 +304,14 @@ export function useHomeProjectCreation({
}
setStatus('正在启动项目总控 Agent');
try {
await enterCreatedHomeProject(invoke, result, mode, prompt, attachments);
await enterCreatedHomeProject(
invoke,
result,
mode,
prompt,
attachments,
startMode,
);
setStatus('已自动创建工作区并开始工作');
return '已自动创建工作区并开始工作';
} catch (error) {
@@ -476,6 +495,7 @@ export function useHomeProjectCreation({
pendingProject.mode,
pendingProject.prompt,
pendingProject.attachments,
pendingProject.startMode,
true,
);
return;
@@ -513,6 +533,28 @@ export function useHomeProjectCreation({
draft.mode,
draft.prompt,
draft.attachments,
draft.mode === 'game' ? 'planning' : 'direct-build',
);
}
async function createHomeDraftDirectBuild(draft: HomeDraft) {
const invoke = resolveTauriInvoke();
if (!invoke) {
throw new Error('需要在 Tauri App 内运行');
}
const selectedPath = await invoke<string | null>(
'pick_local_project_directory',
projectPath.trim() ? { initialPath: projectPath.trim() } : undefined,
);
if (!selectedPath) {
return '已取消';
}
return createHomeProjectFromDirectory(
selectedPath,
draft.mode,
draft.prompt,
draft.attachments,
'direct-build',
);
}
@@ -530,6 +572,7 @@ export function useHomeProjectCreation({
draft.mode,
draft.prompt,
draft.attachments,
draft.mode === 'game' ? 'planning' : 'direct-build',
);
}
@@ -619,6 +662,7 @@ export function useHomeProjectCreation({
pendingNonEmptyProject,
resetLauncherHomeDraft,
createHomeDraft,
createHomeDraftDirectBuild,
createHomeDraftAutomatically,
openProject,
pickAndOpenProject,
@@ -40,7 +40,7 @@ export const commonProjectInternalReadDrafts = [
] as const;
export const taskGroupLabels: Record<GameCreationAppAgentGroup, string> = {
design: '策划组',
design: '设计实现组',
art: '美术组',
code: '程序组',
balance: '数值组',
@@ -25,6 +25,39 @@ const stateLabels: Record<PlanGddStateViewV1['state'], string> = {
rejected: '已退回',
};
export function PlanGddStageProgress({
state,
active = false,
}: {
state: PlanGddStateViewV1 | null;
active?: boolean;
}) {
if (!state || (!active && state.state === 'not_started')) {
return null;
}
const latestVersion =
state.displayGdd?.version ??
state.versions[state.versions.length - 1]?.gddRef.version ??
null;
const clarificationRound = state.session?.clarificationRound ?? 0;
return (
<section className="plan-gdd-stage-progress" aria-label="立项策划阶段进度">
<div className="plan-gdd-stage-progress__header">
<strong>立项策划</strong>
<span>{stateLabels[state.state]}</span>
</div>
<div className="plan-gdd-stage-progress__meta">
<span>{`轮次 ${clarificationRound}/3`}</span>
<span>
{latestVersion === null
? '当前版本:草稿'
: `当前版本:v${latestVersion}`}
</span>
</div>
</section>
);
}
const decisionStateLabels = {
confirmed: '已确认',
default_pending: '待确认默认项',
@@ -11,6 +11,8 @@ import type {
AgentStatusCard,
ChatMessage,
PendingUiConfirmation,
PlanGddDecisionAction,
PlanGddStateViewV1,
} from '../../app/types';
import {
projectProfessionalAgentLabel,
@@ -21,6 +23,7 @@ import {
} from '../agent-runtime';
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
import { taskStatusLabels } from '../project-summary/projectSummary';
import { GddApprovalCard, PlanGddStageProgress } from './GddApprovalCard';
type RuntimePanelProps = ComponentProps<typeof ProjectSupervisorRuntimePanel>;
@@ -39,6 +42,14 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
visibleMessages: ChatMessage[];
visibleProfessionalAgentCards: AgentStatusCard[];
workspaceStatus: string;
planGddState: PlanGddStateViewV1 | null;
planGddHydrateBusy: boolean;
planGddError: string | null;
onPlanGddRefresh: () => void;
onPlanGddDecision: (
action: PlanGddDecisionAction,
comment: string | null,
) => Promise<void>;
};
export function ProjectSupervisorView({
@@ -56,11 +67,29 @@ export function ProjectSupervisorView({
visibleMessages,
visibleProfessionalAgentCards,
workspaceStatus,
planGddState,
planGddHydrateBusy,
planGddError,
onPlanGddRefresh,
onPlanGddDecision,
...runtimePanelProps
}: ProjectSupervisorViewProps) {
return (
<section className="project-supervisor-surface" aria-label="项目总控对话">
<div className="project-supervisor-conversation">
<PlanGddStageProgress
state={planGddState}
active={
runtimePanelProps.runtime?.source === 'project-supervisor-plan'
}
/>
<GddApprovalCard
state={planGddState}
busy={planGddHydrateBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
onDecision={onPlanGddDecision}
/>
<div
className="message-list project-supervisor-message-list"
aria-label="项目总控消息"
@@ -47,7 +47,7 @@ import {
taskGroupLabels,
taskStatusLabels,
} from '../project-summary/projectSummary';
import { GddApprovalCard } from './GddApprovalCard';
import { GddApprovalCard, PlanGddStageProgress } from './GddApprovalCard';
import {
pendingCommandDetail,
pendingCommandTitle,
@@ -363,6 +363,12 @@ export function ProjectWorkspaceChatPane({
</button>
</div>
</header>
<PlanGddStageProgress
state={planGddState}
active={
projectSupervisorRuntime?.source === 'project-supervisor-plan'
}
/>
<GddApprovalCard
state={planGddState}
busy={planGddHydrateBusy}
+53
View File
@@ -3272,6 +3272,59 @@ textarea {
background: #f8fbff;
}
.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;
}
.plan-gdd-stage-progress__header,
.plan-gdd-stage-progress__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 14px;
}
.plan-gdd-stage-progress__header strong {
color: #27364a;
}
.plan-gdd-stage-progress__header span {
padding: 3px 8px;
border-radius: 999px;
background: #eef6ff;
color: #1f6feb;
}
.home-creation-actions {
display: flex;
align-items: center;
gap: 8px;
}
.home-direct-build-button {
min-height: 28px;
padding: 0 9px;
border: 1px solid var(--platform-surface-border);
border-radius: 999px;
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
cursor: pointer;
}
.home-direct-build-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.gdd-approval-card__header {
display: flex;
align-items: flex-start;
@@ -47,6 +47,7 @@ type HomeViewProps = {
homeAgentModeItems: readonly HomeAgentModeItem[];
recentProjectRows: readonly HomeProjectRow[];
onCreateDraft: (draft: HomeDraft) => Promise<string>;
onCreateDraftDirectBuild: (draft: HomeDraft) => Promise<string>;
onCreateDraftAutomatically: (draft: HomeDraft) => Promise<string>;
onProjectsOpen: () => void;
onProjectOpen: (path: string) => void;
@@ -60,6 +61,7 @@ export default function HomeView({
homeAgentModeItems,
recentProjectRows,
onCreateDraft,
onCreateDraftDirectBuild,
onCreateDraftAutomatically,
onProjectsOpen,
onProjectOpen,
@@ -183,14 +185,33 @@ export default function HomeView({
>
<div className="grid grid-cols-[1fr_auto] items-center gap-2.5 text-[12px] text-(--platform-text-soft)">
<UploadButton />
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
type="submit"
aria-label="开启创作"
disabled={homeCreationBusy}
>
<Plus size={16} aria-hidden="true" />
</button>
<div className="home-creation-actions">
{homeAgentMode === 'game' ? (
<button
className="home-direct-build-button"
type="button"
disabled={homeCreationBusy}
onClick={() =>
void createFromHome(
onCreateDraftDirectBuild,
'正在创建工作区',
)
}
>
直接开建
</button>
) : null}
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
type="submit"
aria-label={
homeAgentMode === 'game' ? '进入立项策划' : '开启创作'
}
disabled={homeCreationBusy}
>
<Plus size={16} aria-hidden="true" />
</button>
</div>
</div>
</RichInputArea>
</form>
@@ -1,6 +1,7 @@
import { describe, expect, test, vi } from 'vitest';
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import { PROJECT_SUPERVISOR_PLAN_SOURCE } from '../src/app/constants';
import type {
AgentRuntimeEventRecord,
AgentRuntimeResponseStream,
@@ -618,6 +619,51 @@ describe('Game Chat stream identity and source', () => {
);
});
test('planning entry forwards the exact standard profile and trusted plan source', async () => {
const runtimeResult = {
state: {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'session-plan-entry',
runId: 'plan-run',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
runProfile: 'standard' as const,
status: 'running',
phase: 'planning',
currentTask: '整理游戏立项需求',
currentAction: '',
plan: [],
observations: [],
allowedTools: [],
lastResponse: null,
error: null,
updatedAt: 1,
},
sessionPath: 'session.json',
eventPath: 'events.jsonl',
} satisfies AgentRuntimeResult;
const invoke = vi.fn(async () => runtimeResult);
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/plan-entry',
sessionId: 'session-plan-entry',
prompt: '做一个双摇杆动作游戏',
runtime: null,
runProfile: 'standard',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({
runProfile: 'standard',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
}),
);
});
test('game-chat hydration keeps identical text from a different runtime response identity', () => {
const history = [
{