Files
Genarrative/apps/ai-game-creator-shell/src/App.tsx
T
suzmii 9639a080c6 资源卡拖到对话栏实现批量 @ 引用
- 新增落点模型 resourceCardReferenceDropModel:对话栏矩形量取(0 x 0 判成没有落点,避免画布内拖动被误判)、命中判据、引用构造与落点提示文案。
- 引用构造与工具条「引用」按钮逐字一致(只认已登记 manifest 素材、source 为 resource-card、kind/分类/标签取自资源投影),一条都构造不出来时用提示条说明原因。
- 拖动中指针落到对话栏上即从排版切到引用:卡片不再跟指针走,改铺虚线落点浮层并提示「松手即可 @ 引用 N 项素材」;拖回画布内松手仍是原来的排版语义。
- 批量范围与拖动位移同一个集合(多选整批、单选只引用按住的那张);新增批量事件 RESOURCE_REFERENCE_INSERT_MANY_EVENT,App 侧一次插入整批并只聚焦一次,空批次不派发。
- 对话栏挂 ref(不 document.querySelector)、落点浮层按对话栏当时的屏幕矩形铺在根节点上,不改对话栏自身的定位与排版。
- 测试:新增落点模型用例 5 条与画布集成用例 4 条(单卡/多选/拖回画布/pointercancel),resourceReferenceInput 新增批量事件与一次插入整批 chip 两条,共 39 条定向全绿。
- 文档:功能说明新增「拖拽引用」一节与完成清单条目、PRD 更新时间、decision-log 新增 2026-09-21 条目。
2026-09-21 03:37:43 +08:00

