合并 origin/master 到资源工作台 V3 分支:解 4 处冲突并保留两侧行为
Project CI / Repository checks (pull_request) Successful in 3m14s
Project CI / Frontend tests (pull_request) Successful in 3m57s
Project CI / Backend tests (pull_request) Successful in 6m30s
Project CI / Native shell tests (pull_request) Successful in 17m27s

- WorkspaceLauncher.tsx:保留本分支清单快照 CAS 拒收提示(manifestMergeNotice 提示条、data-manifest-merge-* 观察点、recoverRejectedManifestSnapshot「重新读取清单」)与 activeVersionId/onActiveVersionChange 版本口径,并入 master 的策划/游戏运行态切换(onMakeGame + switchToGameRuntime、suppressInitialGameTurn 抑制首轮、supervisor key 带 agentRuntimeMode、onSwitchToGameRuntime),planningStartMode 统一取 master 的派生值(上下文 startMode 为 planning 且未切到 game 运行时)
- ProjectSupervisorView.tsx:props 同时保留本分支 versions/activeVersionId 与 master 的 designView/onDesignApprove/onDesignClarify/onDesignRetry,DesignAgentSurface 审批链路与本分支资源引用输入区并存;directCodex 输入区保留本分支 @ 引用按钮 + 模型选择 + 发送按钮控制条,发送按钮禁用条件并入 master 的 designView 审批待定口径,模型选择沿用 master 的「对话中可切换」口径
- styles.css:本分支追加的资源画布/输入区样式块与 master 追加的 .design-agent-reasoning 样式块都保留,并补上本分支最后一条规则的收尾大括号
- view/project-development/index.tsx:保留本分支的 image-editor 引用(ImageCanvasProjectAssetPickerDialog、ImageCanvasQuickEditPanelView、ImageCanvasSelectedLayerToolbarView、useImageCanvasFloatingOptionDismiss),去掉 master 对已退役 features/asset-canvas 的 import(本分支已由 resource-canvas 取代),保留 master 的 DesignWorkspacePanel 与 planningStartMode 策划工作台分支
- tests/workspaceLauncherManifestMerge.test.tsx:补 master 新引入的 get_design_agent_runtime_mode mock(返回 null,与 master 各套件同口径),拒收提示断言不变
- tests/appSurface/design-agent.suite.ts:审批待定禁用输入的断言改用本分支 Lexical 输入区的禁用口径(容器 data-disabled + editor.setEditable(false)),断言意图不变
- 验证:npm run typecheck 通过;apps/ai-game-creator-shell typecheck 通过;apps/ai-game-creator-shell/tests 96 个测试文件 1380 通过 4 跳过 0 失败;全仓 vitest 297 通过 2 失败(scripts 下两例为 Windows 权限语义导致的既有失败,相关文件与实现均未参与本次合并);npm run check:encoding 与 git diff --check 通过
This commit is contained in:
2026-09-12 17:31:51 +08:00
120 changed files with 12599 additions and 183 deletions
@@ -64,6 +64,13 @@ export function WorkspaceLauncherShell({
title: string;
message: string;
} | null>(null);
// 「做成游戏」切换记录按项目上下文(路径 + createdAt)定位。运行模式必须随
// currentProjectContext 同步派生,不能靠 effect 后置修正:首帧挂错 lane 会先
// 以游戏运行时挂载并消耗首轮 claim,重挂后的策划实例再也发不出首轮。
const [gameRuntimeSwitch, setGameRuntimeSwitch] = useState<{
projectPath: string;
createdAt: number;
} | null>(null);
const developerAgent = useDeveloperAgentPanel(launcherView);
const homeProject = useHomeProjectCreation({
setStatus,
@@ -87,6 +94,17 @@ export function WorkspaceLauncherShell({
createHomeDraftAutomatically,
openProject,
} = homeProject;
const switchedToGameRuntime =
gameRuntimeSwitch !== null &&
currentProjectContext !== null &&
gameRuntimeSwitch.projectPath === currentProjectContext.projectPath &&
gameRuntimeSwitch.createdAt === currentProjectContext.createdAt;
const planningStartMode =
currentProjectContext?.startMode === 'planning' && !switchedToGameRuntime;
const agentRuntimeMode: 'design' | 'game' = planningStartMode
? 'design'
: 'game';
const suppressInitialGameTurn = switchedToGameRuntime;
const activeProjectContextRef = useRef(currentProjectContext);
const manifestMergeRef = useRef<ProjectManifestMergeState | null>(null);
activeProjectContextRef.current = currentProjectContext;
@@ -418,6 +436,19 @@ export function WorkspaceLauncherShell({
});
}
async function switchToGameRuntime(nextProjectPath: string) {
const invoke = resolveTauriInvoke();
if (!invoke) throw new Error('需要在陶泥儿客户端内运行');
await invoke('set_design_agent_runtime_mode', {
projectPath: nextProjectPath,
activeRuntime: 'game',
});
setGameRuntimeSwitch({
projectPath: nextProjectPath,
createdAt: currentProjectContext?.createdAt ?? 0,
});
}
const currentHelpTitle =
launcherView === 'guide'
? '使用指南'
@@ -557,30 +588,43 @@ export function WorkspaceLauncherShell({
preview={activeProjectPreview}
agentRuntimeSummaries={activeProjectAgentRuntimeSummaries}
agentResults={activeProjectAgentResults}
planningStartMode={currentProjectContext.startMode === 'planning'}
planningStartMode={planningStartMode}
activeVersionId={activeVersionId}
onActiveVersionChange={setActiveVersionId}
onPlay={() =>
requestCurrentProjectPlay(currentProjectContext.projectPath)
}
onMakeGame={() =>
void switchToGameRuntime(currentProjectContext.projectPath)
}
onManifestChange={syncActiveProjectManifest}
onHomeOpen={() => setLauncherView('home')}
onProjectsOpen={() => setLauncherView('projects')}
supervisor={
<ProjectSupervisor
key={currentProjectContext.projectPath}
key={`${currentProjectContext.projectPath}:${agentRuntimeMode}`}
initialProjectPath={currentProjectContext.projectPath}
initialProjectManifest={currentProjectContext.manifest}
initialProjectKind={currentProjectContext.projectKind}
initialSupervisorMessage={currentProjectContext.initialPrompt}
initialCreationType={currentProjectContext.creationType}
initialAttachments={currentProjectContext.attachments}
initialSupervisorMessage={
!suppressInitialGameTurn
? currentProjectContext.initialPrompt
: ''
}
initialCreationType={
!suppressInitialGameTurn
? currentProjectContext.creationType
: null
}
initialAttachments={
!suppressInitialGameTurn
? currentProjectContext.attachments
: []
}
activeVersionId={activeVersionId}
orchestrationMode="single-supervisor"
projectSupervisorOnly
planningStartMode={
currentProjectContext.startMode === 'planning'
}
planningStartMode={planningStartMode}
playRequest={playRequest}
onPlayRequestHandled={handlePlayRequestHandled}
onManifestChange={syncActiveProjectManifest}
@@ -590,6 +634,7 @@ export function WorkspaceLauncherShell({
}
onAgentResultsChange={setActiveProjectAgentResults}
onMakeGameFromApprovedGdd={startGameFromApprovedGdd}
onSwitchToGameRuntime={switchToGameRuntime}
/>
}
/>
@@ -61,6 +61,7 @@ export type ProjectSupervisorComponentProps = {
) => void;
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
onMakeGameFromApprovedGdd?: (projectPath: string) => Promise<void>;
onSwitchToGameRuntime?: (projectPath: string) => Promise<void>;
};
export type WorkspaceLauncherShellProps = WorkspaceLauncherProps & {
@@ -274,6 +274,12 @@ export function useHomeProjectCreation({
attachments: HomeAttachmentDraft[],
startMode: ProjectStartMode,
) {
if (startMode === 'planning') {
await invoke('set_design_agent_runtime_mode', {
projectPath: result.projectPath,
activeRuntime: 'design',
});
}
const importedAttachments = await importHomeAttachments(
invoke,
result.projectPath,
@@ -499,6 +505,11 @@ export function useHomeProjectCreation({
setStatus('这不是已初始化的 AI 游戏项目,请使用新建项目。');
return;
}
const runtimeMode = await invoke<{
activeRuntime: 'design' | 'game';
} | null>('get_design_agent_runtime_mode', {
projectPath: trimmedProjectPath,
});
setStatus('已打开项目');
await enterProjectDevelopment({
projectPath: trimmedProjectPath,
@@ -516,7 +527,7 @@ export function useHomeProjectCreation({
trimmedProjectPath,
),
creationType: null,
startMode: null,
startMode: runtimeMode?.activeRuntime === 'design' ? 'planning' : null,
initialPrompt: '',
attachments: [],
recentRunStatus: directoryStatus.recentRunStatus,
@@ -620,6 +631,7 @@ export function useHomeProjectCreation({
'create_automatic_local_game_project',
{
name: suggestedName,
planning: startMode === 'planning',
},
);
try {
@@ -0,0 +1,126 @@
import { Check, RotateCcw, X } from 'lucide-react';
import { useLayoutEffect, useState } from 'react';
import type { DesignClarificationRequest, DesignView } from '../../app/types';
const PHASE_LABELS: Record<string, string> = {
concept: '概念设计',
top_design: '顶层设计',
architecture: '系统架构',
systems: '系统文档',
tdd: '技术文档',
consultant: '顾问',
};
type DesignAgentSurfaceProps = {
view: DesignView | null;
busy: boolean;
error: string | null;
onApprove: (requestId: string, approved: boolean) => void;
onClarify: (
question: DesignClarificationRequest,
optionIndex: number | null,
text: string,
) => void;
onRetry: () => void;
};
export function DesignAgentSurface({
view,
busy,
error,
onApprove,
onClarify,
onRetry,
}: DesignAgentSurfaceProps) {
const [clarifyText, setClarifyText] = useState('');
const phase = view?.session.currentPhase ?? 'concept';
const pending = view?.session.pendingApproval;
const clarification = view?.session.pendingClarification;
useLayoutEffect(() => {
setClarifyText('');
}, [clarification?.requestId]);
return (
<section className="design-agent-controls" aria-label="策划阶段控制">
<div className="design-agent-controls__header">
<div>
<span>当前阶段</span>
<div className="design-agent-controls__phase-title">
<strong>{PHASE_LABELS[phase] ?? phase}</strong>
</div>
</div>
{view?.running ? (
<span className="design-agent-controls__status">工作中</span>
) : null}
</div>
{error ? <p className="design-agent-controls__error">{error}</p> : null}
{pending && phase !== 'consultant' ? (
<div className="design-agent-approval">
<p>当前阶段产物已提交,是否批准进入下一阶段?</p>
<div>
<button
type="button"
aria-label="批准"
disabled={busy}
onClick={() => onApprove(pending.requestId, true)}
>
<Check size={15} aria-hidden="true" />
批准
</button>
<button
type="button"
aria-label="继续修改"
disabled={busy}
onClick={() => onApprove(pending.requestId, false)}
>
<X size={15} aria-hidden="true" />
继续修改
</button>
</div>
</div>
) : null}
{clarification ? (
<div className="design-agent-clarify">
<p>{clarification.question}</p>
<div className="design-agent-clarify__options">
{clarification.options.map((option, index) => (
<button
key={`${clarification.requestId}-${index}`}
type="button"
disabled={busy}
onClick={() => onClarify(clarification, index, '')}
>
{option}
</button>
))}
</div>
<textarea
rows={2}
value={clarifyText}
disabled={busy}
placeholder="或者直接输入你的回答"
onChange={(event) => setClarifyText(event.currentTarget.value)}
/>
<button
type="button"
disabled={busy || !clarifyText.trim()}
onClick={() => onClarify(clarification, null, clarifyText)}
>
提交回答
</button>
</div>
) : null}
{view?.canRetry ? (
<button
className="design-agent-retry"
type="button"
disabled={busy}
onClick={onRetry}
>
<RotateCcw size={15} aria-hidden="true" />
重试本轮
</button>
) : null}
</section>
);
}
@@ -16,6 +16,7 @@ import type {
PlanGddDecisionAction,
PlanGddStateViewV1,
} from '../../app/types';
import type { DesignClarificationRequest, DesignView } from '../../app/types';
import { ChatMarkdownMessage } from '../../components/ChatMarkdownMessage';
import {
projectProfessionalAgentLabel,
@@ -30,6 +31,7 @@ import {
ConversationModelSelect,
type ConversationModelSelectHandle,
} from './ConversationModelSelect';
import { DesignAgentSurface } from './DesignAgentSurface';
import { PlanGddSurface } from './GddApprovalCard';
import {
pendingCommandDetail,
@@ -91,6 +93,7 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
projectPath: string;
showProfessionalCollaboration?: boolean;
transientReply: string;
designReasoning?: string;
visibleMessages: ChatMessage[];
visibleProfessionalAgentCards: AgentStatusCard[];
workspaceStatus: string;
@@ -106,6 +109,14 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
) => Promise<void>;
onMakeGameFromApprovedGdd?: () => Promise<void>;
versions?: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameIterationVersion[];
designView?: DesignView | null;
onDesignApprove?: (requestId: string, approved: boolean) => void;
onDesignClarify?: (
question: DesignClarificationRequest,
optionIndex: number | null,
text: string,
) => void;
onDesignRetry?: () => void;
};
export function ProjectSupervisorView({
@@ -134,6 +145,7 @@ export function ProjectSupervisorView({
projectPath,
showProfessionalCollaboration = true,
transientReply,
designReasoning = '',
visibleMessages,
visibleProfessionalAgentCards,
workspaceStatus,
@@ -146,6 +158,10 @@ export function ProjectSupervisorView({
onPlanGddDecision,
onMakeGameFromApprovedGdd,
versions,
designView = null,
onDesignApprove,
onDesignClarify,
onDesignRetry,
...runtimePanelProps
}: ProjectSupervisorViewProps) {
const planningSurfaceActive =
@@ -177,6 +193,8 @@ export function ProjectSupervisorView({
disabled={
runtimePanelProps.controlBusy ||
needsUserInput ||
Boolean(designView?.session.pendingApproval) ||
Boolean(designView?.session.pendingClarification) ||
(directCodex && (!modelReady || modelValidating))
}
>
@@ -193,17 +211,28 @@ export function ProjectSupervisorView({
aria-label={directCodex ? '陶泥儿项目对话' : '项目总控对话'}
>
<div className="project-supervisor-conversation">
<PlanGddSurface
state={planGddState}
active={planningSurfaceActive}
projectPath={projectPath}
hydrateBusy={planGddHydrateBusy}
decisionBusy={planGddDecisionBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
onDecision={onPlanGddDecision}
onMakeGame={onMakeGameFromApprovedGdd}
/>
{designView || onDesignApprove ? (
<DesignAgentSurface
view={designView}
busy={runtimePanelProps.controlBusy || planGddDecisionBusy}
error={planGddError}
onApprove={onDesignApprove ?? (() => undefined)}
onClarify={onDesignClarify ?? (() => undefined)}
onRetry={onDesignRetry ?? (() => undefined)}
/>
) : (
<PlanGddSurface
state={planGddState}
active={planningSurfaceActive}
projectPath={projectPath}
hydrateBusy={planGddHydrateBusy}
decisionBusy={planGddDecisionBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
onDecision={onPlanGddDecision}
onMakeGame={onMakeGameFromApprovedGdd}
/>
)}
<div
ref={messagesRef}
className="message-list project-supervisor-message-list"
@@ -230,6 +259,12 @@ export function ProjectSupervisorView({
/>
</div>
))}
{designReasoning ? (
<details className="design-agent-reasoning">
<summary>显示思考过程</summary>
<pre>{designReasoning}</pre>
</details>
) : null}
{directCodex &&
(runtimePanelProps.controlBusy || Boolean(directProcessDetail)) ? (
<section
@@ -393,7 +428,11 @@ export function ProjectSupervisorView({
assets={chatProjectAssets}
projectPath={projectPath}
disabled={
runtimePanelProps.controlBusy || needsUserInput || modelValidating
runtimePanelProps.controlBusy ||
needsUserInput ||
modelValidating ||
Boolean(designView?.session.pendingApproval) ||
Boolean(designView?.session.pendingClarification)
}
rows={3}
value={chatInput}
@@ -423,7 +462,9 @@ export function ProjectSupervisorView({
<div className="project-supervisor-composer-controls-right">
<ConversationModelSelect
ref={modelSelectRef}
disabled={runtimePanelProps.controlBusy || needsUserInput}
// 允许在对话进行中切换模型:写回的是客户端配置,只影响后续轮次,
// 当前回合不受影响;发送按钮仍由 controlBusy / modelReady 把关。
disabled={needsUserInput}
onReady={setModelReady}
projectPath={projectPath}
/>