Files
Genarrative/apps/ai-game-creator-shell/src/App.tsx
T
kdletters 838feaeae9
Project CI / Repository checks (push) Successful in 1m56s
Project CI / Frontend tests (push) Successful in 3m45s
Project CI / Backend tests (push) Successful in 4m41s
Project CI / Native shell tests (push) Failing after 9m45s
修复游戏聊天续写来源匹配
Steer 目标增加 Runtime source 精确匹配

游戏聊天仅续写同来源 Run 并保留跨入口任务隔离

补充模型与 AppSurface 回归夹具和项目记忆
2026-08-04 22:02:32 +08:00

11132 lines
354 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,
defaultProjectPath,
PROJECT_SUPERVISOR_AGENT_ID,
seedManifest,
} from './app/constants';
import { useEscapeToClose } from './app/dialogs';
import { resolveTauriInvoke } from './app/tauri';
import type {
AgentBackgroundSubmitMode,
AgentProgressEvent,
AgentRunControlResult,
AgentRunHistoryItem,
AgentRuntimeResponseStream,
AgentRuntimeResult,
AgentRuntimeState,
AgentRuntimeSteerResult,
AgentRuntimeUserInputRequest,
AgentStatusCard,
ChatMessage,
GameCreatorAgentRuntimeUpdateEvent,
GameCreatorChatAgentReply,
GameCreatorLlmConfigStatus,
GameCreatorRoleAgentChatStreamEvent,
GenerateLocalGameDraftResult,
ImportCanvasExportResult,
InitLocalProjectResult,
LimitedLocalCommandResult,
ListLocalProjectFilesResult,
LocalAgentMemoryResult,
LocalConversationMessageRecord,
LocalConversationResult,
LocalGameMemoryResult,
LocalGameProjectRevisionStatus,
LocalPreviewResult,
LocalPreviewStatus,
LocalProjectCheckpointResult,
LocalProjectCheckpointSummary,
LocalProjectDiffResult,
LocalProjectDirectoryStatus,
LocalProjectExportPackageResult,
LocalProjectExportPackagesResult,
LocalProjectFileEntry,
LocalProjectFileMutationResult,
LocalProjectFileResult,
LocalProjectIndexResult,
LocalProjectRestoreResult,
MemoryScope,
MemoryWriteMode,
OpenCanvasProjectResult,
PendingCommand,
PendingUiConfirmation,
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,
mergeGameChatRuntimeResponseMessagesIntoHistory,
mergeProjectSupervisorConversation,
mergeProjectSupervisorResponseStream,
normalizeAgentRuntimeState,
projectNameFromPath,
projectProfessionalAgentLabel,
projectSupervisorPendingRepairMatchesProfessional,
projectSupervisorResponseStreamIdentity,
readProjectSupervisorActiveSessionId,
sameAgentRuntimeRun,
submitProjectSupervisorRuntimeTask,
taskRowsFromManifest,
} from './features/agent-runtime';
import {
isDeveloperMode,
isTransientProjectOpenMessage,
latestVisibleItems,
persistSupervisorChatDraft,
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,
formatLlmAgentStatusLine,
formatLlmRouteEndpoint,
isMissingAgentRunTraceError,
projectAgentRuntimeSummaries,
readablePassArtifactsFromAgentRunTrace,
sameAgentStatusCard,
summarizeAgentAudit,
summarizeAgentCapabilities,
summarizeAgentConversationReadDrafts,
summarizeAgentLlmRoutes,
summarizeAgentMemoryReadDrafts,
summarizeAgentPassArtifactReadDrafts,
summarizeAgentRunCompletionForChat,
summarizeAgentRunHistoryReadDrafts,
summarizeAgentStatusCardsForChat,
summarizeLimitedLocalCommands,
summarizeRunArtifactReadDrafts,
} from './features/project-summary/agentPresentation';
import {
chatCommandHelp,
checkpointSummaryFromManifest,
commonAgentRunSupportReadDrafts,
commonProjectArtifactReadDrafts,
commonProjectInternalReadDrafts,
commonProjectLogReadDrafts,
firstReadableProjectAssetPath,
formatAgentRunStatus,
formatCanvasAssetSource,
isAbsoluteProjectPath,
isSafeProjectRelativePath,
missingChatCommandArgumentMessage,
projectFileActionDrafts,
projectPathHasControlCharacter,
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 { resolveEmbeddedPreviewUrl } from './features/project-workspace/LocalGamePreviewFrame';
import {
appendMemoryContent,
memoryScopeLabel,
memoryScopePath,
parseOptionalMemoryScope,
parseRememberInput,
} from './features/project-workspace/memoryCommands';
import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation';
import {
isAgentTraceFilePath,
isProjectPolicyConfirmableCommandId,
isRegisteredGameCreationCommandId,
isSafeCanvasProjectId,
isSafeCheckpointId,
needsInitializedChatProject,
resolveChatProjectPath,
resolvePendingCommandProjectPath,
} from './features/project-workspace/projectCommandPolicy';
import { handleProjectSummaryChatCommand } from './features/project-workspace/projectSummaryCommands';
import { ProjectSupervisorView } from './features/project-workspace/ProjectSupervisorView';
import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane';
import {
buildGameChatProgressEvidence,
collectGameChatResultImages,
formatGameChatStageRecord,
gameChatFinalReplyMessages,
gameChatRuntimeEventMessages,
mergeGameChatFinalReplyMessagesIntoHistory,
mergeGameChatRuntimeEventMessagesIntoHistory,
SupervisorChatOnlyView,
} from './features/project-workspace/SupervisorChatOnlyView';
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
import {
type ProjectAgentResultSummary,
type ProjectAgentRuntimeSummary,
} from './view/project-development';
const initialSupervisorMessageClaimsByPage = new WeakMap<Window, Set<string>>();
const LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
'genarrative.game-chat.auto-preview-authorization.v1';
const GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
'genarrative.game-chat.auto-preview-authorization.v2';
type GameChatAutoPreviewAuthorization = {
afterRevision: number;
afterValidatedAt: number;
authorizationId: string;
projectPath: string;
runId: string;
};
type GameChatPlayableRevision = {
runId: string;
revision: number;
validatedAt: number;
};
type GameChatPreviewValidationCandidate = GameChatPlayableRevision & {
eventOrder: number;
playable: boolean;
};
const GAME_CHAT_STAGE_TASK_IDS = [
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
'preview-playtest',
] as const;
function gameChatManifestHasTerminalStageTasks(
manifest: GameCreationAppManifest | null,
) {
if (!manifest) {
return false;
}
return GAME_CHAT_STAGE_TASK_IDS.every((taskId) => {
const status = manifest.tasks.find((task) => task.id === taskId)?.status;
return status === 'completed' || status === 'failed';
});
}
function gameChatRuntimeHasTerminalOutcome(runtime: AgentRuntimeState) {
return (
['completed', 'failed', 'cancelled'].includes(runtime.status) ||
['completed', 'failed', 'cancelled'].includes(runtime.phase)
);
}
function mergeGameChatHydratedConversationMessages(
historyMessages: ChatMessage[],
currentMessages: ChatMessage[],
) {
const mergedMessages = mergeGameChatRuntimeEventMessagesIntoHistory(
mergeGameChatFinalReplyMessagesIntoHistory(
mergeGameChatRuntimeResponseMessagesIntoHistory(
historyMessages,
currentMessages,
),
currentMessages,
),
currentMessages,
);
const historyMessageReferences = new Set(historyMessages);
const pendingMessages = mergedMessages
.filter((message) => !historyMessageReferences.has(message))
.sort((left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0));
// Conversation persistence uses a positional cursor. Keep durable history
// as one prefix so newly observed replies cannot sort ahead of that cursor
// and be skipped during hydration.
return [...historyMessages, ...pendingMessages];
}
function gameChatPlayableRevisionIsAfterAuthorization(
revision: GameChatPlayableRevision,
authorization: GameChatAutoPreviewAuthorization,
) {
return (
revision.revision > authorization.afterRevision ||
(revision.revision === authorization.afterRevision &&
revision.validatedAt > authorization.afterValidatedAt)
);
}
export function latestGameChatPlayableRevision(
runtime: AgentRuntimeState | null,
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
): GameChatPlayableRevision | null {
if (!runtime?.runId) {
return null;
}
const playtestChildren = new Map<string, AgentRuntimeState>();
for (const child of Object.values(runtimeByAgentId)) {
if (
child?.agentId !== 'preview-playtest' ||
child.taskId !== 'preview-playtest' ||
child.source !== 'agent-ready-task-scheduler' ||
child.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
child.parentRunId !== runtime.runId
) {
continue;
}
playtestChildren.set(
`${child.agentId}\n${child.sessionId}\n${child.runId}`,
child,
);
}
let latest: GameChatPreviewValidationCandidate | null = null;
let eventOrder = 0;
for (const child of playtestChildren.values()) {
for (const event of child.recentEvents ?? []) {
eventOrder += 1;
if (
event.agentId !== child.agentId ||
event.taskId !== child.taskId ||
event.sessionId !== child.sessionId ||
event.runId !== child.runId ||
event.eventType !== 'observation' ||
!Number.isSafeInteger(event.updatedAt) ||
event.updatedAt < 0 ||
!event.summary.startsWith('preview.validate') ||
!event.detail?.trim().startsWith('{')
) {
continue;
}
try {
const detail = JSON.parse(event.detail) as {
passed?: unknown;
playtestPassed?: unknown;
revision?: unknown;
};
const playable =
event.summary.startsWith('preview.validateok') &&
detail.passed === true &&
detail.playtestPassed === true;
if (
typeof detail.passed !== 'boolean' ||
typeof detail.revision !== 'number' ||
!Number.isSafeInteger(detail.revision) ||
detail.revision <= 0
) {
continue;
}
const shouldReplace =
!latest ||
detail.revision > latest.revision ||
(detail.revision === latest.revision &&
event.updatedAt > latest.validatedAt) ||
(detail.revision === latest.revision &&
event.updatedAt === latest.validatedAt &&
((latest.playable && !playable) ||
(latest.playable === playable &&
eventOrder > latest.eventOrder)));
if (!shouldReplace) {
continue;
}
latest = {
eventOrder,
playable,
runId: runtime.runId,
revision: detail.revision,
validatedAt: event.updatedAt,
};
} catch {
// Ignore malformed or truncated public evidence and wait for a valid revision.
}
}
}
return latest?.playable
? {
runId: latest.runId,
revision: latest.revision,
validatedAt: latest.validatedAt,
}
: null;
}
function readStoredGameChatAutoPreviewAuthorization(): GameChatAutoPreviewAuthorization | null {
try {
window.localStorage.removeItem(
LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
);
const raw = window.localStorage.getItem(
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as Partial<GameChatAutoPreviewAuthorization>;
const projectPath = parsed.projectPath?.trim() ?? '';
const runId = parsed.runId?.trim() ?? '';
const authorizationId = parsed.authorizationId?.trim() ?? '';
const afterRevision = parsed.afterRevision;
const afterValidatedAt = parsed.afterValidatedAt;
if (
!projectPath ||
!runId ||
!authorizationId ||
typeof afterRevision !== 'number' ||
!Number.isSafeInteger(afterRevision) ||
afterRevision < 0 ||
typeof afterValidatedAt !== 'number' ||
!Number.isSafeInteger(afterValidatedAt) ||
afterValidatedAt < 0 ||
!isAbsoluteProjectPath(projectPath) ||
projectPathHasControlCharacter(projectPath) ||
projectPathHasControlCharacter(authorizationId) ||
projectPathHasControlCharacter(runId)
) {
window.localStorage.removeItem(
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
);
return null;
}
return {
afterRevision,
afterValidatedAt,
authorizationId,
projectPath,
runId,
};
} catch {
return null;
}
}
function storeGameChatAutoPreviewAuthorization(
authorization: GameChatAutoPreviewAuthorization | null,
) {
try {
if (authorization) {
window.localStorage.setItem(
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
JSON.stringify(authorization),
);
} else {
window.localStorage.removeItem(
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
);
}
} catch {
// A volatile in-memory authorization remains sufficient when storage is unavailable.
}
}
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 function consumeInitialGameChatMessage(
searchParams: URLSearchParams,
location: Pick<Location, 'pathname' | 'hash'>,
replaceUrl: (url: string) => void,
) {
const message = searchParams.get('initialMessage')?.trim() ?? '';
if (!message) {
return '';
}
searchParams.delete('initialMessage');
const remainingSearch = searchParams.toString();
replaceUrl(
`${location.pathname}${remainingSearch ? `?${remainingSearch}` : ''}${location.hash}`,
);
return message;
}
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} />;
}
export function GameChatReleaseApp({
initialProjectPath = '',
initialSupervisorMessage = '',
}: {
initialProjectPath?: string;
initialSupervisorMessage?: string;
}) {
return (
<App
initialProjectPath={initialProjectPath}
projectSupervisorOnly
gameChatOnly
initialSupervisorMessage={initialSupervisorMessage}
/>
);
}
type AppProps = {
initialProjectPath?: string;
initialProjectManifest?: GameCreationAppManifest;
projectSupervisorOnly?: boolean;
supervisorChatOnly?: boolean;
gameChatOnly?: boolean;
initialSupervisorMessage?: string;
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
onAgentRuntimeSummariesChange?: (
summaries: ProjectAgentRuntimeSummary[],
) => void;
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
};
export function App({
initialProjectPath: initialProjectPathOverride = '',
initialProjectManifest,
projectSupervisorOnly = false,
supervisorChatOnly = false,
gameChatOnly = false,
initialSupervisorMessage = '',
onPreviewChange,
onAgentRuntimeSummariesChange,
onAgentResultsChange,
}: AppProps = {}) {
const [devMode] = useState(() =>
projectSupervisorOnly ? false : isDeveloperMode(),
);
const [initialProjectPath] = useState(
() => initialProjectPathOverride || readInitialProjectPath(),
);
const eagerSupervisorProject =
projectSupervisorOnly && Boolean(initialProjectPath) && !gameChatOnly;
const [projectPath, setProjectPath] = useState(
initialProjectPath || defaultProjectPath,
);
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 [manifest, setManifest] = useState<GameCreationAppManifest>(
initialProjectManifest ?? seedManifest,
);
const [projectStatus, setProjectStatus] = useState(
eagerSupervisorProject ? '已初始化' : '未初始化',
);
const [preview, setPreview] = useState<LocalPreviewResult | null>(null);
const gameChatPreviewRef = useRef<LocalPreviewResult | null>(null);
gameChatPreviewRef.current = preview;
const [gameChatPreviewRevision, setGameChatPreviewRevision] = useState<
number | null
>(null);
const gameChatPreviewRevisionRef = useRef<number | null>(null);
gameChatPreviewRevisionRef.current = gameChatPreviewRevision;
const [previewStatus, setPreviewStatus] = useState('未启动');
const [gameChatProjectSelectionBusy, setGameChatProjectSelectionBusy] =
useState(false);
const gameChatProjectSelectionVersionRef = useRef(0);
const gameChatAutoPreviewAuthorizationRef =
useRef<GameChatAutoPreviewAuthorization | null>(
readStoredGameChatAutoPreviewAuthorization(),
);
const gameChatAutoPreviewAttemptedRef = useRef(new Set<string>());
const gameChatObservedRunKeysRef = useRef(new Set<string>());
const gameChatArchivedRunKeysRef = useRef(new Set<string>());
const gameChatPendingStageRuntimesRef = useRef(
new Map<string, AgentRuntimeState>(),
);
const gameChatCommittedResponseStreamKeysRef = useRef(new Set<string>());
const initialSupervisorMessageLatchRef = useRef({
projectPath: initialProjectPath,
prompt: initialSupervisorMessage.trim(),
});
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 || gameChatOnly) && initialProjectPath
? readSupervisorChatDraft(initialProjectPath)
: '',
);
const [chatAgentBusy, setChatAgentBusy] = useState(false);
const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState<
string | null
>(null);
const [projectSupervisorRuntime, setProjectSupervisorRuntime] =
useState<AgentRuntimeState | null>(null);
const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] =
useState<AgentRuntimeResponseStream | null>(null);
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
useState('');
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 projectSupervisorRuntimeSyncingRef = useRef(new Set<string>());
const projectSupervisorRefreshConversationRef = useRef<
| ((
invoke: TauriInvoke,
projectPath: string,
sessionId: string,
runId?: string,
) => Promise<void>)
| null
>(null);
const executeChatAgentReplyRef = useRef<(prompt: string) => 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);
function requestRuntimeConfigOpen() {
if (projectSupervisorOnly && !supervisorChatOnly && !gameChatOnly) {
return;
}
setRuntimeConfigOpen(true);
}
function setGameChatAutoPreviewAuthorization(
authorization: GameChatAutoPreviewAuthorization | null,
) {
gameChatAutoPreviewAuthorizationRef.current = authorization;
storeGameChatAutoPreviewAuthorization(authorization);
}
const updateProjectSupervisorRuntime = useCallback(
(
runtime: AgentRuntimeState | null,
previous = projectSupervisorRuntimeRef.current,
) => {
const nextRuntime = runtime
? normalizeAgentRuntimeState(runtime, previous)
: null;
const nextProjectPath = localProjectPathRef.current;
if (
gameChatOnly &&
nextProjectPath &&
nextRuntime?.runId &&
!isAgentRuntimeTerminalState(nextRuntime)
) {
gameChatObservedRunKeysRef.current.add(
`${nextProjectPath}\n${nextRuntime.runId}`,
);
}
projectSupervisorRuntimeRef.current = nextRuntime;
setProjectSupervisorRuntime(nextRuntime);
},
[gameChatOnly],
);
const updateProjectSupervisorResponseStream = useCallback(
(
incoming: AgentRuntimeResponseStream | null | undefined,
runtime: AgentRuntimeState,
) => {
let nextStream = mergeProjectSupervisorResponseStream(
projectSupervisorResponseStreamRef.current,
incoming,
runtime,
);
const candidateStream = nextStream;
if (candidateStream?.status === 'ready' && gameChatOnly) {
const text = candidateStream.accumulatedText.trim();
const responseKey =
projectSupervisorResponseStreamIdentity(candidateStream);
const messageId = `runtime-response:${responseKey}`;
const alreadyCommitted =
gameChatCommittedResponseStreamKeysRef.current.has(responseKey) ||
latestMessagesRef.current.some(
(message) => message.messageId === messageId,
);
if (text && !alreadyCommitted) {
gameChatCommittedResponseStreamKeysRef.current.add(responseKey);
const nextMessage: ChatMessage = {
role: 'assistant',
text: candidateStream.accumulatedText,
messageId,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
updatedAt: candidateStream.updatedAt,
runtimeOwned: true,
};
setMessages((current) => {
if (current.some((message) => message.messageId === messageId)) {
latestMessagesRef.current = current;
return current;
}
const nextMessages = [...current, nextMessage];
latestMessagesRef.current = nextMessages;
return nextMessages;
});
}
// Ready text is now a normal chat message; transientReply must not show
// the same response a second time while polling/event delivery catches up.
nextStream = null;
} else 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);
},
[gameChatOnly],
);
function resetProjectSupervisorState() {
projectSupervisorHistoryLoadVersionRef.current += 1;
projectSupervisorRuntimeResumeProjectPathRef.current = null;
projectSupervisorSessionIdRef.current = null;
projectSupervisorRuntimeRef.current = null;
projectSupervisorExpectedRunIdRef.current = null;
projectSupervisorResponseStreamRef.current = null;
gameChatPendingStageRuntimesRef.current.clear();
gameChatCommittedResponseStreamKeysRef.current.clear();
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}`;
if (projectSupervisorRuntimeSyncingRef.current.has(syncKey)) {
return;
}
const refreshConversation = projectSupervisorRefreshConversationRef.current;
if (!refreshConversation) {
return;
}
projectSupervisorRuntimeSyncingRef.current.add(syncKey);
void refreshConversation(
invoke,
projectPath,
runtime.sessionId,
runtime.runId,
).catch((error) => {
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
if (
localProjectPathRef.current === projectPath &&
projectSupervisorSessionIdRef.current === runtime.sessionId
) {
setProjectSupervisorRuntimeError(
`项目总控 Agent 对话刷新失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
});
}
function flushPendingGameChatStageRecords() {
if (!gameChatOnly || !gameChatManifestHasTerminalStageTasks(manifest)) {
return;
}
for (const [
archiveKey,
pendingRuntime,
] of gameChatPendingStageRuntimesRef.current) {
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
continue;
}
const progress = buildGameChatProgressEvidence(
pendingRuntime,
agentRuntimeById,
manifest,
);
if (!progress) {
continue;
}
const text = formatGameChatStageRecord(
pendingRuntime,
progress,
collectGameChatResultImages(manifest),
);
gameChatArchivedRunKeysRef.current.add(archiveKey);
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
setMessages((current) => {
if (
current.some(
(message) => message.role === 'assistant' && message.text === text,
)
) {
return current;
}
// Conversation hydration can update the saved cursor in the same
// turn as this append. Clamp it to the pre-append list so the new
// stage record remains visible to the persistence effect.
const projectPath = archiveKey.split('\n', 1)[0];
if (savedConversationProjectPathRef.current === projectPath) {
savedConversationCountRef.current = Math.min(
savedConversationCountRef.current,
current.length,
);
}
return [...current, { role: 'assistant', text }];
});
}
}
function appendGameChatRuntimeEventMessages(
nextProjectPath: string,
runtime: AgentRuntimeState,
) {
const eventMessages = gameChatRuntimeEventMessages(
runtime,
agentRuntimeById,
);
if (eventMessages.length === 0) {
return;
}
setMessages((current) => {
const currentMessageIds = new Set(
current
.map((message) => message.messageId?.trim())
.filter((messageId): messageId is string => Boolean(messageId)),
);
const missingMessages = eventMessages.filter(
(message) =>
message.messageId && !currentMessageIds.has(message.messageId),
);
if (missingMessages.length === 0) {
return current;
}
if (savedConversationProjectPathRef.current === nextProjectPath) {
savedConversationCountRef.current = Math.min(
savedConversationCountRef.current,
current.length,
);
}
return [...current, ...missingMessages];
});
}
const appendGameChatFinalReplyMessages = useCallback(
(nextProjectPath: string, runtimeResults: AgentRuntimeResult[]) => {
if (!gameChatOnly || runtimeResults.length === 0) {
return;
}
// A professional Runtime is only part of the active game-chat turn when
// it was delegated by the current Project Supervisor run. This prevents
// a stale child Runtime (or a different app mode) from leaking into the
// project transcript after a restart.
const supervisorRunId =
projectSupervisorRuntimeRef.current?.runId ??
runtimeResults.find(
(result) => result.state.agentId === PROJECT_SUPERVISOR_AGENT_ID,
)?.state.runId;
if (!supervisorRunId) {
return;
}
const messages = runtimeResults.flatMap((result) => {
const runtime = agentRuntimeStateFromResult(result);
if (
runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
runtime.parentRunId !== supervisorRunId
) {
return [];
}
return gameChatFinalReplyMessages([result.responseStream]);
});
if (messages.length === 0) {
return;
}
setMessages((current) => {
const currentMessageIds = new Set(
current
.map((message) => message.messageId?.trim())
.filter((messageId): messageId is string => Boolean(messageId)),
);
const missingMessages = messages.filter(
(message) =>
message.messageId && !currentMessageIds.has(message.messageId),
);
if (missingMessages.length === 0) {
return current;
}
if (savedConversationProjectPathRef.current === nextProjectPath) {
savedConversationCountRef.current = Math.min(
savedConversationCountRef.current,
current.length,
);
}
const orderedMissingMessages = [...missingMessages].sort(
(left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0),
);
const nextMessages = [...current, ...orderedMissingMessages];
latestMessagesRef.current = nextMessages;
return nextMessages;
});
},
[gameChatOnly],
);
function appendGameChatStageRecord(
nextProjectPath: string,
runtime: AgentRuntimeState,
) {
if (!gameChatOnly || !gameChatRuntimeHasTerminalOutcome(runtime)) {
return;
}
const archiveKey = `${nextProjectPath}\n${runtime.runId}`;
// A restored terminal runtime may be the first runtime snapshot observed
// after the app opens. Do not require a prior non-terminal event: the
// durable runtime/manifest pair is sufficient evidence for the record.
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
return;
}
gameChatPendingStageRuntimesRef.current.set(archiveKey, runtime);
flushPendingGameChatStageRecords();
}
useEffect(() => {
const nextProjectPath = localProject?.projectPath;
const runtime = projectSupervisorRuntime;
if (gameChatOnly && nextProjectPath && runtime) {
appendGameChatRuntimeEventMessages(nextProjectPath, runtime);
}
if (
gameChatOnly &&
nextProjectPath &&
runtime &&
gameChatRuntimeHasTerminalOutcome(runtime)
) {
// Hydration can restore a terminal root run without delivering a live
// runtime-update event. Feed that snapshot through the same deferred
// archive path used by live terminal updates. A run that was already
// observed in a non-terminal state is archived by its terminal
// conversation refresh instead, avoiding a hydration race with that
// refresh's saved-message cursor.
const archiveKey = `${nextProjectPath}\n${runtime.runId}`;
const refreshKey = `${nextProjectPath}\n${runtime.sessionId}\n${runtime.runId}`;
if (
!gameChatObservedRunKeysRef.current.has(archiveKey) &&
!projectSupervisorRuntimeSyncingRef.current.has(refreshKey)
) {
appendGameChatStageRecord(nextProjectPath, runtime);
}
}
flushPendingGameChatStageRecords();
// The terminal Runtime can arrive before the durable manifest refresh.
// Retry when either projection changes, but archive each run only once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
agentRuntimeById,
gameChatOnly,
localProject?.projectPath,
manifest,
projectSupervisorRuntime,
]);
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 (gameChatOnly) {
void openGameChatProjectPath(initialProjectPath);
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);
// Initial project opening is guarded by initialProjectOpenedRef.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gameChatOnly, initialProjectPath, projectSupervisorOnly]);
useEffect(() => {
if (projectSupervisorOnly) {
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;
}
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?.();
};
}, [projectSupervisorOnly]);
useEffect(() => {
const listen = window.__TAURI__?.event?.listen;
const invoke = resolveTauriInvoke();
if (!listen) {
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;
}
const nextRuntime = agentRuntimeStateFromResult(payload.runtime);
if (gameChatOnly && payload.agentId !== PROJECT_SUPERVISOR_AGENT_ID) {
appendGameChatFinalReplyMessages(payload.projectPath, [
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?.();
};
}, [
appendGameChatFinalReplyMessages,
gameChatOnly,
updateProjectSupervisorResponseStream,
updateProjectSupervisorRuntime,
]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
const sessionId = projectSupervisorSessionId;
const trackedRunId =
projectSupervisorExpectedRunId ?? projectSupervisorRuntime?.runId ?? null;
if (
!invoke ||
!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,
projectSupervisorSessionId,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
projectSupervisorRuntime?.status,
projectSupervisorExpectedRunId,
]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
const supervisorRunId = projectSupervisorRuntime?.runId ?? null;
if (
!projectSupervisorOnly ||
!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;
}
if (gameChatOnly) {
appendGameChatFinalReplyMessages(nextProjectPath, runtimes);
}
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,
projectSupervisorOnly,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.runId,
projectSupervisorRuntime?.status,
]);
useEffect(() => {
const invoke = resolveTauriInvoke();
const nextProjectPath = localProject?.projectPath ?? null;
if (!gameChatOnly || !invoke || !nextProjectPath) {
return;
}
let disposed = false;
let inFlight = false;
let attemptedRunId: string | null = null;
let attemptedAuthorizationId: string | null = null;
const authorizationMatches = (
expected: GameChatAutoPreviewAuthorization,
) => {
const current = gameChatAutoPreviewAuthorizationRef.current;
return (
current?.authorizationId === expected.authorizationId &&
current.projectPath === expected.projectPath &&
current.runId === expected.runId
);
};
const attemptIsCurrent = (
runId: string,
authorization: GameChatAutoPreviewAuthorization,
) =>
!disposed &&
localProjectPathRef.current === nextProjectPath &&
projectSupervisorRuntimeRef.current?.runId === runId &&
authorizationMatches(authorization);
const clearMatchingAuthorization = (
authorization: GameChatAutoPreviewAuthorization,
) => {
if (authorizationMatches(authorization)) {
setGameChatAutoPreviewAuthorization(null);
}
};
const readCurrentProjectRevision = async () => {
const result = await invoke<LocalGameProjectRevisionStatus>(
'get_local_game_project_revision',
{ projectPath: nextProjectPath },
);
if (!Number.isSafeInteger(result.revision) || result.revision < 0) {
throw new Error('本地游戏项目 revision 无效');
}
return result.revision;
};
const stopStaleStartedPreview = async (
startedPreview: LocalPreviewResult,
) => {
try {
await invoke<boolean>('stop_local_game_preview_if_matches', {
projectPath: nextProjectPath,
expectedPreview: startedPreview,
});
} catch {
// A newer preview identity or a closed project already owns the visible state.
}
};
const syncPreview = async () => {
if (disposed || inFlight) {
return;
}
inFlight = true;
try {
const status = await invoke<LocalPreviewStatus>(
'get_local_game_preview_status',
{ projectPath: nextProjectPath },
);
if (disposed || localProjectPathRef.current !== nextProjectPath) {
return;
}
const currentSupervisor = projectSupervisorRuntimeRef.current;
let playableRevision = latestGameChatPlayableRevision(
currentSupervisor,
agentRuntimeByIdRef.current,
);
if (playableRevision) {
const currentRevision = await readCurrentProjectRevision();
if (
disposed ||
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorRuntimeRef.current?.runId !==
playableRevision.runId
) {
return;
}
if (currentRevision !== playableRevision.revision) {
playableRevision = null;
}
}
const runningPreview =
status.status === 'running' &&
status.url &&
status.port &&
status.root &&
resolveEmbeddedPreviewUrl({
status: status.status,
url: status.url,
})
? {
url: status.url,
port: status.port,
root: status.root,
}
: null;
if (runningPreview) {
updateClientPreview(runningPreview);
setPreviewStatus(`运行中:127.0.0.1:${runningPreview.port}`);
if (
playableRevision &&
playableRevision.revision >
(gameChatPreviewRevisionRef.current ?? 0)
) {
gameChatPreviewRevisionRef.current = playableRevision.revision;
setGameChatPreviewRevision(playableRevision.revision);
}
const runningAuthorization =
gameChatAutoPreviewAuthorizationRef.current;
if (
playableRevision &&
runningAuthorization?.projectPath === nextProjectPath &&
runningAuthorization.runId === playableRevision.runId &&
gameChatPlayableRevisionIsAfterAuthorization(
playableRevision,
runningAuthorization,
)
) {
clearMatchingAuthorization(runningAuthorization);
}
return;
}
const hadRunningPreview = Boolean(gameChatPreviewRef.current);
updateClientPreview(null);
if (hadRunningPreview) {
setPreviewStatus('未启动');
}
const authorization = gameChatAutoPreviewAuthorizationRef.current;
if (
!playableRevision ||
authorization?.projectPath !== nextProjectPath ||
authorization.runId !== playableRevision.runId ||
!gameChatPlayableRevisionIsAfterAuthorization(
playableRevision,
authorization,
)
) {
return;
}
const autoPreviewRunId = playableRevision.runId;
attemptedRunId = autoPreviewRunId;
attemptedAuthorizationId = authorization.authorizationId;
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath },
);
if (!attemptIsCurrent(autoPreviewRunId, authorization)) {
return;
}
setManifest(nextManifest);
const attemptKey = `${nextProjectPath}\n${autoPreviewRunId}\nauthorization:${authorization.authorizationId}\nrevision:${playableRevision.revision}\nvalidatedAt:${playableRevision.validatedAt}`;
if (gameChatAutoPreviewAttemptedRef.current.has(attemptKey)) {
return;
}
const policyView = await invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
);
if (!attemptIsCurrent(autoPreviewRunId, authorization)) {
return;
}
const previewDenied =
policyView.policy.deniedCommands.includes('preview.start') ||
Object.values(policyView.policy.agentPolicies ?? {}).some((policy) =>
policy.deniedCommands.includes('preview.start'),
);
if (previewDenied) {
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
clearMatchingAuthorization(authorization);
const message = '项目权限策略拒绝执行:preview.start';
setCommandLog((current) => [
...current,
'permission.deny preview.start',
]);
setPreviewStatus(message);
return;
}
const revisionBeforeStart = await readCurrentProjectRevision();
if (
!attemptIsCurrent(autoPreviewRunId, authorization) ||
revisionBeforeStart !== playableRevision.revision
) {
return;
}
appendLocalPermissionLog(
nextProjectPath,
'permission.confirm',
'preview.start',
);
let startedPreview: LocalPreviewResult;
try {
startedPreview = await invoke<LocalPreviewResult>(
'start_local_game_preview',
{
projectPath: nextProjectPath,
expectedRevision: playableRevision.revision,
},
);
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
const retryWithNewEvidence =
message.startsWith('项目正在被其他写操作占用:') ||
message.startsWith('本地游戏项目已在验证后发生变化');
if (!retryWithNewEvidence) {
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
clearMatchingAuthorization(authorization);
}
throw error;
}
let revisionAfterStart: number;
try {
revisionAfterStart = await readCurrentProjectRevision();
} catch (error) {
await stopStaleStartedPreview(startedPreview);
throw error;
}
if (
!attemptIsCurrent(autoPreviewRunId, authorization) ||
revisionAfterStart !== playableRevision.revision
) {
await stopStaleStartedPreview(startedPreview);
return;
}
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
clearMatchingAuthorization(authorization);
updateClientPreview(startedPreview);
if (
playableRevision &&
playableRevision.revision > (gameChatPreviewRevisionRef.current ?? 0)
) {
gameChatPreviewRevisionRef.current = playableRevision.revision;
setGameChatPreviewRevision(playableRevision.revision);
}
setPreviewStatus(`运行中:127.0.0.1:${startedPreview.port}`);
setCommandLog((current) => [...current, 'preview.start']);
} catch (error) {
if (
!disposed &&
localProjectPathRef.current === nextProjectPath &&
(!attemptedRunId ||
projectSupervisorRuntimeRef.current?.runId === attemptedRunId) &&
(!attemptedAuthorizationId ||
!gameChatAutoPreviewAuthorizationRef.current ||
gameChatAutoPreviewAuthorizationRef.current.authorizationId ===
attemptedAuthorizationId)
) {
setPreviewStatus(
error instanceof Error ? error.message : String(error),
);
}
} finally {
inFlight = false;
}
};
void syncPreview();
const timer = window.setInterval(() => {
void syncPreview();
}, 1000);
return () => {
disposed = true;
window.clearInterval(timer);
};
// The interval reads the latest Runtime through refs and is recreated only for identity changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
gameChatOnly,
localProject?.projectPath,
projectSupervisorRuntime?.runId,
]);
useLayoutEffect(() => {
if (
(!supervisorChatOnly && !gameChatOnly) ||
!supervisorChatShouldFollowLatestRef.current
) {
return;
}
const messageList = supervisorChatMessagesRef.current;
if (messageList) {
messageList.scrollTop = messageList.scrollHeight;
}
}, [
messages,
projectSupervisorResponseStream?.sequence,
projectSupervisorRuntime?.updatedAt,
projectSupervisorRuntimeError,
gameChatOnly,
supervisorChatOnly,
]);
useEffect(() => {
if (!supervisorChatOnly && !gameChatOnly) {
return;
}
const draftProjectPath = gameChatOnly
? (localProject?.projectPath ?? initialProjectPath)
: initialProjectPath;
persistSupervisorChatDraft(draftProjectPath, chatInput);
}, [
chatInput,
gameChatOnly,
initialProjectPath,
localProject?.projectPath,
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 ||
isTransientProjectOpenMessage(message, nextProjectPath),
)
) {
savedConversationCountRef.current = messages.length;
return;
}
if (conversationWriteInFlightRef.current) {
return;
}
if (pendingUiConfirmation) {
return;
}
if (
projectConversationWriteCancelledRef.current?.projectPath ===
nextProjectPath &&
projectConversationWriteCancelledRef.current.messageCount ===
messages.length
) {
return;
}
if (
projectConversationWriteCancelledRef.current?.projectPath ===
nextProjectPath &&
projectConversationWriteCancelledRef.current.messageCount <
messages.length
) {
projectConversationWriteCancelledRef.current = null;
}
if (projectConversationWriteConfirmedRef.current !== nextProjectPath) {
void invoke<ProjectPermissionPolicyView>(
'read_project_permission_policy',
{ projectPath: nextProjectPath },
)
.then((policyView) => {
if (
!policyView.policy.confirmCommands.includes('conversation.write')
) {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
return;
}
requestProjectPolicyConfirmation(
'conversation.write',
nextProjectPath,
`写入 ${nextProjectPath} 的项目对话`,
() => {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
},
);
setWorkspaceStatus('等待确认保存项目对话');
})
.catch(() => {
projectConversationWriteConfirmedRef.current = nextProjectPath;
setConversationWriteVersion((current) => current + 1);
});
return;
}
conversationWriteInFlightRef.current = true;
void (async () => {
let wroteMessage = false;
for (const [index, message] of pendingMessages.entries()) {
if (message.runtimeOwned) {
savedConversationCountRef.current = start + index + 1;
continue;
}
if (isTransientProjectOpenMessage(message, nextProjectPath)) {
savedConversationCountRef.current = start + index + 1;
continue;
}
await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: null,
...(message.messageId ? { messageId: message.messageId } : {}),
message: {
role: message.role,
content: message.text,
agentId: null,
...(typeof message.updatedAt === 'number'
? { updatedAt: message.updatedAt }
: {}),
},
},
);
wroteMessage = true;
savedConversationCountRef.current = start + index + 1;
}
setWorkspaceStatus((current) =>
current.startsWith('项目对话保存失败') ||
current === '等待确认保存项目对话'
? `已打开:${nextProjectPath}`
: current,
);
if (wroteMessage) {
setCommandLog((current) => [...current, 'conversation.write']);
}
})()
.catch((error) => {
savedConversationCountRef.current = Math.min(
savedConversationCountRef.current,
start,
);
setWorkspaceStatus(
`项目对话保存失败:${
error instanceof Error ? error.message : String(error)
}`,
);
setCommandLog((current) => [
...current,
`conversation.write.failed ${
error instanceof Error ? error.message : String(error)
}`,
]);
})
.finally(() => {
conversationWriteInFlightRef.current = false;
if (
savedConversationCountRef.current < latestMessagesRef.current.length
) {
setConversationWriteVersion((current) => current + 1);
}
});
// Writes are driven by message and confirmation state; the confirmation helper
// intentionally observes the latest policy refs.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
localProject?.projectPath,
messages,
conversationWriteVersion,
pendingUiConfirmation,
projectSupervisorOnly,
]);
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: '准备运行当前本地游戏。' },
]);
}
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,
runId?: 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 (
!gameChatOnly &&
conversationContainsProjectSupervisorResponseStream(
supervisorConversation.messages,
transientResponse,
)
) {
projectSupervisorResponseStreamRef.current = null;
setProjectSupervisorResponseStream(null);
}
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setMessages((current) => {
// React may apply this hydration update after a ready stream callback
// that was queued in the same turn. Read `current` inside the updater
// so a committed game-chat response cannot be overwritten by stale
// `latestMessagesRef` state captured before that callback ran.
const nextMessages = gameChatOnly
? mergeGameChatHydratedConversationMessages(
conversationMessages,
current,
)
: conversationMessages;
savedConversationProjectPathRef.current = nextProjectPath;
savedConversationCountRef.current = gameChatOnly
? conversationMessages.length
: nextMessages.length;
latestMessagesRef.current = nextMessages;
return nextMessages;
});
const terminalRuntime = projectSupervisorRuntimeRef.current;
if (
runId &&
terminalRuntime?.runId === runId &&
isAgentRuntimeTerminalState(terminalRuntime)
) {
appendGameChatStageRecord(nextProjectPath, terminalRuntime);
}
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 = await resumeProjectSupervisorRuntimeTasksIfNeeded(
invoke,
nextProjectPath,
);
const sessionId = await readProjectSupervisorActiveSession(
invoke,
nextProjectPath,
);
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;
let runtimeError = '';
if (sessionId) {
supervisorConversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
projectPath: nextProjectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
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 ?? [],
);
if (
!gameChatOnly &&
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 = gameChatOnly
? mergeGameChatHydratedConversationMessages(
conversationMessages,
current,
)
: 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 = gameChatOnly
? conversationMessages.length
: nextConversationMessages.length;
latestMessagesRef.current = nextConversationMessages;
setWorkspaceStatus((workspaceStatus) => {
if (mode === 'replace') {
return `已读取项目对话历史:${conversationMessages.length} 条`;
}
return workspaceStatus === '等待确认'
? `已打开:${nextProjectPath}`
: workspaceStatus;
});
return nextConversationMessages;
});
} catch (error) {
if (
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
localProjectPathRef.current !== nextProjectPath
) {
return;
}
const message = error instanceof Error ? error.message : String(error);
if (isRuntimeConfigMissingError(message)) {
requestRuntimeConfigOpen();
}
setProjectSupervisorRuntimeError(message);
setWorkspaceStatus((workspaceStatus) =>
workspaceStatus === '等待确认'
? `项目对话读取失败:${message}`
: workspaceStatus,
);
// Keep the default greeting when history is missing or blocked.
}
}
async function openWorkspace(
nextProjectPath: string,
announceToChat: boolean,
) {
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();
if (
gameChatAutoPreviewAuthorizationRef.current?.projectPath !==
nextProjectPath
) {
setGameChatAutoPreviewAuthorization(null);
}
updateClientPreview(null);
gameChatPreviewRevisionRef.current = null;
setGameChatPreviewRevision(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 = await invoke<InitLocalProjectResult>(
'init_local_game_project',
{
projectPath: trimmedProjectPath,
projectId: seedManifest.projectId,
name: gameChatOnly
? projectNameFromPath(trimmedProjectPath)
: 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);
setLocalProject(openedProject);
if (gameChatOnly) {
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('已初始化');
appendLocalPermissionLog(
openedProject.projectPath,
'permission.confirm',
'project.create',
);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `已设置本地项目:${openedProject.projectPath}`,
},
]);
}
void loadProjectConversation(openedProject.projectPath);
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 openGameChatProjectPath(
nextProjectPath: string,
requestedSelectionVersion?: number,
) {
const selectionVersion =
requestedSelectionVersion ??
gameChatProjectSelectionVersionRef.current + 1;
gameChatProjectSelectionVersionRef.current = selectionVersion;
setPendingNonEmptyProjectCreate(null);
const trimmedProjectPath = nextProjectPath.trim();
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
setWorkspaceStatus('请提供工作区绝对路径');
return;
}
if (projectPathHasControlCharacter(trimmedProjectPath)) {
setWorkspaceStatus('工作区路径不能包含控制字符');
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setWorkspaceStatus('需要在 Tauri App 内运行');
return;
}
setGameChatProjectSelectionBusy(true);
try {
const directoryStatus = await invoke<LocalProjectDirectoryStatus>(
'inspect_local_project_directory',
{ projectPath: trimmedProjectPath },
);
if (gameChatProjectSelectionVersionRef.current !== selectionVersion) {
return;
}
if (directoryStatus.exists && !directoryStatus.isDirectory) {
setWorkspaceStatus('项目路径不是文件夹');
return;
}
if (directoryStatus.isGameCreatorProject) {
await openWorkspace(trimmedProjectPath, false);
return;
}
await executeProjectCreate(trimmedProjectPath, false, selectionVersion);
} catch (error) {
setWorkspaceStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
if (gameChatProjectSelectionVersionRef.current === selectionVersion) {
setGameChatProjectSelectionBusy(false);
}
}
}
async function handleGameChatProjectPick() {
const invoke = resolveTauriInvoke();
if (!invoke || gameChatProjectSelectionBusy) {
return;
}
const selectionVersion = gameChatProjectSelectionVersionRef.current + 1;
gameChatProjectSelectionVersionRef.current = selectionVersion;
setPendingNonEmptyProjectCreate(null);
setGameChatProjectSelectionBusy(true);
try {
const selectedPath = await invoke<string | null>(
'pick_local_project_directory',
);
if (gameChatProjectSelectionVersionRef.current !== selectionVersion) {
return;
}
if (!selectedPath) {
setWorkspaceStatus('已取消');
return;
}
await openGameChatProjectPath(selectedPath, selectionVersion);
} catch (error) {
setWorkspaceStatus(
error instanceof Error ? error.message : String(error),
);
} finally {
if (gameChatProjectSelectionVersionRef.current === selectionVersion) {
setGameChatProjectSelectionBusy(false);
}
}
}
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 = 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 executeChatAgentReply(prompt: string) {
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 submissionRunProfile =
supervisorChatOnly && !gameChatOnly
? 'standard'
: 'autonomous-game-build';
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
const steerRuntime = gameChatOnly
? matchingAgentRuntimeForSteer(
[runtimeAtSubmission],
PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
submissionRunProfile,
'project-supervisor-game-chat',
)
: null;
let autoPreviewAfterRevision = 0;
let autoPreviewAfterValidatedAt = 0;
if (steerRuntime) {
const playableAtSubmission = latestGameChatPlayableRevision(
runtimeAtSubmission,
agentRuntimeByIdRef.current,
);
autoPreviewAfterRevision = Math.max(
gameChatPreviewRevisionRef.current ?? 0,
playableAtSubmission?.revision ?? 0,
);
autoPreviewAfterValidatedAt = playableAtSubmission?.validatedAt ?? 0;
try {
const revisionStatus = await invoke<LocalGameProjectRevisionStatus>(
'get_local_game_project_revision',
{ projectPath: nextProjectPath },
);
if (
Number.isSafeInteger(revisionStatus.revision) &&
revisionStatus.revision >= 0
) {
autoPreviewAfterRevision = Math.max(
autoPreviewAfterRevision,
revisionStatus.revision,
);
}
} catch {
// The durable evidence cursor still prevents consuming an older validation.
}
if (
localProjectPathRef.current !== nextProjectPath ||
projectSupervisorSessionIdRef.current !== sessionId
) {
return;
}
}
const submission = await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: nextProjectPath,
sessionId,
prompt,
runtime: runtimeAtSubmission,
runProfile: submissionRunProfile,
...(gameChatOnly ? { source: 'project-supervisor-game-chat' } : {}),
});
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 (gameChatOnly) {
gameChatObservedRunKeysRef.current.add(
`${nextProjectPath}\n${acceptedRunId}`,
);
setGameChatAutoPreviewAuthorization({
afterRevision:
submission.mode === 'steer' ? autoPreviewAfterRevision : 0,
afterValidatedAt:
submission.mode === 'steer' ? autoPreviewAfterValidatedAt : 0,
authorizationId: createAgentChatRunId('game-chat-preview-auth'),
projectPath: nextProjectPath,
runId: 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,
acceptedRunId,
).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 (!gameChatOnly || !latch.prompt || !localProject) {
return;
}
if (localProject.projectPath !== latch.projectPath) {
claimInitialSupervisorMessageForPage(latch.projectPath);
return;
}
if (
chatAgentBusy ||
!claimInitialSupervisorMessageForPage(latch.projectPath)
) {
return;
}
supervisorChatShouldFollowLatestRef.current = true;
setMessages((current) => [
...current,
{
role: 'user',
text: latch.prompt,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
void executeChatAgentReplyRef.current(latch.prompt);
}, [chatAgentBusy, gameChatOnly, initialSupervisorMessage, localProject]);
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,
request.runId,
);
}
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,
expectedGameChatSelectionVersion?: number,
) {
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 (
expectedGameChatSelectionVersion !== undefined &&
gameChatProjectSelectionVersionRef.current !==
expectedGameChatSelectionVersion
) {
return;
}
if (nonEmpty) {
setPendingNonEmptyProjectCreate({
projectPath: trimmedProjectPath,
announceToChat,
});
setWorkspaceStatus('目标文件夹不是空的');
setProjectStatus('等待确认');
return;
}
} catch {
// ponytail: stale test doubles and older shells may miss this helper; init still validates.
}
}
if (
expectedGameChatSelectionVersion !== undefined &&
gameChatProjectSelectionVersionRef.current !==
expectedGameChatSelectionVersion
) {
return;
}
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 = 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',
`command.run_limited ${smoke.commandId}`,
'preview.start',
]);
void refreshManifest(nextProjectPath);
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 refreshManifest(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
return;
}
try {
const nextManifest = await invoke<GameCreationAppManifest>(
'get_local_game_manifest',
{ projectPath: nextProjectPath },
);
setManifest(nextManifest);
} catch {
// Dev-only convenience; command errors are surfaced by the action that triggered them.
}
}
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;
}
setAgentRuntimeById((current) =>
mergeAgentRuntimeStateIntoMap(current, runtime, false),
);
}
async function refreshAgentRuntimes(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
setAgentRuntimeById({});
agentRuntimeResumeProjectPathRef.current = null;
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) {
appendGameChatFinalReplyMessages(nextProjectPath, [runtimeResult]);
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) {
appendGameChatFinalReplyMessages(nextProjectPath, [
runtimeResult,
]);
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) {
appendGameChatFinalReplyMessages(nextProjectPath, [runtimeResult]);
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 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 ((gameChatOnly || supervisorChatOnly) && prompt.startsWith('/')) {
void handleChatSubmit(event);
return;
}
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
}
if (!prompt || chatAgentBusy) {
return;
}
if (supervisorChatOnly || gameChatOnly) {
supervisorChatShouldFollowLatestRef.current = true;
}
setChatInput('');
setMessages((current) => [
...current,
{
role: 'user',
text: prompt,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
void executeChatAgentReply(prompt);
}
const visibleProfessionalAgentCards = agentStatusCards.filter(
(agent) =>
agent.id !== PROJECT_SUPERVISOR_AGENT_ID &&
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
);
if (projectSupervisorOnly && gameChatOnly) {
const gameChatProjectPath = localProject?.projectPath ?? '';
return (
<SupervisorChatOnlyView
chatAgentBusy={chatAgentBusy}
chatInput={chatInput}
gameChatMode
messagesRef={supervisorChatMessagesRef}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={setChatInput}
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
onConfirmConfirmation={confirmUiCommand}
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
onProjectPick={() => void handleGameChatProjectPick()}
onScroll={handleSupervisorChatScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
onToolAction={handleProjectSupervisorToolAction}
onUserInput={handleProjectSupervisorUserInput}
pendingConfirmation={pendingUiConfirmation}
pendingCommand={pendingCommand}
onCancelPendingCommand={handlePendingCommandCancel}
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
pendingNonEmptyProjectCreate={pendingNonEmptyProjectCreate}
onCancelNonEmptyProjectCreate={cancelProjectCreateInNonEmptyFolder}
onConfirmNonEmptyProjectCreate={confirmProjectCreateInNonEmptyFolder}
preview={preview}
previewRevision={gameChatPreviewRevision}
previewStatus={previewStatus}
projectPath={gameChatProjectPath}
projectReady={Boolean(localProject)}
projectSelectionBusy={gameChatProjectSelectionBusy}
runtime={projectSupervisorRuntime}
runtimeByAgentId={agentRuntimeById}
manifest={manifest}
runtimeConfigOpen={runtimeConfigOpen}
runtimeError={projectSupervisorRuntimeError}
transientReply={projectSupervisorTransientReply}
hasConversationControls={projectSupervisorHasConversationControls}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
visibleMessages={visibleMessages}
workspaceStatus={workspaceStatus}
expectedRunId={projectSupervisorExpectedRunId}
/>
);
}
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={projectSupervisorTransientReply}
hasConversationControls={projectSupervisorHasConversationControls}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
visibleMessages={visibleMessages}
workspaceStatus={workspaceStatus}
expectedRunId={projectSupervisorExpectedRunId}
/>
);
}
if (projectSupervisorOnly) {
return (
<ProjectSupervisorView
chatInput={chatInput}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={setChatInput}
onConfirmConfirmation={confirmUiCommand}
onScroll={handleConversationScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
pendingConfirmation={pendingUiConfirmation}
transientReply={projectSupervisorTransientReply}
visibleMessages={visibleMessages}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
workspaceStatus={workspaceStatus}
runtime={projectSupervisorRuntime}
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={chatAgentBusy}
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}
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>
);
}