12247 lines
399 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable react-refresh/only-export-components -- Testable pure helpers currently share this legacy app module. */
import {
type ChangeEvent,
type FormEvent,
type UIEvent,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import {
GAME_CREATION_AGENT_CAPABILITIES,
GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
type GameCreationAgentCapabilityDescriptor,
type GameCreationAgentRunTrace,
type GameCreationAgentToolCallTrace,
type GameCreationAppAssetKind,
type GameCreationAppCommandDescriptor,
type GameCreationAppLimitedRunCommandDescriptor,
type GameCreationAppManifest,
type GameCreationAppPermission,
type GameCreationAppPreviewState,
type GameCreationAppPreviewStatus,
parseGameCreationAppAssetKind,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD,
AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT,
AGENT_RUN_HISTORY_MAX_COUNT,
AGENT_RUN_HISTORY_VISIBLE_STEP,
CONVERSATION_INITIAL_VISIBLE_COUNT,
CONVERSATION_VISIBLE_STEP,
createLocalProjectId,
PROJECT_SUPERVISOR_AGENT_ID,
seedManifest,
} from './app/constants';
import { useEscapeToClose } from './app/dialogs';
import { resolveTauriInvoke } from './app/tauri';
import type {
AgentBackgroundSubmitMode,
AgentProgressEvent,
AgentRunControlResult,
AgentRunHistoryItem,
AgentRuntimeResponseStream,
AgentRuntimeResult,
AgentRuntimeState,
AgentRuntimeSteerResult,
AgentRuntimeUserInputRequest,
AgentStatusCard,
ChatMessage,
DesignAgentInput,
DesignClarificationRequest,
DesignEvent,
DesignView,
DirectTurnCancelView,
GameCreatorAgentRuntimeUpdateEvent,
GameCreatorChatAgentReply,
GameCreatorLlmConfigStatus,
GameCreatorManifestInvalidatedEvent,
GameCreatorRoleAgentChatStreamEvent,
GenerateLocalGameDraftResult,
ImportCanvasExportResult,
InitLocalProjectResult,
LauncherImportedAttachment,
LimitedLocalCommandResult,
ListLocalProjectFilesResult,
LocalAgentMemoryResult,
LocalConversationMessageRecord,
LocalConversationResult,
LocalGameMemoryResult,
LocalGameProjectRevisionStatus,
LocalPreviewResult,
LocalPreviewStatus,
LocalProjectCheckpointResult,
LocalProjectCheckpointSummary,
LocalProjectDiffResult,
LocalProjectExportPackageResult,
LocalProjectExportPackagesResult,
LocalProjectFileEntry,
LocalProjectFileMutationResult,
LocalProjectFileResult,
LocalProjectIndexResult,
LocalProjectKind,
LocalProjectRestoreResult,
MemoryScope,
MemoryWriteMode,
OpenCanvasProjectResult,
PendingCommand,
PendingUiConfirmation,
ProjectPermissionPolicy,
ProjectPermissionPolicyView,
SyncCanvasProjectAssetsResult,
TauriInvoke,
UploadLocalAssetResult,
} from './app/types';
import { useWindowChrome } from './components/windowChromeContext';
import {
agentConversationId,
agentRuntimeCancelStatus,
agentRuntimeConversationStatus,
agentRuntimeNeedsUserInput,
agentRuntimeStartedRunId,
agentRuntimeStartStatus,
agentRuntimeStateFromResult,
agentRuntimeSteerStatus,
conversationContainsProjectSupervisorResponseStream,
createAgentChatRunId,
createDefaultChatMessages,
createLocalConversationDraftMessage,
ensureProjectSupervisorActiveSessionId,
isAgentFinalizationMessageId,
isAgentRuntimeTerminalState,
isMissingAgentRuntimeResumeCommandError,
isRuntimeConfigMissingError,
matchingAgentRuntimeForSteer,
mergeAgentRuntimeStateIntoMap,
mergeProjectSupervisorConversation,
mergeProjectSupervisorResponseStream,
normalizeAgentRuntimeState,
projectNameFromPath,
projectProfessionalAgentLabel,
projectRuntimeVisibleError,
projectSupervisorPendingRepairMatchesProfessional,
readProjectSupervisorActiveSessionId,
resolveProjectSupervisorRuntimeSubmission,
sameAgentRuntimeRun,
submitProjectSupervisorRuntimeTask,
taskRowsFromManifest,
} from './features/agent-runtime';
import {
type DirectCodexTurnAttachment,
toDirectCodexTurnAttachments,
} from './features/app-shell/directCodexTurnAttachments';
import {
isDeveloperMode,
isTransientProjectOpenMessage,
latestVisibleItems,
persistSupervisorChatDraft,
type ProjectSupervisorComponentProps,
readInitialProjectPath,
readSupervisorChatDraft,
type WorkspaceLauncherProps,
writeRecentWorkspace,
} from './features/app-shell/model';
import { uploadLocalFilesAsAttachments } from './features/app-shell/useHomeProjectCreation';
import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
import {
agentConversationReadDraftsFromManifest,
agentMemoryReadDraftsFromManifest,
commandDraftFromSuggestedToolCall,
deriveAgentStatusCards,
formatAgentDialogLlmStatus,
formatAgentLlmConfigWarning,
formatAgentRunControlError,
formatCodexRuntimeCapabilities,
formatLlmAgentStatusLine,
formatLlmRouteEndpoint,
isMissingAgentRunTraceError,
projectAgentRuntimeSummaries,
readablePassArtifactsFromAgentRunTrace,
sameAgentStatusCard,
summarizeAgentAudit,
summarizeAgentCapabilities,
summarizeAgentConversationReadDrafts,
summarizeAgentLlmRoutes,
summarizeAgentMemoryReadDrafts,
summarizeAgentPassArtifactReadDrafts,
summarizeAgentRunCompletionForChat,
summarizeAgentRunHistoryReadDrafts,
summarizeAgentStatusCardsForChat,
summarizeLimitedLocalCommands,
summarizeRunArtifactReadDrafts,
} from './features/project-summary/agentPresentation';
import {
chatCommandHelp,
checkpointSummaryFromManifest,
commonAgentRunSupportReadDrafts,
commonProjectArtifactReadDrafts,
commonProjectInternalReadDrafts,
commonProjectLogReadDrafts,
firstReadableProjectAssetPath,
formatAgentRunStatus,
formatCanvasAssetSource,
isAbsoluteProjectPath,
isSafeProjectRelativePath,
missingChatCommandArgumentMessage,
projectFileActionDrafts,
projectPathHasControlCharacter,
projectPathsMatchForInvalidation,
readableArtifactsFromAgentRunTrace,
sortCheckpointManifestFiles,
summarizeAgentRunSupportFileReadDrafts,
summarizeCommonProjectArtifactReadDrafts,
summarizeCommonProjectLogReadDrafts,
summarizeMainProjectHeader,
summarizeProjectAssetCredits,
summarizeProjectAssets,
summarizeProjectAudioAssets,
summarizeProjectCheckpoint,
summarizeProjectCheckpoints,
summarizeProjectDiff,
summarizeProjectExportPackage,
summarizeProjectExportPackages,
summarizeProjectFileContent,
summarizeProjectFiles,
summarizeProjectIndex,
summarizeProjectInternalReadDrafts,
summarizeProjectPolicy,
summarizeProjectStatus,
summarizeProjectTasks,
summarizeProjectVisualAssets,
} from './features/project-summary/projectSummary';
import { AgentConversationOverlay } from './features/project-workspace/AgentConversationOverlay';
import {
gameDraftStartedMessage,
isMissingProjectFileError,
parseAgentRunTrace,
} from './features/project-workspace/agentRunTrace';
import {
chatQueueFullNotice,
createQueuedChatTurn,
dequeueChatTurn,
enqueueChatTurn,
isChatTurnQueueFull,
type QueuedChatTurn,
removeQueuedChatTurn,
} from './features/project-workspace/chatComposerQueue';
import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
import {
type DirectHistoryAnchorGate,
directHistoryAnchorGateToWaitFor,
reuseOrOpenDirectHistoryAnchorGate,
} from './features/project-workspace/directHistoryAnchorGate';
import { readDirectHistoryPages } from './features/project-workspace/directHistoryPaging';
import {
applyDirectThreadConsumeResult,
type DirectThreadChatState,
directThreadTurnMatchesUser,
emptyDirectThreadChatState,
finishDirectThreadTurn,
mergeDirectHistoryItems,
resolveDirectThreadBootstrap,
selectDirectChatEntries,
} from './features/project-workspace/directThreadChat';
import {
type DirectThreadConsumeResult,
type DirectThreadHistorySlice,
type DirectThreadItem,
type DirectThreadSubscriptionBootstrap,
} from './features/project-workspace/directThreadEvents';
import type { DirectCodexUserContentPart } from './features/project-workspace/generated';
import {
appendMemoryContent,
memoryScopeLabel,
memoryScopePath,
parseOptionalMemoryScope,
parseRememberInput,
} from './features/project-workspace/memoryCommands';
import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation';
import {
isAgentTraceFilePath,
isProjectPolicyConfirmableCommandId,
isRegisteredGameCreationCommandId,
isSafeCanvasProjectId,
isSafeCheckpointId,
needsInitializedChatProject,
resolveChatProjectPath,
resolvePendingCommandProjectPath,
} from './features/project-workspace/projectCommandPolicy';
import { handleProjectSummaryChatCommand } from './features/project-workspace/projectSummaryCommands';
import { ProjectSupervisorView } from './features/project-workspace/ProjectSupervisorView';
import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane';
import type { ResourceReferenceInputHandle } from './features/project-workspace/ResourceReferenceInput';
import type {
ChatComposerDraft,
ChatReference,
} from './features/project-workspace/resourceReferences';
import {
chatComposerDraftToDirectCodexUserItem,
RESOURCE_REFERENCE_INSERT_EVENT,
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
type ResourceReferenceInsertEventDetail,
type ResourceReferenceInsertManyEventDetail,
} from './features/project-workspace/resourceReferences';
import { SupervisorChatOnlyView } from './features/project-workspace/SupervisorChatOnlyView';
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
import { captureAgentRuntimeError } from './services/errorReporting';
import {
currentPlatformSessionGeneration,
requestPlatformSessionRefresh,
} from './services/platformSession';
import {
setAgcPluginProjectPath,
startAvailableAgcEditorPlugins,
} from './services/pluginHost';
import {
canSubscribeTauriEvents,
subscribeTauriEvent,
} from './services/tauriEventSubscription';
import type { HomeCreationType } from './view/home';
import {
type ProjectAgentResultSummary,
type ProjectAgentRuntimeSummary,
} from './view/project-development';
import type { ProjectManifestSnapshotMetadata } from './view/project-development/projectResourceLiveUpdateModel';
const initialSupervisorMessageClaimsByPage = new WeakMap<Window, Set<string>>();
const DIRECT_CODEX_PRODUCT_RUNTIME = true;
const DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX = 'direct-codex:';
const DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX =
'direct-codex-turn-already-running:';
/** 与 Rust 侧 `DirectTaonierActiveInvocationGuard::enter` 的 else 分支文案保持一致。 */
const DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER =
'当前项目已有另一条 Direct 客户端回合正在运行';
// Platform access tokens are short lived. DirectProject can spend several
// minutes in image generation, build and browser validation, so keep the
// client-owned native session current while a turn is running. The singleflight
// refresh in platformSession.ts coalesces this with any 401-triggered refresh.
const DIRECT_CODEX_SESSION_KEEPALIVE_MS = 5 * 60 * 1000;
function isDirectCodexAuthenticationRequired(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return (
message.includes('authentication-required') ||
message.includes('codex-app-server-error:unauthorized') ||
/kind=codex-app-server-unauthorized(?=\s|$)/.test(message) ||
message.includes('登录已失效')
);
}
async function withDirectCodexSessionRefresh<T>(operation: () => Promise<T>) {
const generation = currentPlatformSessionGeneration();
try {
return await operation();
} catch (error) {
if (!isDirectCodexAuthenticationRequired(error)) throw error;
if (currentPlatformSessionGeneration() !== generation) throw error;
const refresh = await requestPlatformSessionRefresh();
if (refresh.status === 'failed') throw error;
if (
refresh.status !== 'refreshed' ||
currentPlatformSessionGeneration() !== refresh.generation
) {
throw new Error('登录账号已变化,原对话请求已停止');
}
return operation();
}
}
function directCodexConversationMessageId(
turnId: string,
role: ChatMessage['role'],
) {
return `${DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX}${turnId}:${role}`;
}
export const MAX_CHAT_COMPOSER_ATTACHMENTS = 8;
export function isDirectCodexTurnAlreadyRunningError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return message
.trimStart()
.startsWith(DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX);
}
/**
* 另一条 Direct 回合占着这个项目时的拒绝。它与上面那条同 clientTurnId 的拒绝分属不同
* 错误分类(Rust 侧刻意不带前缀),但对界面是同一件事:本项目现在有一条我们没接管的
* 回合在跑。所以这里单独判定,让它也走"接管它 + 告诉用户出口"的处理。
*/
export function isDirectCodexAnotherTurnRunningError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return message.includes(DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER);
}
/**
* 用户点了"终止"以后,正在 await 的回合命令会带着 app-server 的中断原因返回
* (`Codex app-server turn 已中断`)。这类错误是用户主动取消,不是失败:界面要给
* "已终止本次回合"而不是把中断当作异常写进运行错误与诊断。
*/
export function isDirectCodexTurnInterruptedError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return message.includes('turn 已中断') || message.includes('已终止本次回合');
}
function isPersistableDirectCodexConversationMessage(message: ChatMessage) {
if (!message.runtimeOwned) {
return false;
}
const messageId = message.messageId?.trim() ?? '';
const roleSuffix = `:${message.role}`;
if (
!messageId.startsWith(DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX) ||
!messageId.endsWith(roleSuffix)
) {
return false;
}
const turnId = messageId.slice(
DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX.length,
-roleSuffix.length,
);
return /^[a-z0-9][a-z0-9-]{5,159}$/iu.test(turnId);
}
function claimInitialSupervisorMessageForPage(projectPath: string, scope = '') {
let claimedProjectPaths = initialSupervisorMessageClaimsByPage.get(window);
if (!claimedProjectPaths) {
claimedProjectPaths = new Set<string>();
initialSupervisorMessageClaimsByPage.set(window, claimedProjectPaths);
}
const claimKey = `${projectPath}\u0000${scope}`;
if (claimedProjectPaths.has(claimKey)) {
return false;
}
claimedProjectPaths.add(claimKey);
return true;
}
/**
* 历史回读与"尚未落盘的运行时消息"合并。
*
* 初始需求是**乐观插入**到 messages 的(latch 命中后先插一条 user 消息,再发起回合),
* 而历史回读在 replace 分支里是无条件整体替换 —— 只要回读晚于乐观插入,那条用户消息
* 就会被冲掉(界面上看不到初始需求,但回合其实已经跑起来了)。
* 这里把当前 messages 里"运行时拥有、且回读结果里没有"的消息保留在末尾(它们是最新的)。
*/
function mergeLoadedConversationWithPendingRuntimeMessages(
loaded: ChatMessage[],
current: ChatMessage[],
): ChatMessage[] {
if (current.length === 0) {
return loaded;
}
const loadedIds = new Set(
loaded
.map((message) => message.messageId)
.filter((id): id is string => Boolean(id)),
);
const loadedTexts = new Set(
loaded.map((message) => `${message.role}\u0000${message.text}`),
);
const pending = current.filter((message) => {
if (!message.runtimeOwned) {
return false;
}
if (message.messageId) {
return !loadedIds.has(message.messageId);
}
return !loadedTexts.has(`${message.role}\u0000${message.text}`);
});
return pending.length > 0 ? [...loaded, ...pending] : loaded;
}
export { AuthenticatedClient } from './app/AuthenticatedClient';
export type { PendingCommand } from './app/types';
export {
deriveAgentStatusCards,
summarizeAgentAudit,
summarizeAgentRunTrace,
} from './features/project-summary/agentPresentation';
export { isAbsoluteProjectPath } from './features/project-summary/projectSummary';
export {
needsInitializedChatProject,
parseRememberInput,
pendingCommandDetail,
resolveChatProjectPath,
resolvePendingCommandProjectPath,
};
export function WorkspaceLauncher(props: WorkspaceLauncherProps) {
return <WorkspaceLauncherShell {...props} ProjectSupervisor={App} />;
}
type AppProps = {
initialProjectPath?: string;
initialProjectManifest?: GameCreationAppManifest;
initialProjectKind?: LocalProjectKind;
orchestrationMode?: 'single-supervisor' | 'professional-dag';
projectSupervisorOnly?: boolean;
planningStartMode?: boolean;
activeVersionId?: ProjectSupervisorComponentProps['activeVersionId'];
supervisorChatOnly?: boolean;
initialSupervisorMessage?: string;
initialSupervisorMessageClaimScope?: string;
initialCreationType?: HomeCreationType | null;
initialAttachments?: LauncherImportedAttachment[];
playRequest?: ProjectSupervisorComponentProps['playRequest'];
onPlayRequestHandled?: ProjectSupervisorComponentProps['onPlayRequestHandled'];
onManifestChange?: (
projectPath: string,
manifest: GameCreationAppManifest,
metadata?: ProjectManifestSnapshotMetadata,
) => void;
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
onAgentRuntimeSummariesChange?: (
summaries: ProjectAgentRuntimeSummary[],
) => void;
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
};
type ExecuteChatAgentReplyInput = {
prompt: string;
clientTurnId?: string;
creationType?: HomeCreationType | null;
attachments?: DirectCodexTurnAttachment[];
directPolicyChecked?: boolean;
references?: ChatReference[];
userItem?: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
};
/**
* `conversation.write` 策略确认后重跑同一轮直连回合的入参。
*
* 确认弹窗会在**同一轮输入**上二次进入 `executeChatAgentReply`。这里从首轮的入参整体派生,
* 而不是手写一遍字段:一旦重跑时漏掉某一项(历史缺陷就是漏了 `references`),
* 用户在确认之后拿到的就不是他原本提交的那一轮——`@` 引用会被静默丢掉。
*/
export function directCodexPolicyRetryInput(
input: ExecuteChatAgentReplyInput,
): ExecuteChatAgentReplyInput {
return { ...input, directPolicyChecked: true };
}
export function App({
initialProjectPath: initialProjectPathOverride = '',
initialProjectManifest,
initialProjectKind = 'web',
orchestrationMode = 'professional-dag',
projectSupervisorOnly = false,
planningStartMode = false,
activeVersionId = null,
supervisorChatOnly = false,
initialSupervisorMessage = '',
initialSupervisorMessageClaimScope = '',
initialCreationType = null,
initialAttachments = [],
playRequest = null,
onPlayRequestHandled,
onManifestChange,
onPreviewChange,
onAgentRuntimeSummariesChange,
onAgentResultsChange,
}: AppProps = {}) {
const { setTitle: setWindowTitle } = useWindowChrome();
const [designAgentActive, setDesignAgentActive] = useState(planningStartMode);
const designAgentActiveRef = useRef(planningStartMode);
designAgentActiveRef.current = designAgentActive;
const [designAgentView, setDesignAgentView] = useState<DesignView | null>(
null,
);
const designAgentTurnRef = useRef<{
projectPath: string;
clientTurnId: string;
} | null>(null);
const designAgentEventSubscriptionReadyRef = useRef<Promise<void> | null>(
null,
);
const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>(
null,
);
const directCodexProductRuntime =
DIRECT_CODEX_PRODUCT_RUNTIME &&
projectSupervisorOnly &&
!supervisorChatOnly &&
!planningStartMode &&
!designAgentActive;
const [devMode] = useState(() =>
projectSupervisorOnly ? false : isDeveloperMode(),
);
const [initialProjectPath] = useState(
() => initialProjectPathOverride || readInitialProjectPath(),
);
const eagerSupervisorProject =
projectSupervisorOnly && Boolean(initialProjectPath);
const [projectPath, setProjectPath] = useState(initialProjectPath);
const [workspaceProjectKind, setWorkspaceProjectKind] =
useState<LocalProjectKind>(initialProjectKind);
const [localProject, setLocalProject] =
useState<InitLocalProjectResult | null>(() =>
eagerSupervisorProject
? {
projectPath: initialProjectPath,
manifestPath: `${initialProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`,
manifest: initialProjectManifest ?? seedManifest,
}
: null,
);
const localProjectPathRef = useRef<string | null>(null);
useEffect(() => {
if (supervisorChatOnly) return;
const nextProjectPath = localProject?.projectPath ?? null;
const previousProjectPath = localProjectPathRef.current;
localProjectPathRef.current = nextProjectPath;
// 未绑定项目时无需触发插件宿主;这也避免启动空首页时产生无意义的 Tauri 调用。
if (!nextProjectPath && !previousProjectPath) return;
let active = true;
void setAgcPluginProjectPath(nextProjectPath)
.then(async () => {
if (active && nextProjectPath) {
await startAvailableAgcEditorPlugins(() => active);
}
})
.catch((error) => {
if (!active || !nextProjectPath) {
return;
}
setWorkspaceStatus(
`编辑器插件未就绪:${
error instanceof Error ? error.message : String(error)
}`,
);
});
return () => {
active = false;
if (nextProjectPath || previousProjectPath) {
void setAgcPluginProjectPath(null).catch(() => undefined);
}
localProjectPathRef.current = null;
};
}, [localProject?.projectPath, supervisorChatOnly]);
const manifestRefreshMountedRef = useRef(true);
const manifestRefreshStatesRef = useRef(
new Map<
string,
{
pending: boolean;
inFlight: Promise<void> | null;
}
>(),
);
const [manifest, setManifest] = useState<GameCreationAppManifest>(
initialProjectManifest ?? seedManifest,
);
const manifestRef = useRef(manifest);
manifestRef.current = manifest;
/**
* 跟随工作台壳的清单快照。
*
* 资源命令(改标签 / 改类型 / 重命名 / 删素材)由资源画布写入,写入后壳重读一次
* manifest、按 CAS 归并进 `currentProjectContext`,再把归并结果继续以这个 prop 传下来。
* 但它是 `useState` 的**初值**:壳里换了新清单不会再进来,于是聊天侧(`@` 选择器的
* 标签统计与候选、`@` 候选菜单)一直用挂载时那份旧清单 —— 用户改完标签,画布已经
* 更新,聊天侧却还是旧标签。
*
* 这里把壳那份快照补进聊天侧状态,判据只有两条:同一项目、内容确实变了
* (按内容的短路是必要的:聊天侧自己的写入会被壳原样回传,只比身份会让两边
* 无意义地互相推一轮)。壳那份快照始终由磁盘重读 + CAS 归并得到,因此不会把
* 聊天侧带到更旧的版本上。
*/
useEffect(() => {
const snapshot = initialProjectManifest;
const current = manifestRef.current;
if (!snapshot || snapshot === current) return;
if (snapshot.projectId !== current.projectId) return;
if (JSON.stringify(snapshot) === JSON.stringify(current)) return;
setManifest(snapshot);
}, [initialProjectManifest]);
const [projectStatus, setProjectStatus] = useState(
eagerSupervisorProject ? '已初始化' : '未初始化',
);
const [preview, setPreview] = useState<LocalPreviewResult | null>(null);
const [previewStatus, setPreviewStatus] = useState('未启动');
const initialSupervisorMessageLatchRef = useRef({
projectPath: initialProjectPath,
prompt: initialSupervisorMessage.trim(),
claimScope: initialSupervisorMessageClaimScope,
creationType: initialCreationType,
attachments: toDirectCodexTurnAttachments(initialAttachments),
});
const handledPlayRequestRef = useRef<string | null>(null);
function updateClientPreview(
nextPreview: LocalPreviewResult | null,
status: GameCreationAppPreviewStatus = nextPreview ? 'running' : 'stopped',
) {
setPreview(nextPreview);
onPreviewChange?.(
nextPreview
? {
status,
url: nextPreview.url,
port: nextPreview.port,
}
: { status },
);
}
const [chatInput, setChatInput] = useState(() =>
supervisorChatOnly && initialProjectPath
? readSupervisorChatDraft(initialProjectPath)
: '',
);
const [chatReferences, setChatReferences] = useState<ChatReference[]>([]);
/**
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
* 交给 `chat_with_game_creator_direct_codex` 的 `attachments`。附件只存在于前端状态,
* 提交后即清空——后端协议不变。
*/
const [chatAttachments, setChatAttachments] = useState<
DirectCodexTurnAttachment[]
>([]);
const [chatAttachmentNotice, setChatAttachmentNotice] = useState('');
/** 回合运行中再次发送的消息:FIFO 本地队列,当前回合结束后依次发出。 */
const [chatTurnQueue, setChatTurnQueue] = useState<QueuedChatTurn[]>([]);
const chatTurnQueueRef = useRef<QueuedChatTurn[]>([]);
chatTurnQueueRef.current = chatTurnQueue;
const [chatComposerNotice, setChatComposerNotice] = useState('');
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
useState(false);
const queuedChatTurnSequenceRef = useRef(0);
const [chatContent, setChatContent] = useState<DirectCodexUserContentPart[]>(
[],
);
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
/**
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
* 队列也属于刚结束的那条对话;留着会把 A 项目的附件路径带进 B 项目的下一个回合。
*/
useEffect(() => {
setChatAttachments([]);
setChatContent([]);
setChatAttachmentNotice('');
setChatComposerNotice('');
setChatTurnQueue([]);
chatTurnQueueRef.current = [];
}, [localProject?.projectPath]);
const [chatAgentBusy, setChatAgentBusy] = useState(false);
const directCodexConversationTurnSequenceRef = useRef(0);
/**
* DirectProject 聊天真相源:历史切片与运行态事件归并成同一份条目。
*
* 运行态只来自 Thread Manager(`subscribe` 的 bootstrap + `notify` 唤醒的 `consume`);
* 前端不再订阅 DirectRuntime 的回合进度事件,也不再各存一份回合流 / 工具卡片。
*/
const [directThreadChat, setDirectThreadChat] =
useState<DirectThreadChatState>(() => emptyDirectThreadChatState());
/** 最新回合是否在跑:历史里留下的半截回合一律按已结束渲染。 */
const directTurnRunning = directThreadChat.turnRunning;
/** 界面忙碌判定:本地正在跑这次 invoke,或订阅告诉还有一条回合没结束。 */
const supervisorChatBusy =
chatAgentBusy || (directCodexProductRuntime && directTurnRunning);
const chatAgentBusyRef = useRef(chatAgentBusy);
const directTurnRunningRef = useRef(directTurnRunning);
chatAgentBusyRef.current = chatAgentBusy;
directTurnRunningRef.current = directTurnRunning;
const previousDirectTurnRunningRef = useRef(directTurnRunning);
const directTurnCompletionPendingRef = useRef(false);
const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState<
string | null
>(null);
function createDirectCodexConversationTurnId() {
directCodexConversationTurnSequenceRef.current += 1;
let randomId = '';
try {
randomId = globalThis.crypto?.randomUUID?.().trim() ?? '';
} catch {
// The timestamp and in-page sequence remain unique enough for a local
// conversation append when WebView crypto is unavailable.
}
if (/^[a-z0-9][a-z0-9-]{5,159}$/iu.test(randomId)) {
return randomId;
}
return `${Date.now().toString(36)}-${directCodexConversationTurnSequenceRef.current.toString(36)}`;
}
/** 换项目 / 清空对话:聊天条目回到初始态(运行态与已加载历史一起清掉)。 */
function resetDirectThreadChat() {
setDirectThreadChat(emptyDirectThreadChatState());
}
const [projectSupervisorRuntime, setProjectSupervisorRuntime] =
useState<AgentRuntimeState | null>(null);
const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] =
useState<AgentRuntimeResponseStream | null>(null);
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
useState('');
const [designAgentTransientReply, setDesignAgentTransientReplyVisible] =
useState('');
const designAgentTransientReplyTargetRef = useRef('');
const designAgentVisibleReplyRef = useRef('');
const designAgentPendingViewRef = useRef<{
clientTurnId: string;
projectPath: string;
view: DesignView;
} | null>(null);
const [designAgentReasoning, setDesignAgentReasoning] = useState('');
const designAgentReasoningTurnRef = useRef<{
projectPath: string;
clientTurnId: string;
text: string;
} | null>(null);
function setDesignAgentTransientReplyTarget(next: string) {
designAgentTransientReplyTargetRef.current = next;
if (!next) {
designAgentVisibleReplyRef.current = '';
setDesignAgentTransientReplyVisible('');
}
}
function designAgentEventSubscriptionReady() {
// 业务动作只读取当前订阅代次;清理与下一次 effect 建立之间不能创建
// 一个没有订阅 effect 接管的悬挂 promise。
return designAgentEventSubscriptionReadyRef.current ?? Promise.resolve();
}
function createDesignAgentEventSubscriptionReady() {
if (!designAgentEventSubscriptionReadyRef.current) {
designAgentEventSubscriptionReadyRef.current = new Promise<void>(
(resolve) => {
designAgentEventSubscriptionResolveRef.current = resolve;
},
);
}
return designAgentEventSubscriptionReadyRef.current;
}
function resolveDesignAgentEventSubscriptionReady() {
designAgentEventSubscriptionResolveRef.current?.();
designAgentEventSubscriptionResolveRef.current = null;
}
useEffect(() => {
const timer = window.setInterval(() => {
const target = designAgentTransientReplyTargetRef.current;
setDesignAgentTransientReplyVisible((current) => {
if (!target) {
designAgentVisibleReplyRef.current = '';
return '';
}
const prefix = target.startsWith(current) ? current : '';
if (prefix === target) {
designAgentVisibleReplyRef.current = target;
return target;
}
const remaining = target.length - prefix.length;
const step = remaining > 160 ? 4 : remaining > 48 ? 2 : 1;
const next = target.slice(0, prefix.length + step);
designAgentVisibleReplyRef.current = next;
return next;
});
}, 50);
return () => window.clearInterval(timer);
}, []);
function designMessagesToChat(view: DesignView): ChatMessage[] {
const reasoningByMessageId = new Map<string, string[]>();
for (const entry of view.reasoningEntries ?? []) {
if (!entry.messageId) {
continue;
}
const texts = reasoningByMessageId.get(entry.messageId) ?? [];
texts.push(entry.text);
reasoningByMessageId.set(entry.messageId, texts);
}
const messages: ChatMessage[] = view.messages
.filter((message) => message.text.trim())
.map((message) => ({
role: message.role === 'user' ? 'user' : 'assistant',
text: message.text,
runtimeOwned: true,
messageId: message.id,
reasoningText: reasoningByMessageId.get(message.id)?.join('\n\n'),
updatedAt: Date.now(),
}));
const initialPrompt = initialSupervisorMessageLatchRef.current.prompt;
if (
initialPrompt &&
!messages.some(
(message) => message.role === 'user' && message.text === initialPrompt,
)
) {
messages.unshift({
role: 'user',
text: initialPrompt,
runtimeOwned: true,
updatedAt: Date.now(),
});
}
return messages;
}
function applyDesignView(view: DesignView, projectPath: string) {
designAgentActiveRef.current = true;
setDesignAgentActive(true);
setDesignAgentView(view);
setProjectSupervisorRuntimeError(view.session.lastError ?? '');
setChatAgentBusy(view.running);
const conversation = designMessagesToChat(view);
setMessages(conversation);
savedConversationProjectPathRef.current = projectPath;
savedConversationCountRef.current = conversation.length;
latestMessagesRef.current = conversation;
}
function commitDesignAgentView(view: DesignView, projectPath: string) {
const pendingTurnId = designAgentPendingViewRef.current?.clientTurnId;
designAgentPendingViewRef.current = null;
applyDesignView(view, projectPath);
setDesignAgentReasoning('');
setDesignAgentTransientReplyTarget('');
if (designAgentTurnRef.current?.clientTurnId === pendingTurnId) {
designAgentTurnRef.current = null;
}
}
function applyDesignAgentViewAfterTransient(
view: DesignView,
projectPath: string,
clientTurnId: string,
) {
let target = designAgentTransientReplyTargetRef.current;
const tracked = designAgentTurnRef.current;
if (!target.trim() && !view.running) {
const latestAssistantText = [...view.messages]
.reverse()
.find((message) => message.role !== 'user' && message.text.trim())
?.text.trim();
if (latestAssistantText) {
setDesignAgentTransientReplyTarget(latestAssistantText);
target = latestAssistantText;
}
}
if (
!view.running &&
tracked?.clientTurnId === clientTurnId &&
target.trim() &&
designAgentVisibleReplyRef.current !== target
) {
designAgentPendingViewRef.current = {
clientTurnId,
projectPath,
view,
};
return;
}
commitDesignAgentView(view, projectPath);
}
useEffect(() => {
const timer = window.setInterval(() => {
const pending = designAgentPendingViewRef.current;
if (!pending) {
return;
}
const target = designAgentTransientReplyTargetRef.current;
if (target && designAgentVisibleReplyRef.current !== target) {
return;
}
commitDesignAgentView(pending.view, pending.projectPath);
}, 50);
return () => window.clearInterval(timer);
// 收尾定时器只需注册一次;它读取 refs,避免随每次渲染重建。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function hydrateDesignAgentSession(nextProjectPath: string) {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath.trim()) {
return null;
}
const view = await invoke<DesignView | null>(
'hydrate_design_agent_session',
{ projectPath: nextProjectPath },
);
if (localProjectPathRef.current !== nextProjectPath) {
return null;
}
if (!view) {
return null;
}
applyDesignView(view, nextProjectPath);
return view;
}
async function executeDesignAgentTurn(
nextProjectPath: string,
input: DesignAgentInput,
clientTurnId = createAgentChatRunId('design-agent-turn'),
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectSupervisorRuntimeError('需要在 Tauri App 内运行。');
return;
}
designAgentTurnRef.current = {
projectPath: nextProjectPath,
clientTurnId,
};
designAgentReasoningTurnRef.current = {
projectPath: nextProjectPath,
clientTurnId,
text: '',
};
designAgentPendingViewRef.current = null;
await designAgentEventSubscriptionReady();
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setDesignAgentTransientReplyTarget('');
setDesignAgentReasoning('');
try {
const view = await invoke<DesignView>('continue_design_agent_session', {
projectPath: nextProjectPath,
clientTurnId,
input,
});
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
applyDesignAgentViewAfterTransient(view, nextProjectPath, clientTurnId);
} catch (error) {
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
} finally {
if (!designAgentPendingViewRef.current) {
designAgentTurnRef.current = null;
setDesignAgentTransientReplyTarget('');
}
setChatAgentBusy(false);
}
}
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
useState<string | null>(null);
const chatInputRef = useRef<HTMLDivElement | null>(null);
const supervisorChatMessagesRef = useRef<HTMLDivElement | null>(null);
const supervisorChatShouldFollowLatestRef = useRef(true);
const [assetStatus, setAssetStatus] = useState('未上传');
const [uploadedAssets, setUploadedAssets] = useState<
UploadLocalAssetResult[]
>([]);
const [assetLocalPath, setAssetLocalPath] = useState('assets/hero.png');
const [assetKind, setAssetKind] = useState('unknown');
const [assetMediaType, setAssetMediaType] = useState(
'application/octet-stream',
);
const [assetSourceKind, setAssetSourceKind] = useState('generated');
const [assetCanvasProjectId, setAssetCanvasProjectId] = useState('');
const [assetResourceId, setAssetResourceId] = useState('');
const [assetObjectId, setAssetObjectId] = useState('');
const [assetGenerationPrompt, setAssetGenerationPrompt] =
useState('首版核心美术素材');
const [canvasExportPath, setCanvasExportPath] = useState(
'/tmp/canvas-export.zip',
);
const [editorBaseUrl, setEditorBaseUrl] = useState('http://127.0.0.1:3000');
const [memoryScope, setMemoryScope] = useState<MemoryScope>('long');
const [memoryDraft, setMemoryDraft] = useState('');
const [memoryStatus, setMemoryStatus] = useState('未读取');
const [limitedCommandStatus, setLimitedCommandStatus] = useState('未运行');
const [limitedLocalCommands, setLimitedLocalCommands] = useState<
GameCreationAppLimitedRunCommandDescriptor[]
>([...GAME_CREATION_APP_LIMITED_RUN_COMMANDS]);
const [projectFiles, setProjectFiles] = useState<LocalProjectFileEntry[]>([]);
const [projectCheckpoints, setProjectCheckpoints] = useState<
LocalProjectCheckpointSummary[]
>([]);
const [filePath, setFilePath] = useState('game/index.html');
const [fileDraft, setFileDraft] = useState('');
const [fileStatus, setFileStatus] = useState('未读取');
const [agentRunTrace, setAgentRunTrace] =
useState<GameCreationAgentRunTrace | null>(null);
const [agentRunHistory, setAgentRunHistory] = useState<AgentRunHistoryItem[]>(
[],
);
const [agentRunHistoryFiles, setAgentRunHistoryFiles] = useState<
LocalProjectFileEntry[]
>([]);
const [agentRunHistoryVisibleCount, setAgentRunHistoryVisibleCount] =
useState(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
const [agentRunHistoryOverflowCount, setAgentRunHistoryOverflowCount] =
useState(0);
const [agentRunHistoryLoadingMore, setAgentRunHistoryLoadingMore] =
useState(false);
const [agentRunStatus, setAgentRunStatus] = useState('未运行');
const [agentRuntimeById, setAgentRuntimeById] = useState<
Record<string, AgentRuntimeState | undefined>
>({});
const agentRuntimeByIdRef = useRef(agentRuntimeById);
agentRuntimeByIdRef.current = agentRuntimeById;
const [professionalAgentResultsById, setProfessionalAgentResultsById] =
useState<Record<string, ProjectAgentResultSummary>>({});
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
const [llmConfigStatus, setLlmConfigStatus] =
useState<GameCreatorLlmConfigStatus | null>(null);
const [workspaceStatus, setWorkspaceStatus] = useState(
eagerSupervisorProject ? `已打开:${initialProjectPath}` : '请选择工作区',
);
const [selectedAgent, setSelectedAgent] = useState<AgentStatusCard | null>(
null,
);
const selectedAgentIdRef = useRef<string | null>(null);
selectedAgentIdRef.current = selectedAgent?.id ?? null;
const [agentConversationInput, setAgentConversationInput] = useState('');
const [agentConversationStatus, setAgentConversationStatus] =
useState('未选择 agent');
const [agentConversationMessages, setAgentConversationMessages] = useState<
LocalConversationMessageRecord[]
>([]);
const [agentConversationRuntime, setAgentConversationRuntime] =
useState<AgentRuntimeState | null>(null);
const [agentConversationSessionId, setAgentConversationSessionId] = useState<
string | null
>(null);
const [agentConversationRunSubmitMode, setAgentConversationRunSubmitMode] =
useState<AgentBackgroundSubmitMode>('steer');
const [agentConversationRuntimeError, setAgentConversationRuntimeError] =
useState('');
const [agentConversationVisibleCount, setAgentConversationVisibleCount] =
useState(CONVERSATION_INITIAL_VISIBLE_COUNT);
const [agentConversationSaving, setAgentConversationSaving] = useState(false);
const [agentConversationBackgroundBusy, setAgentConversationBackgroundBusy] =
useState(false);
const [agentMemoryStatus, setAgentMemoryStatus] = useState('未读取');
const [agentMemoryContent, setAgentMemoryContent] = useState('');
const [messages, setMessages] = useState<ChatMessage[]>(
createDefaultChatMessages,
);
const [conversationVisibleCount, setConversationVisibleCount] = useState(
CONVERSATION_INITIAL_VISIBLE_COUNT,
);
const [directHistoryHasMore, setDirectHistoryHasMore] = useState(false);
const directHistoryOldestItemIdRef = useRef<string | null>(null);
const directHistoryLoadingRef = useRef(false);
const directHistoryAnchorGateRef = useRef<DirectHistoryAnchorGate | null>(
null,
);
const [pendingCommand, setPendingCommand] = useState<PendingCommand | null>(
null,
);
const [pendingUiConfirmation, setPendingUiConfirmation] =
useState<PendingUiConfirmation | null>(null);
const [pendingNonEmptyProjectCreate, setPendingNonEmptyProjectCreate] =
useState<{
projectPath: string;
announceToChat: boolean;
} | null>(null);
const [commandLog, setCommandLog] = useState<string[]>([
`${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`,
]);
const [projectLogStatus, setProjectLogStatus] = useState('未读取');
const [projectLogContent, setProjectLogContent] = useState('');
const [conversationWriteVersion, setConversationWriteVersion] = useState(0);
const savedConversationCountRef = useRef(
eagerSupervisorProject ? createDefaultChatMessages().length : 0,
);
const savedConversationProjectPathRef = useRef<string | null>(
eagerSupervisorProject ? initialProjectPath : null,
);
const projectConversationWriteConfirmedRef = useRef<string | null>(null);
const projectConversationWriteCancelledRef = useRef<{
projectPath: string;
messageCount: number;
} | null>(null);
const latestMessagesRef = useRef<ChatMessage[]>([]);
const conversationWriteInFlightRef = useRef(false);
const projectScopeVersionRef = useRef(0);
const projectSupervisorHistoryLoadVersionRef = useRef(0);
const projectSupervisorRuntimeResumeProjectPathRef = useRef<string | null>(
null,
);
const projectSupervisorSessionIdRef = useRef<string | null>(null);
projectSupervisorSessionIdRef.current = projectSupervisorSessionId;
const projectSupervisorRuntimeRef = useRef<AgentRuntimeState | null>(null);
projectSupervisorRuntimeRef.current = projectSupervisorRuntime;
const projectSupervisorExpectedRunIdRef = useRef<string | null>(null);
projectSupervisorExpectedRunIdRef.current = projectSupervisorExpectedRunId;
const projectSupervisorResponseStreamRef =
useRef<AgentRuntimeResponseStream | null>(null);
projectSupervisorResponseStreamRef.current = projectSupervisorResponseStream;
const refreshManifest = useCallback(
(nextProjectPath = localProjectPathRef.current ?? ''): Promise<void> => {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
return Promise.resolve();
}
const refreshStates = manifestRefreshStatesRef.current;
let refreshState = refreshStates.get(nextProjectPath);
if (!refreshState) {
refreshState = { pending: false, inFlight: null };
refreshStates.set(nextProjectPath, refreshState);
}
refreshState.pending = true;
if (refreshState.inFlight) {
return refreshState.inFlight;
}
const activeRefreshState = refreshState;
const refreshPromise = (async () => {
try {
while (activeRefreshState.pending) {
activeRefreshState.pending = false;
const projectScopeVersion = projectScopeVersionRef.current;
try {
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath },
);
if (
manifestRefreshMountedRef.current &&
localProjectPathRef.current === nextProjectPath &&
projectScopeVersionRef.current === projectScopeVersion
) {
setManifest(nextManifest);
}
} catch {
// Dev-only convenience; command errors are surfaced by the action that triggered them.
}
if (
!manifestRefreshMountedRef.current ||
localProjectPathRef.current !== nextProjectPath ||
projectScopeVersionRef.current !== projectScopeVersion
) {
activeRefreshState.pending = false;
}
}
} finally {
activeRefreshState.inFlight = null;
if (!activeRefreshState.pending) {
refreshStates.delete(nextProjectPath);
}
}
})();
activeRefreshState.inFlight = refreshPromise;
return refreshPromise;
},
[],
);
useEffect(() => {
const refreshStates = manifestRefreshStatesRef.current;
manifestRefreshMountedRef.current = true;
return () => {
manifestRefreshMountedRef.current = false;
refreshStates.clear();
};
}, []);
const projectSupervisorRuntimeSyncingRef = useRef(new Set<string>());
const projectSupervisorRefreshConversationRef = useRef<
| ((
invoke: TauriInvoke,
projectPath: string,
sessionId: string,
) => Promise<void>)
| null
>(null);
const executeChatAgentReplyRef = useRef<
(input: ExecuteChatAgentReplyInput) => Promise<void>
>(async () => undefined);
const agentConversationSavingRef = useRef(false);
const agentConversationBackgroundBusyRef = useRef(false);
const agentConversationLoadVersionRef = useRef(0);
const agentConversationSessionIdRef = useRef<string | null>(null);
agentConversationSessionIdRef.current = agentConversationSessionId;
const agentRuntimeResumeProjectPathRef = useRef<string | null>(null);
const agentRunHistoryLoadingMoreRef = useRef(false);
const initialProjectOpenedRef = useRef(false);
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
const queueRunLocalShortcutRef = useRef<() => void>(() => undefined);
const executeRunLocalRef = useRef<(announceToChat: boolean) => void>(
() => undefined,
);
function requestRuntimeConfigOpen() {
if (projectSupervisorOnly && !supervisorChatOnly) {
return;
}
setRuntimeConfigOpen(true);
}
const updateProjectSupervisorRuntime = useCallback(
(
runtime: AgentRuntimeState | null,
previous = projectSupervisorRuntimeRef.current,
) => {
const nextRuntime = runtime
? normalizeAgentRuntimeState(runtime, previous)
: null;
projectSupervisorRuntimeRef.current = nextRuntime;
setProjectSupervisorRuntime(nextRuntime);
},
[],
);
const updateProjectSupervisorResponseStream = useCallback(
(
incoming: AgentRuntimeResponseStream | null | undefined,
runtime: AgentRuntimeState,
) => {
let nextStream = mergeProjectSupervisorResponseStream(
projectSupervisorResponseStreamRef.current,
incoming,
runtime,
);
const candidateStream = nextStream;
if (
candidateStream &&
latestMessagesRef.current.some(
(message) =>
message.runtimeOwned &&
message.role === 'assistant' &&
message.text === candidateStream.accumulatedText &&
typeof message.updatedAt === 'number' &&
message.updatedAt >= candidateStream.startedAt,
)
) {
// Keep the existing supervisor-chat behavior for restored history.
nextStream = null;
}
projectSupervisorResponseStreamRef.current = nextStream;
setProjectSupervisorResponseStream(nextStream);
},
[],
);
function resetProjectSupervisorState() {
projectSupervisorHistoryLoadVersionRef.current += 1;
projectSupervisorRuntimeResumeProjectPathRef.current = null;
projectSupervisorSessionIdRef.current = null;
projectSupervisorRuntimeRef.current = null;
projectSupervisorExpectedRunIdRef.current = null;
projectSupervisorResponseStreamRef.current = null;
projectSupervisorRuntimeSyncingRef.current.clear();
setProjectSupervisorSessionId(null);
setProjectSupervisorRuntime(null);
setProjectSupervisorExpectedRunId(null);
setProjectSupervisorResponseStream(null);
setProjectSupervisorRuntimeError('');
designAgentPendingViewRef.current = null;
designAgentReasoningTurnRef.current = null;
setDesignAgentReasoning('');
setDesignAgentActive(planningStartMode);
designAgentActiveRef.current = planningStartMode;
designAgentTurnRef.current = null;
setDesignAgentView(null);
}
function syncTerminalProjectSupervisorConversation(
invoke: TauriInvoke,
projectPath: string,
runtime: AgentRuntimeState,
) {
if (!isAgentRuntimeTerminalState(runtime) || !runtime.sessionId) {
return;
}
const syncKey = `${projectPath}\n${runtime.sessionId}\n${runtime.runId}`;
const syncAlreadyStarted =
projectSupervisorRuntimeSyncingRef.current.has(syncKey);
if (syncAlreadyStarted) {
return;
}
const refreshConversation = projectSupervisorRefreshConversationRef.current;
if (!refreshConversation) {
return;
}
projectSupervisorRuntimeSyncingRef.current.add(syncKey);
void refreshConversation(invoke, projectPath, runtime.sessionId)
.then(() => {})
.catch((error) => {
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
if (
localProjectPathRef.current === projectPath &&
projectSupervisorSessionIdRef.current === runtime.sessionId
) {
setProjectSupervisorRuntimeError(
`项目总控 Agent 对话刷新失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
});
}
useEscapeToClose(closeAgentConversation, selectedAgent !== null);
useEscapeToClose(cancelUiCommandConfirmation, pendingUiConfirmation !== null);
useEscapeToClose(
cancelProjectCreateInNonEmptyFolder,
pendingNonEmptyProjectCreate !== null,
);
useEffect(() => {
if (
pendingUiConfirmation?.projectPath &&
pendingUiConfirmation.projectPath !==
resolveCurrentUiConfirmationProjectPath()
) {
cancelUiCommandConfirmation();
}
// The callbacks intentionally read the latest project refs.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localProject?.projectPath, projectPath]);
useEffect(() => {
if (!initialProjectPath || initialProjectOpenedRef.current) {
return;
}
initialProjectOpenedRef.current = true;
if (projectSupervisorOnly) {
if (directCodexProductRuntime) {
if (!isAbsoluteProjectPath(initialProjectPath)) {
setWorkspaceStatus('请提供工作区绝对路径');
return;
}
if (projectPathHasControlCharacter(initialProjectPath)) {
setWorkspaceStatus('工作区路径不能包含控制字符');
return;
}
// This component is mounted inside the already-created project
// workbench. Hydrate localProject before the first direct turn so the
// direct runtime cannot silently create a second workspace.
void openWorkspace(
initialProjectPath,
false,
'open',
initialProjectKind,
);
return;
}
if (!isAbsoluteProjectPath(initialProjectPath)) {
setWorkspaceStatus('请提供工作区绝对路径');
return;
}
if (projectPathHasControlCharacter(initialProjectPath)) {
setWorkspaceStatus('工作区路径不能包含控制字符');
return;
}
localProjectPathRef.current = initialProjectPath;
void loadProjectConversation(initialProjectPath).finally(() => {
if (
localProjectPathRef.current === initialProjectPath &&
!planningStartMode
) {
void refreshAgentRunTrace(initialProjectPath);
}
});
return;
}
void openWorkspace(
initialProjectPath,
false,
initialProjectKind !== 'web' ? 'open' : 'create',
initialProjectKind,
);
// Initial project opening is guarded by initialProjectOpenedRef.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialProjectPath, projectSupervisorOnly]);
useEffect(() => {
if (!directCodexProductRuntime || !supervisorChatBusy) {
return;
}
const timer = window.setInterval(() => {
void requestPlatformSessionRefresh().catch(() => {
// The active DirectProject turn will surface the original auth error;
// keepalive must not replace it with an unrelated background error.
});
}, DIRECT_CODEX_SESSION_KEEPALIVE_MS);
return () => window.clearInterval(timer);
}, [supervisorChatBusy, directCodexProductRuntime]);
/**
* DirectProject 聊天真相源。
*
* `subscribe` 的 bootstrap 就是此刻要处理的事件(游标已在队尾),此后只由
* `game-creator-direct-thread-notify` 唤醒 `consume`;前端不再订阅 DirectRuntime 的回合
* 进度事件,也不轮询"有没有回合在跑"。
*/
useEffect(() => {
if (!directCodexProductRuntime) {
return;
}
const projectPath = localProject?.projectPath ?? null;
const directInvoke = resolveTauriInvoke();
// 闸门先建(或复用首屏读取已经开好的那道):同一轮渲染里首屏会等订阅回执里的锚点。
const anchorGate = reuseOrOpenDirectHistoryAnchorGate(
directHistoryAnchorGateRef.current,
projectPath ?? '',
);
directHistoryAnchorGateRef.current = anchorGate;
if (!projectPath || !directInvoke || !canSubscribeTauriEvents()) {
// 订阅不可用:首屏退化成"取文件尾",不阻塞加载。
anchorGate.settle(null);
return;
}
let disposed = false;
let cleanup: (() => void) | null = null;
let subscriptionId: string | null = null;
let consuming = false;
let consumeAgain = false;
// 通知先于 subscribe 回执到达时,前端还不知道自己的 subscriptionId,没法立刻 consume。
// 记一笔欠账,拿到回执后立刻补一次,事件就不会卡在队伍里等下一次通知。
let notifyBeforeSubscription = false;
const bootstrap = async () => {
const result = await directInvoke<DirectThreadSubscriptionBootstrap>(
'subscribe_direct_project_thread',
{ projectPath },
);
if (disposed) {
anchorGate.settle(null);
return;
}
subscriptionId = result.subscriptionId;
// 首屏边界:回执里这一刻的最后一条已完成条目(含该条)。
anchorGate.settle(result.lastCompletedItemId ?? null);
setDirectThreadChat((state) =>
resolveDirectThreadBootstrap(state, result),
);
if (notifyBeforeSubscription) {
notifyBeforeSubscription = false;
void consume();
}
};
const consume = async () => {
if (!subscriptionId || disposed) return;
if (consuming) {
consumeAgain = true;
return;
}
consuming = true;
try {
do {
consumeAgain = false;
const result = await directInvoke<DirectThreadConsumeResult>(
'consume_direct_project_thread',
{ subscriptionId },
);
if (disposed) return;
setDirectThreadChat((state) =>
applyDirectThreadConsumeResult(state, result),
);
} while (consumeAgain && !disposed);
} catch (error) {
// 订阅被别人顶掉时重订一次;通知允许丢,下一次通知会再唤醒。
if (!disposed && String(error).includes('SUBSCRIPTION_EXPIRED')) {
subscriptionId = null;
try {
await bootstrap();
} catch {
/* 让下一次通知再试。 */
}
}
} finally {
consuming = false;
}
};
const setup = async () => {
try {
const unlisten = await subscribeTauriEvent<{ subscriptionId: string }>(
'game-creator-direct-thread-notify',
(event) => {
if (event.payload.subscriptionId === subscriptionId) {
void consume();
return;
}
if (!subscriptionId) notifyBeforeSubscription = true;
},
);
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
await bootstrap();
} catch {
// 历史仍可使用;订阅失败不伪造忙碌态,锚点缺失时首屏按文件尾取尾屏。
anchorGate.settle(null);
}
};
// 换项目就是换一份聊天:先回到初始态,再订阅新线程。
setDirectThreadChat(emptyDirectThreadChatState());
void setup();
return () => {
disposed = true;
cleanup?.();
// 首屏可能还在等这道闸门(例如切项目打断了订阅),不能让它永远等下去。
anchorGate.settle(null);
};
}, [directCodexProductRuntime, localProject?.projectPath]);
useEffect(() => {
if (projectSupervisorOnly && !directCodexProductRuntime) {
return;
}
if (!canSubscribeTauriEvents()) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void subscribeTauriEvent<AgentProgressEvent>(
'game-creator-agent-progress',
(event) => {
if (event.payload.projectPath !== localProjectPathRef.current) {
return;
}
if (directCodexProductRuntime) {
// DirectProject 的进度已经从线程事件流进聊天条目,这条通用进度事件不再参与。
return;
}
setMessages((current) => [
...current,
{ role: 'assistant', text: event.payload.message },
]);
},
)
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
})
.catch((error) => {
if (disposed) {
return;
}
setAgentRunStatus(
`实时状态不可用:${
error instanceof Error ? error.message : String(error)
}`,
);
});
return () => {
disposed = true;
cleanup?.();
};
}, [directCodexProductRuntime, projectSupervisorOnly]);
useEffect(() => {
const invoke = resolveTauriInvoke();
if (
!canSubscribeTauriEvents() ||
directCodexProductRuntime ||
designAgentActive
) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void subscribeTauriEvent<GameCreatorAgentRuntimeUpdateEvent>(
'game-creator-agent-runtime-update',
(event) => {
const payload = event.payload;
if (payload.projectPath !== localProjectPathRef.current) {
return;
}
if (payload.manifestInvalidated) {
void refreshManifest(payload.projectPath);
}
const nextRuntime = agentRuntimeStateFromResult(payload.runtime);
if (payload.agentId === PROJECT_SUPERVISOR_AGENT_ID) {
const expectedSessionId = projectSupervisorSessionIdRef.current;
const currentRuntime = projectSupervisorRuntimeRef.current;
const expectedRunId = projectSupervisorExpectedRunIdRef.current;
const sameRun =
currentRuntime !== null &&
sameAgentRuntimeRun(nextRuntime, currentRuntime);
const isExpectedRun =
expectedRunId !== null && nextRuntime.runId === expectedRunId;
if (
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
nextRuntime.runId !== payload.runId ||
(expectedSessionId !== null &&
nextRuntime.sessionId !== expectedSessionId) ||
(currentRuntime !== null &&
((!sameRun && !isExpectedRun) ||
(sameRun && nextRuntime.updatedAt < currentRuntime.updatedAt)))
) {
return;
}
if (expectedSessionId === null && nextRuntime.sessionId) {
projectSupervisorSessionIdRef.current = nextRuntime.sessionId;
setProjectSupervisorSessionId(nextRuntime.sessionId);
}
updateProjectSupervisorRuntime(nextRuntime);
if (isExpectedRun) {
projectSupervisorExpectedRunIdRef.current = null;
setProjectSupervisorExpectedRunId(null);
}
updateProjectSupervisorResponseStream(
payload.runtime.responseStream,
nextRuntime,
);
setProjectSupervisorRuntimeError('');
if (invoke) {
syncTerminalProjectSupervisorConversation(
invoke,
payload.projectPath,
nextRuntime,
);
}
}
rememberAgentRuntimeState(nextRuntime);
if (payload.agentId !== selectedAgentIdRef.current) {
return;
}
if (
agentConversationSessionIdRef.current !== null &&
payload.runtime.state.sessionId !==
agentConversationSessionIdRef.current
) {
return;
}
setAgentConversationRuntime((current) =>
normalizeAgentRuntimeState(nextRuntime, current),
);
setAgentConversationRuntimeError('');
},
)
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
})
.catch((error) => {
if (disposed) {
return;
}
setAgentConversationRuntimeError(
`Runtime 订阅不可用:${
error instanceof Error ? error.message : String(error)
}`,
);
});
return () => {
disposed = true;
cleanup?.();
};
// The subscription deliberately invokes the current render's helper; its
// mutable state is read through refs, so resubscribing for that helper is
// neither necessary nor safe.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
directCodexProductRuntime,
designAgentActive,
refreshManifest,
updateProjectSupervisorResponseStream,
updateProjectSupervisorRuntime,
]);
useEffect(() => {
const ready = createDesignAgentEventSubscriptionReady();
if (!canSubscribeTauriEvents() || !designAgentActive) {
resolveDesignAgentEventSubscriptionReady();
return () => {
if (designAgentEventSubscriptionReadyRef.current === ready) {
designAgentEventSubscriptionReadyRef.current = null;
createDesignAgentEventSubscriptionReady();
}
};
}
let cleanup: (() => void) | null = null;
let disposed = false;
void subscribeTauriEvent<DesignEvent>('design-agent-update', (event) => {
const payload = event.payload;
const tracked = designAgentTurnRef.current;
if (
payload.projectPath !== localProjectPathRef.current ||
(tracked && payload.clientTurnId !== tracked.clientTurnId)
) {
return;
}
if (payload.kind === 'text' && payload.text != null) {
setDesignAgentTransientReplyTarget(payload.text);
}
if (payload.reasoningText != null) {
const reasoningTurn = designAgentReasoningTurnRef.current;
if (
reasoningTurn &&
reasoningTurn.projectPath === payload.projectPath &&
reasoningTurn.clientTurnId === payload.clientTurnId
) {
reasoningTurn.text = payload.reasoningText;
setDesignAgentReasoning(payload.reasoningText);
}
}
if (payload.kind === 'tool' && payload.text) {
setDesignAgentTransientReplyTarget(payload.text);
}
if (payload.view) {
applyDesignAgentViewAfterTransient(
payload.view,
payload.projectPath,
payload.clientTurnId,
);
}
})
.then((unlisten) => {
resolveDesignAgentEventSubscriptionReady();
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
})
.catch(() => {
resolveDesignAgentEventSubscriptionReady();
});
return () => {
disposed = true;
cleanup?.();
resolveDesignAgentEventSubscriptionReady();
if (designAgentEventSubscriptionReadyRef.current === ready) {
designAgentEventSubscriptionReadyRef.current = null;
createDesignAgentEventSubscriptionReady();
}
};
// applyDesignView 读的是 refs 和当前项目路径,
// 把它写进依赖会在每轮回复时重订事件。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [designAgentActive]);
useEffect(() => {
if (!canSubscribeTauriEvents()) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void subscribeTauriEvent<GameCreatorManifestInvalidatedEvent>(
'game-creator-manifest-invalidated',
(event) => {
const activeProjectPath = localProjectPathRef.current;
if (
!activeProjectPath ||
!projectPathsMatchForInvalidation(
event.payload.projectPath,
activeProjectPath,
)
) {
return;
}
void refreshManifest(activeProjectPath);
},
)
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
})
.catch(() => {
// In-process Runtime events continue to carry the same invalidation signal.
});
return () => {
disposed = true;
cleanup?.();
};
}, [refreshManifest]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
const sessionId = projectSupervisorSessionId;
const trackedRunId =
projectSupervisorExpectedRunId ?? projectSupervisorRuntime?.runId ?? null;
if (
!invoke ||
directCodexProductRuntime ||
designAgentActive ||
!nextProjectPath ||
!sessionId ||
!trackedRunId ||
(!projectSupervisorRuntime && !projectSupervisorExpectedRunId) ||
(projectSupervisorRuntime !== null &&
isAgentRuntimeTerminalState(projectSupervisorRuntime) &&
!projectSupervisorExpectedRunId)
) {
return;
}
let disposed = false;
let inFlight = false;
const pollRuntime = async () => {
if (disposed || inFlight) {
return;
}
inFlight = true;
const runtimeBeforePoll = projectSupervisorRuntimeRef.current;
const responseStreamBeforePoll =
projectSupervisorResponseStreamRef.current;
try {
const result = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
},
);
const currentRuntime = projectSupervisorRuntimeRef.current;
if (
disposed ||
currentRuntime !== runtimeBeforePoll ||
projectSupervisorResponseStreamRef.current !==
responseStreamBeforePoll ||
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
if (
nextRuntime.sessionId !== sessionId ||
nextRuntime.runId !== trackedRunId ||
(currentRuntime !== null &&
sameAgentRuntimeRun(nextRuntime, currentRuntime) &&
nextRuntime.updatedAt < currentRuntime.updatedAt)
) {
return;
}
updateProjectSupervisorRuntime(nextRuntime);
if (nextRuntime.runId === projectSupervisorExpectedRunIdRef.current) {
projectSupervisorExpectedRunIdRef.current = null;
setProjectSupervisorExpectedRunId(null);
}
updateProjectSupervisorResponseStream(
result.responseStream,
nextRuntime,
);
setProjectSupervisorRuntimeError('');
syncTerminalProjectSupervisorConversation(
invoke,
nextProjectPath,
nextRuntime,
);
} catch (error) {
if (
!disposed &&
localProjectPathRef.current === nextProjectPath &&
projectSupervisorSessionIdRef.current === sessionId
) {
const message =
error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
}
} finally {
inFlight = false;
}
};
void pollRuntime();
const timer = window.setInterval(() => {
void pollRuntime();
}, 750);
return () => {
disposed = true;
window.clearInterval(timer);
};
// Polling restarts only when the tracked runtime identity or phase changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
localProject?.projectPath,
directCodexProductRuntime,
designAgentActive,
projectSupervisorSessionId,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
projectSupervisorRuntime?.status,
projectSupervisorExpectedRunId,
]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
const supervisorRunId = projectSupervisorRuntime?.runId ?? null;
if (
!projectSupervisorOnly ||
directCodexProductRuntime ||
designAgentActive ||
!invoke ||
!nextProjectPath ||
!supervisorRunId
) {
return;
}
let disposed = false;
let inFlight = false;
const pollProfessionalRuntimes = async () => {
if (disposed || inFlight) {
return;
}
inFlight = true;
try {
const runtimes = await invoke<AgentRuntimeResult[]>(
'read_game_creator_agent_runtimes',
{ projectPath: nextProjectPath },
);
if (
disposed ||
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
) {
return;
}
const nextRuntimes = runtimes.map((runtimeResult) =>
agentRuntimeStateFromResult(runtimeResult),
);
const authoritativeProfessionalAgentIds = new Set(
nextRuntimes
.filter(
(runtime) =>
['agent-delegate', 'agent-delegate-retry'].includes(
runtime.source,
) &&
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
runtime.parentRunId === supervisorRunId,
)
.map((runtime) => runtime.agentId),
);
setAgentRuntimeById((current) => {
const reconciled = { ...current };
for (const [key, runtime] of Object.entries(reconciled)) {
if (
runtime &&
['agent-delegate', 'agent-delegate-retry'].includes(
runtime.source,
) &&
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
runtime.parentRunId === supervisorRunId &&
!authoritativeProfessionalAgentIds.has(runtime.agentId)
) {
delete reconciled[key];
}
}
return nextRuntimes.reduce(
(next, runtime) =>
mergeAgentRuntimeStateIntoMap(next, runtime, true),
reconciled,
);
});
} catch {
// Preserve the last truthful professional Runtime snapshot on transient read failures.
} finally {
inFlight = false;
}
};
void pollProfessionalRuntimes();
const timer = window.setInterval(() => {
void pollProfessionalRuntimes();
}, 1000);
return () => {
disposed = true;
window.clearInterval(timer);
};
// Track the stable projection fields; the Runtime object identity changes on every merge.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
localProject?.projectPath,
directCodexProductRuntime,
designAgentActive,
projectSupervisorOnly,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
projectSupervisorRuntime?.status,
]);
useLayoutEffect(() => {
if (
(!supervisorChatOnly &&
!(
projectSupervisorOnly &&
(directCodexProductRuntime || designAgentActive)
)) ||
!supervisorChatShouldFollowLatestRef.current
) {
return;
}
const messageList = supervisorChatMessagesRef.current;
if (messageList) {
messageList.scrollTop = messageList.scrollHeight;
}
}, [
messages,
projectSupervisorResponseStream?.sequence,
projectSupervisorRuntime?.updatedAt,
projectSupervisorRuntimeError,
directThreadChat,
directCodexProductRuntime,
designAgentActive,
projectSupervisorOnly,
supervisorChatOnly,
]);
useEffect(() => {
if (!supervisorChatOnly) {
return;
}
persistSupervisorChatDraft(initialProjectPath, chatInput);
}, [chatInput, initialProjectPath, supervisorChatOnly]);
useEffect(() => {
latestMessagesRef.current = messages;
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath;
if (!invoke || !nextProjectPath) {
return;
}
if (savedConversationProjectPathRef.current !== nextProjectPath) {
savedConversationProjectPathRef.current = nextProjectPath;
savedConversationCountRef.current = 0;
projectConversationWriteConfirmedRef.current = null;
projectConversationWriteCancelledRef.current = null;
}
// DirectProject history is written by Rust from raw app-server items.
// The browser only renders that projection and must not append chat rows.
if (projectSupervisorOnly && directCodexProductRuntime) {
// `messages` is only an optimistic UI projection in Direct mode; it is
// intentionally not proof of durability. Rust owns the raw response
// history, so this effect must not route these rows through the generic
// browser conversation writer.
savedConversationCountRef.current = messages.length;
return;
}
const start = savedConversationCountRef.current;
const pendingMessages = messages.slice(start);
if (pendingMessages.length === 0) {
return;
}
if (
projectSupervisorOnly &&
pendingMessages.every(
(message) =>
(message.runtimeOwned &&
!isPersistableDirectCodexConversationMessage(message)) ||
isTransientProjectOpenMessage(message, nextProjectPath),
)
) {
savedConversationCountRef.current = messages.length;
return;
}
if (conversationWriteInFlightRef.current) {
return;
}
if (pendingUiConfirmation) {
return;
}
if (
projectConversationWriteCancelledRef.current?.projectPath ===
nextProjectPath &&
projectConversationWriteCancelledRef.current.messageCount ===
messages.length
) {
return;
}
if (
projectConversationWriteCancelledRef.current?.projectPath ===
nextProjectPath &&
projectConversationWriteCancelledRef.current.messageCount <
messages.length
) {
projectConversationWriteCancelledRef.current = null;
}
if (projectConversationWriteConfirmedRef.current !== nextProjectPath) {
void invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
)
.then((policyView) => {
if (
!policyView.policy.confirmCommands.includes('conversation.write')
) {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
return;
}
requestProjectPolicyConfirmation(
'conversation.write',
nextProjectPath,
`写入 ${nextProjectPath} 的项目对话`,
() => {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
},
);
setWorkspaceStatus('等待确认保存项目对话');
})
.catch(() => {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
});
return;
}
conversationWriteInFlightRef.current = true;
void (async () => {
let wroteMessage = false;
for (const [index, message] of pendingMessages.entries()) {
if (
message.runtimeOwned &&
!isPersistableDirectCodexConversationMessage(message)
) {
savedConversationCountRef.current = start + index + 1;
continue;
}
if (isTransientProjectOpenMessage(message, nextProjectPath)) {
savedConversationCountRef.current = start + index + 1;
continue;
}
await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: null,
...(message.messageId ? { messageId: message.messageId } : {}),
message: {
role: message.role === 'user' ? 'user' : 'assistant',
content: message.text,
agentId: null,
...(typeof message.updatedAt === 'number'
? { updatedAt: message.updatedAt }
: {}),
},
},
);
wroteMessage = true;
savedConversationCountRef.current = start + index + 1;
}
setWorkspaceStatus((current) =>
current.startsWith('项目对话保存失败') ||
current === '等待确认保存项目对话'
? `已打开:${nextProjectPath}`
: current,
);
if (wroteMessage) {
setCommandLog((current) => [...current, 'conversation.write']);
}
})()
.catch((error) => {
savedConversationCountRef.current = Math.min(
savedConversationCountRef.current,
start,
);
setWorkspaceStatus(
`项目对话保存失败:${
error instanceof Error ? error.message : String(error)
}`,
);
setCommandLog((current) => [
...current,
`conversation.write.failed ${
error instanceof Error ? error.message : String(error)
}`,
]);
})
.finally(() => {
conversationWriteInFlightRef.current = false;
if (
savedConversationCountRef.current < latestMessagesRef.current.length
) {
setConversationWriteVersion((current) => current + 1);
}
});
// Writes are driven by message and confirmation state; the confirmation helper
// intentionally observes the latest policy refs.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
localProject?.projectPath,
messages,
conversationWriteVersion,
pendingUiConfirmation,
projectSupervisorOnly,
directCodexProductRuntime,
]);
function appendLocalPermissionLog(
projectPath: string | null,
event:
| 'permission.pending'
| 'permission.confirm'
| 'permission.cancel'
| 'command.auto',
commandId: GameCreationAppCommandDescriptor['id'],
) {
const invoke = resolveTauriInvoke();
if (!invoke || !projectPath) {
return;
}
let result: Promise<unknown>;
try {
result = Promise.resolve(
invoke('append_local_permission_log', {
projectPath,
event,
commandId,
}),
);
} catch (error) {
setCommandLog((current) => [
...current,
`permission.log.failed ${commandId}: ${
error instanceof Error ? error.message : String(error)
}`,
]);
return;
}
void result.catch((error) => {
setCommandLog((current) => [
...current,
`permission.log.failed ${commandId}: ${
error instanceof Error ? error.message : String(error)
}`,
]);
});
}
function resolvePermissionLogProjectPath(command: PendingCommand) {
if (command.id === 'project.create') {
return null;
}
return resolveChatProjectPath(localProject);
}
function resolveUiPermissionLogProjectPath(
commandId: GameCreationAppCommandDescriptor['id'],
) {
if (commandId === 'project.create') {
return null;
}
if (needsInitializedChatProject(commandId)) {
return resolveChatProjectPath(localProject);
}
return projectPath;
}
function resolveCurrentUiConfirmationProjectPath() {
const openedProjectPath = localProjectPathRef.current?.trim();
if (openedProjectPath) {
return openedProjectPath;
}
const draftProjectPath = projectPath.trim();
if (
!draftProjectPath ||
!isAbsoluteProjectPath(draftProjectPath) ||
projectPathHasControlCharacter(draftProjectPath)
) {
return null;
}
return draftProjectPath;
}
function queuePendingCommand(command: PendingCommand) {
setPendingCommand(command);
setCommandLog((current) => [
...current,
`permission.pending ${command.id}`,
]);
appendLocalPermissionLog(
resolvePermissionLogProjectPath(command),
'permission.pending',
command.id,
);
}
function requestCommandConfirmation(
commandId: GameCreationAppCommandDescriptor['id'],
detail: string,
onConfirm: () => void,
) {
const permission = GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === commandId,
)?.permission as GameCreationAppPermission | undefined;
if (permission === 'deny' || !permission) {
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
return;
}
if (permission === 'confirm') {
pendingUiConfirmationActionRef.current = onConfirm;
setPendingUiConfirmation({ commandId, detail, projectPath: null });
setCommandLog((current) => [
...current,
`permission.pending ${commandId}`,
]);
appendLocalPermissionLog(
resolveUiPermissionLogProjectPath(commandId),
'permission.pending',
commandId,
);
return;
}
onConfirm();
}
function requestProjectPolicyConfirmation(
commandId: GameCreationAppCommandDescriptor['id'],
projectPath: string,
detail: string,
onConfirm: () => void,
) {
pendingUiConfirmationActionRef.current = onConfirm;
setPendingUiConfirmation({ commandId, detail, projectPath });
setCommandLog((current) => [...current, `permission.pending ${commandId}`]);
appendLocalPermissionLog(projectPath, 'permission.pending', commandId);
}
function markProjectPolicyDenied(
commandId: GameCreationAppCommandDescriptor['id'],
message: string,
) {
if (commandId.startsWith('project.') || commandId === 'task.list') {
setProjectStatus(message);
setWorkspaceStatus(message);
}
if (
commandId.startsWith('file.') ||
commandId === 'agent.trace_read' ||
commandId === 'project.index' ||
commandId === 'project.diff' ||
commandId === 'project.export_package' ||
commandId === 'project.export_list'
) {
setFileStatus(message);
}
if (commandId.startsWith('asset.') || commandId.startsWith('canvas.')) {
setAssetStatus(message);
}
if (commandId.startsWith('preview.')) {
setPreviewStatus(message);
setWorkspaceStatus(message);
}
if (commandId.startsWith('agent.')) {
setAgentRunStatus(message);
}
if (commandId.startsWith('memory.')) {
setMemoryStatus(message);
}
}
async function queueProjectPolicyConfirmationIfNeeded(
invoke: TauriInvoke,
commandId: GameCreationAppCommandDescriptor['id'],
projectPath: string,
detail: string,
readyMessage: string,
onConfirm: () => void,
) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath },
);
if (policyView.policy.deniedCommands.includes(commandId)) {
const message = `项目权限策略拒绝执行:${commandId}`;
markProjectPolicyDenied(commandId, message);
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
return true;
}
if (!policyView.policy.confirmCommands.includes(commandId)) {
return false;
}
requestProjectPolicyConfirmation(commandId, projectPath, detail, onConfirm);
setMessages((current) => [
...current,
{ role: 'assistant', text: readyMessage },
]);
return true;
}
async function denyPendingCommandIfNeeded(
commandId: GameCreationAppCommandDescriptor['id'],
projectPath: string | null,
) {
const invoke = resolveTauriInvoke();
if (!invoke || !projectPath) {
return false;
}
try {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath },
);
if (!policyView.policy.deniedCommands.includes(commandId)) {
return false;
}
const message = `项目权限策略拒绝执行:${commandId}`;
markProjectPolicyDenied(commandId, message);
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
return true;
} catch {
return false;
}
}
async function confirmUiCommand() {
const pending = pendingUiConfirmation;
if (!pending) {
return;
}
if (
pending.projectPath &&
pending.projectPath !== resolveCurrentUiConfirmationProjectPath()
) {
cancelUiCommandConfirmation();
return;
}
const permissionProjectPath =
pending.projectPath ??
resolveUiPermissionLogProjectPath(pending.commandId);
if (
await denyPendingCommandIfNeeded(pending.commandId, permissionProjectPath)
) {
pendingUiConfirmationActionRef.current = null;
setPendingUiConfirmation(null);
return;
}
const action = pendingUiConfirmationActionRef.current;
pendingUiConfirmationActionRef.current = null;
setPendingUiConfirmation(null);
setCommandLog((current) => [
...current,
`permission.confirm ${pending.commandId}`,
]);
appendLocalPermissionLog(
permissionProjectPath,
'permission.confirm',
pending.commandId,
);
action?.();
}
function cancelUiCommandConfirmation() {
const pending = pendingUiConfirmation;
if (!pending) {
return;
}
if (pending.commandId === 'conversation.write') {
const nextProjectPath = resolveChatProjectPath(localProject);
if (nextProjectPath) {
projectConversationWriteCancelledRef.current = {
projectPath: nextProjectPath,
messageCount: latestMessagesRef.current.length,
};
setWorkspaceStatus((current) =>
current === '等待确认保存项目对话'
? `已打开:${nextProjectPath}`
: current,
);
}
}
if (pending.commandId === 'conversation.read') {
if (pending.detail.includes('Agent 对话')) {
setAgentConversationStatus((current) =>
current === '等待确认' ? '已取消读取 Agent 对话' : current,
);
setAgentMemoryStatus((current) =>
current === '等待确认' ? '已取消读取 Agent 对话' : current,
);
} else {
setWorkspaceStatus((current) =>
current === '等待确认' ? '已取消读取项目对话' : current,
);
}
}
if (pending.commandId === 'memory.read') {
setMemoryStatus((current) =>
current === '等待确认' ? '已取消读取项目记忆' : current,
);
setAgentMemoryStatus((current) =>
current === '等待确认' ? '已取消读取 Agent 私有记忆' : current,
);
if (!pending.detail.includes('Agent 私有记忆')) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '已取消读取项目记忆。' },
]);
}
}
if (pending.commandId === 'memory.write') {
setMemoryStatus((current) =>
current === '等待确认' ? '已取消保存项目记忆' : current,
);
setAgentMemoryStatus((current) =>
current === '等待确认' ? '已取消保存 Agent 私有记忆' : current,
);
}
if (
pending.commandId === 'file.list' ||
pending.commandId === 'file.read' ||
pending.commandId === 'agent.trace_read'
) {
setFileStatus((current) =>
current === '等待确认' ? '已取消读取项目文件' : current,
);
}
if (pending.commandId === 'agent.trace_read') {
setAgentRunStatus((current) =>
current === '等待确认' || current === '等待确认读取 Agent trace'
? '已取消读取 Agent trace'
: current,
);
}
if (
pending.commandId === 'project.status' ||
pending.commandId === 'task.list'
) {
const status =
pending.commandId === 'task.list'
? '已取消读取任务拆分'
: '已取消读取项目状态';
setProjectStatus(status);
setWorkspaceStatus(status);
}
if (pending.commandId === 'asset.list') {
setAssetStatus('已取消读取项目资产');
setWorkspaceStatus('已取消读取项目资产');
}
if (pending.commandId === 'asset.upload') {
setAssetStatus('已取消上传资产');
}
if (pending.commandId === 'canvas.project_open') {
setAssetStatus('已取消打开画板');
}
if (pending.commandId === 'canvas.project_sync') {
setAssetStatus('已取消同步画板项目');
}
if (pending.commandId === 'canvas.asset_import') {
setAssetStatus('已取消导入画板资产');
}
if (pending.commandId === 'canvas.asset_generate') {
setAssetStatus('已取消生成美术素材');
}
if (pending.commandId === 'canvas.export_import') {
setAssetStatus('已取消导入画板导出包');
}
if (
pending.commandId === 'preview.start' ||
pending.commandId === 'preview.open' ||
pending.commandId === 'preview.stop' ||
pending.commandId === 'preview.status'
) {
setPreviewStatus('已取消预览操作');
setWorkspaceStatus('已取消预览操作');
setMessages((current) => [
...current,
{ role: 'assistant', text: '已取消预览操作' },
]);
}
if (
pending.commandId === 'agent.run_status' ||
pending.commandId === 'agent.audit'
) {
setAgentRunStatus(
pending.commandId === 'agent.audit'
? '已取消 Agent 审计'
: '已取消读取 Agent run 状态',
);
}
if (
pending.commandId === 'agent.resume' &&
pending.detail.includes('未完成的 Agent Runtime 任务')
) {
setAgentRunStatus('已取消恢复 Agent Runtime 任务');
}
if (
pending.commandId === 'project.index' ||
pending.commandId === 'project.diff' ||
pending.commandId === 'project.export_package'
) {
const status =
pending.commandId === 'project.index'
? '已取消刷新项目索引'
: pending.commandId === 'project.diff'
? '已取消对比项目 checkpoint'
: '已取消导出本地试玩包';
setFileStatus(status);
setWorkspaceStatus(status);
}
if (pending.commandId === 'project.create') {
setProjectStatus('已取消');
setWorkspaceStatus('已取消');
}
if (pending.commandId === 'file.write') {
setFileStatus('已取消保存项目文件');
}
if (pending.commandId === 'file.delete') {
setFileStatus('已取消删除项目文件');
}
if (pending.commandId === 'memory.write') {
setMemoryStatus('已取消写入项目记忆');
}
if (pending.commandId === 'memory.delete') {
setMemoryStatus('已取消删除项目记忆');
}
if (pending.commandId === 'asset.register') {
setAssetStatus('已取消登记资产');
}
if (pending.commandId === 'command.run_limited') {
setLimitedCommandStatus('已取消运行');
}
pendingUiConfirmationActionRef.current = null;
setPendingUiConfirmation(null);
setCommandLog((current) => [
...current,
`permission.cancel ${pending.commandId}`,
]);
appendLocalPermissionLog(
pending.projectPath ??
resolveUiPermissionLogProjectPath(pending.commandId),
'permission.cancel',
pending.commandId,
);
}
function handleRuntimeConfigOpen() {
setRuntimeConfigOpen(true);
}
function queueRunLocalShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'game.run_local' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备运行当前本地游戏。' },
]);
}
queueRunLocalShortcutRef.current = queueRunLocalShortcut;
executeRunLocalRef.current = executeRunLocal;
useEffect(() => {
const nextProjectPath = localProject?.projectPath;
if (
!projectSupervisorOnly ||
!playRequest ||
!nextProjectPath ||
playRequest.projectPath !== nextProjectPath
) {
return;
}
const requestKey = `${playRequest.projectPath}\n${playRequest.requestId}`;
if (handledPlayRequestRef.current === requestKey) {
return;
}
handledPlayRequestRef.current = requestKey;
onPlayRequestHandled?.(playRequest.requestId);
void executeRunLocalRef.current(true);
}, [
localProject?.projectPath,
onPlayRequestHandled,
playRequest,
projectSupervisorOnly,
]);
function queueStaticSmokeShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({
id: 'command.run_limited',
commandId: 'game.static_smoke',
});
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备运行静态入口自检。' },
]);
}
function queueProjectCheckpointShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'project.checkpoint' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备保存当前项目快照。' },
]);
}
function queueProjectExportPackageShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'project.export_package' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备导出本地试玩包。' },
]);
}
function queuePreviewStartShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'preview.start' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备启动本地预览。' },
]);
}
function queuePreviewOpenShortcut() {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'preview.open' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备打开当前本地预览。' },
]);
}
async function handleOpenLauncherWindow() {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请回到首页的项目组切换项目。' },
]);
}
async function handleRevealCurrentProjectDirectory() {
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内打开项目目录。' },
]);
return;
}
try {
await invoke('open_local_project_directory', {
projectPath: nextProjectPath,
});
setMessages((current) => [
...current,
{ role: 'assistant', text: '已打开项目目录。' },
]);
} catch (error) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
function requireChatProjectForUserAction() {
const nextProjectPath = resolveChatProjectPath(localProject);
if (nextProjectPath) {
return nextProjectPath;
}
setMemoryStatus('请先初始化本地项目');
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
return null;
}
async function handleProjectInit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
requestCommandConfirmation(
'project.create',
`创建 ${projectPath}`,
() => void executeProjectCreate(projectPath, false),
);
}
async function resumeProjectSupervisorRuntimeTasksIfNeeded(
invoke: TauriInvoke,
nextProjectPath: string,
) {
if (
projectSupervisorRuntimeResumeProjectPathRef.current === nextProjectPath
) {
return '';
}
try {
await invoke<AgentRuntimeResult[]>(
'resume_game_creator_agent_runtime_tasks',
{ projectPath: nextProjectPath },
);
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
return '';
} catch (error) {
if (isMissingAgentRuntimeResumeCommandError(error)) {
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
return '';
}
const message = error instanceof Error ? error.message : String(error);
if (
message.includes('项目权限策略要求用户确认:agent.resume') ||
message.includes('项目权限策略拒绝执行:agent.resume')
) {
return '';
}
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
const runtimeError = `项目总控 Agent 恢复失败:${message}`;
setProjectSupervisorRuntimeError(runtimeError);
return runtimeError;
}
}
async function readProjectSupervisorActiveSession(
invoke: TauriInvoke,
nextProjectPath: string,
) {
return readProjectSupervisorActiveSessionId(invoke, nextProjectPath);
}
async function refreshProjectSupervisorConversation(
invoke: TauriInvoke,
nextProjectPath: string,
sessionId: string,
) {
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
const [projectConversation, supervisorConversation] = await Promise.all([
invoke<LocalConversationResult>('read_local_conversation', {
projectPath: nextProjectPath,
agentId: null,
}),
invoke<LocalConversationResult>('read_local_conversation', {
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
}),
]);
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
const conversationMessages = mergeProjectSupervisorConversation(
projectConversation.messages,
supervisorConversation.messages,
);
const transientResponse = projectSupervisorResponseStreamRef.current;
if (
conversationContainsProjectSupervisorResponseStream(
supervisorConversation.messages,
transientResponse,
)
) {
projectSupervisorResponseStreamRef.current = null;
setProjectSupervisorResponseStream(null);
}
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setMessages((current) => {
// 不能整体替换:乐观插入、尚未落盘的用户消息会被冲掉(初始需求看不到就是这个原因)。
const nextMessages = mergeLoadedConversationWithPendingRuntimeMessages(
conversationMessages,
current,
);
savedConversationProjectPathRef.current = nextProjectPath;
savedConversationCountRef.current = nextMessages.length;
latestMessagesRef.current = nextMessages;
return nextMessages;
});
setProjectSupervisorRuntimeError('');
}
projectSupervisorRefreshConversationRef.current =
refreshProjectSupervisorConversation;
async function loadProjectConversation(
nextProjectPath: string,
skipPolicyConfirm = false,
mode: 'initial' | 'replace' = 'initial',
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
return;
}
if (!skipPolicyConfirm) {
try {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('conversation.read')) {
requestProjectPolicyConfirmation(
'conversation.read',
nextProjectPath,
`读取 ${nextProjectPath} 的项目对话历史`,
() => void loadProjectConversation(nextProjectPath, true, mode),
);
setWorkspaceStatus('等待确认');
return;
}
} catch {
return;
}
}
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
try {
if (planningStartMode) {
try {
const design = await hydrateDesignAgentSession(nextProjectPath);
if (design) {
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath
) {
return;
}
setWorkspaceStatus((workspaceStatus) =>
workspaceStatus === '等待确认'
? `已打开:${nextProjectPath}`
: workspaceStatus,
);
return;
}
designAgentActiveRef.current = true;
setDesignAgentActive(true);
setDesignAgentView(null);
projectSupervisorSessionIdRef.current = null;
setProjectSupervisorSessionId(null);
updateProjectSupervisorRuntime(null);
setMessages(createDefaultChatMessages());
savedConversationProjectPathRef.current = nextProjectPath;
savedConversationCountRef.current = 0;
latestMessagesRef.current = [];
setWorkspaceStatus((workspaceStatus) =>
workspaceStatus === '等待确认'
? `已打开:${nextProjectPath}`
: workspaceStatus,
);
return;
} catch (error) {
setProjectSupervisorRuntimeError(
`策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`,
);
return;
}
}
const resumeError = directCodexProductRuntime
? ''
: await resumeProjectSupervisorRuntimeTasksIfNeeded(
invoke,
nextProjectPath,
);
if (directCodexProductRuntime) {
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
}
// Direct Codex owns its app-server conversation identity. The project
// workbench only hydrates the project conversation and must not inspect
// or create a legacy Supervisor Session while opening the project.
const sessionId = directCodexProductRuntime
? null
: await readProjectSupervisorActiveSession(invoke, nextProjectPath);
let runtimeError = '';
let loadedDirectHistoryHasMore = false;
let loadedDirectHistoryFirstItemId: string | null = null;
// 首屏连拉到的条目先暂存,待下方 staleness 守卫通过后再并入聊天 reducer:
// 迟到的切片属于已经切走的项目,不能在守卫之前就写进全局聊天状态。
let loadedDirectHistoryItems: DirectThreadItem[] = [];
// 首屏切片的新端边界只认订阅回执里的 `lastCompletedItemId`(含该条):比它更新的条目
// 只能来自运行态事件。打开项目时订阅 effect 还没跑,这里就先把闸门开好,订阅侧会复用
// 同一道闸门并 settle 它。同一道闸门只锚定一次:同一订阅下再读一次(例如重开同一个项目)
// 没有新回执可等,退回"按当前文件尾取尾屏",与 `/history` 手动重读一致。
let directHistoryThroughItemId: string | null = null;
if (directCodexProductRuntime && mode !== 'replace') {
const anchorGate = directHistoryAnchorGateToWaitFor(
directHistoryAnchorGateRef.current,
nextProjectPath,
);
if (anchorGate) {
directHistoryAnchorGateRef.current = anchorGate;
directHistoryThroughItemId = await anchorGate.anchor;
anchorGate.consumed = true;
}
}
const projectConversation = directCodexProductRuntime
? (() => {
// 首屏铺的就是这份视图的基线,"新回合"比较没有意义:判据退化为"这一页得有能渲染
// 的条目",所以基线取空,免得拿上一份对话的残留状态去比。首屏失败仍按原有语义
// 抛出,交给调用方的 catch 处理。
return readDirectHistoryPages({
existingEntries: [],
beforeItemId: null,
readSlice: (beforeItemId) =>
invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
beforeItemId
? {
projectPath: nextProjectPath,
beforeItemId,
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
}
: {
projectPath: nextProjectPath,
...(directHistoryThroughItemId
? { throughItemId: directHistoryThroughItemId }
: {}),
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
},
),
}).then((pages) => {
if (pages.error) {
throw pages.error;
}
loadedDirectHistoryHasMore = pages.hasMore;
loadedDirectHistoryFirstItemId = pages.firstItemId;
loadedDirectHistoryItems = pages.items;
return {
path: nextProjectPath,
agentId: null,
messages: [],
} satisfies LocalConversationResult;
});
})()
: invoke<LocalConversationResult>('read_local_conversation', {
projectPath: nextProjectPath,
agentId: null,
});
const resolvedProjectConversation = await projectConversation;
// 首屏历史直接进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。
let supervisorConversation: LocalConversationResult | null = null;
let runtime: AgentRuntimeState | null = null;
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
if (sessionId) {
supervisorConversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
},
);
}
if (sessionId) {
try {
const runtimeResult = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
},
);
runtime = agentRuntimeStateFromResult(runtimeResult);
runtimeResponseStream = runtimeResult.responseStream ?? null;
} catch (error) {
runtimeError = error instanceof Error ? error.message : String(error);
}
}
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath
) {
return;
}
const conversationMessages = mergeProjectSupervisorConversation(
resolvedProjectConversation.messages,
supervisorConversation?.messages ?? [],
);
if (
conversationContainsProjectSupervisorResponseStream(
supervisorConversation?.messages ?? [],
runtimeResponseStream,
)
) {
runtimeResponseStream = null;
}
projectSupervisorSessionIdRef.current = sessionId;
setProjectSupervisorSessionId(sessionId);
updateProjectSupervisorRuntime(runtime);
if (runtime) {
updateProjectSupervisorResponseStream(runtimeResponseStream, runtime);
} else {
projectSupervisorResponseStreamRef.current = null;
setProjectSupervisorResponseStream(null);
}
setProjectSupervisorRuntimeError(runtimeError || resumeError);
if (directCodexProductRuntime) {
setDirectHistoryHasMore(loadedDirectHistoryHasMore);
directHistoryOldestItemIdRef.current = loadedDirectHistoryFirstItemId;
if (loadedDirectHistoryItems.length > 0) {
const items = loadedDirectHistoryItems;
// 首屏历史进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。
setDirectThreadChat((state) => mergeDirectHistoryItems(state, items));
}
}
setMessages((current) => {
// replace 分支同样不能丢掉尚未落盘的运行时消息(初始需求)。
const nextConversationMessages =
mergeLoadedConversationWithPendingRuntimeMessages(
conversationMessages,
current,
);
// 空对话(没有默认问候之后的新常态)同样应当接受回读结果。
const isEmptyConversation = current.length === 0;
const hasOnlyDefaultGreeting =
current.length === 1 &&
current[0]?.role === 'assistant' &&
current[0]?.text === '想做什么游戏?';
const hasOnlyOpenStatus =
current.length === 2 &&
current[0]?.role === 'assistant' &&
current[0]?.text === '想做什么游戏?' &&
current[1]?.role === 'assistant' &&
current[1]?.text === `已设置本地项目:${nextProjectPath}`;
if (
mode !== 'replace' &&
!isEmptyConversation &&
!hasOnlyDefaultGreeting &&
!hasOnlyOpenStatus
) {
return current;
}
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
savedConversationProjectPathRef.current = nextProjectPath;
savedConversationCountRef.current = nextConversationMessages.length;
latestMessagesRef.current = nextConversationMessages;
setWorkspaceStatus((workspaceStatus) => {
if (mode === 'replace') {
return `已读取项目对话历史:${conversationMessages.length} 条`;
}
return workspaceStatus === '等待确认'
? `已打开:${nextProjectPath}`
: workspaceStatus;
});
return nextConversationMessages;
});
} catch (error) {
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath
) {
return;
}
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
setWorkspaceStatus((workspaceStatus) =>
workspaceStatus === '等待确认'
? `项目对话读取失败:${message}`
: workspaceStatus,
);
// Keep the default greeting when history is missing or blocked.
}
}
async function openWorkspace(
nextProjectPath: string,
announceToChat: boolean,
mode: 'create' | 'open' = 'create',
projectKind: LocalProjectKind = 'web',
) {
const trimmedProjectPath = nextProjectPath.trim();
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
setWorkspaceStatus('请提供工作区绝对路径');
return;
}
if (projectPathHasControlCharacter(trimmedProjectPath)) {
setWorkspaceStatus('工作区路径不能包含控制字符');
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setWorkspaceStatus('需要在 Tauri App 内运行');
setProjectStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const projectScopeVersion = projectScopeVersionRef.current + 1;
projectScopeVersionRef.current = projectScopeVersion;
resetProjectSupervisorState();
resetDirectThreadChat();
updateClientPreview(null);
setPreviewStatus('未启动');
setWorkspaceStatus('正在打开');
setProjectStatus('正在初始化');
setSelectedAgent(null);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationSessionId(null);
setAgentConversationRunSubmitMode('steer');
setAgentConversationRuntimeError('');
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentConversationStatus('未选择 agent');
setAgentConversationSaving(false);
setAgentMemoryStatus('未读取');
setAgentMemoryContent('');
agentConversationSavingRef.current = false;
agentConversationLoadVersionRef.current += 1;
try {
const result =
mode === 'open'
? {
projectPath: trimmedProjectPath,
manifestPath: `${trimmedProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`,
manifest: await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: trimmedProjectPath },
),
}
: await invoke<InitLocalProjectResult>('init_local_game_project', {
projectPath: trimmedProjectPath,
projectId: createLocalProjectId(),
name: seedManifest.name,
});
if (projectScopeVersionRef.current !== projectScopeVersion) {
return;
}
const openedProjectPath = result.projectPath.trim();
if (
!openedProjectPath ||
!isAbsoluteProjectPath(openedProjectPath) ||
projectPathHasControlCharacter(openedProjectPath)
) {
const message = '本地项目路径无效';
setWorkspaceStatus(message);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
return;
}
const openedProject = { ...result, projectPath: openedProjectPath };
const conversationMessages = createDefaultChatMessages();
if (
pendingUiConfirmation?.projectPath &&
pendingUiConfirmation.projectPath !== openedProject.projectPath
) {
cancelUiCommandConfirmation();
}
localProjectPathRef.current = openedProject.projectPath;
setProjectPath(openedProject.projectPath);
setWorkspaceProjectKind(projectKind);
setLocalProject(openedProject);
if (supervisorChatOnly) {
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
}
setChatReferences([]);
setChatContent([]);
setManifest(openedProject.manifest);
setProjectFiles([]);
setProjectCheckpoints([]);
setAgentRunHistory([]);
setAgentRunHistoryFiles([]);
setAgentRuntimeById({});
setMessages(conversationMessages);
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
savedConversationProjectPathRef.current = openedProject.projectPath;
savedConversationCountRef.current = conversationMessages.length;
writeRecentWorkspace(openedProject.projectPath);
setWorkspaceStatus(`已打开:${openedProject.projectPath}`);
setProjectStatus(mode === 'open' ? '已打开' : '已初始化');
appendLocalPermissionLog(
openedProject.projectPath,
'permission.confirm',
mode === 'open' ? 'project.status' : 'project.create',
);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已设置本地项目:${openedProject.projectPath}`,
},
]);
}
// Direct Codex must not resume or inspect legacy Supervisor work. Its
// own project conversation is hydrated above; the old run trace remains
// available only in the explicit legacy diagnostic surface.
void loadProjectConversation(openedProject.projectPath);
if (!directCodexProductRuntime) {
void refreshAgentRunTrace(openedProject.projectPath);
}
} catch (error) {
if (projectScopeVersionRef.current !== projectScopeVersion) {
return;
}
const message = error instanceof Error ? error.message : String(error);
setWorkspaceStatus(message);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function openAgentConversation(
agent: AgentStatusCard,
skipConversationPolicyConfirm = false,
skipMemoryPolicyConfirm = false,
) {
const loadVersion = agentConversationLoadVersionRef.current + 1;
agentConversationLoadVersionRef.current = loadVersion;
setSelectedAgent(agent);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationSessionId(null);
setAgentConversationRunSubmitMode('steer');
setAgentConversationRuntimeError('');
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentMemoryContent('');
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke || !nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
setAgentMemoryStatus('请先初始化本地项目');
return;
}
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
if (agentConversationLoadVersionRef.current === loadVersion) {
setLlmConfigStatus(status);
}
} catch {
// Agent conversation history is still useful when config status is unavailable.
}
try {
if (
!skipConversationPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'conversation.read',
nextProjectPath,
`读取 ${agent.title} Agent 对话`,
'准备读取 Agent 对话。',
() =>
void openAgentConversation(agent, true, skipMemoryPolicyConfirm),
))
) {
setAgentConversationStatus('等待确认');
setAgentMemoryStatus('等待确认');
return;
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAgentConversationStatus(message);
setAgentMemoryStatus(message);
return;
}
setAgentConversationStatus('正在读取');
setAgentMemoryStatus('正在读取');
try {
const result = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current !== loadVersion) {
return;
}
setAgentConversationMessages(result.messages);
setAgentConversationSessionId(result.sessionId ?? null);
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: agent.id,
...(result.sessionId ? { sessionId: result.sessionId } : {}),
},
);
if (agentConversationLoadVersionRef.current === loadVersion) {
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
}
} catch (error) {
if (agentConversationLoadVersionRef.current === loadVersion) {
setAgentConversationRuntime(null);
setAgentConversationRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentConversationStatus(
`已读取 ${result.messages.length} 条:${result.path}`,
);
setCommandLog((current) => [...current, 'conversation.read']);
} catch (error) {
if (agentConversationLoadVersionRef.current !== loadVersion) {
return;
}
setAgentConversationMessages([]);
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
}
try {
if (
!skipMemoryPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'memory.read',
nextProjectPath,
`读取 ${agent.title} Agent 私有记忆`,
'准备读取 Agent 私有记忆。',
() => void openAgentConversation(agent, true, true),
))
) {
setAgentMemoryStatus('等待确认');
return;
}
} catch (error) {
if (agentConversationLoadVersionRef.current !== loadVersion) {
return;
}
setAgentMemoryStatus(
error instanceof Error ? error.message : String(error),
);
return;
}
try {
const result = await invoke<LocalAgentMemoryResult>(
'read_local_agent_memory',
{
projectPath: nextProjectPath,
taskId: agent.id,
},
);
if (agentConversationLoadVersionRef.current !== loadVersion) {
return;
}
setAgentMemoryContent(result.content);
setAgentMemoryStatus(
result.exists ? `已读取:${result.path}` : '私有记忆为空',
);
setCommandLog((current) => [...current, 'memory.agent.read']);
} catch (error) {
if (agentConversationLoadVersionRef.current !== loadVersion) {
return;
}
setAgentMemoryContent('');
setAgentMemoryStatus(
error instanceof Error ? error.message : String(error),
);
}
}
function closeAgentConversation() {
agentConversationLoadVersionRef.current += 1;
agentConversationSavingRef.current = false;
agentConversationBackgroundBusyRef.current = false;
setSelectedAgent(null);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationSessionId(null);
setAgentConversationRunSubmitMode('steer');
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentConversationStatus('未选择 agent');
setAgentConversationSaving(false);
setAgentConversationBackgroundBusy(false);
setAgentMemoryStatus('未读取');
setAgentMemoryContent('');
}
function prepareSuggestedToolCommandDraft(
toolCall: GameCreationAgentToolCallTrace,
) {
const commandDraft = commandDraftFromSuggestedToolCall(toolCall);
if (!commandDraft) {
return;
}
prepareChatCommandDraft(commandDraft);
closeAgentConversation();
}
function prepareChatCommandDraft(commandDraft: string) {
setChatInput(commandDraft);
setChatReferences([]);
setChatContent([]);
window.setTimeout(() => chatInputRef.current?.focus(), 0);
}
function handleChatComposerChange(draft: ChatComposerDraft) {
setChatInput(draft.text);
setChatReferences(draft.references);
setChatContent(draft.content ?? []);
}
useEffect(() => {
const handleResourceReferenceInsert = (event: Event) => {
const detail = (event as CustomEvent<ResourceReferenceInsertEventDetail>)
.detail;
if (!detail?.reference) return;
chatComposerRef.current?.insertReferences([detail.reference]);
chatComposerRef.current?.focus();
};
/**
* 画布拖拽的批量 @ 引用:与单条入口同一个消费点,只是一次把整批插进去。
*
* 逐条派发单条事件也能跑,但每次都会重建一次草稿并重新聚焦,而且插入顺序只能靠事件顺序兜着;
* 批次为空时静默返回(不开一次空事务)。
*/
const handleResourceReferenceInsertMany = (event: Event) => {
const detail = (
event as CustomEvent<ResourceReferenceInsertManyEventDetail>
).detail;
const references = detail?.references ?? [];
if (references.length === 0) return;
chatComposerRef.current?.insertReferences(references);
chatComposerRef.current?.focus();
};
window.addEventListener(
RESOURCE_REFERENCE_INSERT_EVENT,
handleResourceReferenceInsert,
);
window.addEventListener(
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
handleResourceReferenceInsertMany,
);
return () => {
window.removeEventListener(
RESOURCE_REFERENCE_INSERT_EVENT,
handleResourceReferenceInsert,
);
window.removeEventListener(
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
handleResourceReferenceInsertMany,
);
};
}, []);
function prepareProjectAssetRegisterDraft(localPath: string) {
prepareChatCommandDraft(projectFileActionDrafts(localPath).assetCommand);
}
function prepareAgentEvidenceReadDraft(localPath: string) {
if (!isSafeProjectRelativePath(localPath)) {
return;
}
prepareChatCommandDraft(projectFileActionDrafts(localPath).readCommand);
closeAgentConversation();
}
async function handlePickCanvasExportFileDraft() {
if (!requireChatProjectForUserAction()) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内选择画板导出包。' },
]);
return;
}
try {
const selectedPath = await invoke<string | null>('pick_local_file');
if (!selectedPath) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '已取消选择画板导出包。' },
]);
return;
}
setCanvasExportPath(selectedPath);
prepareChatCommandDraft(`/import-canvas-export ${selectedPath} `);
setAssetStatus(`已选择画板导出包:${selectedPath}`);
setMessages((current) => [
...current,
{ role: 'assistant', text: `已选择画板导出包:${selectedPath}` },
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function saveAgentConversationMessage(
agent: AgentStatusCard,
content: string,
skipPolicyConfirm = false,
) {
if (!agent || !content || agentConversationSavingRef.current) {
return;
}
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'conversation.write',
nextProjectPath,
`写入 ${agent.title} Agent 对话`,
'准备保存 Agent 对话。',
() => void saveAgentConversationMessage(agent, content, true),
))
) {
setAgentConversationStatus('等待确认');
return;
}
} catch (error) {
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
return;
}
agentConversationSavingRef.current = true;
setAgentConversationSaving(true);
setAgentConversationInput('');
setAgentConversationStatus('正在保存用户消息');
let savedUserResult: LocalConversationResult | null = null;
let stopStreamListen: (() => void) | null = null;
let streamListenDisposed = false;
let streamListenReady = false;
try {
savedUserResult = await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: agent.id,
message: {
role: 'user',
content,
agentId: null,
},
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const savedUserMessages = savedUserResult.messages;
setAgentConversationMessages(savedUserMessages);
setAgentConversationStatus('正在连接 Agent LLM');
const streamRunId = createAgentChatRunId('agent-conversation');
if (canSubscribeTauriEvents()) {
try {
stopStreamListen =
await subscribeTauriEvent<GameCreatorRoleAgentChatStreamEvent>(
'game-creator-role-agent-chat-stream',
(event) => {
const payload = event.payload;
if (
payload.projectPath !== nextProjectPath ||
payload.agentId !== agent.id ||
payload.runId !== streamRunId ||
agentConversationLoadVersionRef.current !== saveVersion
) {
return;
}
if (payload.runtimeState) {
setAgentConversationRuntime((current) => {
const nextRuntime = normalizeAgentRuntimeState(
payload.runtimeState!,
current,
);
rememberAgentRuntimeState(nextRuntime);
return nextRuntime;
});
setAgentConversationRuntimeError('');
}
if (payload.status === 'started') {
setAgentConversationStatus(
payload.runtimeSummary ?? 'Agent 已连接,正在等待回复',
);
return;
}
if (payload.status === 'delta') {
const draftText =
payload.accumulatedText || payload.deltaText;
if (draftText) {
setAgentConversationMessages([
...savedUserMessages,
createLocalConversationDraftMessage(draftText),
]);
}
setAgentConversationStatus(
payload.finishReason
? `Agent 回复结束:${payload.finishReason}`
: '正在接收 Agent 回复',
);
return;
}
if (payload.status === 'completed') {
setAgentConversationStatus(
payload.runtimeSummary ?? 'Agent 回复完成,正在保存',
);
return;
}
if (payload.status === 'failed') {
setAgentConversationStatus(
payload.runtimeSummary ??
'Agent 流式回复失败,正在记录错误',
);
}
},
);
streamListenReady = true;
if (streamListenDisposed) {
stopStreamListen();
stopStreamListen = null;
}
} catch {
setAgentConversationStatus('实时状态不可用,正在使用普通回复模式');
}
}
const reply = streamListenReady
? await invoke<GameCreatorChatAgentReply>(
'chat_with_game_creator_role_agent_stream',
{
projectPath: nextProjectPath,
agentId: agent.id,
prompt: content,
runId: streamRunId,
},
)
: await invoke<GameCreatorChatAgentReply>(
'chat_with_game_creator_role_agent',
{
projectPath: nextProjectPath,
agentId: agent.id,
prompt: content,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current === saveVersion) {
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
}
} catch (error) {
if (agentConversationLoadVersionRef.current === saveVersion) {
setAgentConversationRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentConversationStatus('正在保存 Agent 回复');
setAgentConversationMessages([
...savedUserMessages,
createLocalConversationDraftMessage(reply.replyText),
]);
const assistantResult = await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: agent.id,
message: {
role: 'assistant',
content: reply.replyText,
agentId: null,
},
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(assistantResult.messages);
setAgentConversationStatus(
`已保存 ${assistantResult.messages.length} 条:${assistantResult.path}`,
);
setCommandLog((current) => [...current, 'conversation.write']);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
void captureAgentRuntimeError(error, agent.id);
if (savedUserResult) {
const message = `已保存用户消息;Agent 回复失败:${
error instanceof Error ? error.message : String(error)
}`;
try {
const errorResult = await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: agent.id,
message: {
role: 'assistant',
content: message,
agentId: null,
},
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(errorResult.messages);
} catch {
setAgentConversationMessages([
...savedUserResult.messages,
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: message,
agentId: null,
updatedAt: Date.now(),
},
]);
}
setAgentConversationStatus(message);
} else {
setAgentConversationInput(content);
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
}
} finally {
streamListenDisposed = true;
stopStreamListen?.();
agentConversationSavingRef.current = false;
setAgentConversationSaving(false);
}
}
async function startSelectedAgentBackgroundTask(
agent: AgentStatusCard,
content: string,
skipPolicyConfirm = false,
submitMode = agentConversationRunSubmitMode,
) {
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
return;
}
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const steerRuntime =
submitMode === 'steer'
? matchingAgentRuntimeForSteer(
[agentConversationRuntime],
agent.id,
agentConversationSessionId,
)
: null;
const saveVersion = agentConversationLoadVersionRef.current;
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'conversation.write',
nextProjectPath,
steerRuntime
? `向 ${agent.title} 当前 Run 追加指令`
: `启动 ${agent.title} 后台任务`,
steerRuntime
? '准备向当前 Agent Run 追加指令。'
: '准备启动 Agent 后台任务。',
() =>
void startSelectedAgentBackgroundTask(
agent,
content,
true,
submitMode,
),
))
) {
setAgentConversationStatus('等待确认');
return;
}
} catch (error) {
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
return;
}
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
if (steerRuntime) {
setAgentConversationStatus(
`正在向当前 Run 追加指令:${steerRuntime.runId}`,
);
} else {
setAgentConversationInput('');
setAgentConversationStatus('正在启动 Agent 后台任务');
}
try {
let runtime: AgentRuntimeResult;
let successStatus: string;
if (steerRuntime) {
const steer = await invoke<AgentRuntimeSteerResult>(
'steer_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
sessionId: steerRuntime.sessionId,
runId: steerRuntime.runId,
steerId: createAgentChatRunId('agent-steer'),
instruction: content,
},
);
runtime = steer.runtime;
successStatus = agentRuntimeSteerStatus(steer);
} else {
runtime = await invoke<AgentRuntimeResult>(
'start_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
task: content,
runId: createAgentChatRunId('agent-background-task'),
...(agentConversationSessionId
? { sessionId: agentConversationSessionId }
: {}),
},
);
successStatus = agentRuntimeStartStatus(runtime);
}
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationInput('');
setAgentConversationRunSubmitMode('steer');
setAgentConversationSessionId(nextRuntime.sessionId);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
try {
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
sessionId: nextRuntime.sessionId,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(conversation.messages);
setAgentConversationSessionId(
conversation.sessionId ?? nextRuntime.sessionId,
);
} catch (error) {
if (agentConversationLoadVersionRef.current === saveVersion) {
setAgentConversationRuntimeError(
`对话刷新失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
setAgentConversationStatus(successStatus);
setCommandLog((current) => [
...current,
steerRuntime ? 'agent.runtime.steer' : 'agent.runtime.background_task',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationInput(content);
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function cancelSelectedAgentRuntimeTask(
agent: AgentStatusCard,
runId: string,
) {
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
setAgentConversationStatus('正在取消 Agent 后台任务');
try {
const runtime = await invoke<AgentRuntimeResult>(
'cancel_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
runId,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
setAgentConversationStatus(agentRuntimeCancelStatus(nextRuntime, runId));
setCommandLog((current) => [
...current,
'agent.runtime.background_task.cancel',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function retrySelectedAgentRuntimeTask(
agent: AgentStatusCard,
runId: string,
) {
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
setAgentConversationStatus('正在重试 Agent 后台任务');
try {
const runtime = await invoke<AgentRuntimeResult>(
'retry_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
runId,
nextRunId: createAgentChatRunId('agent-background-retry'),
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(conversation.messages);
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
setCommandLog((current) => [
...current,
'agent.runtime.background_task.retry',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function confirmSelectedAgentRuntimeTask(
agent: AgentStatusCard,
runId: string,
actionId: string,
) {
if (
!agent ||
!runId ||
!actionId ||
agentConversationBackgroundBusyRef.current
) {
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
setAgentConversationStatus('正在确认并继续 Agent 后台任务');
try {
const runtime = await invoke<AgentRuntimeResult>(
'confirm_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
runId,
actionId,
note: '开发者已确认待执行工具动作',
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(conversation.messages);
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
setCommandLog((current) => [
...current,
'agent.runtime.tool_confirmation.approved',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function rejectSelectedAgentRuntimeTask(
agent: AgentStatusCard,
runId: string,
actionId: string,
) {
if (
!agent ||
!runId ||
!actionId ||
agentConversationBackgroundBusyRef.current
) {
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentConversationStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
setAgentConversationStatus('正在拒绝工具动作并继续 Agent 后台任务');
try {
const runtime = await invoke<AgentRuntimeResult>(
'reject_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: agent.id,
runId,
actionId,
note: '开发者拒绝待执行工具动作',
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(runtime);
setAgentConversationRuntime(nextRuntime);
rememberAgentRuntimeState(nextRuntime);
setAgentConversationRuntimeError('');
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(conversation.messages);
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
setCommandLog((current) => [
...current,
'agent.runtime.tool_confirmation.rejected',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function answerSelectedAgentRuntimeUserInput(
agent: AgentStatusCard,
request: AgentRuntimeUserInputRequest,
responseId: string,
answers: Record<string, string>,
) {
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
const currentRequest = agentConversationRuntime?.userInputRequest;
if (
!invoke ||
!nextProjectPath ||
!responseId ||
agentConversationBackgroundBusyRef.current
) {
return;
}
if (
request.agentId !== agent.id ||
request.sessionId !== agentConversationSessionId ||
request.runId !== agentConversationRuntime?.runId ||
currentRequest?.requestId !== request.requestId ||
currentRequest?.actionId !== request.actionId
) {
setAgentConversationRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
agentConversationBackgroundBusyRef.current = true;
setAgentConversationBackgroundBusy(true);
setAgentConversationRuntimeError('');
setAgentConversationStatus('正在提交回答并继续当前 Run');
try {
const result = await invoke<AgentRuntimeResult>(
'answer_game_creator_agent_runtime_user_input',
{
projectPath: nextProjectPath,
agentId: agent.id,
runId: request.runId,
actionId: request.actionId,
requestId: request.requestId,
responseId,
answers,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const runtimeState = agentRuntimeStateFromResult(
result,
agentConversationRuntime,
);
if (
runtimeState.agentId !== agent.id ||
runtimeState.sessionId !== request.sessionId ||
runtimeState.runId !== request.runId
) {
throw new Error('Agent 回答后 Runtime 身份不匹配');
}
setAgentConversationRuntime(runtimeState);
rememberAgentRuntimeState(runtimeState);
setAgentConversationSessionId(runtimeState.sessionId);
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: agent.id,
sessionId: runtimeState.sessionId,
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(conversation.messages);
setAgentConversationSessionId(
conversation.sessionId ?? runtimeState.sessionId,
);
setAgentConversationRuntimeError('');
setAgentConversationStatus(agentRuntimeConversationStatus(runtimeState));
setCommandLog((current) => [
...current,
'agent.runtime.user_input.answered',
]);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
const message = error instanceof Error ? error.message : String(error);
setAgentConversationRuntimeError(message);
setAgentConversationStatus(`回答提交失败:${message}`);
} finally {
agentConversationBackgroundBusyRef.current = false;
setAgentConversationBackgroundBusy(false);
}
}
async function saveSelectedAgentPrivateMemory(
agent: AgentStatusCard,
content: string,
skipPolicyConfirm = false,
) {
if (!agent || !content || agentConversationSavingRef.current) {
return;
}
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke) {
setAgentMemoryStatus('需要在 Tauri App 内运行');
return;
}
if (!nextProjectPath) {
setAgentMemoryStatus('请先初始化本地项目');
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
try {
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.deniedCommands.includes('memory.write')) {
const message = '项目权限策略拒绝执行:memory.write';
markProjectPolicyDenied('memory.write', message);
setAgentMemoryStatus(message);
setCommandLog((current) => [
...current,
'permission.deny memory.write',
]);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
return;
}
if (policyView.policy.confirmCommands.includes('memory.write')) {
requestProjectPolicyConfirmation(
'memory.write',
nextProjectPath,
`写入 ${agent.title} Agent 私有记忆`,
() => void saveSelectedAgentPrivateMemory(agent, content, true),
);
setAgentMemoryStatus('等待确认');
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备保存 Agent 私有记忆。' },
]);
return;
}
}
} catch (error) {
setAgentMemoryStatus(
error instanceof Error ? error.message : String(error),
);
return;
}
agentConversationSavingRef.current = true;
setAgentConversationSaving(true);
setAgentConversationInput('');
setAgentMemoryStatus('正在保存');
try {
const result = await invoke<LocalAgentMemoryResult>(
'write_local_agent_memory',
{
projectPath: nextProjectPath,
taskId: agent.id,
content: appendMemoryContent(agentMemoryContent, content),
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentMemoryContent(result.content);
setAgentMemoryStatus(`已追加私有记忆:${result.path}`);
setAgentConversationStatus(`已写入 ${agent.title} 私有记忆。`);
setCommandLog((current) => [...current, 'memory.agent.write']);
} catch (error) {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationInput(content);
setAgentMemoryStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
agentConversationSavingRef.current = false;
setAgentConversationSaving(false);
}
}
function handleAgentConversationSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const agent = selectedAgent;
const content = agentConversationInput.trim();
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
return;
}
if (!agent || !content || agentConversationSavingRef.current) {
return;
}
void saveAgentConversationMessage(agent, content);
}
function handleAgentPrivateMemorySubmit() {
const agent = selectedAgent;
const content = agentConversationInput.trim();
if (!agent || !content || agentConversationSavingRef.current) {
return;
}
void saveSelectedAgentPrivateMemory(agent, content);
}
function handleAgentBackgroundTaskSubmit() {
const agent = selectedAgent;
const content = agentConversationInput.trim();
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
return;
}
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
return;
}
void startSelectedAgentBackgroundTask(agent, content);
}
function showChatHelp() {
setCommandLog((current) => [...current, 'help.show']);
setMessages((current) => [
...current,
{
role: 'assistant',
text: `可用命令:\n${chatCommandHelp.join('\n')}`,
},
]);
}
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const prompt = chatInput.trim();
const references = chatReferences;
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
if ((!prompt && references.length === 0) || chatAgentBusy) {
return;
}
setChatInput('');
setChatReferences([]);
setChatContent([]);
setMessages((current) => [
...current,
{
role: 'user',
text: prompt,
runtimeOwned: !prompt.startsWith('/'),
},
]);
if (prompt === '/help') {
showChatHelp();
return;
}
if (prompt === '/config') {
handleRuntimeConfigOpen();
return;
}
if (prompt === '/capabilities' || prompt === '/能力') {
void executeAgentCapabilitiesChat();
return;
}
if (prompt === '/audit' || prompt === '/审计') {
void executeAgentAuditChat();
return;
}
const missingArgumentMessage = missingChatCommandArgumentMessage(prompt);
if (missingArgumentMessage) {
setMessages((current) => [
...current,
{ role: 'assistant', text: missingArgumentMessage },
]);
return;
}
if (prompt.startsWith('/project ')) {
const nextProjectPath = prompt.slice('/project '.length).trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供本地项目绝对路径。' },
]);
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '本地项目路径不能包含控制字符。' },
]);
return;
}
queuePendingCommand({
id: 'project.create',
projectPath: nextProjectPath,
});
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备初始化本地项目:${nextProjectPath}` },
]);
return;
}
if (prompt.startsWith('/generate ') || prompt.startsWith('/draft ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const generationPrompt = prompt
.slice(
prompt.startsWith('/generate ')
? '/generate '.length
: '/draft '.length,
)
.trim();
if (!generationPrompt) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '格式:/generate 创作想法' },
]);
return;
}
queuePendingCommand({
id: 'game.generate_draft',
prompt: generationPrompt,
});
setMessages((current) => [
...current,
{
role: 'assistant',
text: `准备生成本地游戏草案:${generationPrompt}`,
},
]);
return;
}
if (prompt === '/status') {
void executeProjectStatus(true);
return;
}
if (
handleProjectSummaryChatCommand({
agentRunHistory,
agentRunTrace,
agentStatusCards,
manifest,
prompt,
requireChatProjectForUserAction,
setMessages,
})
) {
return;
}
if (prompt === '/index') {
void queueOrExecuteProjectIndex();
return;
}
if (prompt === '/checkpoint') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'project.checkpoint' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备保存当前项目快照。' },
]);
return;
}
if (prompt === '/checkpoints') {
void executeProjectCheckpoints(true);
return;
}
if (prompt.startsWith('/diff ')) {
const checkpointId = prompt.slice('/diff '.length).trim();
if (!checkpointId) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '格式:/diff checkpoint-id' },
]);
return;
}
if (!isSafeCheckpointId(checkpointId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: 'checkpoint id 非法。' },
]);
return;
}
void executeProjectDiff(checkpointId, true);
return;
}
if (prompt.startsWith('/restore ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const checkpointId = prompt.slice('/restore '.length).trim();
if (!checkpointId) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '格式:/restore checkpoint-id' },
]);
return;
}
if (!isSafeCheckpointId(checkpointId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: 'checkpoint id 非法。' },
]);
return;
}
queuePendingCommand({ id: 'project.restore', checkpointId });
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备回滚到 checkpoint:${checkpointId}` },
]);
return;
}
if (prompt === '/policy') {
void executeProjectPolicyRead(true);
return;
}
if (
prompt.startsWith('/policy-deny ') ||
prompt.startsWith('/policy-allow ') ||
prompt.startsWith('/policy-confirm ') ||
prompt.startsWith('/policy-auto ')
) {
if (!requireChatProjectForUserAction()) {
return;
}
const mode = prompt.startsWith('/policy-deny ')
? 'deny'
: prompt.startsWith('/policy-allow ')
? 'allow'
: prompt.startsWith('/policy-confirm ')
? 'confirm'
: 'auto';
const commandPrefix =
mode === 'deny'
? '/policy-deny '
: mode === 'allow'
? '/policy-allow '
: mode === 'confirm'
? '/policy-confirm '
: '/policy-auto ';
const commandId = prompt.slice(commandPrefix.length).trim();
if (!commandId) {
setMessages((current) => [
...current,
{
role: 'assistant',
text:
mode === 'deny' || mode === 'allow'
? '格式:/policy-deny file.write'
: '格式:/policy-confirm project.index',
},
]);
return;
}
if (!isRegisteredGameCreationCommandId(commandId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `未知内置命令:${commandId}` },
]);
return;
}
if (
(mode === 'confirm' || mode === 'auto') &&
!isProjectPolicyConfirmableCommandId(commandId)
) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
}
void queueProjectPolicyMutation(commandId, mode);
return;
}
if (
prompt.startsWith('/agent-policy-deny ') ||
prompt.startsWith('/agent-policy-allow ') ||
prompt.startsWith('/agent-policy-confirm ') ||
prompt.startsWith('/agent-policy-auto ')
) {
if (!requireChatProjectForUserAction()) {
return;
}
const mode = prompt.startsWith('/agent-policy-deny ')
? 'deny'
: prompt.startsWith('/agent-policy-allow ')
? 'allow'
: prompt.startsWith('/agent-policy-confirm ')
? 'confirm'
: 'auto';
const commandPrefix =
mode === 'deny'
? '/agent-policy-deny '
: mode === 'allow'
? '/agent-policy-allow '
: mode === 'confirm'
? '/agent-policy-confirm '
: '/agent-policy-auto ';
const args = prompt.slice(commandPrefix.length).trim();
const [agentId = '', commandId = ''] = args.split(/\s+/, 2);
if (!agentId || !commandId) {
setMessages((current) => [
...current,
{
role: 'assistant',
text:
mode === 'deny' || mode === 'allow'
? '格式:/agent-policy-deny design-director file.read'
: '格式:/agent-policy-confirm design-director memory.write',
},
]);
return;
}
if (!isRegisteredGameCreationCommandId(commandId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `未知内置命令:${commandId}` },
]);
return;
}
if (
(mode === 'confirm' || mode === 'auto') &&
!isProjectPolicyConfirmableCommandId(commandId)
) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
}
void queueAgentPolicyMutation(agentId, commandId, mode);
return;
}
if (prompt === '/llm-status') {
void executeLlmConfigStatus();
return;
}
if (prompt === '/llm-routes') {
void executeLlmRouteSummary();
return;
}
if (prompt === '/open-project' || prompt === '/show-project') {
if (!requireChatProjectForUserAction()) {
return;
}
void handleRevealCurrentProjectDirectory();
return;
}
if (prompt === '/switch-project') {
void handleOpenLauncherWindow();
return;
}
if (prompt === '/files') {
void executeProjectFiles(true);
return;
}
if (prompt === '/assets') {
void executeProjectAssets(true);
return;
}
if (prompt === '/credits') {
if (!requireChatProjectForUserAction()) {
return;
}
const summary = summarizeProjectAssetCredits(manifest);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
return;
}
if (prompt === '/art') {
if (!requireChatProjectForUserAction()) {
return;
}
const summary = summarizeProjectVisualAssets(manifest);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
return;
}
if (prompt === '/audio') {
if (!requireChatProjectForUserAction()) {
return;
}
const summary = summarizeProjectAudioAssets(manifest);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
return;
}
if (prompt === '/artifacts') {
if (!requireChatProjectForUserAction()) {
return;
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeCommonProjectArtifactReadDrafts(),
draftCommand: `/read ${commonProjectArtifactReadDrafts[0].path}`,
draftCommandLabel: commonProjectArtifactReadDrafts[0].label,
},
]);
return;
}
if (prompt === '/run-artifacts') {
if (!requireChatProjectForUserAction()) {
return;
}
const artifacts = agentRunTrace
? readableArtifactsFromAgentRunTrace(agentRunTrace)
: [];
const firstArtifact = artifacts[0] ?? null;
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeRunArtifactReadDrafts(agentRunTrace),
draftCommand: firstArtifact
? `/read ${firstArtifact.path}`
: undefined,
draftCommandLabel: firstArtifact ? '读取首个 Run 产物' : undefined,
},
]);
return;
}
if (prompt === '/passes') {
if (!requireChatProjectForUserAction()) {
return;
}
const artifacts = agentRunTrace
? readablePassArtifactsFromAgentRunTrace(agentRunTrace)
: [];
const firstArtifact = artifacts[0] ?? null;
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentPassArtifactReadDrafts(agentRunTrace),
draftCommand: firstArtifact
? `/read ${firstArtifact.path}`
: undefined,
draftCommandLabel: firstArtifact ? '读取首个轮次产物' : undefined,
},
]);
return;
}
if (prompt === '/runs') {
if (!requireChatProjectForUserAction()) {
return;
}
const firstHistory = agentRunHistory[0] ?? null;
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentRunHistoryReadDrafts(
agentRunTrace,
agentRunHistory,
),
draftCommand: firstHistory
? `/read ${firstHistory.path}`
: agentRunTrace
? '/trace'
: undefined,
draftCommandLabel: firstHistory
? '读取首个历史 Run'
: agentRunTrace
? '查看最近 Run'
: undefined,
},
]);
return;
}
if (prompt === '/run-files') {
if (!requireChatProjectForUserAction()) {
return;
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentRunSupportFileReadDrafts(),
draftCommand: `/read ${commonAgentRunSupportReadDrafts[0].path}`,
draftCommandLabel: commonAgentRunSupportReadDrafts[0].label,
},
]);
return;
}
if (prompt === '/internals') {
if (!requireChatProjectForUserAction()) {
return;
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectInternalReadDrafts(),
draftCommand: `/read ${commonProjectInternalReadDrafts[0].path}`,
draftCommandLabel: commonProjectInternalReadDrafts[0].label,
},
]);
return;
}
if (prompt === '/logs') {
if (!requireChatProjectForUserAction()) {
return;
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeCommonProjectLogReadDrafts(),
draftCommand: `/read ${commonProjectLogReadDrafts[0].path}`,
draftCommandLabel: commonProjectLogReadDrafts[0].label,
},
]);
return;
}
if (prompt.startsWith('/asset-register ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const [
localPath,
kind = 'unknown',
mediaType = 'application/octet-stream',
] = prompt.slice('/asset-register '.length).trim().split(/\s+/);
if (!localPath) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '格式:/asset-register assets/hero.png [kind] [mediaType]',
},
]);
return;
}
if (!isSafeProjectRelativePath(localPath)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '资产路径必须是项目内相对路径。' },
]);
return;
}
queuePendingCommand({
id: 'asset.register',
localPath,
kind: parseGameCreationAppAssetKind(kind, 'chat.asset-register.kind'),
mediaType,
});
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备登记项目资产:${localPath}` },
]);
return;
}
if (prompt.startsWith('/read ')) {
const relativePath = prompt.slice('/read '.length).trim();
if (!relativePath) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '格式:/read game/index.html' },
]);
return;
}
if (!isSafeProjectRelativePath(relativePath)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '文件路径必须是项目内相对路径。' },
]);
return;
}
void executeProjectFileReadChat(relativePath);
return;
}
if (prompt === '/tasks') {
void executeProjectTasks(true);
return;
}
if (prompt === '/agents') {
if (!requireChatProjectForUserAction()) {
return;
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentStatusCardsForChat(
agentStatusCards,
llmConfigStatus,
),
},
]);
return;
}
if (prompt === '/agent-conversations') {
if (!requireChatProjectForUserAction()) {
return;
}
const drafts = agentConversationReadDraftsFromManifest(manifest);
const firstDraft = drafts[0] ?? null;
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentConversationReadDrafts(manifest),
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
draftCommandLabel: firstDraft
? `读取${firstDraft.task.title}对话`
: undefined,
},
]);
return;
}
if (prompt === '/agent-memories') {
if (!requireChatProjectForUserAction()) {
return;
}
const drafts = agentMemoryReadDraftsFromManifest(manifest);
const firstDraft = drafts[0] ?? null;
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentMemoryReadDrafts(manifest),
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
draftCommandLabel: firstDraft
? `读取${firstDraft.task.title}记忆`
: undefined,
},
]);
return;
}
if (prompt === '/trace' || prompt === '/loop') {
void executeAgentTraceChat();
return;
}
if (prompt === '/agent-status') {
void executeAgentRunControl('status', undefined, true);
return;
}
if (prompt === '/agent-kill') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'agent.kill' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备标记最近 run 为 killed。' },
]);
return;
}
if (prompt === '/agent-retry') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'agent.retry' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备重新运行最近 run 的目标。' },
]);
return;
}
if (prompt === '/agent-resume' || prompt.startsWith('/agent-resume ')) {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({
id: 'agent.resume',
detail: prompt.slice('/agent-resume'.length).trim(),
});
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备恢复最近 run。' },
]);
return;
}
if (prompt === '/history') {
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
void loadProjectConversation(nextProjectPath, false, 'replace');
return;
}
if (prompt === '/commands' || prompt === '/limited-commands') {
void executeLimitedCommandList();
return;
}
if (prompt === '/smoke') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({
id: 'command.run_limited',
commandId: 'game.static_smoke',
});
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备运行静态入口自检。' },
]);
return;
}
if (prompt === '/run') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'game.run_local' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备运行当前本地游戏。' },
]);
return;
}
if (prompt === '/export') {
queueProjectExportPackageShortcut();
return;
}
if (prompt === '/exports') {
void executeProjectExportPackages(true);
return;
}
if (prompt === '/preview') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'preview.start' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备启动本地预览。' },
]);
return;
}
if (prompt === '/open-preview') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'preview.open' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备打开当前本地预览。' },
]);
return;
}
if (prompt === '/preview-status') {
void executePreviewStatus(true);
return;
}
if (prompt === '/preview-stop') {
if (!requireChatProjectForUserAction()) {
return;
}
void executePreviewStop(true);
return;
}
if (prompt === '/memory' || prompt.startsWith('/memory ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const scope = parseOptionalMemoryScope(prompt.slice('/memory'.length));
if (!scope) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '格式:/memory [short|long|blackboard]',
},
]);
return;
}
void executeMemoryReadChat(scope);
return;
}
if (prompt.startsWith('/remember ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const { scope, content } = parseRememberInput(
prompt.slice('/remember '.length),
);
if (!content) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供要追加的记忆内容。' },
]);
return;
}
queuePendingCommand({
id: 'memory.write',
scope,
content,
mode: 'append',
});
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备追加${memoryScopeLabel(scope)}记忆。` },
]);
return;
}
if (prompt.startsWith('/memory-set ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const { scope, content } = parseRememberInput(
prompt.slice('/memory-set '.length),
);
if (!content) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供要保存的记忆内容。' },
]);
return;
}
queuePendingCommand({
id: 'memory.write',
scope,
content,
mode: 'replace',
});
setMessages((current) => [
...current,
{
role: 'assistant',
text: `准备覆盖保存${memoryScopeLabel(scope)}记忆。`,
},
]);
return;
}
if (prompt === '/forget-memory' || prompt.startsWith('/forget-memory ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const scope = parseOptionalMemoryScope(
prompt.slice('/forget-memory'.length),
);
if (!scope) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '格式:/forget-memory [short|long|blackboard]',
},
]);
return;
}
queuePendingCommand({ id: 'memory.delete', scope });
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备删除${memoryScopeLabel(scope)}记忆。` },
]);
return;
}
if (prompt.startsWith('/canvas ')) {
const canvasProjectId = prompt.slice('/canvas '.length).trim();
if (!canvasProjectId) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供画板项目 ID。' },
]);
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
]);
return;
}
queuePendingCommand({ id: 'canvas.project_open', canvasProjectId });
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备打开画板项目:${canvasProjectId}` },
]);
return;
}
if (prompt.startsWith('/sync-canvas-project ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const canvasProjectId = prompt
.slice('/sync-canvas-project '.length)
.trim();
if (!canvasProjectId) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供画板项目 ID。' },
]);
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
]);
return;
}
queuePendingCommand({ id: 'canvas.project_sync', canvasProjectId });
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备同步画板项目资源:${canvasProjectId}` },
]);
return;
}
if (prompt.startsWith('/generate-art ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const generationPrompt = prompt.slice('/generate-art '.length).trim();
if (!generationPrompt) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请提供美术生成提示词。' },
]);
return;
}
queuePendingCommand({
id: 'canvas.asset_generate',
prompt: generationPrompt,
});
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备生成首版美术素材。' },
]);
return;
}
if (prompt.startsWith('/import-canvas-asset ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const [localPath, canvasProjectId, canvasAssetId, kind, mediaType] =
prompt.slice('/import-canvas-asset '.length).trim().split(/\s+/);
if (!localPath || !canvasProjectId || !canvasAssetId) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID',
},
]);
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
]);
return;
}
if (!isSafeProjectRelativePath(localPath)) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '画板资产路径必须是项目内相对路径。',
},
]);
return;
}
const canvasAssetObjectId = canvasAssetId.startsWith('object:')
? canvasAssetId.slice('object:'.length).trim()
: '';
if (canvasAssetId.startsWith('object:') && !canvasAssetObjectId) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '请提供 object: 后面的资产对象 ID。',
},
]);
return;
}
queuePendingCommand({
id: 'canvas.asset_import',
localPath,
canvasProjectId,
canvasAssetId: canvasAssetObjectId ? '' : canvasAssetId,
canvasAssetObjectId,
kind: parseGameCreationAppAssetKind(
kind ?? 'unknown',
'chat.canvas-asset-import.kind',
),
mediaType: mediaType ?? 'application/octet-stream',
});
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备导入画板资产:${localPath}` },
]);
return;
}
if (prompt.startsWith('/import-canvas-export ')) {
if (!requireChatProjectForUserAction()) {
return;
}
const [exportPath, canvasProjectId] = prompt
.slice('/import-canvas-export '.length)
.trim()
.split(/\s+/);
if (!exportPath || !canvasProjectId) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID',
},
]);
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
]);
return;
}
if (
!isAbsoluteProjectPath(exportPath) ||
projectPathHasControlCharacter(exportPath)
) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: '画板导出 ZIP 路径必须是绝对路径。',
},
]);
return;
}
queuePendingCommand({
id: 'canvas.export_import',
exportPath,
canvasProjectId,
});
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备导入画板导出包:${exportPath}` },
]);
return;
}
if (prompt.startsWith('/')) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `未知命令:${prompt}。输入 /help 查看可用命令。`,
},
]);
return;
}
const clientTurnId = createDirectCodexConversationTurnId();
const userItem = chatComposerDraftToDirectCodexUserItem(
{ text: prompt, references, content: chatContent },
directCodexConversationMessageId(clientTurnId, 'user'),
);
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
}
async function executeLlmConfigStatus() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
setLlmConfigStatus(status);
setCommandLog((current) => [...current, 'llm.config_check']);
const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine);
const summary = status.configured
? `${formatLlmRouteEndpoint(status)}。`
: `官方智能服务未就绪;请登录或重新登录后重试。${formatCodexRuntimeCapabilities(status)},账号状态 ${
status.accountCredentialState === 'login_required'
? '需要登录'
: '暂不可用'
}。`;
setMessages((current) => [
...current,
{
role: 'assistant',
text: [summary, ...agentLines].join('\n'),
},
]);
} catch (error) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function executeLlmRouteSummary() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
setLlmConfigStatus(status);
setCommandLog((current) => [...current, 'llm.route_summary']);
const summary = summarizeAgentLlmRoutes(status);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
} catch (error) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function executeAgentCapabilitiesChat() {
const invoke = resolveTauriInvoke();
let capabilities: readonly GameCreationAgentCapabilityDescriptor[] =
GAME_CREATION_AGENT_CAPABILITIES;
if (invoke) {
try {
const nativeCapabilities = await invoke<
GameCreationAgentCapabilityDescriptor[]
>('get_game_creation_agent_capabilities');
if (nativeCapabilities.length > 0) {
capabilities = nativeCapabilities;
}
} catch {
capabilities = GAME_CREATION_AGENT_CAPABILITIES;
}
}
setCommandLog((current) => [...current, 'agent.capabilities']);
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeAgentCapabilities(capabilities) },
]);
}
async function resolveProjectSupervisorSessionForSubmission(
invoke: TauriInvoke,
nextProjectPath: string,
) {
const currentSessionId = projectSupervisorSessionIdRef.current;
if (currentSessionId) {
return currentSessionId;
}
const sessionId = await ensureProjectSupervisorActiveSessionId(
invoke,
nextProjectPath,
);
if (!sessionId) {
throw new Error('项目总控 Agent active Session 不可用');
}
if (localProjectPathRef.current !== nextProjectPath) {
return null;
}
projectSupervisorSessionIdRef.current = sessionId;
setProjectSupervisorSessionId(sessionId);
return sessionId;
}
async function refreshDirectProjectManifest(nextProjectPath: string) {
try {
await refreshManifest(nextProjectPath);
} catch {
// The direct turn result is still authoritative for the chat. A missing
// or legacy manifest must not hide the Codex reply.
}
}
async function executeChatAgentReply({
prompt,
clientTurnId: directConversationTurnId,
creationType,
attachments,
directPolicyChecked = false,
references,
userItem,
}: ExecuteChatAgentReplyInput) {
if (designAgentActiveRef.current) {
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setProjectSupervisorRuntimeError('请先初始化本地项目');
return;
}
if (designAgentActiveRef.current) {
await executeDesignAgentTurn(
nextProjectPath,
{ type: 'message', text: prompt },
directConversationTurnId ?? createAgentChatRunId('design-agent-turn'),
);
return;
}
return;
}
// Product default: send the conversation directly to Codex app-server.
// The legacy Supervisor/harness path remains below for rollback and tests.
if (directCodexProductRuntime) {
const directInvoke = resolveTauriInvoke();
// Capture the project snapshot before any asynchronous policy/session work.
// `resolveChatProjectPath` only returns a path and TypeScript cannot infer
// that the source project is still non-null after an await; keeping the
// immutable snapshot also prevents a project switch from changing the
// projectId used by this turn halfway through submission.
const directProject = localProject;
const directProjectPath = resolveChatProjectPath(directProject);
const directProjectId = directProject?.manifest.projectId;
if (directProjectPath && directProjectId && directInvoke) {
const clientTurnId =
directConversationTurnId ?? createDirectCodexConversationTurnId();
const effectiveUserItem =
userItem ??
chatComposerDraftToDirectCodexUserItem(
{ text: prompt, references: references ?? [], content: [] },
directCodexConversationMessageId(clientTurnId, 'user'),
);
if (
!directPolicyChecked &&
projectConversationWriteConfirmedRef.current !== directProjectPath
) {
// 首轮的入参整体留一份给「确认后重跑」用,不在回调里重列字段。
const policyRetryInput = directCodexPolicyRetryInput({
prompt,
clientTurnId,
creationType,
attachments,
references,
userItem: effectiveUserItem,
});
try {
const policyPaused = await queueProjectPolicyConfirmationIfNeeded(
directInvoke,
'conversation.write',
directProjectPath,
'写入 DirectProject 对话历史',
'DirectProject 对话写入需要确认。',
() => {
projectConversationWriteConfirmedRef.current =
directProjectPath;
void executeChatAgentReply(policyRetryInput);
},
);
if (policyPaused) {
return;
}
projectConversationWriteConfirmedRef.current = directProjectPath;
} catch (error) {
if (localProjectPathRef.current === directProjectPath) {
setProjectSupervisorRuntimeError(
`DirectProject 对话权限检查失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
return;
}
}
const directUserMessageId = directCodexConversationMessageId(
clientTurnId,
'user',
);
const directAssistantMessageId = directCodexConversationMessageId(
clientTurnId,
'assistant',
);
const appendDirectUserMessageIfMissing = (
current: ChatMessage[],
): ChatMessage[] => {
if (
current.some((message) => message.messageId === directUserMessageId)
) {
return current;
}
let optimisticIndex = -1;
for (let index = current.length - 1; index >= 0; index -= 1) {
const message = current[index];
if (
message?.role === 'user' &&
message.text === prompt &&
!message.messageId
) {
optimisticIndex = index;
break;
}
}
if (optimisticIndex >= 0) {
return current.map((message, index) =>
index === optimisticIndex
? {
...message,
runtimeOwned: true,
messageId: directUserMessageId,
updatedAt: Date.now(),
}
: message,
);
}
return [
...current,
{
role: 'user' as const,
text: prompt,
runtimeOwned: true,
messageId: directUserMessageId,
updatedAt: Date.now(),
},
];
};
const appendDirectAssistantMessage = (
current: ChatMessage[],
text: string,
failed = false,
): ChatMessage[] => {
const messageId = failed
? `direct-codex:${clientTurnId}:failure`
: directAssistantMessageId;
const nextMessage: ChatMessage = {
role: 'assistant',
text,
runtimeOwned: true,
messageId,
updatedAt: Date.now(),
};
const withUser = appendDirectUserMessageIfMissing(current);
const existingIndex = withUser.findIndex(
(message) => message.messageId === messageId,
);
if (existingIndex < 0) {
return [...withUser, nextMessage];
}
return withUser.map((message, index) =>
index === existingIndex ? nextMessage : message,
);
};
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const directTurnInput: {
projectPath: string;
prompt: string;
clientTurnId: string;
creationType?: HomeCreationType;
attachments?: DirectCodexTurnAttachment[];
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
} = {
projectPath: directProjectPath,
prompt,
clientTurnId,
userItem: effectiveUserItem,
};
if (creationType) {
directTurnInput.creationType = creationType;
}
if (attachments?.length) {
directTurnInput.attachments = attachments;
}
directTurnInput.userItem = effectiveUserItem;
// 回复正文按条目身份从线程事件 / 历史切片进聊天,本地不再补一条,
// 因此这里只等回合跑完,不接返回值。
await withDirectCodexSessionRefresh(() => {
return directInvoke<string>(
'chat_with_game_creator_direct_codex',
directTurnInput,
);
});
// Rust already persisted the complete raw response items. Invalidate
// any history snapshot captured before the turn completed.
if (localProjectPathRef.current === directProjectPath) {
projectSupervisorHistoryLoadVersionRef.current += 1;
}
if (localProjectPathRef.current === directProjectPath) {
await refreshDirectProjectManifest(directProjectPath);
}
} catch (error) {
if (
isDirectCodexTurnAlreadyRunningError(error) ||
isDirectCodexAnotherTurnRunningError(error)
) {
if (localProjectPathRef.current === directProjectPath) {
setProjectSupervisorRuntimeError(
'陶泥儿仍在处理上一条消息,可在输入盒点「终止」结束它,或等它结束后再发送。',
);
}
return;
}
if (localProjectPathRef.current !== directProjectPath) {
return;
}
if (isDirectCodexTurnInterruptedError(error)) {
// 用户主动终止:不是失败,不写运行错误与诊断,只把回合标记成已终止。
setProjectSupervisorRuntimeError('');
setChatComposerNotice('已终止本次回合');
setMessages((current) =>
appendDirectAssistantMessage(current, '已终止本次回合。', true),
);
return;
}
void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID);
const message =
error instanceof Error ? error.message : String(error);
let persistedDetail = '';
const detailRef = message.match(
/详情:(\.agent\/runtime\/errors\/[^\s;]+)/,
)?.[1];
if (detailRef && directInvoke) {
try {
persistedDetail = await directInvoke<string>(
'read_agent_runtime_error_detail',
{
projectPath: directProjectPath,
detailRef,
},
);
} catch {
persistedDetail = '';
}
}
const visibleMessage = projectRuntimeVisibleError(
persistedDetail ? `${message}\n\n${persistedDetail}` : message,
'陶泥儿智能创作',
true,
);
projectSupervisorHistoryLoadVersionRef.current += 1;
if (localProjectPathRef.current === directProjectPath) {
setProjectSupervisorRuntimeError(visibleMessage);
setMessages((current) =>
appendDirectAssistantMessage(current, visibleMessage, true),
);
}
} finally {
try {
if (localProjectPathRef.current === directProjectPath) {
await refreshManifest(directProjectPath);
}
} finally {
if (localProjectPathRef.current === directProjectPath) {
setChatAgentBusy(false);
setDirectCodexTurnCancelling(false);
chatAgentBusyRef.current = false;
// 本地 invoke 正常收尾仍可直接推进队列;恢复场景则由 turn.completed effect 推进。
dispatchNextQueuedChatTurn();
}
}
}
return;
}
const message = directInvoke
? '当前项目尚未准备好,无法启动智能创作。'
: '需要在 Tauri App 内运行,无法启动智能创作。';
setProjectSupervisorRuntimeError(message);
setMessages((current) => [
...current,
{
role: 'assistant',
text: message,
runtimeOwned: true,
...(directConversationTurnId
? {
messageId: directCodexConversationMessageId(
directConversationTurnId,
'assistant',
),
}
: {}),
updatedAt: Date.now(),
},
]);
return;
}
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const sessionId = await resolveProjectSupervisorSessionForSubmission(
invoke,
nextProjectPath,
);
if (!sessionId || localProjectPathRef.current !== nextProjectPath) {
return;
}
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
const submissionRoute = resolveProjectSupervisorRuntimeSubmission({
workspaceProjectKind,
orchestrationMode,
supervisorChatOnly,
});
const submission = await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: nextProjectPath,
sessionId,
prompt,
runtime: runtimeAtSubmission,
runProfile: submissionRoute.runProfile,
source: submissionRoute.source,
});
const runtimeResult = submission.runtimeResult;
const acceptedRunId = submission.acceptedRunId.trim();
if (!acceptedRunId) {
throw new Error('项目总控 Agent 请求未进入后台队列');
}
setCommandLog((current) => [
...current,
`agent.runtime.${submission.mode} project-supervisor`,
]);
const runtime = agentRuntimeStateFromResult(
runtimeResult,
projectSupervisorRuntimeRef.current,
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
if (
runtime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
runtime.sessionId !== sessionId
) {
throw new Error('项目总控 Agent Runtime Session 身份不匹配');
}
const acceptedRuntimeReady = runtime.runId === acceptedRunId;
if (!acceptedRuntimeReady) {
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
setProjectSupervisorExpectedRunId(acceptedRunId);
}
if (acceptedRuntimeReady) {
projectSupervisorExpectedRunIdRef.current = null;
setProjectSupervisorExpectedRunId(null);
updateProjectSupervisorRuntime(runtime);
updateProjectSupervisorResponseStream(
runtimeResult.responseStream,
runtime,
);
}
setProjectSupervisorRuntimeError('');
const refreshConversation =
projectSupervisorRefreshConversationRef.current;
if (refreshConversation) {
void refreshConversation(invoke, nextProjectPath, sessionId).catch(
(error) => {
if (
localProjectPathRef.current === nextProjectPath &&
projectSupervisorSessionIdRef.current === sessionId
) {
setProjectSupervisorRuntimeError(
`项目总控 Agent 对话刷新失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
},
);
}
if (acceptedRuntimeReady) {
syncTerminalProjectSupervisorConversation(
invoke,
nextProjectPath,
runtime,
);
}
} catch (error) {
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID);
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
setMessages((current) => [
...current,
{
role: 'assistant',
text: message,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
} finally {
setChatAgentBusy(false);
}
}
executeChatAgentReplyRef.current = executeChatAgentReply;
useEffect(() => {
const latch = initialSupervisorMessageLatchRef.current;
if (
(!directCodexProductRuntime && !planningStartMode) ||
!latch.prompt ||
!localProject
) {
return;
}
if (localProject.projectPath !== latch.projectPath) {
// 不能在这里先"占用"这条初始消息:项目路径可能因为分隔符/大小写/时序先落到别的
// 路径上,一旦占用,真正匹配的项目就再也不会收到这条消息,用户的输入被静默丢掉。
// 这里只等待,占用留给下面真正要发送的那一步。
return;
}
if (
chatAgentBusy ||
!claimInitialSupervisorMessageForPage(latch.projectPath, latch.claimScope)
) {
return;
}
supervisorChatShouldFollowLatestRef.current = true;
const directConversationTurnId = directCodexProductRuntime
? createDirectCodexConversationTurnId()
: undefined;
setMessages((current) => [
...current,
{
role: 'user',
text: latch.prompt,
runtimeOwned: true,
...(directConversationTurnId
? {
messageId: directCodexConversationMessageId(
directConversationTurnId,
'user',
),
}
: {}),
updatedAt: Date.now(),
},
]);
void executeChatAgentReplyRef.current({
prompt: latch.prompt,
clientTurnId: directConversationTurnId,
creationType: latch.creationType,
attachments: latch.attachments,
});
}, [
chatAgentBusy,
directCodexProductRuntime,
initialCreationType,
initialSupervisorMessage,
localProject,
planningStartMode,
]);
async function handleProjectSupervisorToolAction(
decision: 'confirm' | 'reject',
) {
const nextProjectPath = resolveChatProjectPath(localProject);
const runtime = projectSupervisorRuntimeRef.current;
const pendingAction = runtime?.pendingToolAction;
const sessionId = projectSupervisorSessionIdRef.current;
const invoke = resolveTauriInvoke();
if (
!invoke ||
!nextProjectPath ||
!runtime ||
!pendingAction?.actionId ||
!sessionId ||
chatAgentBusy
) {
return;
}
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const command =
decision === 'confirm'
? 'confirm_game_creator_agent_runtime_task'
: 'reject_game_creator_agent_runtime_task';
const result = await invoke<AgentRuntimeResult>(command, {
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
runId: runtime.runId,
actionId: pendingAction.actionId,
note:
decision === 'confirm'
? '用户已确认待执行工具动作'
: '用户已拒绝待执行工具动作',
});
const nextRuntime = agentRuntimeStateFromResult(
result,
projectSupervisorRuntimeRef.current,
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId ||
nextRuntime.sessionId !== sessionId ||
nextRuntime.runId !== runtime.runId
) {
return;
}
updateProjectSupervisorRuntime(nextRuntime);
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
setCommandLog((current) => [
...current,
`agent.runtime.${decision} project-supervisor`,
]);
syncTerminalProjectSupervisorConversation(
invoke,
nextProjectPath,
nextRuntime,
);
} catch (error) {
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
} finally {
setChatAgentBusy(false);
}
}
async function handleProjectProfessionalAgentToolAction(
runtime: AgentRuntimeState,
decision: 'confirm' | 'reject',
) {
const nextProjectPath = resolveChatProjectPath(localProject);
const currentRuntime = agentRuntimeById[runtime.agentId];
const pendingAction = currentRuntime?.pendingToolAction;
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
const invoke = resolveTauriInvoke();
if (
!invoke ||
!nextProjectPath ||
!currentRuntime ||
!pendingAction?.actionId ||
chatAgentBusy
) {
throw new Error('专业 Agent 待确认动作已变化,请等待状态刷新');
}
if (
currentRuntime.runId !== runtime.runId ||
pendingAction.actionId !== runtime.pendingToolAction?.actionId ||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
currentRuntime.parentRunId !== supervisorRunId
) {
throw new Error('专业 Agent Runtime 身份已变化,请等待状态刷新');
}
setChatAgentBusy(true);
try {
const command =
decision === 'confirm'
? 'confirm_game_creator_agent_runtime_task'
: 'reject_game_creator_agent_runtime_task';
const result = await invoke<AgentRuntimeResult>(command, {
projectPath: nextProjectPath,
agentId: currentRuntime.agentId,
runId: currentRuntime.runId,
actionId: pendingAction.actionId,
note:
decision === 'confirm'
? '用户已确认专业 Agent 待执行工具动作'
: '用户已拒绝专业 Agent 待执行工具动作',
});
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
) {
return;
}
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
if (
nextRuntime.agentId !== currentRuntime.agentId ||
nextRuntime.runId !== currentRuntime.runId
) {
throw new Error('专业 Agent 操作后 Runtime 身份不匹配');
}
rememberAgentRuntimeState(nextRuntime);
setCommandLog((current) => [
...current,
`agent.runtime.${decision} ${currentRuntime.agentId}`,
]);
} catch (error) {
throw new Error(
`专业 Agent 操作失败:${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
setChatAgentBusy(false);
}
}
async function handleProjectProfessionalAgentRetry(
runtime: AgentRuntimeState,
): Promise<string> {
const nextProjectPath = resolveChatProjectPath(localProject);
const currentRuntime = agentRuntimeById[runtime.agentId];
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
throw new Error('专业 Agent 状态已变化,请等待刷新后重试');
}
const supervisorRuntime = projectSupervisorRuntimeRef.current;
if (!supervisorRuntime || isAgentRuntimeTerminalState(supervisorRuntime)) {
throw new Error('项目总控已停止,请先重试项目总控');
}
if (
currentRuntime.runId !== runtime.runId ||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
currentRuntime.parentRunId !== supervisorRunId ||
!(
currentRuntime.status === 'failed' || currentRuntime.phase === 'failed'
) ||
currentRuntime.pendingToolAction
) {
throw new Error('专业 Agent 当前状态不允许直接重试');
}
if (
projectSupervisorPendingRepairMatchesProfessional(
projectSupervisorRuntimeRef.current?.pendingToolAction,
currentRuntime,
)
) {
await handleProjectSupervisorToolAction('confirm');
return `已确认项目总控安排${projectProfessionalAgentLabel(
currentRuntime.agentId,
)}返工,正在同步新一轮状态`;
}
setChatAgentBusy(true);
try {
const result = await invoke<AgentRuntimeResult>(
'confirm_retry_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: currentRuntime.agentId,
runId: currentRuntime.runId,
nextRunId: createAgentChatRunId('project-professional-retry'),
},
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
) {
return '项目已切换,未把旧项目的重试状态合并到当前界面';
}
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
if (
nextRuntime.agentId !== currentRuntime.agentId ||
nextRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
nextRuntime.parentRunId !== supervisorRunId
) {
throw new Error('专业 Agent 重试后的 Runtime 身份不匹配');
}
rememberAgentRuntimeState(nextRuntime);
setCommandLog((current) => [
...current,
`agent.runtime.retry ${currentRuntime.agentId}`,
]);
return '重试请求已受理,正在同步新一轮状态';
} catch (error) {
throw new Error(
`专业 Agent 重试失败:${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
setChatAgentBusy(false);
}
}
async function handleProjectSupervisorRetry(
runtime: AgentRuntimeState,
): Promise<string> {
const nextProjectPath = resolveChatProjectPath(localProject);
const currentRuntime = projectSupervisorRuntimeRef.current;
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
throw new Error('项目总控状态已变化,请等待刷新后重试');
}
const needsReconciliation =
currentRuntime.status === 'needs-reconciliation' ||
currentRuntime.phase === 'needs-reconciliation';
if (needsReconciliation) {
if (
currentRuntime.runId !== runtime.runId ||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID
) {
throw new Error('项目总控待核对任务已变化,请等待刷新');
}
const previousRunId = currentRuntime.runId;
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const result = await invoke<AgentRuntimeResult>(
'cancel_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
runId: previousRunId,
},
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
) {
return '项目已切换,未把旧项目的取消状态合并到当前界面';
}
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
if (
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
nextRuntime.runId !== previousRunId ||
nextRuntime.sessionId !== currentRuntime.sessionId
) {
throw new Error('项目总控取消后的 Runtime 身份不匹配');
}
updateProjectSupervisorRuntime(nextRuntime);
updateProjectSupervisorResponseStream(
result.responseStream,
nextRuntime,
);
setCommandLog((current) => [
...current,
'agent.runtime.cancel project-supervisor reconciliation',
]);
const queuePending = nextRuntime.taskQueue?.pending ?? 0;
if (
nextRuntime.status === 'cancelled' ||
nextRuntime.phase === 'cancelled'
) {
return queuePending > 0
? `旧任务已结束,队列中还有 ${queuePending} 个待处理任务,队列将继续处理`
: '旧任务已结束,当前队列为空,可重新启动项目总控';
}
return '已提交结束旧任务请求,正在同步取消状态';
} catch (error) {
throw new Error(
`项目总控旧任务结束失败:${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
setChatAgentBusy(false);
}
}
const cancelledWithEmptyQueue =
(currentRuntime.status === 'cancelled' ||
currentRuntime.phase === 'cancelled') &&
(currentRuntime.taskQueue?.pending ?? 0) === 0;
if (
currentRuntime.runId !== runtime.runId ||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
!(
currentRuntime.status === 'failed' ||
currentRuntime.phase === 'failed' ||
cancelledWithEmptyQueue
) ||
currentRuntime.pendingToolAction
) {
throw new Error('项目总控当前状态不允许重试');
}
const previousRunId = currentRuntime.runId;
const nextRunId = createAgentChatRunId('project-supervisor-retry');
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const result = await invoke<AgentRuntimeResult>(
'confirm_retry_game_creator_agent_runtime_task',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
runId: previousRunId,
nextRunId,
},
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
) {
return '项目已切换,未把旧项目的重试状态合并到当前界面';
}
const inferredRunId = agentRuntimeStartedRunId(result, nextRunId);
const acceptedRunId =
result.acceptedRunId?.trim() ||
(inferredRunId === nextRunId ? nextRunId : '');
if (!acceptedRunId) {
throw new Error('项目总控重试请求未进入后台队列');
}
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
setProjectSupervisorExpectedRunId(acceptedRunId);
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
if (nextRuntime.runId === acceptedRunId) {
if (
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
nextRuntime.sessionId !== currentRuntime.sessionId
) {
throw new Error('项目总控重试后的 Runtime 身份不匹配');
}
updateProjectSupervisorRuntime(nextRuntime);
updateProjectSupervisorResponseStream(
result.responseStream,
nextRuntime,
);
projectSupervisorExpectedRunIdRef.current = null;
setProjectSupervisorExpectedRunId(null);
}
setCommandLog((current) => [
...current,
'agent.runtime.retry project-supervisor',
]);
return nextRuntime.runId === acceptedRunId
? '已在当前项目重新启动项目总控'
: '重试已受理,正在同步新一轮项目总控状态';
} catch (error) {
throw new Error(
`项目总控重试失败:${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
setChatAgentBusy(false);
}
}
async function handleProjectSupervisorUserInput(
request: AgentRuntimeUserInputRequest,
responseId: string,
answers: Record<string, string>,
) {
const nextProjectPath = resolveChatProjectPath(localProject);
const runtime = projectSupervisorRuntimeRef.current;
const sessionId = projectSupervisorSessionIdRef.current;
const currentRequest = runtime?.userInputRequest;
const invoke = resolveTauriInvoke();
if (
!invoke ||
!nextProjectPath ||
!runtime ||
!sessionId ||
!responseId ||
chatAgentBusy
) {
return;
}
if (
request.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
request.sessionId !== sessionId ||
request.runId !== runtime.runId ||
currentRequest?.requestId !== request.requestId ||
currentRequest?.actionId !== request.actionId
) {
setProjectSupervisorRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
return;
}
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const result = await invoke<AgentRuntimeResult>(
'answer_game_creator_agent_runtime_user_input',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
runId: request.runId,
actionId: request.actionId,
requestId: request.requestId,
responseId,
answers,
},
);
const nextRuntime = agentRuntimeStateFromResult(
result,
projectSupervisorRuntimeRef.current,
);
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
if (
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
nextRuntime.sessionId !== sessionId ||
nextRuntime.runId !== request.runId
) {
throw new Error('项目总控 Agent 回答后 Runtime 身份不匹配');
}
updateProjectSupervisorRuntime(nextRuntime);
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
const refreshConversation =
projectSupervisorRefreshConversationRef.current;
if (refreshConversation) {
await refreshConversation(invoke, nextProjectPath, sessionId);
}
setCommandLog((current) => [
...current,
'agent.runtime.user_input.answered project-supervisor',
]);
setProjectSupervisorRuntimeError('');
syncTerminalProjectSupervisorConversation(
invoke,
nextProjectPath,
nextRuntime,
);
} catch (error) {
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(`回答提交失败:${message}`);
} finally {
setChatAgentBusy(false);
}
}
async function executeGameDraft(prompt: string) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
setCommandLog((current) => [...current, 'game.generate_draft']);
setProjectStatus('正在调用 LLM');
setMessages((current) => [
...current,
{ role: 'assistant', text: gameDraftStartedMessage() },
]);
const result = await invoke<GenerateLocalGameDraftResult>(
'generate_local_game_draft',
{ projectPath: nextProjectPath, prompt },
);
const generatedProjectPath = result.projectPath.trim();
if (
!generatedProjectPath ||
!isAbsoluteProjectPath(generatedProjectPath) ||
projectPathHasControlCharacter(generatedProjectPath)
) {
const message = '生成结果项目路径无效';
setProjectStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
return;
}
setLocalProject({
projectPath: generatedProjectPath,
manifestPath: `${generatedProjectPath}/.agent/manifest.json`,
manifest: result.manifest,
});
setManifest(result.manifest);
setProjectStatus('已生成可试玩原型');
setCommandLog((current) => [
...current,
'memory.write',
'file.write game/index.html',
]);
const completionSummary =
await refreshAgentRunTrace(generatedProjectPath);
try {
const previewResult = await invoke<LocalPreviewResult>(
'start_local_game_preview',
{ projectPath: generatedProjectPath },
);
updateClientPreview(previewResult);
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
setCommandLog((current) => [...current, 'preview.start']);
void refreshManifest(generatedProjectPath);
setMessages((current) => [
...current,
{
role: 'assistant',
text: [
`已保存并在客户端运行视图启动预览:${previewResult.url}`,
completionSummary?.text,
]
.filter(Boolean)
.join('\n\n'),
draftCommand: completionSummary?.draftCommand,
},
]);
} catch (previewError) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: [
`已保存草案:${result.designPath}。预览启动失败:${
previewError instanceof Error
? previewError.message
: String(previewError)
}`,
completionSummary?.text,
]
.filter(Boolean)
.join('\n\n'),
draftCommand: completionSummary?.draftCommand,
},
]);
}
} catch (error) {
void refreshAgentRunTrace(nextProjectPath);
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setMessages((current) => [
...current,
{
role: 'assistant',
text: message,
},
]);
}
}
function markPendingCommandCanceled(command: PendingCommand) {
if (command.id === 'project.create') {
setWorkspaceStatus('已取消');
setProjectStatus('已取消');
return;
}
if (command.id === 'game.generate_draft') {
setProjectStatus('已取消生成游戏草案');
return;
}
if (
command.id === 'game.run_local' ||
command.id === 'command.run_limited'
) {
setLimitedCommandStatus('已取消运行');
return;
}
if (command.id === 'project.checkpoint') {
setFileStatus('已取消保存 checkpoint');
return;
}
if (command.id === 'project.export_package') {
setFileStatus('已取消导出本地试玩包');
return;
}
if (command.id === 'project.index') {
setFileStatus('已取消索引项目');
setWorkspaceStatus('已取消索引项目');
return;
}
if (command.id === 'project.restore') {
setFileStatus('已取消回滚项目');
return;
}
if (command.id === 'project.policy_write') {
setProjectStatus('已取消修改权限策略');
return;
}
if (command.id === 'preview.start' || command.id === 'preview.open') {
setPreviewStatus('已取消预览操作');
setWorkspaceStatus('已取消预览操作');
return;
}
if (
command.id === 'agent.kill' ||
command.id === 'agent.retry' ||
command.id === 'agent.resume'
) {
setAgentRunStatus('已取消 Agent run 操作');
return;
}
if (command.id === 'memory.write') {
setMemoryStatus('已取消写入项目记忆');
return;
}
if (command.id === 'memory.delete') {
setMemoryStatus('已取消删除项目记忆');
return;
}
if (command.id === 'asset.upload') {
setAssetStatus('已取消上传资产');
return;
}
if (command.id === 'asset.register') {
setAssetStatus('已取消登记资产');
return;
}
if (command.id === 'canvas.project_open') {
setAssetStatus('已取消打开画板');
return;
}
if (command.id === 'canvas.project_sync') {
setAssetStatus('已取消同步画板项目');
return;
}
if (command.id === 'canvas.asset_import') {
setAssetStatus('已取消导入画板资产');
return;
}
if (command.id === 'canvas.asset_generate') {
setAssetStatus('已取消生成美术素材');
return;
}
if (command.id === 'canvas.export_import') {
setAssetStatus('已取消导入画板导出包');
}
}
function handlePendingCommandCancel() {
const command = pendingCommand;
setPendingCommand(null);
if (command) {
markPendingCommandCanceled(command);
appendLocalPermissionLog(
resolvePermissionLogProjectPath(command),
'permission.cancel',
command.id,
);
setCommandLog((current) => [
...current,
`permission.cancel ${command.id}`,
]);
}
setMessages((current) => [
...current,
{ role: 'assistant', text: '已取消。' },
]);
}
async function handlePendingCommandConfirm() {
const command = pendingCommand;
if (!command) {
return;
}
if (
needsInitializedChatProject(command.id) &&
!resolveChatProjectPath(localProject)
) {
setPendingCommand(null);
appendLocalPermissionLog(
resolvePermissionLogProjectPath(command),
'permission.cancel',
command.id,
);
setCommandLog((current) => [
...current,
`permission.cancel ${command.id} missing-project`,
]);
requireChatProjectForUserAction();
return;
}
if (
await denyPendingCommandIfNeeded(
command.id,
resolvePermissionLogProjectPath(command),
)
) {
setPendingCommand(null);
return;
}
setPendingCommand(null);
appendLocalPermissionLog(
resolvePermissionLogProjectPath(command),
'permission.confirm',
command.id,
);
setCommandLog((current) => [
...current,
`permission.confirm ${command.id}`,
]);
if (command.id === 'asset.upload') {
void executeAssetUpload(command.file);
} else if (command.id === 'asset.register') {
void executeAssetRegister(
resolveChatProjectPath(localProject) ?? '',
command.localPath,
command.kind,
command.mediaType,
'generated',
'',
'',
'',
true,
);
} else if (command.id === 'game.run_local') {
void executeRunLocal(true);
} else if (command.id === 'command.run_limited') {
void executeLimitedCommand(command.commandId, true);
} else if (command.id === 'project.create') {
void executeProjectCreate(command.projectPath, true);
} else if (command.id === 'project.checkpoint') {
void executeProjectCheckpoint(true);
} else if (command.id === 'project.export_package') {
void executeProjectExportPackage(true);
} else if (command.id === 'project.index') {
void executeProjectIndex(true);
} else if (command.id === 'project.restore') {
void executeProjectRestore(command.checkpointId, true);
} else if (command.id === 'project.policy_write') {
void executeProjectPolicyWrite(command.policy, true);
} else if (command.id === 'preview.start') {
void executePreviewStart(true);
} else if (command.id === 'preview.open') {
void executePreviewOpen(true);
} else if (
command.id === 'agent.kill' ||
command.id === 'agent.retry' ||
command.id === 'agent.resume'
) {
void executeAgentRunControl(
command.id.slice('agent.'.length),
command.detail,
true,
);
} else if (command.id === 'memory.write') {
void executeMemoryWrite(
command.scope,
command.content,
command.mode ?? 'append',
);
} else if (command.id === 'memory.delete') {
void executeMemoryDeleteChat(command.scope);
} else if (command.id === 'canvas.project_open') {
void executeCanvasProjectOpen(command.canvasProjectId, true);
} else if (command.id === 'canvas.project_sync') {
void executeCanvasProjectSync(command.canvasProjectId, true);
} else if (command.id === 'canvas.asset_import') {
void executeCanvasAssetImport(
null,
command.localPath,
command.kind,
command.mediaType,
command.canvasProjectId,
command.canvasAssetId,
command.canvasAssetObjectId ?? '',
true,
);
} else if (command.id === 'canvas.asset_generate') {
void executeCanvasAssetGenerate(command.prompt, true);
} else if (command.id === 'canvas.export_import') {
void executeCanvasExportImport(
command.exportPath,
command.canvasProjectId,
true,
);
} else if (command.id === 'game.generate_draft') {
void executeGameDraft(command.prompt);
}
}
async function executeProjectCreate(
nextProjectPath: string,
announceToChat: boolean,
) {
const trimmedProjectPath = nextProjectPath.trim();
const invoke = resolveTauriInvoke();
if (
invoke &&
trimmedProjectPath &&
isAbsoluteProjectPath(trimmedProjectPath) &&
!projectPathHasControlCharacter(trimmedProjectPath)
) {
try {
const nonEmpty = await invoke<boolean>(
'is_local_project_directory_non_empty',
{ projectPath: trimmedProjectPath },
);
if (nonEmpty) {
setPendingNonEmptyProjectCreate({
projectPath: trimmedProjectPath,
announceToChat,
});
setWorkspaceStatus('目标文件夹不是空的');
setProjectStatus('等待确认');
return;
}
} catch {
// ponytail: stale test doubles and older shells may miss this helper; init still validates.
}
}
await openWorkspace(nextProjectPath, announceToChat);
}
function cancelProjectCreateInNonEmptyFolder() {
const pendingCreate = pendingNonEmptyProjectCreate;
setPendingNonEmptyProjectCreate(null);
setWorkspaceStatus('已取消');
setProjectStatus('已取消');
if (pendingCreate?.announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已取消在非空文件夹中新建项目:${pendingCreate.projectPath}`,
},
]);
}
}
function confirmProjectCreateInNonEmptyFolder() {
const pendingCreate = pendingNonEmptyProjectCreate;
if (!pendingCreate) {
return;
}
setPendingNonEmptyProjectCreate(null);
void openWorkspace(pendingCreate.projectPath, pendingCreate.announceToChat);
}
async function executeProjectStatus(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setProjectStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'project.status',
nextProjectPath,
`读取 ${nextProjectPath} 的项目状态`,
'准备读取项目状态。',
() => void executeProjectStatus(true, true),
))
) {
return;
}
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath, commandId: 'project.status' },
);
setManifest(nextManifest);
setProjectStatus('已读取状态');
setCommandLog((current) => [...current, 'project.status']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectStatus(nextManifest, nextProjectPath),
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectIndex(announceToChat: boolean) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setLimitedCommandStatus('请先初始化本地项目');
return;
}
try {
const result = await invoke<LocalProjectIndexResult>(
'build_local_project_index',
{ projectPath: nextProjectPath },
);
setFileStatus(`已索引 ${result.fileCount} 个文件`);
setCommandLog((current) => [...current, 'project.index']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectIndex(result) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function queueOrExecuteProjectIndex() {
const invoke = resolveTauriInvoke();
if (!invoke) {
void executeProjectIndex(true);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (result.policy.confirmCommands.includes('project.index')) {
queuePendingCommand({ id: 'project.index' });
setMessages((current) => [
...current,
{ role: 'assistant', text: '准备刷新本地项目索引。' },
]);
return;
}
void executeProjectIndex(true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function executeProjectCheckpoint(announceToChat: boolean) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<LocalProjectCheckpointResult>(
'create_local_project_checkpoint',
{ projectPath: nextProjectPath },
);
setFileStatus(`已保存 checkpoint:${result.checkpointId}`);
setCommandLog((current) => [...current, 'project.checkpoint']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectCheckpoint(result) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectExportPackage(announceToChat: boolean) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<LocalProjectExportPackageResult>(
'export_local_project_package',
{ projectPath: nextProjectPath },
);
setFileStatus(`已导出本地试玩包:${result.packageRelativePath}`);
setCommandLog((current) => [...current, 'project.export_package']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectExportPackage(result),
draftCommand: '/open-project',
draftCommandLabel: '显示目录',
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectExportPackages(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (!nextProjectPath) {
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'project.export_list',
nextProjectPath,
`列出 ${nextProjectPath} 的本地试玩包`,
'准备列出本地试玩包。',
() => void executeProjectExportPackages(true, true),
))
) {
return;
}
const result = await invoke<LocalProjectExportPackagesResult>(
'list_local_project_export_packages',
{ projectPath: nextProjectPath },
);
setFileStatus(`已列出 ${result.packages.length} 个本地试玩包`);
setCommandLog((current) => [...current, 'project.export_list']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectExportPackages(result),
draftCommand:
result.packages.length > 0 ? '/open-project' : '/export',
draftCommandLabel:
result.packages.length > 0 ? '显示目录' : '导出试玩包',
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectCheckpoints(
announceToChat: boolean,
skipListPolicyConfirm = false,
skipReadPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (!nextProjectPath) {
return;
}
try {
if (
announceToChat &&
!skipListPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'file.list',
nextProjectPath,
`列出 ${nextProjectPath} 的 checkpoint`,
'准备列出项目 checkpoint。',
() => void executeProjectCheckpoints(true, true, false),
))
) {
return;
}
const result = await invoke<ListLocalProjectFilesResult>(
'list_local_project_files',
{ projectPath: nextProjectPath },
);
const manifestFiles = sortCheckpointManifestFiles(result.files);
const visibleFiles = manifestFiles.slice(0, 5);
if (
announceToChat &&
visibleFiles.length > 0 &&
!skipReadPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'file.read',
nextProjectPath,
`读取 ${nextProjectPath} 的 checkpoint manifest`,
'准备读取 checkpoint manifest。',
() => void executeProjectCheckpoints(true, true, true),
))
) {
return;
}
const checkpoints: LocalProjectCheckpointSummary[] = [];
for (const file of visibleFiles) {
const manifest = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath: file.path,
commandId: 'file.read',
},
);
checkpoints.push(checkpointSummaryFromManifest(file, manifest.content));
}
setProjectFiles(result.files);
setProjectCheckpoints(checkpoints);
setFileStatus(`已列出 ${manifestFiles.length} 个 checkpoint`);
setCommandLog((current) => [
...current,
'file.list',
...(visibleFiles.length > 0 ? ['file.read checkpoint manifests'] : []),
]);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectCheckpoints(
checkpoints,
manifestFiles.length - visibleFiles.length,
),
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectDiff(
checkpointId: string,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'project.diff',
nextProjectPath,
`对比 ${nextProjectPath} 的 checkpoint:${checkpointId}`,
'准备对比项目 checkpoint。',
() => void executeProjectDiff(checkpointId, true, true),
))
) {
return;
}
const result = await invoke<LocalProjectDiffResult>(
'diff_local_project_checkpoint',
{ projectPath: nextProjectPath, checkpointId },
);
setFileStatus(`已对比 checkpoint:${result.checkpointId}`);
setCommandLog((current) => [...current, 'project.diff']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectDiff(result) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectRestore(
checkpointId: string,
announceToChat: boolean,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<LocalProjectRestoreResult>(
'restore_local_project_checkpoint',
{ projectPath: nextProjectPath, checkpointId },
);
setFileStatus(
`已回滚 ${result.restoredCount} 个文件,删除 ${result.deletedCount} 个新增文件`,
);
setCommandLog((current) => [...current, 'project.restore']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已回滚 ${result.restoredCount} 个文件到 ${nextProjectPath},删除 ${result.deletedCount} 个新增文件:${result.checkpointId}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectPolicyRead(announceToChat: boolean) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
setProjectStatus('已读取权限策略');
setCommandLog((current) => [...current, 'project.policy_read']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectPolicy(result) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function queueProjectPolicyMutation(
commandId: string,
mode: 'deny' | 'allow' | 'confirm' | 'auto',
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
const alreadyDenied = result.policy.deniedCommands.includes(commandId);
const alreadyConfirmed =
result.policy.confirmCommands.includes(commandId);
if (mode === 'deny' && alreadyDenied) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `命令已在拒绝列表中:${commandId}` },
]);
return;
}
if (mode === 'allow' && !alreadyDenied) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `命令不在拒绝列表中:${commandId}` },
]);
return;
}
if (mode === 'confirm' && alreadyConfirmed) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `命令已在确认列表中:${commandId}` },
]);
return;
}
if (mode === 'auto' && !alreadyConfirmed) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `命令不在确认列表中:${commandId}` },
]);
return;
}
const deniedCommands =
mode === 'deny'
? [...new Set([...result.policy.deniedCommands, commandId])]
: mode === 'allow'
? result.policy.deniedCommands.filter(
(value) => value !== commandId,
)
: mode === 'confirm'
? result.policy.deniedCommands.filter(
(value) => value !== commandId,
)
: result.policy.deniedCommands;
const confirmCommands =
mode === 'confirm'
? [...new Set([...result.policy.confirmCommands, commandId])]
: mode === 'deny'
? result.policy.confirmCommands.filter(
(value) => value !== commandId,
)
: mode === 'auto'
? result.policy.confirmCommands.filter(
(value) => value !== commandId,
)
: result.policy.confirmCommands;
queuePendingCommand({
id: 'project.policy_write',
policy: { ...result.policy, deniedCommands, confirmCommands },
});
const action =
mode === 'deny'
? '拒绝'
: mode === 'allow'
? '允许'
: mode === 'confirm'
? '确认'
: '自动执行';
setMessages((current) => [
...current,
{
role: 'assistant',
text: `准备${action}命令:${commandId}`,
},
]);
} catch (error) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function queueAgentPolicyMutation(
agentId: string,
commandId: string,
mode: 'deny' | 'allow' | 'confirm' | 'auto',
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
const agentPolicies = { ...(result.policy.agentPolicies ?? {}) };
const agentPolicy = agentPolicies[agentId] ?? {
deniedCommands: [],
confirmCommands: [],
};
const alreadyDenied = agentPolicy.deniedCommands.includes(commandId);
const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId);
if (mode === 'deny' && alreadyDenied) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`,
},
]);
return;
}
if (mode === 'allow' && !alreadyDenied) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`,
},
]);
return;
}
if (mode === 'confirm' && alreadyConfirmed) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`,
},
]);
return;
}
if (mode === 'auto' && !alreadyConfirmed) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`,
},
]);
return;
}
const deniedCommands =
mode === 'deny'
? [...new Set([...agentPolicy.deniedCommands, commandId])]
: mode === 'allow'
? agentPolicy.deniedCommands.filter((value) => value !== commandId)
: mode === 'confirm'
? agentPolicy.deniedCommands.filter(
(value) => value !== commandId,
)
: agentPolicy.deniedCommands;
const confirmCommands =
mode === 'confirm'
? [...new Set([...agentPolicy.confirmCommands, commandId])]
: mode === 'deny'
? agentPolicy.confirmCommands.filter((value) => value !== commandId)
: mode === 'auto'
? agentPolicy.confirmCommands.filter(
(value) => value !== commandId,
)
: agentPolicy.confirmCommands;
agentPolicies[agentId] = {
deniedCommands,
confirmCommands,
};
queuePendingCommand({
id: 'project.policy_write',
policy: {
...result.policy,
agentPolicies,
},
});
const action =
mode === 'deny'
? '拒绝'
: mode === 'allow'
? '允许'
: mode === 'confirm'
? '确认'
: '自动执行';
setMessages((current) => [
...current,
{
role: 'assistant',
text: `准备${action} Agent ${agentId} 命令:${commandId}`,
},
]);
} catch (error) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function executeProjectPolicyWrite(
policy: ProjectPermissionPolicy,
announceToChat: boolean,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<ProjectPermissionPolicyView>(
'write_project_permission_policy',
{ projectPath: nextProjectPath, policy },
);
setProjectStatus('已写入权限策略');
setCommandLog((current) => [...current, 'project.policy_write']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectPolicy(result) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectFiles(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setFileStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'file.list',
nextProjectPath,
`列出 ${nextProjectPath} 的项目文件`,
'准备列出项目文件。',
() => void executeProjectFiles(true, true),
))
) {
return;
}
const result = await invoke<ListLocalProjectFilesResult>(
'list_local_project_files',
{ projectPath: nextProjectPath },
);
setProjectFiles(result.files);
setFileStatus(`已列出 ${result.files.length} 项`);
setCommandLog((current) => [...current, 'file.list']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectFiles(result.files) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectAssets(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setAssetStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'asset.list',
nextProjectPath,
`列出 ${nextProjectPath} 的项目资产`,
'准备列出项目资产。',
() => void executeProjectAssets(true, true),
))
) {
return;
}
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath, commandId: 'asset.list' },
);
setManifest(nextManifest);
setAssetStatus(`已列出 ${nextManifest.assets.length} 个资产`);
setCommandLog((current) => [...current, 'asset.list']);
if (announceToChat) {
const firstReadableAsset = firstReadableProjectAssetPath(nextManifest);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeProjectAssets(nextManifest),
draftCommand: firstReadableAsset
? `/read ${firstReadableAsset}`
: undefined,
draftCommandLabel: firstReadableAsset ? '读取首个资产' : undefined,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeProjectFileReadChat(
relativePath: string,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setFileStatus('请先初始化本地项目');
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
return;
}
try {
const commandId = isAgentTraceFilePath(relativePath)
? 'agent.trace_read'
: 'file.read';
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
commandId,
nextProjectPath,
`读取 ${nextProjectPath} 的 ${relativePath}`,
'准备读取项目文件。',
() => void executeProjectFileReadChat(relativePath, true),
))
) {
return;
}
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{ projectPath: nextProjectPath, relativePath, commandId },
);
setFilePath(result.path);
setFileDraft(result.content);
setFileStatus(`已读取:${result.path}`);
setCommandLog((current) => [...current, 'file.read']);
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectFileContent(result) },
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setFileStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function executeProjectTasks(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setProjectStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'task.list',
nextProjectPath,
`读取 ${nextProjectPath} 的任务拆分`,
'准备读取任务拆分。',
() => void executeProjectTasks(true, true),
))
) {
return;
}
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath, commandId: 'task.list' },
);
setManifest(nextManifest);
setProjectStatus('已读取任务拆分');
setCommandLog((current) => [...current, 'task.list']);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeProjectTasks(nextManifest) },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setProjectStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeAgentTraceChat(skipPolicyConfirm = false) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'agent.trace_read',
nextProjectPath,
`读取 ${nextProjectPath} 的最近 Agent run trace`,
'准备读取 Agent run trace。',
() => void executeAgentTraceChat(true),
))
) {
return;
}
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
},
);
const trace = parseAgentRunTrace(result.content);
setAgentRunTrace(trace);
setAgentRunStatus(formatAgentRunStatus(trace));
setCommandLog((current) => [
...current,
'agent.trace_read',
'file.read .agent/run.latest.json',
]);
const traceSummary = summarizeAgentRunCompletionForChat(trace);
setMessages((current) => [
...current,
{ role: 'assistant', ...traceSummary },
]);
} catch (error) {
const rawMessage = error instanceof Error ? error.message : String(error);
const message = isMissingAgentRunTraceError(rawMessage)
? '暂无最近 Agent trace。先生成一次游戏草案后再查看。'
: rawMessage;
setAgentRunStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function executeAgentRunControl(
action: string,
detail: string | undefined,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
const commandId =
action === 'status'
? 'agent.run_status'
: (`agent.${action}` as GameCreationAppCommandDescriptor['id']);
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
commandId,
nextProjectPath,
action === 'status'
? `查看 ${nextProjectPath} 的最近 Agent run 状态`
: `执行 ${nextProjectPath} 的 ${commandId}`,
action === 'status'
? '准备查看 Agent run 状态。'
: '准备执行 Agent run 操作。',
() =>
void executeAgentRunControl(action, detail, announceToChat, true),
))
) {
return;
}
const result = await invoke<AgentRunControlResult>('control_agent_run', {
projectPath: nextProjectPath,
action,
detail,
});
setAgentRunStatus(
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
);
setCommandLog((current) => [
...current,
commandId,
'file.write .agent/activity.jsonl',
'file.write .agent/output.jsonl',
'file.write .agent/context.bundle.json',
]);
if (announceToChat && action === 'status') {
appendLocalPermissionLog(
nextProjectPath,
'command.auto',
'agent.run_status',
);
}
let traceReadQueued = false;
if (action === 'status') {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
requestProjectPolicyConfirmation(
'agent.trace_read',
nextProjectPath,
`读取 ${nextProjectPath} 的最近 Agent run trace`,
() => void refreshAgentRunTrace(nextProjectPath),
);
traceReadQueued = true;
}
}
if (!traceReadQueued) {
await refreshAgentRunTrace(nextProjectPath);
}
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: [
result.message,
`run:${result.runId}`,
`状态:${result.status} / ${result.lifecycleStatus}`,
`下一步:${result.nextStep}`,
`事件:${result.activityPath}`,
`输出:${result.outputPath}`,
`上下文包:${result.contextBundlePath}`,
].join('\n'),
draftCommand: '/read .agent/output.jsonl',
draftCommandLabel: '读取 Run 输出',
},
]);
}
} catch (error) {
const rawMessage = error instanceof Error ? error.message : String(error);
const message = formatAgentRunControlError(action, rawMessage);
setAgentRunStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
function queueAgentRunControlFromPanel(action: 'kill' | 'retry' | 'resume') {
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: `agent.${action}` });
setAgentRunStatus('等待确认');
}
async function executeAgentAuditChat(
confirmedPolicyCommands: GameCreationAppCommandDescriptor['id'][] = [],
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
return;
}
try {
if (
!confirmedPolicyCommands.includes('agent.audit') &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'agent.audit',
nextProjectPath,
`审计 ${nextProjectPath} 的 Agent 能力证据`,
'准备审计 Agent 能力证据。',
() => void executeAgentAuditChat(['agent.audit']),
))
) {
return;
}
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
const auditReadCommands: Array<GameCreationAppCommandDescriptor['id']> = [
'project.status',
'file.list',
'file.read',
'agent.trace_read',
];
const confirmCommandId = auditReadCommands.find(
(commandId) =>
policyView.policy.confirmCommands.includes(commandId) &&
!confirmedPolicyCommands.includes(commandId),
);
if (confirmCommandId) {
requestProjectPolicyConfirmation(
confirmCommandId,
nextProjectPath,
`审计 ${nextProjectPath} 需要读取 ${confirmCommandId}`,
() =>
void executeAgentAuditChat([
...confirmedPolicyCommands,
confirmCommandId,
]),
);
setMessages((current) => [
...current,
{ role: 'assistant', text: `准备确认审计读取:${confirmCommandId}` },
]);
return;
}
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath, commandId: 'agent.audit' },
);
const fileResult = await invoke<ListLocalProjectFilesResult>(
'list_local_project_files',
{ projectPath: nextProjectPath },
);
let commandLogContent = '';
try {
const commandLogResult = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath: '.agent/logs/command.log',
commandId: 'file.read',
},
);
commandLogContent = commandLogResult.content;
} catch {
commandLogContent = '';
}
let trace: GameCreationAgentRunTrace | null = null;
try {
const traceResult = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
},
);
trace = parseAgentRunTrace(traceResult.content);
} catch {
trace = null;
}
setManifest(nextManifest);
setProjectFiles(fileResult.files);
setAgentRunTrace(trace);
setAgentRunStatus(
trace ? formatAgentRunStatus(trace) : '还没有最近一次 Agent run',
);
setCommandLog((current) => [
...current,
'agent.audit',
'project.status',
'file.list',
...(commandLogContent ? ['file.read .agent/logs/command.log'] : []),
...(trace ? ['agent.trace_read'] : []),
]);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summarizeAgentAudit(
nextManifest,
nextProjectPath,
fileResult.files,
trace,
commandLogContent,
),
},
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAgentRunStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function handlePreviewStart() {
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
requestCommandConfirmation(
'preview.start',
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
() => void executePreviewStart(false),
);
}
async function executePreviewStart(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setPreviewStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setPreviewStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
setPreviewStatus('正在启动');
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'preview.start',
nextProjectPath,
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
'准备启动本地预览。',
() => void executePreviewStart(true, true),
))
) {
return;
}
const result = await invoke<LocalPreviewResult>(
'start_local_game_preview',
{ projectPath: nextProjectPath },
);
updateClientPreview(result);
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
void refreshManifest(nextProjectPath);
setCommandLog((current) => [...current, 'preview.start']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `预览已在客户端运行视图启动:${result.url}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executePreviewOpen(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setPreviewStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (announceToChat && !nextProjectPath) {
return;
}
try {
if (
announceToChat &&
nextProjectPath &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'preview.open',
nextProjectPath,
`打开 ${nextProjectPath} 的本地预览`,
'准备打开当前本地预览。',
() => void executePreviewOpen(true, true),
))
) {
return;
}
const result = await invoke<LocalPreviewStatus>(
'activate_local_game_preview',
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
);
if (
result.status === 'running' &&
result.url &&
result.port &&
result.root
) {
updateClientPreview({
url: result.url,
port: result.port,
root: result.root,
});
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
}
setCommandLog((current) => [...current, 'preview.open']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: result.url
? `已切换到客户端运行视图:${result.url}`
: '已切换到客户端运行视图。',
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handlePreviewStatus() {
void executePreviewStatus(false);
}
async function executePreviewStatus(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setPreviewStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (announceToChat && !nextProjectPath) {
return;
}
try {
if (
announceToChat &&
nextProjectPath &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'preview.status',
nextProjectPath,
`查看 ${nextProjectPath} 的预览状态`,
'准备查看预览状态。',
() => void executePreviewStatus(true, true),
))
) {
return;
}
const result = await invoke<LocalPreviewStatus>(
'get_local_game_preview_status',
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
);
if (
result.status === 'running' &&
result.url &&
result.port &&
result.root
) {
updateClientPreview({
url: result.url,
port: result.port,
root: result.root,
});
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
} else {
updateClientPreview(null);
setPreviewStatus('未启动');
}
setCommandLog((current) => [...current, 'preview.status']);
if (announceToChat && nextProjectPath) {
appendLocalPermissionLog(
nextProjectPath,
'command.auto',
'preview.status',
);
}
if (announceToChat) {
const previewRunning = result.status === 'running' && result.url;
setMessages((current) => [
...current,
{
role: 'assistant',
text: previewRunning ? `预览运行中:${result.url}` : '预览未启动。',
draftCommand: previewRunning ? '/open-preview' : '/preview',
draftCommandLabel: previewRunning
? '填入打开预览命令'
: '填入启动预览命令',
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handlePreviewStop() {
void executePreviewStop(false);
}
async function executePreviewStop(
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setPreviewStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setPreviewStatus('请先初始化本地项目');
return;
}
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'preview.stop',
nextProjectPath,
`停止 ${nextProjectPath} 的本地预览`,
'准备停止本地预览。',
() => void executePreviewStop(true, true),
))
) {
return;
}
await invoke<LocalPreviewStatus>('stop_local_game_preview', {
projectPath: nextProjectPath,
});
updateClientPreview(null);
setPreviewStatus('已停止');
setCommandLog((current) => [...current, 'preview.stop']);
if (announceToChat) {
appendLocalPermissionLog(
nextProjectPath,
'command.auto',
'preview.stop',
);
}
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '预览已停止。' },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handleMemoryRead(skipPolicyConfirm = false) {
const nextProjectPath = validateMemoryPanelProjectPath();
if (!nextProjectPath) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
return;
}
setMemoryStatus('正在读取');
try {
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('memory.read')) {
requestProjectPolicyConfirmation(
'memory.read',
nextProjectPath,
`读取 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
() => void handleMemoryRead(true),
);
setMemoryStatus('等待确认');
return;
}
}
const result = await invoke<LocalGameMemoryResult>(
'read_local_game_memory',
{ projectPath: nextProjectPath, scope: memoryScope },
);
setMemoryDraft(result.content);
setMemoryStatus(
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
);
setCommandLog((current) => [...current, 'memory.read']);
} catch (error) {
setMemoryStatus(error instanceof Error ? error.message : String(error));
}
}
function validateMemoryPanelProjectPath() {
const nextProjectPath = projectPath.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setMemoryStatus('请提供本地项目绝对路径。');
return null;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setMemoryStatus('本地项目路径不能包含控制字符。');
return null;
}
return nextProjectPath;
}
async function handleMemoryWrite() {
const nextProjectPath = validateMemoryPanelProjectPath();
if (!nextProjectPath) {
return;
}
requestCommandConfirmation(
'memory.write',
`保存 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
() => void executeMemoryWritePanel(nextProjectPath),
);
}
async function executeMemoryWritePanel(nextProjectPath: string) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
return;
}
setMemoryStatus('正在保存');
try {
const result = await invoke<LocalGameMemoryResult>(
'write_local_game_memory',
{
projectPath: nextProjectPath,
scope: memoryScope,
content: memoryDraft,
},
);
setMemoryStatus(`已保存:${result.path}`);
setCommandLog((current) => [...current, 'memory.write']);
} catch (error) {
setMemoryStatus(error instanceof Error ? error.message : String(error));
}
}
async function handleMemoryDelete() {
const nextProjectPath = validateMemoryPanelProjectPath();
if (!nextProjectPath) {
return;
}
requestCommandConfirmation(
'memory.delete',
`删除 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
() => void executeMemoryDeletePanel(nextProjectPath),
);
}
async function executeMemoryDeletePanel(nextProjectPath: string) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
return;
}
setMemoryStatus('正在删除');
try {
const result = await invoke<LocalGameMemoryResult>(
'delete_local_game_memory',
{ projectPath: nextProjectPath, scope: memoryScope },
);
setMemoryDraft('');
setMemoryStatus(`已删除:${result.path}`);
setCommandLog((current) => [...current, 'memory.delete']);
} catch (error) {
setMemoryStatus(error instanceof Error ? error.message : String(error));
}
}
async function executeMemoryReadChat(
scope: MemoryScope,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'memory.read',
nextProjectPath,
`读取 ${nextProjectPath}/${memoryScopePath(scope)}`,
'准备读取项目记忆。',
() => void executeMemoryReadChat(scope, true),
))
) {
return;
}
const result = await invoke<LocalGameMemoryResult>(
'read_local_game_memory',
{ projectPath: nextProjectPath, scope },
);
setMemoryScope(scope);
setMemoryDraft(result.content);
setMemoryStatus(
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
);
setCommandLog((current) => [...current, 'memory.read']);
setMessages((current) => [
...current,
{
role: 'assistant',
text: result.exists
? `${memoryScopeLabel(scope)}记忆:\n${result.content}`
: `${memoryScopeLabel(scope)}记忆为空。`,
},
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setMemoryStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function executeMemoryWrite(
scope: MemoryScope,
content: string,
mode: MemoryWriteMode,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
const currentMemory =
mode === 'append'
? await invoke<LocalGameMemoryResult>('read_local_game_memory', {
projectPath: nextProjectPath,
scope,
})
: null;
const result = await invoke<LocalGameMemoryResult>(
'write_local_game_memory',
{
projectPath: nextProjectPath,
scope,
content:
mode === 'append'
? appendMemoryContent(currentMemory?.content ?? '', content)
: content,
},
);
setMemoryScope(scope);
setMemoryDraft(result.content);
setMemoryStatus(`已保存:${result.path}`);
setCommandLog((current) => [
...current,
...(mode === 'append' ? ['memory.read'] : []),
'memory.write',
]);
setMessages((current) => [
...current,
{
role: 'assistant',
text:
mode === 'append'
? `已追加${memoryScopeLabel(scope)}记忆。`
: `已保存${memoryScopeLabel(scope)}记忆。`,
},
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setMemoryStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function executeMemoryDeleteChat(scope: MemoryScope) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setMemoryStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
try {
const result = await invoke<LocalGameMemoryResult>(
'delete_local_game_memory',
{ projectPath: nextProjectPath, scope },
);
setMemoryScope(scope);
setMemoryDraft('');
setMemoryStatus(`已删除:${result.path}`);
setCommandLog((current) => [...current, 'memory.delete']);
setMessages((current) => [
...current,
{ role: 'assistant', text: `已删除${memoryScopeLabel(scope)}记忆。` },
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setMemoryStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
async function handleLimitedCommandRun(
command: GameCreationAppLimitedRunCommandDescriptor,
) {
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
requestCommandConfirmation(
'command.run_limited',
`运行 ${command.title} 于 ${nextProjectPath}`,
() => void executeLimitedCommand(command.id, false),
);
}
async function refreshLimitedLocalCommands() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLimitedCommandStatus('需要在 Tauri App 内运行');
return;
}
setLimitedCommandStatus('正在读取内置命令');
try {
const commands = await invoke<
GameCreationAppLimitedRunCommandDescriptor[]
>('get_limited_local_commands');
setLimitedLocalCommands(
commands.length > 0
? commands
: [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS],
);
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
setCommandLog((current) => [...current, 'command.list_limited']);
} catch (error) {
setLimitedCommandStatus(
error instanceof Error ? error.message : String(error),
);
}
}
async function handleProjectLogRead(
relativePath: string,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setProjectLogStatus('需要在 Tauri App 内运行');
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setProjectLogStatus('请先初始化本地项目');
return;
}
setProjectLogStatus(`正在读取 ${relativePath}`);
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'file.read',
nextProjectPath,
`读取 ${nextProjectPath}/${relativePath}`,
'准备读取项目日志。',
() => void handleProjectLogRead(relativePath, true),
))
) {
setProjectLogStatus('等待确认');
return;
}
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath,
commandId: 'file.read',
},
);
setProjectLogContent(result.content);
setProjectLogStatus(`已读取:${result.path}`);
setCommandLog((current) => [...current, `file.read ${result.path}`]);
} catch (error) {
setProjectLogContent('');
setProjectLogStatus(
error instanceof Error ? error.message : String(error),
);
}
}
async function executeLimitedCommandList() {
const invoke = resolveTauriInvoke();
let commands: GameCreationAppLimitedRunCommandDescriptor[] = [
...GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
];
if (invoke) {
try {
const nativeCommands = await invoke<
GameCreationAppLimitedRunCommandDescriptor[]
>('get_limited_local_commands');
if (nativeCommands.length > 0) {
commands = nativeCommands;
}
} catch {
commands = [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS];
}
}
setLimitedLocalCommands(commands);
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
setCommandLog((current) => [...current, 'command.list_limited']);
setMessages((current) => [
...current,
{ role: 'assistant', text: summarizeLimitedLocalCommands(commands) },
]);
}
async function executeRunLocal(announceToChat: boolean) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLimitedCommandStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setLimitedCommandStatus('请先初始化本地项目');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
]);
}
return;
}
setLimitedCommandStatus('正在运行');
try {
const smoke = directCodexProductRuntime
? null
: await invoke<LimitedLocalCommandResult>('run_limited_local_command', {
projectPath: nextProjectPath,
commandId: 'game.static_smoke',
});
const previewResult = await invoke<LocalPreviewResult>(
'start_local_game_preview',
{ projectPath: nextProjectPath },
);
updateClientPreview(previewResult);
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
setLimitedCommandStatus(smoke?.output ?? '本地预览已启动');
setCommandLog((current) => [
...current,
'game.run_local',
...(smoke ? [`command.run_limited ${smoke.commandId}`] : []),
'preview.start',
]);
void refreshManifest(nextProjectPath);
if (!directCodexProductRuntime) {
void refreshAgentRunTrace(nextProjectPath);
}
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `运行通过,已载入客户端运行视图:${previewResult.url}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setLimitedCommandStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function executeLimitedCommand(
commandId: string,
announceToChat: boolean,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLimitedCommandStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setLimitedCommandStatus('请先初始化本地项目');
return;
}
setLimitedCommandStatus('正在运行');
try {
const result = await invoke<LimitedLocalCommandResult>(
'run_limited_local_command',
{ projectPath: nextProjectPath, commandId },
);
const summary = `${result.output}\n日志:${result.logPath}`;
setLimitedCommandStatus(result.output);
setCommandLog((current) => [
...current,
`command.run_limited ${result.commandId}`,
]);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: summary },
]);
}
void refreshManifest(nextProjectPath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setLimitedCommandStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handleFileList(skipPolicyConfirm = false) {
const nextProjectPath = validateFilePanelProjectPath();
if (!nextProjectPath) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
return;
}
setFileStatus('正在列出');
try {
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('file.list')) {
requestProjectPolicyConfirmation(
'file.list',
nextProjectPath,
`列出 ${nextProjectPath} 的项目文件`,
() => void handleFileList(true),
);
setFileStatus('等待确认');
return;
}
}
const result = await invoke<ListLocalProjectFilesResult>(
'list_local_project_files',
{ projectPath: nextProjectPath },
);
setProjectFiles(result.files);
setFileStatus(`已列出 ${result.files.length} 项`);
setCommandLog((current) => [...current, 'file.list']);
} catch (error) {
setFileStatus(error instanceof Error ? error.message : String(error));
}
}
function validateFilePanelProjectPath() {
const nextProjectPath = projectPath.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setFileStatus('请提供本地项目绝对路径。');
return null;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setFileStatus('本地项目路径不能包含控制字符。');
return null;
}
return nextProjectPath;
}
function validateFilePanelPath() {
if (!isSafeProjectRelativePath(filePath)) {
setFileStatus('文件路径必须是项目内相对路径。');
return null;
}
return filePath.trim();
}
async function handleFileRead(skipPolicyConfirm = false) {
const nextProjectPath = validateFilePanelProjectPath();
if (!nextProjectPath) {
return;
}
const relativePath = validateFilePanelPath();
if (!relativePath) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
return;
}
setFileStatus('正在读取');
try {
const commandId = isAgentTraceFilePath(relativePath)
? 'agent.trace_read'
: 'file.read';
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes(commandId)) {
requestProjectPolicyConfirmation(
commandId,
nextProjectPath,
`读取 ${nextProjectPath} 的 ${relativePath}`,
() => void handleFileRead(true),
);
setFileStatus('等待确认');
return;
}
}
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath,
commandId,
},
);
setFilePath(result.path);
setFileDraft(result.content);
setFileStatus(`已读取:${result.path}`);
setCommandLog((current) => [...current, 'file.read']);
} catch (error) {
setFileStatus(error instanceof Error ? error.message : String(error));
}
}
async function handleFileWrite() {
const nextProjectPath = validateFilePanelProjectPath();
if (!nextProjectPath) {
return;
}
const relativePath = validateFilePanelPath();
if (!relativePath) {
return;
}
requestCommandConfirmation(
'file.write',
`保存 ${nextProjectPath}/${relativePath}`,
() => void executeFileWrite(nextProjectPath, relativePath),
);
}
async function executeFileWrite(
nextProjectPath: string,
relativePath: string,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
return;
}
setFileStatus('正在保存');
try {
const result = await invoke<LocalProjectFileMutationResult>(
'write_local_project_file',
{ projectPath: nextProjectPath, relativePath, content: fileDraft },
);
setFileStatus(`已保存:${result.path}`);
setCommandLog((current) => [...current, 'file.write']);
} catch (error) {
setFileStatus(error instanceof Error ? error.message : String(error));
}
}
async function handleFileDelete() {
const nextProjectPath = validateFilePanelProjectPath();
if (!nextProjectPath) {
return;
}
const relativePath = validateFilePanelPath();
if (!relativePath) {
return;
}
requestCommandConfirmation(
'file.delete',
`删除 ${nextProjectPath}/${relativePath}`,
() => void executeFileDelete(nextProjectPath, relativePath),
);
}
async function executeFileDelete(
nextProjectPath: string,
relativePath: string,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setFileStatus('需要在 Tauri App 内运行');
return;
}
setFileStatus('正在删除');
try {
const result = await invoke<LocalProjectFileMutationResult>(
'delete_local_project_file',
{ projectPath: nextProjectPath, relativePath },
);
setFileDraft('');
setFileStatus(result.deleted ? `已删除:${result.path}` : '文件不存在');
setCommandLog((current) => [...current, 'file.delete']);
} catch (error) {
setFileStatus(error instanceof Error ? error.message : String(error));
}
}
async function handleAssetRegister() {
const nextProjectPath = projectPath.trim();
const localPath = assetLocalPath.trim();
const canvasProjectId = assetCanvasProjectId.trim();
const resourceId = assetResourceId.trim();
const nextAssetObjectId = assetObjectId.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setAssetStatus('请提供本地项目绝对路径。');
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setAssetStatus('本地项目路径不能包含控制字符。');
return;
}
if (!isSafeProjectRelativePath(localPath)) {
setAssetStatus('资产路径必须是项目内相对路径。');
return;
}
if (assetSourceKind === 'canvas') {
if (!canvasProjectId) {
setAssetStatus('请提供画板项目 ID。');
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setAssetStatus('画板项目 ID 不能包含控制字符。');
return;
}
if (!resourceId && !nextAssetObjectId) {
setAssetStatus('请提供资源 ID 或资产对象 ID。');
return;
}
}
requestCommandConfirmation(
'asset.register',
`登记 ${nextProjectPath}/${localPath}`,
() =>
void executeAssetRegister(
nextProjectPath,
localPath,
parseGameCreationAppAssetKind(assetKind, 'ui.asset-register.kind'),
assetMediaType,
assetSourceKind,
canvasProjectId,
resourceId,
nextAssetObjectId,
),
);
}
async function executeAssetRegister(
nextProjectPath: string,
localPath: string,
kind: GameCreationAppAssetKind,
mediaType: string,
sourceKind: string,
canvasProjectId: string,
resourceId: string,
nextAssetObjectId: string,
announceToChat = false,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
setAssetStatus('正在登记');
try {
if (
announceToChat &&
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'asset.register',
nextProjectPath,
`登记 ${nextProjectPath}/${localPath}`,
'准备登记项目资产。',
() =>
void executeAssetRegister(
nextProjectPath,
localPath,
kind,
mediaType,
sourceKind,
canvasProjectId,
resourceId,
nextAssetObjectId,
true,
true,
),
))
) {
return;
}
const result = await invoke<UploadLocalAssetResult>(
'register_local_asset',
{
projectPath: nextProjectPath,
localPath,
kind,
mediaType,
sourceKind,
canvasProjectId,
resourceId,
assetObjectId: nextAssetObjectId,
taskId: '',
prompt: '',
model: '',
},
);
setUploadedAssets((current) => {
const filtered = current.filter((asset) => asset.id !== result.id);
return [...filtered, result];
});
setAssetStatus(`已登记:${result.localPath}`);
setCommandLog((current) => [...current, 'asset.register']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `已登记资产:${result.localPath}` },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handleCanvasAssetImport() {
const nextProjectPath = projectPath.trim();
const localPath = assetLocalPath.trim();
const canvasProjectId = assetCanvasProjectId.trim();
const resourceId = assetResourceId.trim();
const nextAssetObjectId = assetObjectId.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setAssetStatus('请提供本地项目绝对路径。');
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setAssetStatus('本地项目路径不能包含控制字符。');
return;
}
if (!isSafeProjectRelativePath(localPath)) {
setAssetStatus('画板资产路径必须是项目内相对路径。');
return;
}
if (!canvasProjectId) {
setAssetStatus('请提供画板项目 ID。');
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setAssetStatus('画板项目 ID 不能包含控制字符。');
return;
}
if (!resourceId && !nextAssetObjectId) {
setAssetStatus('请提供资源 ID 或资产对象 ID。');
return;
}
requestCommandConfirmation(
'canvas.asset_import',
`导入 ${nextProjectPath}/${localPath}`,
() =>
void executeCanvasAssetImport(
nextProjectPath,
localPath,
parseGameCreationAppAssetKind(
assetKind,
'ui.canvas-asset-import.kind',
),
assetMediaType,
canvasProjectId,
resourceId,
nextAssetObjectId,
false,
),
);
}
async function executeCanvasAssetImport(
nextProjectPath: string | null,
localPath: string,
kind: GameCreationAppAssetKind,
mediaType: string,
canvasProjectId: string,
resourceId: string,
assetObjectId: string,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const targetProjectPath = announceToChat
? requireChatProjectForUserAction()
: nextProjectPath;
if (!targetProjectPath) {
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'canvas.asset_import',
targetProjectPath,
`导入 ${targetProjectPath}/${localPath} 的画板来源资产`,
'准备导入画板资产。',
() =>
void executeCanvasAssetImport(
nextProjectPath,
localPath,
kind,
mediaType,
canvasProjectId,
resourceId,
assetObjectId,
announceToChat,
true,
),
))
) {
setAssetStatus('等待确认导入画板资产');
return;
}
setAssetStatus('正在导入画板资产');
const result = await invoke<UploadLocalAssetResult>(
'import_canvas_asset',
{
projectPath: targetProjectPath,
localPath,
kind,
mediaType,
canvasProjectId,
resourceId,
assetObjectId,
taskId: '',
prompt: '',
model: '',
},
);
setUploadedAssets((current) => {
const filtered = current.filter((asset) => asset.id !== result.id);
return [...filtered, result];
});
const canvasSource = formatCanvasAssetSource({
canvasProjectId,
canvasAssetId: resourceId,
canvasAssetObjectId: assetObjectId,
});
setAssetStatus(`已导入画板资产 ${canvasSource}:${result.localPath}`);
setCommandLog((current) => [...current, 'canvas.asset_import']);
void refreshManifest(targetProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已导入画板资产 ${canvasSource}:${result.localPath}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
function handleCanvasAssetGenerate() {
const nextProjectPath = projectPath.trim();
const prompt = assetGenerationPrompt.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setAssetStatus('请提供本地项目绝对路径。');
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setAssetStatus('本地项目路径不能包含控制字符。');
return;
}
if (!prompt) {
setAssetStatus('请提供美术生成提示词。');
return;
}
requestCommandConfirmation(
'canvas.asset_generate',
`生成 ${nextProjectPath} 的首版美术素材`,
() => void executeCanvasAssetGenerate(prompt, false),
);
}
async function executeCanvasAssetGenerate(
prompt: string,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath.trim();
if (!nextProjectPath) {
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'canvas.asset_generate',
nextProjectPath,
`生成 ${nextProjectPath} 的首版美术素材`,
'准备生成首版美术素材。',
() => void executeCanvasAssetGenerate(prompt, announceToChat, true),
))
) {
setAssetStatus('等待确认生成美术素材');
return;
}
setAssetStatus('正在生成美术素材');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '正在生成首版美术素材。' },
]);
}
const result = await invoke<UploadLocalAssetResult>(
'generate_platform_art_asset',
{
projectPath: nextProjectPath,
prompt,
},
);
setUploadedAssets((current) => {
const filtered = current.filter((asset) => asset.id !== result.id);
return [...filtered, result];
});
setAssetStatus(`已生成美术素材:${result.localPath}`);
setCommandLog((current) => [...current, 'canvas.asset_generate']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `已生成美术素材:${result.localPath}` },
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
function handleCanvasProjectSync() {
const nextProjectPath = projectPath.trim();
const canvasProjectId = assetCanvasProjectId.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setAssetStatus('请提供本地项目绝对路径。');
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setAssetStatus('本地项目路径不能包含控制字符。');
return;
}
if (!canvasProjectId) {
setAssetStatus('请提供画板项目 ID。');
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setAssetStatus('画板项目 ID 不能包含控制字符。');
return;
}
requestCommandConfirmation(
'canvas.project_sync',
`同步画板项目资源:${canvasProjectId}`,
() => void executeCanvasProjectSync(canvasProjectId, false),
);
}
async function executeCanvasProjectSync(
canvasProjectId: string,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'canvas.project_sync',
nextProjectPath,
`同步 ${nextProjectPath} 的画板项目资源:${canvasProjectId}`,
'准备同步画板项目资源。',
() =>
void executeCanvasProjectSync(
canvasProjectId,
announceToChat,
true,
),
))
) {
setAssetStatus('等待确认同步画板项目');
return;
}
setAssetStatus('正在同步画板项目资源');
const result = await invoke<SyncCanvasProjectAssetsResult>(
'sync_canvas_project_assets',
{
projectPath: nextProjectPath,
canvasProjectId,
},
);
setUploadedAssets((current) => {
const byId = new Map(current.map((asset) => [asset.id, asset]));
for (const asset of result.assets) {
byId.set(asset.id, asset);
}
return [...byId.values()];
});
setAssetStatus(
`已同步画板项目:${result.importedCount} 个资产,${result.importRoot}`,
);
setCommandLog((current) => [...current, 'canvas.project_sync']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已同步 ${result.importedCount} 个画板资产自 ${result.canvasProjectId}:${result.importRoot}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
function handleCanvasExportImport() {
const nextProjectPath = projectPath.trim();
const exportPath = canvasExportPath.trim();
const canvasProjectId = assetCanvasProjectId.trim();
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
setAssetStatus('请提供本地项目绝对路径。');
return;
}
if (projectPathHasControlCharacter(nextProjectPath)) {
setAssetStatus('本地项目路径不能包含控制字符。');
return;
}
if (!canvasProjectId) {
setAssetStatus('请提供画板项目 ID。');
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setAssetStatus('画板项目 ID 不能包含控制字符。');
return;
}
if (
!isAbsoluteProjectPath(exportPath) ||
projectPathHasControlCharacter(exportPath)
) {
setAssetStatus('画板导出 ZIP 路径必须是绝对路径。');
return;
}
requestCommandConfirmation(
'canvas.export_import',
`导入画板导出包:${exportPath}`,
() => void executeCanvasExportImport(exportPath, canvasProjectId, false),
);
}
async function executeCanvasExportImport(
exportPath: string,
canvasProjectId: string,
announceToChat: boolean,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
const nextProjectPath = announceToChat
? requireChatProjectForUserAction()
: projectPath;
if (!nextProjectPath) {
return;
}
try {
if (
!skipPolicyConfirm &&
(await queueProjectPolicyConfirmationIfNeeded(
invoke,
'canvas.export_import',
nextProjectPath,
`导入 ${nextProjectPath} 的画板导出包:${exportPath}`,
'准备导入画板导出包。',
() =>
void executeCanvasExportImport(
exportPath,
canvasProjectId,
announceToChat,
true,
),
))
) {
setAssetStatus('等待确认导入画板导出包');
return;
}
setAssetStatus('正在导入画板导出包');
const result = await invoke<ImportCanvasExportResult>(
'import_canvas_export',
{
projectPath: nextProjectPath,
exportPath,
canvasProjectId,
},
);
setUploadedAssets((current) => {
const byId = new Map(current.map((asset) => [asset.id, asset]));
for (const asset of result.assets) {
byId.set(asset.id, asset);
}
return [...byId.values()];
});
setAssetStatus(
`已导入画板导出包:${result.importedCount} 个资产,${result.importRoot}`,
);
setCommandLog((current) => [...current, 'canvas.export_import']);
void refreshManifest(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已导入 ${result.importedCount} 个画板资产自 ${canvasProjectId}:${result.importRoot}`,
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handleCanvasProjectOpen() {
const canvasProjectId = assetCanvasProjectId.trim();
if (!canvasProjectId) {
setAssetStatus('请提供画板项目 ID。');
return;
}
if (!isSafeCanvasProjectId(canvasProjectId)) {
setAssetStatus('画板项目 ID 不能包含控制字符。');
return;
}
requestCommandConfirmation(
'canvas.project_open',
`打开本机画板项目 ${canvasProjectId}`,
() => void executeCanvasProjectOpen(canvasProjectId, false),
);
}
async function executeCanvasProjectOpen(
canvasProjectId: string,
announceToChat: boolean,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
}
return;
}
setAssetStatus('正在打开画板');
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: `正在打开画板:${canvasProjectId}` },
]);
}
try {
const result = await invoke<OpenCanvasProjectResult>(
'open_canvas_project',
{
canvasProjectId,
editorBaseUrl,
},
);
setAssetStatus(`已打开画板:${result.url}`);
setCommandLog((current) => [...current, 'canvas.project_open']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已打开画板:${result.url}`,
draftCommand: `/sync-canvas-project ${canvasProjectId}`,
draftCommandLabel: '同步此画板',
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssetStatus(message);
if (announceToChat) {
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
}
async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
const file = event.currentTarget.files?.[0];
event.currentTarget.value = '';
if (!file) {
return;
}
if (!requireChatProjectForUserAction()) {
return;
}
queuePendingCommand({ id: 'asset.upload', file });
setMessages((current) => [
...current,
{ role: 'user', text: `上传文件:${file.name}` },
{ role: 'assistant', text: `准备保存文件到本地项目:${file.name}` },
]);
}
async function executeAssetUpload(file: File) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAssetStatus('需要在 Tauri App 内运行');
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
setAssetStatus('正在上传');
try {
const bytes = Array.from(new Uint8Array(await file.arrayBuffer()));
const result = await invoke<UploadLocalAssetResult>(
'upload_local_asset',
{
projectPath: nextProjectPath,
fileName: file.name,
mediaType: file.type || 'application/octet-stream',
bytes,
},
);
setLocalProject({
projectPath: nextProjectPath,
manifestPath: result.manifestPath,
manifest,
});
setUploadedAssets((current) => [...current, result]);
setAssetStatus(`已上传:${result.localPath}`);
setCommandLog((current) => [
...current,
'asset.upload',
'asset.register',
]);
void refreshManifest(nextProjectPath);
setMessages((current) => [
...current,
{ role: 'assistant', text: `已保存文件:${result.localPath}` },
]);
} catch (error) {
setAssetStatus(error instanceof Error ? error.message : String(error));
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function loadAgentRunTraceFile(
relativePath: string,
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
return null;
}
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
return null;
}
try {
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath,
commandId: isAgentTraceFilePath(relativePath)
? 'agent.trace_read'
: 'file.read',
},
);
const trace = parseAgentRunTrace(result.content);
setAgentRunTrace(trace);
setAgentRunStatus(formatAgentRunStatus(trace));
setCommandLog((current) => [...current, `file.read ${relativePath}`]);
return summarizeAgentRunCompletionForChat(trace);
} catch (error) {
setAgentRunTrace(null);
setAgentRunStatus(
relativePath === '.agent/run.latest.json' &&
isMissingProjectFileError(error)
? '还没有最近一次 Agent run'
: error instanceof Error
? error.message
: String(error),
);
return null;
}
}
async function handleAgentRunTraceFileOpen(
relativePath: string,
skipPolicyConfirm = false,
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
return;
}
const nextProjectPath = resolveChatProjectPath(localProject) ?? '';
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
return;
}
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
requestProjectPolicyConfirmation(
'agent.trace_read',
nextProjectPath,
`读取 ${nextProjectPath} 的 ${relativePath}`,
() => void handleAgentRunTraceFileOpen(relativePath, true),
);
setAgentRunStatus('等待确认读取 Agent trace');
return;
}
}
await loadAgentRunTraceFile(relativePath, nextProjectPath);
}
async function readAgentRunHistoryItems(
invoke: TauriInvoke,
runFiles: LocalProjectFileEntry[],
nextProjectPath: string,
) {
const history = await Promise.all(
runFiles.map(async (file) => {
try {
const traceResult = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath: file.path,
commandId: 'agent.trace_read',
},
);
return {
path: file.path,
size: file.size,
trace: parseAgentRunTrace(traceResult.content),
} satisfies AgentRunHistoryItem;
} catch {
return null;
}
}),
);
return history
.filter((item): item is AgentRunHistoryItem => item !== null)
.sort((left, right) => right.trace.updatedAt - left.trace.updatedAt);
}
async function refreshAgentRunHistory(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
setAgentRunHistory([]);
setAgentRunHistoryFiles([]);
setAgentRunHistoryOverflowCount(0);
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
setAgentRunHistoryLoadingMore(false);
agentRunHistoryLoadingMoreRef.current = false;
return;
}
try {
const result = await invoke<ListLocalProjectFilesResult>(
'list_local_project_files',
{ projectPath: nextProjectPath },
);
const runFiles = result.files
.filter(
(file) =>
file.kind === 'file' &&
file.path.startsWith('.agent/runs/') &&
file.path.endsWith('.json'),
)
.sort(
(left, right) =>
(right.modifiedAt ?? 0) - (left.modifiedAt ?? 0) ||
right.path.localeCompare(left.path),
);
const runFilesToLoad = runFiles.slice(0, AGENT_RUN_HISTORY_MAX_COUNT);
const firstPage = await readAgentRunHistoryItems(
invoke,
runFilesToLoad.slice(0, AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT),
nextProjectPath,
);
setAgentRunHistory(firstPage);
setAgentRunHistoryFiles(runFilesToLoad);
setAgentRunHistoryOverflowCount(
Math.max(0, runFiles.length - runFilesToLoad.length),
);
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
setAgentRunHistoryLoadingMore(false);
agentRunHistoryLoadingMoreRef.current = false;
} catch {
setAgentRunHistory([]);
setAgentRunHistoryFiles([]);
setAgentRunHistoryOverflowCount(0);
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
setAgentRunHistoryLoadingMore(false);
agentRunHistoryLoadingMoreRef.current = false;
}
}
function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) {
if (!runtime) {
return;
}
const next = mergeAgentRuntimeStateIntoMap(
agentRuntimeByIdRef.current,
runtime,
false,
);
agentRuntimeByIdRef.current = next;
setAgentRuntimeById(next);
}
async function refreshAgentRuntimes(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
setAgentRuntimeById({});
agentRuntimeResumeProjectPathRef.current = null;
return;
}
if (directCodexProductRuntime) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
setAgentRuntimeById({});
return;
}
try {
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
try {
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
'resume_game_creator_agent_runtime_tasks',
{ projectPath: nextProjectPath },
);
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
for (const runtimeResult of resumedRuntimes) {
rememberAgentRuntimeState(
agentRuntimeStateFromResult(runtimeResult),
);
}
} catch (error) {
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
const message =
error instanceof Error ? error.message : String(error);
if (isMissingAgentRuntimeResumeCommandError(error)) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
} else if (
message.includes('项目权限策略要求用户确认:agent.resume')
) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
requestProjectPolicyConfirmation(
'agent.resume',
nextProjectPath,
`恢复 ${nextProjectPath} 中未完成的 Agent Runtime 任务`,
() => {
void invoke<AgentRuntimeResult[]>(
'confirm_resume_game_creator_agent_runtime_tasks',
{ projectPath: nextProjectPath },
)
.then((resumedRuntimes) => {
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
for (const runtimeResult of resumedRuntimes) {
rememberAgentRuntimeState(
agentRuntimeStateFromResult(runtimeResult),
);
}
setAgentRunStatus('已确认恢复 Agent Runtime 任务');
})
.catch((resumeError) => {
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
agentRuntimeResumeProjectPathRef.current = null;
setAgentRunStatus(
`Agent Runtime 恢复失败:${
resumeError instanceof Error
? resumeError.message
: String(resumeError)
}`,
);
});
},
);
setAgentRunStatus('等待确认恢复 Agent Runtime 任务');
} else if (message.includes('项目权限策略拒绝执行:agent.resume')) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
markProjectPolicyDenied('agent.resume', message);
} else {
agentRuntimeResumeProjectPathRef.current = null;
setAgentRunStatus(`Agent Runtime 恢复失败:${message}`);
}
}
}
const runtimes = await invoke<AgentRuntimeResult[]>(
'read_game_creator_agent_runtimes',
{ projectPath: nextProjectPath },
);
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
const nextRuntimes: AgentRuntimeState[] = [];
for (const runtimeResult of runtimes) {
nextRuntimes.push(agentRuntimeStateFromResult(runtimeResult));
}
const supervisorRuntimeIndex = nextRuntimes.findIndex(
(runtime) => runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID,
);
const persistedSupervisorRuntime =
supervisorRuntimeIndex >= 0
? nextRuntimes[supervisorRuntimeIndex]!
: null;
if (projectSupervisorOnly && persistedSupervisorRuntime) {
const currentRuntime = projectSupervisorRuntimeRef.current;
if (
!currentRuntime ||
persistedSupervisorRuntime.updatedAt >= currentRuntime.updatedAt
) {
if (persistedSupervisorRuntime.sessionId) {
projectSupervisorSessionIdRef.current =
persistedSupervisorRuntime.sessionId;
setProjectSupervisorSessionId(persistedSupervisorRuntime.sessionId);
}
updateProjectSupervisorRuntime(persistedSupervisorRuntime);
updateProjectSupervisorResponseStream(
runtimes[supervisorRuntimeIndex]?.responseStream,
persistedSupervisorRuntime,
);
setProjectSupervisorRuntimeError('');
}
}
setAgentRuntimeById((current) =>
nextRuntimes.reduce(
(next, runtime) => mergeAgentRuntimeStateIntoMap(next, runtime, true),
current,
),
);
} catch {
if (localProjectPathRef.current === nextProjectPath) {
setAgentRuntimeById({});
}
}
}
async function refreshAgentRunTrace(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
await refreshAgentRuntimes(nextProjectPath);
await refreshAgentRunHistory(nextProjectPath);
return null;
}
const summary = await loadAgentRunTraceFile(
'.agent/run.latest.json',
nextProjectPath,
);
await refreshAgentRuntimes(nextProjectPath);
await refreshAgentRunHistory(nextProjectPath);
return summary;
}
async function handleAgentRunTraceRefresh(skipPolicyConfirm = false) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
return;
}
try {
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
requestProjectPolicyConfirmation(
'agent.trace_read',
nextProjectPath,
`读取 ${nextProjectPath} 的最近 Agent run trace`,
() => void handleAgentRunTraceRefresh(true),
);
setAgentRunStatus('等待确认');
return;
}
}
await refreshAgentRunTrace(nextProjectPath);
} catch (error) {
setAgentRunStatus(error instanceof Error ? error.message : String(error));
}
}
async function scheduleReadyAgentTasks(skipPolicyConfirm = false) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentRunStatus('需要在 Tauri App 内运行');
return;
}
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
return;
}
try {
if (!skipPolicyConfirm) {
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (
policyView.policy.confirmCommands.includes('agent.schedule_ready')
) {
requestProjectPolicyConfirmation(
'agent.schedule_ready',
nextProjectPath,
`调度 ${nextProjectPath} 中已 Ready 的 Agent 任务`,
() => void scheduleReadyAgentTasks(true),
);
setAgentRunStatus('等待确认调度 Ready 任务');
return;
}
}
setAgentRunStatus('正在调度 Ready 任务');
const scheduledRuntimes = await invoke<AgentRuntimeResult[]>(
'schedule_game_creator_agent_ready_tasks',
{ projectPath: nextProjectPath, limit: 16 },
);
for (const runtimeResult of scheduledRuntimes) {
rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult));
}
await refreshManifest(nextProjectPath);
await refreshAgentRuntimes(nextProjectPath);
const message =
scheduledRuntimes.length > 0
? `已调度 ${scheduledRuntimes.length} 个 Ready 任务。`
: '没有可调度的 Ready 任务。';
setAgentRunStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAgentRunStatus(message);
setMessages((current) => [
...current,
{ role: 'assistant', text: message },
]);
}
}
const agentStatusCards = deriveAgentStatusCards(
manifest,
agentRunTrace ?? agentRunHistory[0]?.trace ?? null,
agentRuntimeById,
);
const professionalResultCandidates = taskRowsFromManifest(manifest).map(
(task) => ({
agentId: agentConversationId(task),
label: projectProfessionalAgentLabel(agentConversationId(task)),
runtimeUpdatedAt:
agentRuntimeById[agentConversationId(task)]?.updatedAt ?? 0,
}),
);
const professionalResultCandidateKey = professionalResultCandidates
.map((candidate) => `${candidate.agentId}:${candidate.runtimeUpdatedAt}`)
.join('|');
useEffect(() => {
const nextProjectPath = localProject?.projectPath;
if (!projectSupervisorOnly || !nextProjectPath || !onManifestChange) {
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
return;
}
let cancelled = false;
void (async () => {
for (let attempt = 0; attempt < 2; attempt += 1) {
const before = await invoke<LocalGameProjectRevisionStatus>(
'get_local_game_project_revision',
{ projectPath: nextProjectPath },
);
const currentManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath },
);
const after = await invoke<LocalGameProjectRevisionStatus>(
'get_local_game_project_revision',
{ projectPath: nextProjectPath },
);
if (before.revision !== after.revision) {
continue;
}
if (!cancelled) {
onManifestChange(nextProjectPath, currentManifest, {
projectId: currentManifest.projectId,
revision: after.revision,
source: 'supervisor',
});
}
return;
}
})().catch(() => undefined);
return () => {
cancelled = true;
};
}, [
localProject?.projectPath,
manifest,
onManifestChange,
projectSupervisorOnly,
]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
if (
!projectSupervisorOnly ||
directCodexProductRuntime ||
!invoke ||
!nextProjectPath ||
professionalResultCandidates.length === 0
) {
setProfessionalAgentResultsById({});
return;
}
let disposed = false;
void Promise.all(
professionalResultCandidates.map(async (candidate) => {
try {
const conversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: candidate.agentId,
},
);
const finalMessage = conversation.messages
.slice()
.reverse()
.find(
(message) =>
message.role === 'assistant' &&
message.content.trim() &&
isAgentFinalizationMessageId(message.messageId),
);
if (!finalMessage) {
return {
agentId: candidate.agentId,
status: 'missing' as const,
};
}
return {
agentId: candidate.agentId,
status: 'loaded' as const,
result: {
agentId: candidate.agentId,
runId: finalMessage.messageId!,
label: candidate.label,
title: `${candidate.label} 文本回执`,
content: finalMessage.content.trim(),
updatedAt: finalMessage.updatedAt,
} satisfies ProjectAgentResultSummary,
};
} catch {
return {
agentId: candidate.agentId,
status: 'failed' as const,
};
}
}),
).then((results) => {
if (disposed || localProjectPathRef.current !== nextProjectPath) {
return;
}
setProfessionalAgentResultsById((current) => {
const candidateAgentIds = new Set(
professionalResultCandidates.map((candidate) => candidate.agentId),
);
const next = Object.fromEntries(
Object.entries(current).filter(([agentId]) =>
candidateAgentIds.has(agentId),
),
);
for (const outcome of results) {
if (outcome.status === 'loaded') {
next[outcome.agentId] = outcome.result;
} else if (outcome.status === 'missing') {
delete next[outcome.agentId];
}
}
return next;
});
});
return () => {
disposed = true;
};
// The semantic candidate key replaces the freshly allocated candidates array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
directCodexProductRuntime,
localProject?.projectPath,
professionalResultCandidateKey,
projectSupervisorOnly,
projectSupervisorRuntime?.runId,
]);
useEffect(() => {
if (!projectSupervisorOnly || !onAgentRuntimeSummariesChange) {
return;
}
onAgentRuntimeSummariesChange(
projectAgentRuntimeSummaries(
manifest,
projectSupervisorRuntime,
agentRuntimeById,
),
);
}, [
agentRuntimeById,
manifest,
onAgentRuntimeSummariesChange,
projectSupervisorOnly,
projectSupervisorRuntime,
]);
useEffect(() => {
if (!projectSupervisorOnly || !onAgentResultsChange) {
return;
}
onAgentResultsChange(Object.values(professionalAgentResultsById));
}, [
onAgentResultsChange,
professionalAgentResultsById,
projectSupervisorOnly,
]);
const mainProjectSummary = localProject
? summarizeMainProjectHeader(manifest, agentStatusCards)
: null;
const visibleAgentRunHistory = agentRunHistory.slice(
0,
agentRunHistoryVisibleCount,
);
const visibleMainProjectFiles = projectFiles
.filter((file) => file.kind === 'file' && !file.path.startsWith('.agent/'))
.slice(0, 8);
const visibleMainProjectAssets = manifest.assets
.filter(
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
)
.slice(0, 8);
const chatProjectAssets = manifest.assets.filter(
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
);
// `@` 面板「当前版本素材」的版本来源:版本列表来自 manifest,
// 当前版本由工作台壳(`WorkspaceLauncherShell`)持有的那一份状态给出,
// 传 `null` 表示回退到 manifest 中最新的版本。
const chatProjectVersions = manifest.versions ?? [];
const chatActiveVersionId = activeVersionId;
const visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5);
const currentProjectTitle = localProject
? manifest.name.trim() || projectNameFromPath(localProject.projectPath)
: seedManifest.name;
useEffect(() => {
if (!supervisorChatOnly) {
return;
}
setWindowTitle(localProject ? currentProjectTitle : undefined);
return () => setWindowTitle(undefined);
}, [currentProjectTitle, localProject, setWindowTitle, supervisorChatOnly]);
const agentRunHistoryOmittedCount =
Math.max(0, agentRunHistoryFiles.length - agentRunHistoryVisibleCount) +
agentRunHistoryOverflowCount;
const canShowMoreAgentRunHistory =
agentRunHistoryVisibleCount < agentRunHistoryFiles.length;
async function showMoreAgentRunHistory() {
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke || agentRunHistoryLoadingMoreRef.current) {
return;
}
if (!nextProjectPath) {
setAgentRunStatus('请先初始化本地项目');
return;
}
const nextVisibleCount = Math.min(
agentRunHistoryVisibleCount + AGENT_RUN_HISTORY_VISIBLE_STEP,
agentRunHistoryFiles.length,
);
const nextFiles = agentRunHistoryFiles.slice(
agentRunHistoryVisibleCount,
nextVisibleCount,
);
agentRunHistoryLoadingMoreRef.current = true;
setAgentRunHistoryLoadingMore(true);
try {
const nextItems = await readAgentRunHistoryItems(
invoke,
nextFiles,
nextProjectPath,
);
setAgentRunHistory((current) =>
[...current, ...nextItems].sort(
(left, right) => right.trace.updatedAt - left.trace.updatedAt,
),
);
setAgentRunHistoryVisibleCount(nextVisibleCount);
} finally {
agentRunHistoryLoadingMoreRef.current = false;
setAgentRunHistoryLoadingMore(false);
}
}
function handleAgentRunHistoryScroll(event: UIEvent<HTMLElement>) {
if (!canShowMoreAgentRunHistory) {
return;
}
const target = event.currentTarget;
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 24) {
void showMoreAgentRunHistory();
}
}
const visibleMessages = latestVisibleItems(
messages,
conversationVisibleCount,
);
const hiddenConversationCount = Math.max(
0,
messages.length - visibleMessages.length,
);
const hasEarlierConversationMessages =
hiddenConversationCount > 0 ||
(directCodexProductRuntime && directHistoryHasMore);
const projectSupervisorTransientReply =
projectSupervisorResponseStream?.accumulatedText.trim() ?? '';
const projectSupervisorNeedsUserInput = agentRuntimeNeedsUserInput(
projectSupervisorRuntime,
);
const projectSupervisorHasConversationControls = Boolean(
projectSupervisorRuntime?.pendingToolAction ||
projectSupervisorRuntime?.userInputRequest ||
projectSupervisorNeedsUserInput,
);
const visibleAgentConversationMessages = latestVisibleItems(
agentConversationMessages,
agentConversationVisibleCount,
);
const hiddenAgentConversationCount = Math.max(
0,
agentConversationMessages.length - visibleAgentConversationMessages.length,
);
const selectedAgentLlmStatus = selectedAgent
? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent)
: null;
const selectedAgentLlmWarning = selectedAgent
? formatAgentLlmConfigWarning(llmConfigStatus, selectedAgent)
: null;
const selectedAgentNeedsUserInput = agentRuntimeNeedsUserInput(
agentConversationRuntime,
);
const selectedAgentSteerRuntime = selectedAgent
? matchingAgentRuntimeForSteer(
[agentConversationRuntime],
selectedAgent.id,
agentConversationSessionId,
)
: null;
async function showEarlierConversationMessages() {
if (directCodexProductRuntime && directHistoryHasMore) {
const invoke = resolveTauriInvoke();
const projectPath = localProject?.projectPath;
if (invoke && projectPath && !directHistoryLoadingRef.current) {
directHistoryLoadingRef.current = true;
try {
// 一次点击连拉,直到出现新回合或取不动为止:一屏全是工具卡片 / 思考文本时,
// 单发一页会让用户点了"显示更早"却看不到任何变化。
const pages = await readDirectHistoryPages({
existingEntries: selectDirectChatEntries(directThreadChat),
beforeItemId: directHistoryOldestItemIdRef.current,
readSlice: (beforeItemId) =>
invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
beforeItemId
? {
projectPath,
beforeItemId,
limit: CONVERSATION_VISIBLE_STEP,
}
: { projectPath, limit: CONVERSATION_VISIBLE_STEP },
),
});
if (localProjectPathRef.current !== projectPath) {
return;
}
// 连拉到的页一次性进聊天 reducer:与运行态同形、同身份,重复读取只补不重。
setDirectThreadChat((state) =>
mergeDirectHistoryItems(state, pages.items),
);
// 首页或中途读取失败时保留旧值:失败页没有可靠的 hasMore,不能因为
// 一次瞬时 IO 抖动把「显示更早」按钮永久收掉。
if (!pages.error) {
setDirectHistoryHasMore(pages.hasMore);
}
directHistoryOldestItemIdRef.current =
pages.firstItemId ?? directHistoryOldestItemIdRef.current;
if (pages.error) {
// 已经取到的页照常进视图,失败只走下面同一个报错出口。
throw pages.error;
}
} catch (error) {
setWorkspaceStatus(
`读取更早的对话历史失败:${error instanceof Error ? error.message : String(error)}`,
);
} finally {
directHistoryLoadingRef.current = false;
}
}
return;
}
setConversationVisibleCount((current) =>
Math.min(messages.length, current + CONVERSATION_VISIBLE_STEP),
);
}
function showEarlierAgentConversationMessages() {
setAgentConversationVisibleCount((current) =>
Math.min(
agentConversationMessages.length,
current + CONVERSATION_VISIBLE_STEP,
),
);
}
function handleConversationScroll(event: UIEvent<HTMLElement>) {
if (!hasEarlierConversationMessages) {
return;
}
if (event.currentTarget.scrollTop <= 24) {
showEarlierConversationMessages();
}
}
function handleSupervisorChatScroll(event: UIEvent<HTMLDivElement>) {
handleConversationScroll(event);
const messageList = event.currentTarget;
const distanceFromBottom =
messageList.scrollHeight -
messageList.scrollTop -
messageList.clientHeight;
supervisorChatShouldFollowLatestRef.current =
distanceFromBottom <= AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD;
}
function handleAgentConversationScroll(event: UIEvent<HTMLElement>) {
if (hiddenAgentConversationCount === 0) {
return;
}
if (event.currentTarget.scrollTop <= 24) {
showEarlierAgentConversationMessages();
}
}
useEffect(() => {
if (!selectedAgent) {
return;
}
const refreshedAgent = agentStatusCards.find(
(agent) => agent.id === selectedAgent.id,
);
if (!refreshedAgent) {
closeAgentConversation();
return;
}
if (!sameAgentStatusCard(selectedAgent, refreshedAgent)) {
setSelectedAgent(refreshedAgent);
}
}, [agentStatusCards, selectedAgent]);
/**
* 发起一轮 direct-codex 对话回合:提交与队列出队共用同一条路径,避免两条入口的
* 消息落盘/回合 id/附件参数走样。
*/
function startDirectCodexConversationTurn(input: {
prompt: string;
attachments?: DirectCodexTurnAttachment[];
references?: ChatReference[];
content?: DirectCodexUserContentPart[];
}) {
const clientTurnId = createDirectCodexConversationTurnId();
supervisorChatShouldFollowLatestRef.current = true;
setMessages((current) => [
...current,
{
role: 'user',
text: input.prompt,
runtimeOwned: true,
messageId: directCodexConversationMessageId(clientTurnId, 'user'),
updatedAt: Date.now(),
},
]);
void executeChatAgentReply({
prompt: input.prompt,
clientTurnId,
attachments: input.attachments?.length ? input.attachments : undefined,
references: input.references,
userItem: chatComposerDraftToDirectCodexUserItem(
{
text: input.prompt,
references: input.references ?? [],
content: input.content ?? [],
},
directCodexConversationMessageId(clientTurnId, 'user'),
),
});
}
/**
* 输入盒上传本地文件:复用首页建项目那条 `upload_local_asset` 链路把文件写进项目,
* 再以**项目相对路径**生成回合附件(绝对路径会被 Rust 侧附件规则判为失败)。
*/
async function handleChatComposerUploadFiles(files: readonly File[]) {
const invoke = resolveTauriInvoke();
const nextProjectPath = resolveChatProjectPath(localProject);
if (!invoke || !nextProjectPath) {
setChatAttachmentNotice('需要先打开本地项目,才能上传文件');
return;
}
const remaining = MAX_CHAT_COMPOSER_ATTACHMENTS - chatAttachments.length;
const accepted = files.slice(0, Math.max(remaining, 0));
if (accepted.length === 0) {
setChatAttachmentNotice(
`最多同时携带 ${MAX_CHAT_COMPOSER_ATTACHMENTS} 个附件,请先移除已有附件`,
);
return;
}
setChatAttachmentNotice('正在上传文件');
try {
const imported = await uploadLocalFilesAsAttachments(
invoke,
nextProjectPath,
accepted,
);
const attachments = toDirectCodexTurnAttachments(imported);
if (localProjectPathRef.current !== nextProjectPath) {
return;
}
setChatAttachments((current) =>
[...current, ...attachments].slice(0, MAX_CHAT_COMPOSER_ATTACHMENTS),
);
const failed = attachments.filter(
(attachment) => attachment.status === 'failed',
);
setChatAttachmentNotice(
failed.length > 0
? `${failed.length} 个文件未能上传:${failed[0]?.name ?? ''}`
: `已上传 ${attachments.length} 个文件,将在下次发送时作为本轮附件`,
);
void refreshManifest(nextProjectPath);
} catch (error) {
if (localProjectPathRef.current === nextProjectPath) {
setChatAttachmentNotice(
error instanceof Error ? error.message : String(error),
);
}
}
}
function removeChatComposerAttachment(index: number) {
setChatAttachments((current) =>
current.filter((_, currentIndex) => currentIndex !== index),
);
}
/** 回合运行中再次发送:进本地 FIFO 队列;队列满时拒绝并保留草稿,不静默丢消息。 */
function enqueueChatTurnForRunningTurn(input: {
prompt: string;
attachments: DirectCodexTurnAttachment[];
references: ChatReference[];
content: DirectCodexUserContentPart[];
}): boolean {
if (isChatTurnQueueFull(chatTurnQueueRef.current)) {
setChatComposerNotice(chatQueueFullNotice());
return false;
}
queuedChatTurnSequenceRef.current += 1;
const turn = createQueuedChatTurn({
id: `queued-chat-turn-${Date.now()}-${queuedChatTurnSequenceRef.current}`,
prompt: input.prompt,
attachments: input.attachments,
references: input.references,
content: input.content,
createdAt: Date.now(),
});
const nextQueue = enqueueChatTurn(chatTurnQueueRef.current, turn);
chatTurnQueueRef.current = nextQueue;
setChatTurnQueue(nextQueue);
setChatComposerNotice('已加入发送队列,当前回合结束后自动发送');
return true;
}
function cancelQueuedChatTurn(id: string) {
const nextQueue = removeQueuedChatTurn(chatTurnQueueRef.current, id);
chatTurnQueueRef.current = nextQueue;
setChatTurnQueue(nextQueue);
if (nextQueue.length === 0) {
setChatComposerNotice('');
}
}
/** 队首出队并立即发出:只在 DirectProject 收到 `turn.completed` 后调用。 */
function dispatchNextQueuedChatTurn() {
if (chatAgentBusyRef.current || directTurnRunningRef.current) {
return;
}
directTurnCompletionPendingRef.current = false;
const { next, rest } = dequeueChatTurn(chatTurnQueueRef.current);
if (!next) {
return;
}
chatTurnQueueRef.current = rest;
setChatTurnQueue(rest);
if (rest.length === 0) {
setChatComposerNotice('');
}
startDirectCodexConversationTurn({
prompt: next.prompt,
attachments: next.attachments,
references: next.references,
content: next.content,
});
}
/**
* 回合事件可能先于本地 invoke 的 finally 到达,因此先记 pending,等本地忙碌态复位后再出队。
*/
useEffect(() => {
if (!directCodexProductRuntime) {
directTurnCompletionPendingRef.current = false;
previousDirectTurnRunningRef.current = directTurnRunning;
return;
}
const wasRunning = previousDirectTurnRunningRef.current;
previousDirectTurnRunningRef.current = directTurnRunning;
if (wasRunning && !directTurnRunning) {
directTurnCompletionPendingRef.current = true;
}
if (
directTurnCompletionPendingRef.current &&
!chatAgentBusy &&
!directTurnRunning
) {
directTurnCompletionPendingRef.current = false;
dispatchNextQueuedChatTurn();
}
// 队列出队函数读取本轮 render 的项目输入;这里只由三个生命周期信号触发。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chatAgentBusy, directCodexProductRuntime, directTurnRunning]);
/** 终止当前 direct-codex 回合:只取消这一轮,UI 由回合的 finally 复位。 */
async function handleCancelDirectCodexTurn() {
if (directCodexTurnCancelling) {
return;
}
const invoke = resolveTauriInvoke();
const directProjectPath = resolveChatProjectPath(localProject);
// 繁忙判据与「终止」按钮的可见条件一致:本地 invoke 正在跑,或订阅说还有一条回合没结束。
// 只认 `turn.started` 推导出来的 `directTurnRunning` 会漏掉"刚提交、事件还没到"的窗口。
if (!invoke || !directProjectPath || !supervisorChatBusy) {
setProjectSupervisorRuntimeError('当前没有正在运行的回合,无法终止。');
return;
}
setDirectCodexTurnCancelling(true);
setChatComposerNotice('正在终止当前回合');
try {
const result = await invoke<DirectTurnCancelView>(
'cancel_direct_codex_turn',
{ projectPath: directProjectPath },
);
const message = result?.message?.trim();
if (result?.outcome === 'released') {
// 这一轮已经没有人替它收尾(执行进程已退出 / 从没进执行器),Rust 侧强制释放了
// 守卫并补了终态事件;这里按同一个收口函数同步把界面复位,不等 IPC 通知。
// 时刻取宿主观测到的这一刻:终止返回就是这一轮的终态,原生随后补的事件若先到,
// 收口已经是冻结值,不会被抬高,也不会复活成"永远运行中"。
// 但取消回包可能晚于新回合的开始:先核对身份(clientTurnId 对应的本轮用户条目),
// 只收口确实是这一轮的那一次,避免在新回合的回调里把旧轮的时间盖上来。
const cancelledUserItemId = result.clientTurnId
? directCodexConversationMessageId(result.clientTurnId, 'user')
: '';
setDirectThreadChat((state) =>
directThreadTurnMatchesUser(state, cancelledUserItemId)
? finishDirectThreadTurn(state, Date.now())
: state,
);
setChatAgentBusy(false);
setProjectSupervisorRuntimeError('');
setChatComposerNotice(
message ?? '已结束这一轮占用,可以直接重新发送消息',
);
return;
}
if (message) {
setChatComposerNotice(message);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setProjectSupervisorRuntimeError(`终止失败:${message}`);
setChatComposerNotice('');
} finally {
setDirectCodexTurnCancelling(false);
}
}
function handleProjectSupervisorOnlySubmit(
event: FormEvent<HTMLFormElement>,
) {
event.preventDefault();
const prompt = chatInput.trim();
const references = chatReferences;
const pendingAttachments = chatAttachments;
if (
!directCodexProductRuntime &&
supervisorChatOnly &&
prompt.startsWith('/')
) {
void handleChatSubmit(event);
return;
}
if (
!directCodexProductRuntime &&
agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)
) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
if (
!prompt &&
references.length === 0 &&
chatContent.length === 0 &&
pendingAttachments.length === 0
) {
return;
}
if (supervisorChatBusy) {
// 回合运行中再次发送:direct-codex 面板把消息放进本地 FIFO 队列,当前回合结束后
// 依次发出;其它面板保持原有"运行中不接受新输入"的行为。
if (directCodexProductRuntime) {
const enqueued = enqueueChatTurnForRunningTurn({
prompt,
attachments: pendingAttachments,
references,
content: chatContent,
});
if (enqueued) {
setChatInput('');
setChatContent([]);
setChatReferences([]);
setChatAttachments([]);
setChatAttachmentNotice('');
}
}
return;
}
if (directCodexProductRuntime && prompt === '/history') {
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
setChatInput('');
setChatReferences([]);
setChatContent([]);
void loadProjectConversation(nextProjectPath, false, 'replace');
return;
}
if (supervisorChatOnly || directCodexProductRuntime || designAgentActive) {
supervisorChatShouldFollowLatestRef.current = true;
}
if (directCodexProductRuntime) {
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
setChatInput('');
setChatReferences([]);
setChatAttachments([]);
setChatContent([]);
setChatAttachmentNotice('');
setChatComposerNotice('');
startDirectCodexConversationTurn({
prompt,
attachments: pendingAttachments,
references,
content: chatContent,
});
return;
}
setChatInput('');
setChatReferences([]);
setChatContent([]);
setMessages((current) => [
...current,
{
role: 'user',
text: prompt,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
void executeChatAgentReply({ prompt, references });
}
const visibleProfessionalAgentCards = agentStatusCards.filter(
(agent) =>
agent.id !== PROJECT_SUPERVISOR_AGENT_ID &&
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
);
const useDesignAgentSurface = Boolean(designAgentView) || designAgentActive;
if (projectSupervisorOnly && supervisorChatOnly) {
const supervisorProjectPath =
localProject?.projectPath || initialProjectPath || projectPath;
return (
<SupervisorChatOnlyView
activeVersionId={chatActiveVersionId}
chatAgentBusy={chatAgentBusy}
chatInput={chatInput}
chatReferences={chatReferences}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
messagesRef={supervisorChatMessagesRef}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={handleChatComposerChange}
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
onConfirmConfirmation={confirmUiCommand}
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
onScroll={handleSupervisorChatScroll}
onShowEarlierMessages={showEarlierConversationMessages}
hasEarlierConversationMessages={hasEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
onToolAction={handleProjectSupervisorToolAction}
onUserInput={handleProjectSupervisorUserInput}
pendingConfirmation={pendingUiConfirmation}
pendingCommand={pendingCommand}
onCancelPendingCommand={handlePendingCommandCancel}
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
projectPath={supervisorProjectPath}
runtime={projectSupervisorRuntime}
runtimeConfigOpen={runtimeConfigOpen}
runtimeError={projectSupervisorRuntimeError}
transientReply={
designAgentActive
? designAgentTransientReply
: projectSupervisorTransientReply
}
hasConversationControls={projectSupervisorHasConversationControls}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
visibleMessages={visibleMessages}
workspaceStatus={workspaceStatus}
expectedRunId={projectSupervisorExpectedRunId}
versions={chatProjectVersions}
/>
);
}
if (projectSupervisorOnly) {
return (
<ProjectSupervisorView
initialSupervisorMessage={initialSupervisorMessage}
activeVersionId={chatActiveVersionId}
attachments={chatAttachments}
attachmentNotice={chatAttachmentNotice}
chatInput={chatInput}
chatReferences={chatReferences}
composerNotice={chatComposerNotice}
onCancelQueuedTurn={cancelQueuedChatTurn}
onCancelTurn={() => void handleCancelDirectCodexTurn()}
onRemoveAttachment={removeChatComposerAttachment}
onUploadFiles={(files) => void handleChatComposerUploadFiles(files)}
queuedTurns={chatTurnQueue}
turnCancelling={directCodexTurnCancelling}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
directCodex={directCodexProductRuntime}
directTurnRunning={directCodexProductRuntime && directTurnRunning}
directTurnStartedAt={directThreadChat.turnStartedAt}
directEntries={
directCodexProductRuntime
? selectDirectChatEntries(directThreadChat)
: []
}
hiddenConversationCount={hiddenConversationCount}
hasEarlierConversationMessages={hasEarlierConversationMessages}
messagesRef={supervisorChatMessagesRef}
needsUserInput={
directCodexProductRuntime ? false : projectSupervisorNeedsUserInput
}
onCancelConfirmation={cancelUiCommandConfirmation}
onCancelPendingCommand={handlePendingCommandCancel}
onChatInputChange={handleChatComposerChange}
onConfirmConfirmation={confirmUiCommand}
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
onScroll={handleSupervisorChatScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
pendingConfirmation={
directCodexProductRuntime ? null : pendingUiConfirmation
}
pendingCommand={directCodexProductRuntime ? pendingCommand : null}
projectPath={localProject?.projectPath ?? projectPath}
conversationMessages={messages}
transientReply={
designAgentActive
? designAgentTransientReply
: directCodexProductRuntime
? ''
: projectSupervisorTransientReply
}
showDesignReasoning={designAgentActive}
designReasoning={designAgentReasoning}
designReasoningEntries={
useDesignAgentSurface ? (designAgentView?.reasoningEntries ?? []) : []
}
visibleMessages={visibleMessages}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
showProfessionalCollaboration={
!directCodexProductRuntime && orchestrationMode === 'professional-dag'
}
workspaceStatus={workspaceStatus}
designView={useDesignAgentSurface ? designAgentView : null}
onDesignApprove={
useDesignAgentSurface
? (requestId, approved) => {
const nextProjectPath = resolveChatProjectPath(localProject);
const invoke = resolveTauriInvoke();
if (!nextProjectPath || !invoke) {
return;
}
const clientTurnId = createAgentChatRunId('design-agent-turn');
designAgentTurnRef.current = {
projectPath: nextProjectPath,
clientTurnId,
};
designAgentReasoningTurnRef.current = {
projectPath: nextProjectPath,
clientTurnId,
text: '',
};
designAgentPendingViewRef.current = null;
setDesignAgentTransientReplyTarget('');
setDesignAgentReasoning('');
setChatAgentBusy(true);
void designAgentEventSubscriptionReady()
.then(() =>
invoke<DesignView>('decide_design_phase', {
projectPath: nextProjectPath,
clientTurnId,
requestId,
approved,
}),
)
.then((view) => {
if (
localProjectPathRef.current !== nextProjectPath ||
designAgentTurnRef.current?.projectPath !==
nextProjectPath ||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
) {
return;
}
applyDesignAgentViewAfterTransient(
view,
nextProjectPath,
clientTurnId,
);
})
.catch((error) => {
if (
localProjectPathRef.current !== nextProjectPath ||
designAgentTurnRef.current?.projectPath !==
nextProjectPath ||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
) {
return;
}
setProjectSupervisorRuntimeError(String(error));
})
.finally(() => {
if (
localProjectPathRef.current !== nextProjectPath ||
designAgentTurnRef.current?.projectPath !==
nextProjectPath ||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
) {
return;
}
if (!designAgentPendingViewRef.current) {
designAgentTurnRef.current = null;
setDesignAgentTransientReplyTarget('');
}
setChatAgentBusy(false);
});
}
: undefined
}
onDesignClarify={
useDesignAgentSurface
? (question: DesignClarificationRequest, optionIndex, text) => {
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
return;
}
void executeDesignAgentTurn(nextProjectPath, {
type: 'clarification',
requestId: question.requestId,
optionIndex,
text: text.trim() ? text : null,
});
}
: undefined
}
onDesignRetry={
useDesignAgentSurface
? () => {
const nextProjectPath = resolveChatProjectPath(localProject);
if (!nextProjectPath) {
return;
}
void executeDesignAgentTurn(nextProjectPath, { type: 'retry' });
}
: undefined
}
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={
directCodexProductRuntime ? supervisorChatBusy : chatAgentBusy
}
readOnly={directCodexProductRuntime}
versions={chatProjectVersions}
professionalResultsByAgentId={professionalAgentResultsById}
onToolAction={handleProjectSupervisorToolAction}
onSupervisorRetry={handleProjectSupervisorRetry}
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
onProfessionalRetry={handleProjectProfessionalAgentRetry}
onUserInput={handleProjectSupervisorUserInput}
/>
);
}
return (
<main
className={`app-shell ${devMode ? 'app-shell--dev' : 'app-shell--user'}`}
>
<ProjectWorkspaceChatPane
activeVersionId={chatActiveVersionId}
agentRunStatus={agentRunStatus}
agentRuntimeById={agentRuntimeById}
agentStatusCards={agentStatusCards}
cancelProjectCreateInNonEmptyFolder={
cancelProjectCreateInNonEmptyFolder
}
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
chatAgentBusy={chatAgentBusy}
chatInput={chatInput}
chatReferences={chatReferences}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
chatInputRef={chatInputRef}
confirmProjectCreateInNonEmptyFolder={
confirmProjectCreateInNonEmptyFolder
}
confirmUiCommand={confirmUiCommand}
currentProjectTitle={currentProjectTitle}
devMode={devMode}
executeAgentAuditChat={executeAgentAuditChat}
executeAgentCapabilitiesChat={executeAgentCapabilitiesChat}
executeAgentRunControl={executeAgentRunControl}
executeAgentTraceChat={executeAgentTraceChat}
executeLimitedCommandList={executeLimitedCommandList}
executeLlmConfigStatus={executeLlmConfigStatus}
executeMemoryReadChat={executeMemoryReadChat}
executePreviewStatus={executePreviewStatus}
executePreviewStop={executePreviewStop}
executeProjectAssets={executeProjectAssets}
executeProjectCheckpoints={executeProjectCheckpoints}
executeProjectDiff={executeProjectDiff}
executeProjectFileReadChat={executeProjectFileReadChat}
executeProjectFiles={executeProjectFiles}
executeProjectPolicyRead={executeProjectPolicyRead}
executeProjectStatus={executeProjectStatus}
executeProjectTasks={executeProjectTasks}
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
handleAssetUpload={handleAssetUpload}
handleChatSubmit={handleChatSubmit}
handleConversationScroll={handleConversationScroll}
handleOpenLauncherWindow={handleOpenLauncherWindow}
handlePendingCommandCancel={handlePendingCommandCancel}
handlePendingCommandConfirm={handlePendingCommandConfirm}
handlePickCanvasExportFileDraft={handlePickCanvasExportFileDraft}
handleProjectProfessionalAgentRetry={
handleProjectProfessionalAgentRetry
}
handleProjectProfessionalAgentToolAction={
handleProjectProfessionalAgentToolAction
}
handleProjectSupervisorRetry={handleProjectSupervisorRetry}
handleProjectSupervisorToolAction={handleProjectSupervisorToolAction}
handleProjectSupervisorUserInput={handleProjectSupervisorUserInput}
handleRevealCurrentProjectDirectory={
handleRevealCurrentProjectDirectory
}
handleRuntimeConfigOpen={handleRuntimeConfigOpen}
hiddenConversationCount={hiddenConversationCount}
hasEarlierConversationMessages={hasEarlierConversationMessages}
llmConfigStatus={llmConfigStatus}
loadProjectConversation={loadProjectConversation}
localProject={localProject}
mainProjectSummary={mainProjectSummary}
openAgentConversation={openAgentConversation}
pendingCommand={pendingCommand}
pendingNonEmptyProjectCreate={pendingNonEmptyProjectCreate}
pendingUiConfirmation={pendingUiConfirmation}
prepareChatCommandDraft={prepareChatCommandDraft}
prepareProjectAssetRegisterDraft={prepareProjectAssetRegisterDraft}
previewStatus={previewStatus}
professionalAgentResultsById={professionalAgentResultsById}
projectPath={projectPath}
projectSupervisorNeedsUserInput={projectSupervisorNeedsUserInput}
projectSupervisorRuntime={projectSupervisorRuntime}
projectSupervisorRuntimeError={projectSupervisorRuntimeError}
projectSupervisorTransientReply={projectSupervisorTransientReply}
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
queuePendingCommand={queuePendingCommand}
queuePreviewOpenShortcut={queuePreviewOpenShortcut}
queuePreviewStartShortcut={queuePreviewStartShortcut}
queueProjectCheckpointShortcut={queueProjectCheckpointShortcut}
queueProjectExportPackageShortcut={queueProjectExportPackageShortcut}
queueRunLocalShortcut={queueRunLocalShortcut}
queueStaticSmokeShortcut={queueStaticSmokeShortcut}
scheduleReadyAgentTasks={scheduleReadyAgentTasks}
onChatComposerChange={handleChatComposerChange}
showChatHelp={showChatHelp}
showEarlierConversationMessages={showEarlierConversationMessages}
visibleMainProjectAssets={visibleMainProjectAssets}
versions={chatProjectVersions}
visibleMainProjectCheckpoints={visibleMainProjectCheckpoints}
visibleMainProjectFiles={visibleMainProjectFiles}
visibleMessages={visibleMessages}
workspaceStatus={workspaceStatus}
/>
{runtimeConfigOpen ? (
<RuntimeConfigDialog
projectPath={localProject?.projectPath}
onClose={() => setRuntimeConfigOpen(false)}
onLog={(entry) => setCommandLog((current) => [...current, entry])}
/>
) : null}
{selectedAgent ? (
<AgentConversationOverlay
agentConversationBackgroundBusy={agentConversationBackgroundBusy}
agentConversationInput={agentConversationInput}
agentConversationMessages={agentConversationMessages}
agentConversationRunSubmitMode={agentConversationRunSubmitMode}
agentConversationRuntime={agentConversationRuntime}
agentConversationRuntimeError={agentConversationRuntimeError}
agentConversationSaving={agentConversationSaving}
agentConversationStatus={agentConversationStatus}
agentMemoryContent={agentMemoryContent}
agentMemoryStatus={agentMemoryStatus}
answerSelectedAgentRuntimeUserInput={
answerSelectedAgentRuntimeUserInput
}
cancelSelectedAgentRuntimeTask={cancelSelectedAgentRuntimeTask}
closeAgentConversation={closeAgentConversation}
confirmSelectedAgentRuntimeTask={confirmSelectedAgentRuntimeTask}
executeLlmConfigStatus={executeLlmConfigStatus}
handleAgentBackgroundTaskSubmit={handleAgentBackgroundTaskSubmit}
handleAgentConversationScroll={handleAgentConversationScroll}
handleAgentConversationSubmit={handleAgentConversationSubmit}
handleAgentPrivateMemorySubmit={handleAgentPrivateMemorySubmit}
hiddenAgentConversationCount={hiddenAgentConversationCount}
openAgentConversation={openAgentConversation}
prepareAgentEvidenceReadDraft={prepareAgentEvidenceReadDraft}
prepareSuggestedToolCommandDraft={prepareSuggestedToolCommandDraft}
rejectSelectedAgentRuntimeTask={rejectSelectedAgentRuntimeTask}
retrySelectedAgentRuntimeTask={retrySelectedAgentRuntimeTask}
selectedAgent={selectedAgent}
selectedAgentLlmStatus={selectedAgentLlmStatus}
selectedAgentLlmWarning={selectedAgentLlmWarning}
selectedAgentNeedsUserInput={selectedAgentNeedsUserInput}
selectedAgentSteerRuntime={selectedAgentSteerRuntime}
setAgentConversationInput={setAgentConversationInput}
setAgentConversationRunSubmitMode={setAgentConversationRunSubmitMode}
setRuntimeConfigOpen={setRuntimeConfigOpen}
showEarlierAgentConversationMessages={
showEarlierAgentConversationMessages
}
visibleAgentConversationMessages={visibleAgentConversationMessages}
/>
) : null}
{devMode ? (
<section className="developer-pane" aria-label="开发环境">
<DeveloperRuntimePanels
agentRunHistory={agentRunHistory}
agentRunHistoryLoadingMore={agentRunHistoryLoadingMore}
agentRunHistoryOmittedCount={agentRunHistoryOmittedCount}
agentRunStatus={agentRunStatus}
agentRunTrace={agentRunTrace}
canShowMoreAgentRunHistory={canShowMoreAgentRunHistory}
handleAgentRunHistoryScroll={handleAgentRunHistoryScroll}
handleAgentRunTraceFileOpen={handleAgentRunTraceFileOpen}
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
manifest={manifest}
prepareChatCommandDraft={prepareChatCommandDraft}
refreshManifest={refreshManifest}
showMoreAgentRunHistory={showMoreAgentRunHistory}
visibleAgentRunHistory={visibleAgentRunHistory}
/>
<DeveloperProjectPanels
assetCanvasProjectId={assetCanvasProjectId}
assetGenerationPrompt={assetGenerationPrompt}
assetKind={assetKind}
assetLocalPath={assetLocalPath}
assetMediaType={assetMediaType}
assetObjectId={assetObjectId}
assetResourceId={assetResourceId}
assetSourceKind={assetSourceKind}
assetStatus={assetStatus}
canvasExportPath={canvasExportPath}
commandLog={commandLog}
editorBaseUrl={editorBaseUrl}
fileDraft={fileDraft}
filePath={filePath}
fileStatus={fileStatus}
handleAssetRegister={handleAssetRegister}
handleCanvasAssetGenerate={handleCanvasAssetGenerate}
handleCanvasAssetImport={handleCanvasAssetImport}
handleCanvasExportImport={handleCanvasExportImport}
handleCanvasProjectOpen={handleCanvasProjectOpen}
handleCanvasProjectSync={handleCanvasProjectSync}
handleFileDelete={handleFileDelete}
handleFileList={handleFileList}
handleFileRead={handleFileRead}
handleFileWrite={handleFileWrite}
handleLimitedCommandRun={handleLimitedCommandRun}
handleMemoryDelete={handleMemoryDelete}
handleMemoryRead={handleMemoryRead}
handleMemoryWrite={handleMemoryWrite}
handlePreviewStart={handlePreviewStart}
handlePreviewStatus={handlePreviewStatus}
handlePreviewStop={handlePreviewStop}
handleProjectInit={handleProjectInit}
handleProjectLogRead={handleProjectLogRead}
limitedCommandStatus={limitedCommandStatus}
limitedLocalCommands={limitedLocalCommands}
localProject={localProject}
memoryDraft={memoryDraft}
memoryScope={memoryScope}
memoryStatus={memoryStatus}
preview={preview}
previewStatus={previewStatus}
projectFiles={projectFiles}
projectLogContent={projectLogContent}
projectLogStatus={projectLogStatus}
projectPath={projectPath}
projectStatus={projectStatus}
refreshLimitedLocalCommands={refreshLimitedLocalCommands}
setAssetCanvasProjectId={setAssetCanvasProjectId}
setAssetGenerationPrompt={setAssetGenerationPrompt}
setAssetKind={setAssetKind}
setAssetLocalPath={setAssetLocalPath}
setAssetMediaType={setAssetMediaType}
setAssetObjectId={setAssetObjectId}
setAssetResourceId={setAssetResourceId}
setAssetSourceKind={setAssetSourceKind}
setCanvasExportPath={setCanvasExportPath}
setEditorBaseUrl={setEditorBaseUrl}
setFileDraft={setFileDraft}
setFilePath={setFilePath}
setMemoryDraft={setMemoryDraft}
setMemoryScope={setMemoryScope}
setProjectPath={setProjectPath}
uploadedAssets={uploadedAssets}
/>
</section>
) : null}
</main>
);
}