Files
Genarrative/apps/ai-game-creator-shell/src/App.tsx
T
kdletters c09a511aaa
Project CI / Repository checks (push) Successful in 2m24s
Project CI / Frontend tests (push) Successful in 2m54s
Project CI / Backend tests (push) Successful in 4m33s
Project CI / Native shell tests (push) Successful in 14m29s
Codex/fix pr188 191 ci (#194)
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/194
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-08-25 15:41:55 +08:00

11166 lines
357 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 GameCreationAppCommandDescriptor,
type GameCreationAppLimitedRunCommandDescriptor,
type GameCreationAppManifest,
type GameCreationAppPermission,
type GameCreationAppPreviewState,
type GameCreationAppPreviewStatus,
} 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,
PROJECT_SUPERVISOR_PLAN_SOURCE,
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,
GameCreatorAgentRuntimeUpdateEvent,
GameCreatorChatAgentReply,
GameCreatorDirectTurnUpdateEvent,
GameCreatorLlmConfigStatus,
GameCreatorManifestInvalidatedEvent,
GameCreatorRoleAgentChatStreamEvent,
GenerateLocalGameDraftResult,
ImportCanvasExportResult,
InitLocalProjectResult,
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,
PlanGddDecisionAction,
PlanGddStateViewV1,
ProjectPermissionPolicy,
ProjectPermissionPolicyView,
SyncCanvasProjectAssetsResult,
TauriInvoke,
UploadLocalAssetResult,
} from './app/types';
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 {
isDeveloperMode,
isTransientProjectOpenMessage,
latestVisibleItems,
persistSupervisorChatDraft,
type ProjectSupervisorComponentProps,
readInitialProjectPath,
readSupervisorChatDraft,
type WorkspaceLauncherProps,
writeRecentWorkspace,
} from './features/app-shell/model';
import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
import {
agentConversationReadDraftsFromManifest,
agentMemoryReadDraftsFromManifest,
commandDraftFromSuggestedToolCall,
deriveAgentStatusCards,
formatAgentDialogLlmStatus,
formatAgentLlmConfigWarning,
formatAgentRunControlError,
formatCodexAgentModeLabel,
formatLlmAgentStatusLine,
formatLlmRouteEndpoint,
isCodexAgentMode,
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,
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 { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
import {
appendMemoryContent,
memoryScopeLabel,
memoryScopePath,
parseOptionalMemoryScope,
parseRememberInput,
} from './features/project-workspace/memoryCommands';
import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation';
import { planningStateNeedsRuntimeRefresh } from './features/project-workspace/planningLane';
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 { SupervisorChatOnlyView } from './features/project-workspace/SupervisorChatOnlyView';
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
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:';
function directCodexActivityText(activity: string | null | undefined) {
switch (activity) {
case 'request-accepted':
return '已接收需求';
case 'understanding':
return '正在理解需求';
case 'project-inspection':
return '正在检查项目';
case 'file-change':
return '正在修改项目文件';
case 'controlled-tool':
return '正在执行受控工具';
case 'validation':
return '正在验证结果';
case 'response-finalization':
return '正在整理回复';
case 'none':
default:
return '陶泥儿正在处理';
}
}
const DIRECT_CODEX_TURN_UPDATE_STATUSES = new Set([
'accepted',
'running',
'streaming',
'finalizing',
'completed',
'failed',
]);
function directCodexConversationMessageId(
turnId: string,
role: ChatMessage['role'],
) {
return `${DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX}${turnId}:${role}`;
}
function directCodexConversationTurnId(
message: ChatMessage,
role: ChatMessage['role'],
) {
if (message.role !== role) {
return null;
}
const messageId = message.messageId?.trim() ?? '';
const roleSuffix = `:${role}`;
if (
!messageId.startsWith(DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX) ||
!messageId.endsWith(roleSuffix)
) {
return null;
}
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) ? turnId : null;
}
export function unansweredDirectCodexConversationTurn(messages: ChatMessage[]) {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message) {
continue;
}
const turnId = directCodexConversationTurnId(message, 'user');
if (!turnId) {
continue;
}
const assistantMessageId = directCodexConversationMessageId(
turnId,
'assistant',
);
if (
messages.some(
(candidate) =>
candidate.role === 'assistant' &&
candidate.messageId?.trim() === assistantMessageId,
)
) {
continue;
}
return { prompt: message.text, turnId };
}
return null;
}
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);
}
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) {
let claimedProjectPaths = initialSupervisorMessageClaimsByPage.get(window);
if (!claimedProjectPaths) {
claimedProjectPaths = new Set<string>();
initialSupervisorMessageClaimsByPage.set(window, claimedProjectPaths);
}
if (claimedProjectPaths.has(projectPath)) {
return false;
}
claimedProjectPaths.add(projectPath);
return true;
}
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;
supervisorChatOnly?: boolean;
initialSupervisorMessage?: string;
initialCreationType?: HomeCreationType | null;
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;
};
export function App({
initialProjectPath: initialProjectPathOverride = '',
initialProjectManifest,
initialProjectKind = 'web',
orchestrationMode = 'professional-dag',
projectSupervisorOnly = false,
planningStartMode = false,
supervisorChatOnly = false,
initialSupervisorMessage = '',
initialCreationType = null,
playRequest = null,
onPlayRequestHandled,
onManifestChange,
onPreviewChange,
onAgentRuntimeSummariesChange,
onAgentResultsChange,
}: AppProps = {}) {
// 做方案入口独立成链:立项策划需要委派、澄清 pending 与 GDD 审批,这些只存在于
// Supervisor Runtime;direct-codex 是单回合「生成→试玩→修」循环,没有对应机制。
// 因此策划入口不走产品默认的 direct-codex,做游戏与做素材保持 master 的新默认。
const directCodexProductRuntime =
DIRECT_CODEX_PRODUCT_RUNTIME &&
projectSupervisorOnly &&
!supervisorChatOnly &&
!planningStartMode;
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);
localProjectPathRef.current = localProject?.projectPath ?? null;
const manifestRefreshMountedRef = useRef(true);
const manifestRefreshStatesRef = useRef(
new Map<
string,
{
pending: boolean;
inFlight: Promise<void> | null;
}
>(),
);
const [manifest, setManifest] = useState<GameCreationAppManifest>(
initialProjectManifest ?? seedManifest,
);
const [projectStatus, setProjectStatus] = useState(
eagerSupervisorProject ? '已初始化' : '未初始化',
);
const [preview, setPreview] = useState<LocalPreviewResult | null>(null);
const [previewStatus, setPreviewStatus] = useState('未启动');
const initialSupervisorMessageLatchRef = useRef({
projectPath: initialProjectPath,
prompt: initialSupervisorMessage.trim(),
creationType: initialCreationType,
});
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 [chatAgentBusy, setChatAgentBusy] = useState(false);
const [directCodexProgress, setDirectCodexProgress] = useState('');
const [directCodexProgressUpdatedAt, setDirectCodexProgressUpdatedAt] =
useState<number | null>(null);
const [directCodexTransientReply, setDirectCodexTransientReply] =
useState('');
const [
directCodexTransientReplyUpdatedAt,
setDirectCodexTransientReplyUpdatedAt,
] = useState<number | null>(null);
const activeDirectCodexTurnRef = useRef<{
projectPath: string;
turnId: string;
lastSequence: number;
receivedDirectUpdate: boolean;
} | null>(null);
const recoveredDirectCodexTurnClaimsRef = useRef(new Set<string>());
const directCodexClaimReleaseOnConversationWriteFailureRef = useRef(
new Map<string, string>(),
);
const directCodexConversationTurnSequenceRef = useRef(0);
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 resetDirectCodexTurn() {
activeDirectCodexTurnRef.current = null;
setDirectCodexProgress('');
setDirectCodexProgressUpdatedAt(null);
setDirectCodexTransientReply('');
setDirectCodexTransientReplyUpdatedAt(null);
}
function clearDirectCodexTransientReply(projectPath: string, turnId: string) {
const activeTurn = activeDirectCodexTurnRef.current;
if (
activeTurn?.projectPath !== projectPath ||
activeTurn.turnId !== turnId
) {
return false;
}
activeDirectCodexTurnRef.current = null;
setDirectCodexTransientReply('');
setDirectCodexTransientReplyUpdatedAt(null);
return true;
}
const [projectSupervisorRuntime, setProjectSupervisorRuntime] =
useState<AgentRuntimeState | null>(null);
const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] =
useState<AgentRuntimeResponseStream | null>(null);
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
useState('');
const [planGddState, setPlanGddState] = useState<PlanGddStateViewV1 | null>(
null,
);
const planGddStateRef = useRef<PlanGddStateViewV1 | null>(null);
planGddStateRef.current = planGddState;
const planGddHydrateSequenceRef = useRef(0);
const [planGddHydrateBusy, setPlanGddHydrateBusy] = useState(false);
const [planGddDecisionBusy, setPlanGddDecisionBusy] = useState(false);
const [planGddError, setPlanGddError] = useState<string | null>(null);
const planGddDecisionResponseIdsRef = useRef(
new Map<
string,
{
action: PlanGddDecisionAction;
comment: string | null;
responseId: string;
}
>(),
);
const hydratePlanGddState = useCallback(
async (nextProjectPath?: string) => {
const targetProjectPath =
nextProjectPath?.trim() || localProjectPathRef.current || projectPath;
const invoke = resolveTauriInvoke();
if (!invoke || !targetProjectPath.trim()) {
setPlanGddState(null);
return;
}
const requestSequence = ++planGddHydrateSequenceRef.current;
setPlanGddHydrateBusy(true);
setPlanGddError(null);
try {
const nextState = await invoke<PlanGddStateViewV1>(
'hydrate_game_creator_plan_gdd_state',
{ projectPath: targetProjectPath },
);
if (
requestSequence === planGddHydrateSequenceRef.current &&
localProjectPathRef.current === targetProjectPath
) {
setPlanGddState(nextState);
}
} catch (error) {
// 项目写锁争用是瞬时的:后端已经等过一个短窗口,仍然没抢到只说明此刻
// 运行时正在写盘。这条 effect 每次监工状态变化都会再跑一次,下一拍就能
// 拿到,所以保留上一份状态、不动错误位——否则一次刚落盘成功的审批会在
// 卡里显示成失败(decide 成功后紧跟的这次 hydrate 正是最容易撞上的时刻)。
const transientContention =
String(error).includes('项目正在被其他写操作占用:');
if (
!transientContention &&
requestSequence === planGddHydrateSequenceRef.current &&
localProjectPathRef.current === targetProjectPath
) {
setPlanGddError(String(error));
}
} finally {
if (requestSequence === planGddHydrateSequenceRef.current) {
setPlanGddHydrateBusy(false);
}
}
},
[projectPath],
);
const decidePlanGdd = useCallback(
async (action: PlanGddDecisionAction, comment: string | null) => {
const current = planGddStateRef.current;
const pending = current?.pendingApproval;
const targetProjectPath =
resolveChatProjectPath(localProject) ?? projectPath;
const invoke = resolveTauriInvoke();
if (!pending || !current?.displayGdd || !invoke || !targetProjectPath) {
throw new Error('当前没有可提交的 GDD 审批决定');
}
// 方案 §13.2:busy、超时与网络重试复用同一 responseId,但用户改变 action 或
// comment 后必须换新的。旧键只含 `approvalRequestId:action`,改写修改意见时会
// 带着旧 responseId 提交,命中后端「同 responseId 的审批意图不一致」硬错误。
// 判据方向是宁可多换不可少换:多换的最坏后果是 receipt 已存在时把 replayed 降级
// 成 already-decided,两者都是 Ok;少换是硬错误。
const previousDecision = planGddDecisionResponseIdsRef.current.get(
pending.approvalRequestId,
);
const responseId =
previousDecision &&
previousDecision.action === action &&
previousDecision.comment === comment
? previousDecision.responseId
: `gdd-response-${crypto.randomUUID()}`;
planGddDecisionResponseIdsRef.current.set(pending.approvalRequestId, {
action,
comment,
responseId,
});
setPlanGddDecisionBusy(true);
setPlanGddError(null);
try {
await invoke('decide_game_creator_plan_gdd', {
projectPath: targetProjectPath,
gddId: pending.gddRef.gddId,
version: pending.gddRef.version,
fingerprint: pending.gddRef.fingerprint,
pendingActionId: pending.pendingActionId,
approvalRequestId: pending.approvalRequestId,
responseId,
action,
comment,
});
await hydratePlanGddState(targetProjectPath);
} catch (error) {
// 方案 §18.3 要求 decision 返回后以 hydrate 对权威文件的重验为准,失败分支同样
// 适用:不重灌就会让卡片停在已失效的 pending 身份上,三个决定按钮仍可点,且
// `recoveryPending` 永远翻不成真、「重试恢复」入口不渲染,卡内没有出路。
// 两句顺序不能反——`hydratePlanGddState` 入口会 `setPlanGddError(null)`,
// 先写错误再 hydrate 等于把这条错误擦掉。它自身从不抛出,不需要再包一层。
await hydratePlanGddState(targetProjectPath);
setPlanGddError(String(error));
throw error;
} finally {
setPlanGddDecisionBusy(false);
}
},
[hydratePlanGddState, localProject, projectPath],
);
useEffect(() => {
const targetProjectPath = localProject?.projectPath;
if (!targetProjectPath) {
setPlanGddState(null);
setPlanGddError(null);
return;
}
void hydratePlanGddState(targetProjectPath);
}, [hydratePlanGddState, localProject?.projectPath]);
useEffect(() => {
// 存在性判据故意走 `status`(必选字段,为 `undefined` 当且仅当 runtime 为 null)而不是整个
// 对象:依赖里只挖 phase/status/updatedAt 三个标量,是为了只在监工状态真的动了时
// 重灌。把 `projectSupervisorRuntime` 本体写进依赖会让每一轮轮询新建的对象身份都触发一次
// hydrate,白烧 IPC。
if (
!localProject?.projectPath ||
projectSupervisorRuntime?.status === undefined
) {
return;
}
// 后端 hydrate 会抢项目写锁并扫 authority,不是纯内存读。没有这道门,做游戏和做
// 素材链路的每一拍监工心跳都会去抢一次项目写锁——而那两条链路根本不产生策划状态。
// 策划状态读 ref 而不进依赖:hydrate 成功就会换一个 `planGddState` 对象身份,写进
// 依赖等于 hydrate 触发 hydrate。
if (
!planningStateNeedsRuntimeRefresh(
projectSupervisorRuntime?.source,
planGddStateRef.current,
)
) {
return;
}
void hydratePlanGddState(localProject.projectPath);
}, [
hydratePlanGddState,
localProject?.projectPath,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.source,
projectSupervisorRuntime?.status,
projectSupervisorRuntime?.updatedAt,
]);
useEffect(() => {
const hydrateOnResume = () => {
if (document.visibilityState === 'hidden' || !localProject?.projectPath) {
return;
}
void hydratePlanGddState(localProject.projectPath);
};
window.addEventListener('focus', hydrateOnResume);
document.addEventListener('visibilitychange', hydrateOnResume);
return () => {
window.removeEventListener('focus', hydrateOnResume);
document.removeEventListener('visibilitychange', hydrateOnResume);
};
}, [hydratePlanGddState, localProject?.projectPath]);
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
useState<string | null>(null);
const chatInputRef = useRef<HTMLInputElement | 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('asset');
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 [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<
(
prompt: string,
directConversationTurnId?: string,
creationType?: HomeCreationType | null,
) => 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('');
}
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) {
void refreshAgentRunTrace(initialProjectPath);
}
});
return;
}
void openWorkspace(
initialProjectPath,
false,
initialProjectKind === 'godot' ? 'open' : 'create',
initialProjectKind,
);
// Initial project opening is guarded by initialProjectOpenedRef.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialProjectPath, projectSupervisorOnly]);
useEffect(() => {
if (!directCodexProductRuntime) {
return;
}
const listen = window.__TAURI__?.event?.listen;
if (!listen) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void listen<GameCreatorDirectTurnUpdateEvent>(
'game-creator-direct-turn-update',
(event) => {
const payload = event.payload;
const activeTurn = activeDirectCodexTurnRef.current;
if (
!activeTurn ||
payload.projectPath !== localProjectPathRef.current ||
payload.projectPath !== activeTurn.projectPath ||
payload.turnId !== activeTurn.turnId ||
!Number.isSafeInteger(payload.sequence) ||
payload.sequence < 0 ||
!DIRECT_CODEX_TURN_UPDATE_STATUSES.has(payload.status) ||
payload.sequence <= activeTurn.lastSequence
) {
return;
}
activeTurn.lastSequence = payload.sequence;
activeTurn.receivedDirectUpdate = true;
const updatedAt =
Number.isFinite(payload.updatedAt) && payload.updatedAt > 0
? payload.updatedAt
: Date.now();
if (payload.status === 'failed') {
activeDirectCodexTurnRef.current = null;
setDirectCodexProgress('处理失败,正在同步错误');
setDirectCodexProgressUpdatedAt(updatedAt);
setDirectCodexTransientReply('');
setDirectCodexTransientReplyUpdatedAt(null);
return;
}
if (payload.status === 'completed') {
setDirectCodexProgress('回复已生成,正在提交');
setDirectCodexProgressUpdatedAt(updatedAt);
} else if (payload.activity != null) {
setDirectCodexProgress(directCodexActivityText(payload.activity));
setDirectCodexProgressUpdatedAt(updatedAt);
}
if (typeof payload.accumulatedText === 'string') {
setDirectCodexTransientReply(payload.accumulatedText);
setDirectCodexTransientReplyUpdatedAt(updatedAt);
}
},
)
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
cleanup = unlisten;
})
.catch(() => {
// The existing safe progress event remains the activity fallback.
});
return () => {
disposed = true;
cleanup?.();
};
}, [directCodexProductRuntime]);
useEffect(() => {
if (projectSupervisorOnly && !directCodexProductRuntime) {
return;
}
const listen = window.__TAURI__?.event?.listen;
if (!listen) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void listen<AgentProgressEvent>('game-creator-agent-progress', (event) => {
if (event.payload.projectPath !== localProjectPathRef.current) {
return;
}
if (directCodexProductRuntime) {
const activeTurn = activeDirectCodexTurnRef.current;
if (
!activeTurn ||
activeTurn.projectPath !== event.payload.projectPath ||
activeTurn.receivedDirectUpdate
) {
return;
}
setDirectCodexProgress(event.payload.message);
setDirectCodexProgressUpdatedAt(Date.now());
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 listen = window.__TAURI__?.event?.listen;
const invoke = resolveTauriInvoke();
if (!listen || directCodexProductRuntime) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void listen<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,
refreshManifest,
updateProjectSupervisorResponseStream,
updateProjectSupervisorRuntime,
]);
useEffect(() => {
const listen = window.__TAURI__?.event?.listen;
if (!listen) {
return;
}
let cleanup: (() => void) | null = null;
let disposed = false;
void listen<GameCreatorManifestInvalidatedEvent>(
'game-creator-manifest-invalidated',
(event) => {
if (event.payload.projectPath !== localProjectPathRef.current) {
return;
}
void refreshManifest(event.payload.projectPath);
},
)
.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 ||
!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,
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 ||
!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,
projectSupervisorOnly,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
projectSupervisorRuntime?.status,
]);
useLayoutEffect(() => {
if (
(!supervisorChatOnly &&
!(projectSupervisorOnly && directCodexProductRuntime)) ||
!supervisorChatShouldFollowLatestRef.current
) {
return;
}
const messageList = supervisorChatMessagesRef.current;
if (messageList) {
messageList.scrollTop = messageList.scrollHeight;
}
}, [
messages,
projectSupervisorResponseStream?.sequence,
projectSupervisorRuntime?.updatedAt,
projectSupervisorRuntimeError,
directCodexProgress,
directCodexProgressUpdatedAt,
directCodexTransientReply,
directCodexTransientReplyUpdatedAt,
directCodexProductRuntime,
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;
}
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;
let failedDirectTerminalMessageCount: number | null = null;
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;
}
try {
await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: null,
...(message.messageId ? { messageId: message.messageId } : {}),
message: {
role: message.role,
content: message.text,
agentId: null,
...(typeof message.updatedAt === 'number'
? { updatedAt: message.updatedAt }
: {}),
},
},
);
} catch (error) {
const claimKey = message.messageId
? directCodexClaimReleaseOnConversationWriteFailureRef.current.get(
message.messageId,
)
: undefined;
if (claimKey) {
directCodexClaimReleaseOnConversationWriteFailureRef.current.delete(
message.messageId!,
);
recoveredDirectCodexTurnClaimsRef.current.delete(claimKey);
failedDirectTerminalMessageCount = start + index + 1;
}
throw error;
}
if (message.messageId) {
const claimKey =
directCodexClaimReleaseOnConversationWriteFailureRef.current.get(
message.messageId,
);
if (claimKey) {
directCodexClaimReleaseOnConversationWriteFailureRef.current.delete(
message.messageId,
);
if (localProjectPathRef.current === nextProjectPath) {
// Any history read started before this terminal append may hold
// a user-only snapshot. Invalidate it before releasing the
// in-memory claim so that stale hydration cannot replay the
// same billable Direct turn.
projectSupervisorHistoryLoadVersionRef.current += 1;
}
recoveredDirectCodexTurnClaimsRef.current.delete(claimKey);
}
}
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 = failedDirectTerminalMessageCount
? Math.max(
savedConversationCountRef.current,
failedDirectTerminalMessageCount,
)
: 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,
]);
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(() => {
const nextMessages = conversationMessages;
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 {
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 = '';
const projectConversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: null,
},
);
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(
projectConversation.messages,
supervisorConversation?.messages ?? [],
);
const unansweredDirectTurn = directCodexProductRuntime
? unansweredDirectCodexConversationTurn(conversationMessages)
: null;
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);
setMessages((current) => {
const nextConversationMessages = conversationMessages;
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' &&
!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;
});
if (unansweredDirectTurn) {
const claimKey = `${nextProjectPath}\u0000${unansweredDirectTurn.turnId}`;
void Promise.resolve().then(() => {
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath ||
activeDirectCodexTurnRef.current ||
recoveredDirectCodexTurnClaimsRef.current.has(claimKey)
) {
return;
}
recoveredDirectCodexTurnClaimsRef.current.add(claimKey);
void executeChatAgentReply(
unansweredDirectTurn.prompt,
unansweredDirectTurn.turnId,
);
});
}
} 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();
resetDirectCodexTurn();
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));
}
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);
window.setTimeout(() => chatInputRef.current?.focus(), 0);
}
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');
const listen = window.__TAURI__?.event?.listen;
if (listen) {
try {
stopStreamListen = await listen<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;
}
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();
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
if (!prompt || chatAgentBusy) {
return;
}
setChatInput('');
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.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.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 = 'asset',
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, 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: kind ?? 'asset',
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;
}
void executeChatAgentReply(prompt);
}
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 = isCodexAgentMode(status.agentMode)
? status.configured
? `${formatCodexAgentModeLabel(status.agentMode)} 已检测到;登录与网络将在首次节点调用时验证。`
: `${formatCodexAgentModeLabel(status.agentMode)} 未就绪:${
status.error ?? 'Codex CLI 不可用'
}。`
: status.configured
? `LLM 已配置:${formatLlmRouteEndpoint(status)}。`
: `LLM 未就绪:${status.error ?? '配置不完整'}。${
status.reasoningEffort ? `推理 ${status.reasoningEffort},` : ''
}联网检索 ${status.webSearchEnabled ? '开启' : '关闭'},API Key:${
status.apiKeyPresent ? '已读取' : '未读取'
}。`;
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: string,
directConversationTurnId?: string,
creationType?: HomeCreationType | null,
) {
// 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();
const directProjectPath = resolveChatProjectPath(localProject);
if (directProjectPath && directInvoke) {
const clientTurnId =
directConversationTurnId ?? createDirectCodexConversationTurnId();
const directUserMessageId = directCodexConversationMessageId(
clientTurnId,
'user',
);
const directAssistantMessageId = directCodexConversationMessageId(
clientTurnId,
'assistant',
);
const appendDirectUserMessageIfMissing = (
current: ChatMessage[],
): ChatMessage[] => {
return current.some(
(message) => message.messageId === directUserMessageId,
)
? current
: [
...current,
{
role: 'user' as const,
text: prompt,
runtimeOwned: true,
messageId: directUserMessageId,
updatedAt: Date.now(),
},
];
};
const appendDirectAssistantMessage = (
current: ChatMessage[],
text: string,
): ChatMessage[] => {
const nextMessage: ChatMessage = {
role: 'assistant',
text,
runtimeOwned: true,
messageId: directAssistantMessageId,
updatedAt: Date.now(),
};
const withUser = appendDirectUserMessageIfMissing(current);
const existingIndex = withUser.findIndex(
(message) => message.messageId === directAssistantMessageId,
);
if (existingIndex < 0) {
return [...withUser, nextMessage];
}
return withUser.map((message, index) =>
index === existingIndex ? nextMessage : message,
);
};
const persistDirectAssistantMessage = (text: string) =>
directInvoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: directProjectPath,
agentId: null,
messageId: directAssistantMessageId,
message: {
role: 'assistant',
content: text,
agentId: null,
},
},
);
const recoveredDirectCodexTurnClaimKey = `${directProjectPath}\u0000${clientTurnId}`;
recoveredDirectCodexTurnClaimsRef.current.add(
recoveredDirectCodexTurnClaimKey,
);
activeDirectCodexTurnRef.current = {
projectPath: directProjectPath,
turnId: clientTurnId,
lastSequence: -1,
receivedDirectUpdate: false,
};
setChatAgentBusy(true);
setDirectCodexProgress('已发送消息,正在等待陶泥儿回复');
setDirectCodexProgressUpdatedAt(Date.now());
setDirectCodexTransientReply('');
setDirectCodexTransientReplyUpdatedAt(null);
setProjectSupervisorRuntimeError('');
try {
// Persist the original user intent and stable turn identity before
// Codex can start any billable or externally visible work. The
// regular conversation writer may race this call, but messageId
// idempotency makes both writers converge on the same record.
await directInvoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: directProjectPath,
agentId: null,
messageId: directUserMessageId,
message: {
role: 'user',
content: prompt,
agentId: null,
},
},
);
const directTurnInput: {
projectPath: string;
prompt: string;
clientTurnId: string;
creationType?: HomeCreationType;
} = {
projectPath: directProjectPath,
prompt,
clientTurnId,
};
if (creationType) {
directTurnInput.creationType = creationType;
}
const reply = await directInvoke<string>(
'chat_with_game_creator_direct_codex',
directTurnInput,
);
try {
await persistDirectAssistantMessage(reply);
} catch (error) {
if (localProjectPathRef.current === directProjectPath) {
setProjectSupervisorRuntimeError(
`陶泥儿回复保存失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
// Rust persists a successful Direct reply before returning Ok. The
// browser append is redundant, so the hydrated-turn claim can be
// released without reopening the Provider side effect. Invalidate
// any user-only history snapshot captured before Rust committed the
// terminal reply first.
if (localProjectPathRef.current === directProjectPath) {
projectSupervisorHistoryLoadVersionRef.current += 1;
}
recoveredDirectCodexTurnClaimsRef.current.delete(
recoveredDirectCodexTurnClaimKey,
);
if (localProjectPathRef.current === directProjectPath) {
clearDirectCodexTransientReply(directProjectPath, clientTurnId);
setMessages((current) =>
appendDirectAssistantMessage(current, reply),
);
setDirectCodexProgress('正在刷新项目状态');
setDirectCodexProgressUpdatedAt(Date.now());
await refreshDirectProjectManifest(directProjectPath);
}
} catch (error) {
if (isDirectCodexTurnAlreadyRunningError(error)) {
recoveredDirectCodexTurnClaimsRef.current.delete(
recoveredDirectCodexTurnClaimKey,
);
if (localProjectPathRef.current === directProjectPath) {
clearDirectCodexTransientReply(directProjectPath, clientTurnId);
setProjectSupervisorRuntimeError(
'陶泥儿仍在处理这条消息,请稍候刷新对话。',
);
}
return;
}
const message =
error instanceof Error ? error.message : String(error);
const visibleMessage = projectRuntimeVisibleError(
message,
'陶泥儿智能创作',
true,
);
try {
await persistDirectAssistantMessage(visibleMessage);
if (localProjectPathRef.current === directProjectPath) {
projectSupervisorHistoryLoadVersionRef.current += 1;
}
recoveredDirectCodexTurnClaimsRef.current.delete(
recoveredDirectCodexTurnClaimKey,
);
} catch {
if (localProjectPathRef.current === directProjectPath) {
// Do not release the claim while the React conversation writer
// can still persist this terminal record. That writer releases
// the claim only after its exact append resolves or rejects.
directCodexClaimReleaseOnConversationWriteFailureRef.current.set(
directAssistantMessageId,
recoveredDirectCodexTurnClaimKey,
);
} else {
recoveredDirectCodexTurnClaimsRef.current.delete(
recoveredDirectCodexTurnClaimKey,
);
}
}
if (localProjectPathRef.current === directProjectPath) {
clearDirectCodexTransientReply(directProjectPath, clientTurnId);
setProjectSupervisorRuntimeError(visibleMessage);
setMessages((current) =>
appendDirectAssistantMessage(current, visibleMessage),
);
}
} finally {
try {
if (localProjectPathRef.current === directProjectPath) {
await refreshManifest(directProjectPath);
}
} finally {
setChatAgentBusy(false);
setDirectCodexProgress('');
const activeTurn = activeDirectCodexTurnRef.current;
if (
!activeTurn ||
(activeTurn.projectPath === directProjectPath &&
activeTurn.turnId === clientTurnId)
) {
resetDirectCodexTurn();
}
}
}
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;
// 做方案入口独立成链。首次提交时本项目还没有任何 Supervisor run,只能靠首页
// 带下来的 startMode 判断;之后由 plan 根 run 自己的 source 接管,做游戏与
// 做素材两条链路不受影响。
const planningEntry =
workspaceProjectKind === 'web' &&
(runtimeAtSubmission?.source === PROJECT_SUPERVISOR_PLAN_SOURCE ||
(planningStartMode && !runtimeAtSubmission));
if (
planningEntry &&
runtimeAtSubmission &&
runtimeAtSubmission.source === PROJECT_SUPERVISOR_PLAN_SOURCE &&
!isAgentRuntimeTerminalState(runtimeAtSubmission)
) {
setProjectSupervisorRuntimeError('请先完成当前立项策划步骤');
return;
}
const submissionRoute = resolveProjectSupervisorRuntimeSubmission({
workspaceProjectKind,
orchestrationMode,
supervisorChatOnly,
planningEntry,
});
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;
}
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) {
claimInitialSupervisorMessageForPage(latch.projectPath);
return;
}
if (
chatAgentBusy ||
!claimInitialSupervisorMessageForPage(latch.projectPath)
) {
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(
latch.prompt,
directConversationTurnId,
latch.creationType,
);
}, [
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,
assetKind,
assetMediaType,
assetSourceKind,
canvasProjectId,
resourceId,
nextAssetObjectId,
),
);
}
async function executeAssetRegister(
nextProjectPath: string,
localPath: string,
kind: string,
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,
assetKind,
assetMediaType,
canvasProjectId,
resourceId,
nextAssetObjectId,
false,
),
);
}
async function executeCanvasAssetImport(
nextProjectPath: string | null,
localPath: string,
kind: string,
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 ||
!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
}, [
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 visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5);
const currentProjectTitle = localProject
? manifest.name.trim() || projectNameFromPath(localProject.projectPath)
: seedManifest.name;
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 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;
function showEarlierConversationMessages() {
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 (hiddenConversationCount === 0) {
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]);
function handleProjectSupervisorOnlySubmit(
event: FormEvent<HTMLFormElement>,
) {
event.preventDefault();
const prompt = chatInput.trim();
if (
!directCodexProductRuntime &&
supervisorChatOnly &&
prompt.startsWith('/')
) {
void handleChatSubmit(event);
return;
}
if (
!directCodexProductRuntime &&
agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)
) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
if (!prompt || chatAgentBusy) {
return;
}
if (directCodexProductRuntime && prompt === '/history') {
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
return;
}
setChatInput('');
void loadProjectConversation(nextProjectPath, false, 'replace');
return;
}
if (supervisorChatOnly || directCodexProductRuntime) {
supervisorChatShouldFollowLatestRef.current = true;
}
const directConversationTurnId = directCodexProductRuntime
? createDirectCodexConversationTurnId()
: undefined;
setChatInput('');
setMessages((current) => [
...current,
{
role: 'user',
text: prompt,
runtimeOwned: true,
...(directConversationTurnId
? {
messageId: directCodexConversationMessageId(
directConversationTurnId,
'user',
),
}
: {}),
updatedAt: Date.now(),
},
]);
void executeChatAgentReply(prompt, directConversationTurnId);
}
const visibleProfessionalAgentCards = agentStatusCards.filter(
(agent) =>
agent.id !== PROJECT_SUPERVISOR_AGENT_ID &&
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
);
if (projectSupervisorOnly && supervisorChatOnly) {
const supervisorProjectPath =
localProject?.projectPath || initialProjectPath || projectPath;
return (
<SupervisorChatOnlyView
chatAgentBusy={chatAgentBusy}
chatInput={chatInput}
messagesRef={supervisorChatMessagesRef}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={setChatInput}
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
onConfirmConfirmation={confirmUiCommand}
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
onScroll={handleSupervisorChatScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
onToolAction={handleProjectSupervisorToolAction}
onUserInput={handleProjectSupervisorUserInput}
pendingConfirmation={pendingUiConfirmation}
pendingCommand={pendingCommand}
onCancelPendingCommand={handlePendingCommandCancel}
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
projectPath={supervisorProjectPath}
runtime={projectSupervisorRuntime}
runtimeConfigOpen={runtimeConfigOpen}
runtimeError={projectSupervisorRuntimeError}
transientReply={
directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
}
hasConversationControls={projectSupervisorHasConversationControls}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
visibleMessages={visibleMessages}
workspaceStatus={workspaceStatus}
expectedRunId={projectSupervisorExpectedRunId}
/>
);
}
if (projectSupervisorOnly) {
return (
<ProjectSupervisorView
chatInput={chatInput}
directCodex={directCodexProductRuntime}
directActivity={directCodexProductRuntime ? directCodexProgress : ''}
hiddenConversationCount={hiddenConversationCount}
messagesRef={supervisorChatMessagesRef}
needsUserInput={
directCodexProductRuntime ? false : projectSupervisorNeedsUserInput
}
onCancelConfirmation={cancelUiCommandConfirmation}
onCancelPendingCommand={handlePendingCommandCancel}
onChatInputChange={setChatInput}
onConfirmConfirmation={confirmUiCommand}
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
onScroll={handleSupervisorChatScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
pendingConfirmation={
directCodexProductRuntime ? null : pendingUiConfirmation
}
pendingCommand={directCodexProductRuntime ? pendingCommand : null}
projectPath={localProject?.projectPath ?? projectPath}
transientReply={
directCodexProductRuntime
? directCodexTransientReply
: projectSupervisorTransientReply
}
visibleMessages={visibleMessages}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
showProfessionalCollaboration={
!directCodexProductRuntime && orchestrationMode === 'professional-dag'
}
workspaceStatus={workspaceStatus}
planGddState={planGddState}
planGddHydrateBusy={planGddHydrateBusy || planGddDecisionBusy}
planGddError={planGddError}
onPlanGddRefresh={() => void hydratePlanGddState()}
onPlanGddDecision={decidePlanGdd}
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={chatAgentBusy}
readOnly={directCodexProductRuntime}
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
agentRunStatus={agentRunStatus}
agentRuntimeById={agentRuntimeById}
agentStatusCards={agentStatusCards}
cancelProjectCreateInNonEmptyFolder={
cancelProjectCreateInNonEmptyFolder
}
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
chatAgentBusy={chatAgentBusy}
chatInput={chatInput}
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}
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}
planGddState={planGddState}
planGddHydrateBusy={planGddHydrateBusy || planGddDecisionBusy}
planGddError={planGddError}
onPlanGddRefresh={() => void hydratePlanGddState()}
onPlanGddDecision={decidePlanGdd}
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
queuePendingCommand={queuePendingCommand}
queuePreviewOpenShortcut={queuePreviewOpenShortcut}
queuePreviewStartShortcut={queuePreviewStartShortcut}
queueProjectCheckpointShortcut={queueProjectCheckpointShortcut}
queueProjectExportPackageShortcut={queueProjectExportPackageShortcut}
queueRunLocalShortcut={queueRunLocalShortcut}
queueStaticSmokeShortcut={queueStaticSmokeShortcut}
scheduleReadyAgentTasks={scheduleReadyAgentTasks}
setChatInput={setChatInput}
showChatHelp={showChatHelp}
showEarlierConversationMessages={showEarlierConversationMessages}
visibleMainProjectAssets={visibleMainProjectAssets}
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>
);
}