9639a080c6
- 新增落点模型 resourceCardReferenceDropModel:对话栏矩形量取(0 x 0 判成没有落点,避免画布内拖动被误判)、命中判据、引用构造与落点提示文案。 - 引用构造与工具条「引用」按钮逐字一致(只认已登记 manifest 素材、source 为 resource-card、kind/分类/标签取自资源投影),一条都构造不出来时用提示条说明原因。 - 拖动中指针落到对话栏上即从排版切到引用:卡片不再跟指针走,改铺虚线落点浮层并提示「松手即可 @ 引用 N 项素材」;拖回画布内松手仍是原来的排版语义。 - 批量范围与拖动位移同一个集合(多选整批、单选只引用按住的那张);新增批量事件 RESOURCE_REFERENCE_INSERT_MANY_EVENT,App 侧一次插入整批并只聚焦一次,空批次不派发。 - 对话栏挂 ref(不 document.querySelector)、落点浮层按对话栏当时的屏幕矩形铺在根节点上,不改对话栏自身的定位与排版。 - 测试:新增落点模型用例 5 条与画布集成用例 4 条(单卡/多选/拖回画布/pointercancel),resourceReferenceInput 新增批量事件与一次插入整批 chip 两条,共 39 条定向全绿。 - 文档:功能说明新增「拖拽引用」一节与完成清单条目、PRD 更新时间、decision-log 新增 2026-09-21 条目。
12247 lines
399 KiB
TypeScript
12247 lines
399 KiB
TypeScript
/* eslint-disable react-refresh/only-export-components -- Testable pure helpers currently share this legacy app module. */
|
||
|
||
import {
|
||
type ChangeEvent,
|
||
type FormEvent,
|
||
type UIEvent,
|
||
useCallback,
|
||
useEffect,
|
||
useLayoutEffect,
|
||
useRef,
|
||
useState,
|
||
} from 'react';
|
||
|
||
import {
|
||
GAME_CREATION_AGENT_CAPABILITIES,
|
||
GAME_CREATION_APP_COMMANDS,
|
||
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||
type GameCreationAgentCapabilityDescriptor,
|
||
type GameCreationAgentRunTrace,
|
||
type GameCreationAgentToolCallTrace,
|
||
type GameCreationAppAssetKind,
|
||
type GameCreationAppCommandDescriptor,
|
||
type GameCreationAppLimitedRunCommandDescriptor,
|
||
type GameCreationAppManifest,
|
||
type GameCreationAppPermission,
|
||
type GameCreationAppPreviewState,
|
||
type GameCreationAppPreviewStatus,
|
||
parseGameCreationAppAssetKind,
|
||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import {
|
||
AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD,
|
||
AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT,
|
||
AGENT_RUN_HISTORY_MAX_COUNT,
|
||
AGENT_RUN_HISTORY_VISIBLE_STEP,
|
||
CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
CONVERSATION_VISIBLE_STEP,
|
||
createLocalProjectId,
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
seedManifest,
|
||
} from './app/constants';
|
||
import { useEscapeToClose } from './app/dialogs';
|
||
import { resolveTauriInvoke } from './app/tauri';
|
||
import type {
|
||
AgentBackgroundSubmitMode,
|
||
AgentProgressEvent,
|
||
AgentRunControlResult,
|
||
AgentRunHistoryItem,
|
||
AgentRuntimeResponseStream,
|
||
AgentRuntimeResult,
|
||
AgentRuntimeState,
|
||
AgentRuntimeSteerResult,
|
||
AgentRuntimeUserInputRequest,
|
||
AgentStatusCard,
|
||
ChatMessage,
|
||
DesignAgentInput,
|
||
DesignClarificationRequest,
|
||
DesignEvent,
|
||
DesignView,
|
||
DirectTurnCancelView,
|
||
GameCreatorAgentRuntimeUpdateEvent,
|
||
GameCreatorChatAgentReply,
|
||
GameCreatorLlmConfigStatus,
|
||
GameCreatorManifestInvalidatedEvent,
|
||
GameCreatorRoleAgentChatStreamEvent,
|
||
GenerateLocalGameDraftResult,
|
||
ImportCanvasExportResult,
|
||
InitLocalProjectResult,
|
||
LauncherImportedAttachment,
|
||
LimitedLocalCommandResult,
|
||
ListLocalProjectFilesResult,
|
||
LocalAgentMemoryResult,
|
||
LocalConversationMessageRecord,
|
||
LocalConversationResult,
|
||
LocalGameMemoryResult,
|
||
LocalGameProjectRevisionStatus,
|
||
LocalPreviewResult,
|
||
LocalPreviewStatus,
|
||
LocalProjectCheckpointResult,
|
||
LocalProjectCheckpointSummary,
|
||
LocalProjectDiffResult,
|
||
LocalProjectExportPackageResult,
|
||
LocalProjectExportPackagesResult,
|
||
LocalProjectFileEntry,
|
||
LocalProjectFileMutationResult,
|
||
LocalProjectFileResult,
|
||
LocalProjectIndexResult,
|
||
LocalProjectKind,
|
||
LocalProjectRestoreResult,
|
||
MemoryScope,
|
||
MemoryWriteMode,
|
||
OpenCanvasProjectResult,
|
||
PendingCommand,
|
||
PendingUiConfirmation,
|
||
ProjectPermissionPolicy,
|
||
ProjectPermissionPolicyView,
|
||
SyncCanvasProjectAssetsResult,
|
||
TauriInvoke,
|
||
UploadLocalAssetResult,
|
||
} from './app/types';
|
||
import { useWindowChrome } from './components/windowChromeContext';
|
||
import {
|
||
agentConversationId,
|
||
agentRuntimeCancelStatus,
|
||
agentRuntimeConversationStatus,
|
||
agentRuntimeNeedsUserInput,
|
||
agentRuntimeStartedRunId,
|
||
agentRuntimeStartStatus,
|
||
agentRuntimeStateFromResult,
|
||
agentRuntimeSteerStatus,
|
||
conversationContainsProjectSupervisorResponseStream,
|
||
createAgentChatRunId,
|
||
createDefaultChatMessages,
|
||
createLocalConversationDraftMessage,
|
||
ensureProjectSupervisorActiveSessionId,
|
||
isAgentFinalizationMessageId,
|
||
isAgentRuntimeTerminalState,
|
||
isMissingAgentRuntimeResumeCommandError,
|
||
isRuntimeConfigMissingError,
|
||
matchingAgentRuntimeForSteer,
|
||
mergeAgentRuntimeStateIntoMap,
|
||
mergeProjectSupervisorConversation,
|
||
mergeProjectSupervisorResponseStream,
|
||
normalizeAgentRuntimeState,
|
||
projectNameFromPath,
|
||
projectProfessionalAgentLabel,
|
||
projectRuntimeVisibleError,
|
||
projectSupervisorPendingRepairMatchesProfessional,
|
||
readProjectSupervisorActiveSessionId,
|
||
resolveProjectSupervisorRuntimeSubmission,
|
||
sameAgentRuntimeRun,
|
||
submitProjectSupervisorRuntimeTask,
|
||
taskRowsFromManifest,
|
||
} from './features/agent-runtime';
|
||
import {
|
||
type DirectCodexTurnAttachment,
|
||
toDirectCodexTurnAttachments,
|
||
} from './features/app-shell/directCodexTurnAttachments';
|
||
import {
|
||
isDeveloperMode,
|
||
isTransientProjectOpenMessage,
|
||
latestVisibleItems,
|
||
persistSupervisorChatDraft,
|
||
type ProjectSupervisorComponentProps,
|
||
readInitialProjectPath,
|
||
readSupervisorChatDraft,
|
||
type WorkspaceLauncherProps,
|
||
writeRecentWorkspace,
|
||
} from './features/app-shell/model';
|
||
import { uploadLocalFilesAsAttachments } from './features/app-shell/useHomeProjectCreation';
|
||
import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
|
||
import {
|
||
agentConversationReadDraftsFromManifest,
|
||
agentMemoryReadDraftsFromManifest,
|
||
commandDraftFromSuggestedToolCall,
|
||
deriveAgentStatusCards,
|
||
formatAgentDialogLlmStatus,
|
||
formatAgentLlmConfigWarning,
|
||
formatAgentRunControlError,
|
||
formatCodexRuntimeCapabilities,
|
||
formatLlmAgentStatusLine,
|
||
formatLlmRouteEndpoint,
|
||
isMissingAgentRunTraceError,
|
||
projectAgentRuntimeSummaries,
|
||
readablePassArtifactsFromAgentRunTrace,
|
||
sameAgentStatusCard,
|
||
summarizeAgentAudit,
|
||
summarizeAgentCapabilities,
|
||
summarizeAgentConversationReadDrafts,
|
||
summarizeAgentLlmRoutes,
|
||
summarizeAgentMemoryReadDrafts,
|
||
summarizeAgentPassArtifactReadDrafts,
|
||
summarizeAgentRunCompletionForChat,
|
||
summarizeAgentRunHistoryReadDrafts,
|
||
summarizeAgentStatusCardsForChat,
|
||
summarizeLimitedLocalCommands,
|
||
summarizeRunArtifactReadDrafts,
|
||
} from './features/project-summary/agentPresentation';
|
||
import {
|
||
chatCommandHelp,
|
||
checkpointSummaryFromManifest,
|
||
commonAgentRunSupportReadDrafts,
|
||
commonProjectArtifactReadDrafts,
|
||
commonProjectInternalReadDrafts,
|
||
commonProjectLogReadDrafts,
|
||
firstReadableProjectAssetPath,
|
||
formatAgentRunStatus,
|
||
formatCanvasAssetSource,
|
||
isAbsoluteProjectPath,
|
||
isSafeProjectRelativePath,
|
||
missingChatCommandArgumentMessage,
|
||
projectFileActionDrafts,
|
||
projectPathHasControlCharacter,
|
||
projectPathsMatchForInvalidation,
|
||
readableArtifactsFromAgentRunTrace,
|
||
sortCheckpointManifestFiles,
|
||
summarizeAgentRunSupportFileReadDrafts,
|
||
summarizeCommonProjectArtifactReadDrafts,
|
||
summarizeCommonProjectLogReadDrafts,
|
||
summarizeMainProjectHeader,
|
||
summarizeProjectAssetCredits,
|
||
summarizeProjectAssets,
|
||
summarizeProjectAudioAssets,
|
||
summarizeProjectCheckpoint,
|
||
summarizeProjectCheckpoints,
|
||
summarizeProjectDiff,
|
||
summarizeProjectExportPackage,
|
||
summarizeProjectExportPackages,
|
||
summarizeProjectFileContent,
|
||
summarizeProjectFiles,
|
||
summarizeProjectIndex,
|
||
summarizeProjectInternalReadDrafts,
|
||
summarizeProjectPolicy,
|
||
summarizeProjectStatus,
|
||
summarizeProjectTasks,
|
||
summarizeProjectVisualAssets,
|
||
} from './features/project-summary/projectSummary';
|
||
import { AgentConversationOverlay } from './features/project-workspace/AgentConversationOverlay';
|
||
import {
|
||
gameDraftStartedMessage,
|
||
isMissingProjectFileError,
|
||
parseAgentRunTrace,
|
||
} from './features/project-workspace/agentRunTrace';
|
||
import {
|
||
chatQueueFullNotice,
|
||
createQueuedChatTurn,
|
||
dequeueChatTurn,
|
||
enqueueChatTurn,
|
||
isChatTurnQueueFull,
|
||
type QueuedChatTurn,
|
||
removeQueuedChatTurn,
|
||
} from './features/project-workspace/chatComposerQueue';
|
||
import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
|
||
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
|
||
import {
|
||
type DirectHistoryAnchorGate,
|
||
directHistoryAnchorGateToWaitFor,
|
||
reuseOrOpenDirectHistoryAnchorGate,
|
||
} from './features/project-workspace/directHistoryAnchorGate';
|
||
import { readDirectHistoryPages } from './features/project-workspace/directHistoryPaging';
|
||
import {
|
||
applyDirectThreadConsumeResult,
|
||
type DirectThreadChatState,
|
||
directThreadTurnMatchesUser,
|
||
emptyDirectThreadChatState,
|
||
finishDirectThreadTurn,
|
||
mergeDirectHistoryItems,
|
||
resolveDirectThreadBootstrap,
|
||
selectDirectChatEntries,
|
||
} from './features/project-workspace/directThreadChat';
|
||
import {
|
||
type DirectThreadConsumeResult,
|
||
type DirectThreadHistorySlice,
|
||
type DirectThreadItem,
|
||
type DirectThreadSubscriptionBootstrap,
|
||
} from './features/project-workspace/directThreadEvents';
|
||
import type { DirectCodexUserContentPart } from './features/project-workspace/generated';
|
||
import {
|
||
appendMemoryContent,
|
||
memoryScopeLabel,
|
||
memoryScopePath,
|
||
parseOptionalMemoryScope,
|
||
parseRememberInput,
|
||
} from './features/project-workspace/memoryCommands';
|
||
import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation';
|
||
import {
|
||
isAgentTraceFilePath,
|
||
isProjectPolicyConfirmableCommandId,
|
||
isRegisteredGameCreationCommandId,
|
||
isSafeCanvasProjectId,
|
||
isSafeCheckpointId,
|
||
needsInitializedChatProject,
|
||
resolveChatProjectPath,
|
||
resolvePendingCommandProjectPath,
|
||
} from './features/project-workspace/projectCommandPolicy';
|
||
import { handleProjectSummaryChatCommand } from './features/project-workspace/projectSummaryCommands';
|
||
import { ProjectSupervisorView } from './features/project-workspace/ProjectSupervisorView';
|
||
import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane';
|
||
import type { ResourceReferenceInputHandle } from './features/project-workspace/ResourceReferenceInput';
|
||
import type {
|
||
ChatComposerDraft,
|
||
ChatReference,
|
||
} from './features/project-workspace/resourceReferences';
|
||
import {
|
||
chatComposerDraftToDirectCodexUserItem,
|
||
RESOURCE_REFERENCE_INSERT_EVENT,
|
||
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
|
||
type ResourceReferenceInsertEventDetail,
|
||
type ResourceReferenceInsertManyEventDetail,
|
||
} from './features/project-workspace/resourceReferences';
|
||
import { SupervisorChatOnlyView } from './features/project-workspace/SupervisorChatOnlyView';
|
||
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
|
||
import { captureAgentRuntimeError } from './services/errorReporting';
|
||
import {
|
||
currentPlatformSessionGeneration,
|
||
requestPlatformSessionRefresh,
|
||
} from './services/platformSession';
|
||
import {
|
||
setAgcPluginProjectPath,
|
||
startAvailableAgcEditorPlugins,
|
||
} from './services/pluginHost';
|
||
import {
|
||
canSubscribeTauriEvents,
|
||
subscribeTauriEvent,
|
||
} from './services/tauriEventSubscription';
|
||
import type { HomeCreationType } from './view/home';
|
||
import {
|
||
type ProjectAgentResultSummary,
|
||
type ProjectAgentRuntimeSummary,
|
||
} from './view/project-development';
|
||
import type { ProjectManifestSnapshotMetadata } from './view/project-development/projectResourceLiveUpdateModel';
|
||
|
||
const initialSupervisorMessageClaimsByPage = new WeakMap<Window, Set<string>>();
|
||
const DIRECT_CODEX_PRODUCT_RUNTIME = true;
|
||
const DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX = 'direct-codex:';
|
||
const DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX =
|
||
'direct-codex-turn-already-running:';
|
||
/** 与 Rust 侧 `DirectTaonierActiveInvocationGuard::enter` 的 else 分支文案保持一致。 */
|
||
const DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER =
|
||
'当前项目已有另一条 Direct 客户端回合正在运行';
|
||
// Platform access tokens are short lived. DirectProject can spend several
|
||
// minutes in image generation, build and browser validation, so keep the
|
||
// client-owned native session current while a turn is running. The singleflight
|
||
// refresh in platformSession.ts coalesces this with any 401-triggered refresh.
|
||
const DIRECT_CODEX_SESSION_KEEPALIVE_MS = 5 * 60 * 1000;
|
||
|
||
function isDirectCodexAuthenticationRequired(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return (
|
||
message.includes('authentication-required') ||
|
||
message.includes('codex-app-server-error:unauthorized') ||
|
||
/kind=codex-app-server-unauthorized(?=\s|$)/.test(message) ||
|
||
message.includes('登录已失效')
|
||
);
|
||
}
|
||
|
||
async function withDirectCodexSessionRefresh<T>(operation: () => Promise<T>) {
|
||
const generation = currentPlatformSessionGeneration();
|
||
try {
|
||
return await operation();
|
||
} catch (error) {
|
||
if (!isDirectCodexAuthenticationRequired(error)) throw error;
|
||
if (currentPlatformSessionGeneration() !== generation) throw error;
|
||
const refresh = await requestPlatformSessionRefresh();
|
||
if (refresh.status === 'failed') throw error;
|
||
if (
|
||
refresh.status !== 'refreshed' ||
|
||
currentPlatformSessionGeneration() !== refresh.generation
|
||
) {
|
||
throw new Error('登录账号已变化,原对话请求已停止');
|
||
}
|
||
return operation();
|
||
}
|
||
}
|
||
|
||
function directCodexConversationMessageId(
|
||
turnId: string,
|
||
role: ChatMessage['role'],
|
||
) {
|
||
return `${DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX}${turnId}:${role}`;
|
||
}
|
||
|
||
export const MAX_CHAT_COMPOSER_ATTACHMENTS = 8;
|
||
|
||
export function isDirectCodexTurnAlreadyRunningError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return message
|
||
.trimStart()
|
||
.startsWith(DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX);
|
||
}
|
||
|
||
/**
|
||
* 另一条 Direct 回合占着这个项目时的拒绝。它与上面那条同 clientTurnId 的拒绝分属不同
|
||
* 错误分类(Rust 侧刻意不带前缀),但对界面是同一件事:本项目现在有一条我们没接管的
|
||
* 回合在跑。所以这里单独判定,让它也走"接管它 + 告诉用户出口"的处理。
|
||
*/
|
||
export function isDirectCodexAnotherTurnRunningError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return message.includes(DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER);
|
||
}
|
||
|
||
/**
|
||
* 用户点了"终止"以后,正在 await 的回合命令会带着 app-server 的中断原因返回
|
||
* (`Codex app-server turn 已中断`)。这类错误是用户主动取消,不是失败:界面要给
|
||
* "已终止本次回合"而不是把中断当作异常写进运行错误与诊断。
|
||
*/
|
||
export function isDirectCodexTurnInterruptedError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return message.includes('turn 已中断') || message.includes('已终止本次回合');
|
||
}
|
||
|
||
function isPersistableDirectCodexConversationMessage(message: ChatMessage) {
|
||
if (!message.runtimeOwned) {
|
||
return false;
|
||
}
|
||
const messageId = message.messageId?.trim() ?? '';
|
||
const roleSuffix = `:${message.role}`;
|
||
if (
|
||
!messageId.startsWith(DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX) ||
|
||
!messageId.endsWith(roleSuffix)
|
||
) {
|
||
return false;
|
||
}
|
||
const turnId = messageId.slice(
|
||
DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX.length,
|
||
-roleSuffix.length,
|
||
);
|
||
return /^[a-z0-9][a-z0-9-]{5,159}$/iu.test(turnId);
|
||
}
|
||
|
||
function claimInitialSupervisorMessageForPage(projectPath: string, scope = '') {
|
||
let claimedProjectPaths = initialSupervisorMessageClaimsByPage.get(window);
|
||
if (!claimedProjectPaths) {
|
||
claimedProjectPaths = new Set<string>();
|
||
initialSupervisorMessageClaimsByPage.set(window, claimedProjectPaths);
|
||
}
|
||
const claimKey = `${projectPath}\u0000${scope}`;
|
||
if (claimedProjectPaths.has(claimKey)) {
|
||
return false;
|
||
}
|
||
claimedProjectPaths.add(claimKey);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 历史回读与"尚未落盘的运行时消息"合并。
|
||
*
|
||
* 初始需求是**乐观插入**到 messages 的(latch 命中后先插一条 user 消息,再发起回合),
|
||
* 而历史回读在 replace 分支里是无条件整体替换 —— 只要回读晚于乐观插入,那条用户消息
|
||
* 就会被冲掉(界面上看不到初始需求,但回合其实已经跑起来了)。
|
||
* 这里把当前 messages 里"运行时拥有、且回读结果里没有"的消息保留在末尾(它们是最新的)。
|
||
*/
|
||
function mergeLoadedConversationWithPendingRuntimeMessages(
|
||
loaded: ChatMessage[],
|
||
current: ChatMessage[],
|
||
): ChatMessage[] {
|
||
if (current.length === 0) {
|
||
return loaded;
|
||
}
|
||
const loadedIds = new Set(
|
||
loaded
|
||
.map((message) => message.messageId)
|
||
.filter((id): id is string => Boolean(id)),
|
||
);
|
||
const loadedTexts = new Set(
|
||
loaded.map((message) => `${message.role}\u0000${message.text}`),
|
||
);
|
||
const pending = current.filter((message) => {
|
||
if (!message.runtimeOwned) {
|
||
return false;
|
||
}
|
||
if (message.messageId) {
|
||
return !loadedIds.has(message.messageId);
|
||
}
|
||
return !loadedTexts.has(`${message.role}\u0000${message.text}`);
|
||
});
|
||
return pending.length > 0 ? [...loaded, ...pending] : loaded;
|
||
}
|
||
|
||
export { AuthenticatedClient } from './app/AuthenticatedClient';
|
||
export type { PendingCommand } from './app/types';
|
||
export {
|
||
deriveAgentStatusCards,
|
||
summarizeAgentAudit,
|
||
summarizeAgentRunTrace,
|
||
} from './features/project-summary/agentPresentation';
|
||
export { isAbsoluteProjectPath } from './features/project-summary/projectSummary';
|
||
export {
|
||
needsInitializedChatProject,
|
||
parseRememberInput,
|
||
pendingCommandDetail,
|
||
resolveChatProjectPath,
|
||
resolvePendingCommandProjectPath,
|
||
};
|
||
|
||
export function WorkspaceLauncher(props: WorkspaceLauncherProps) {
|
||
return <WorkspaceLauncherShell {...props} ProjectSupervisor={App} />;
|
||
}
|
||
|
||
type AppProps = {
|
||
initialProjectPath?: string;
|
||
initialProjectManifest?: GameCreationAppManifest;
|
||
initialProjectKind?: LocalProjectKind;
|
||
orchestrationMode?: 'single-supervisor' | 'professional-dag';
|
||
projectSupervisorOnly?: boolean;
|
||
planningStartMode?: boolean;
|
||
activeVersionId?: ProjectSupervisorComponentProps['activeVersionId'];
|
||
supervisorChatOnly?: boolean;
|
||
initialSupervisorMessage?: string;
|
||
initialSupervisorMessageClaimScope?: string;
|
||
initialCreationType?: HomeCreationType | null;
|
||
initialAttachments?: LauncherImportedAttachment[];
|
||
playRequest?: ProjectSupervisorComponentProps['playRequest'];
|
||
onPlayRequestHandled?: ProjectSupervisorComponentProps['onPlayRequestHandled'];
|
||
onManifestChange?: (
|
||
projectPath: string,
|
||
manifest: GameCreationAppManifest,
|
||
metadata?: ProjectManifestSnapshotMetadata,
|
||
) => void;
|
||
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
|
||
onAgentRuntimeSummariesChange?: (
|
||
summaries: ProjectAgentRuntimeSummary[],
|
||
) => void;
|
||
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
|
||
};
|
||
|
||
type ExecuteChatAgentReplyInput = {
|
||
prompt: string;
|
||
clientTurnId?: string;
|
||
creationType?: HomeCreationType | null;
|
||
attachments?: DirectCodexTurnAttachment[];
|
||
directPolicyChecked?: boolean;
|
||
references?: ChatReference[];
|
||
userItem?: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
||
};
|
||
|
||
/**
|
||
* `conversation.write` 策略确认后重跑同一轮直连回合的入参。
|
||
*
|
||
* 确认弹窗会在**同一轮输入**上二次进入 `executeChatAgentReply`。这里从首轮的入参整体派生,
|
||
* 而不是手写一遍字段:一旦重跑时漏掉某一项(历史缺陷就是漏了 `references`),
|
||
* 用户在确认之后拿到的就不是他原本提交的那一轮——`@` 引用会被静默丢掉。
|
||
*/
|
||
export function directCodexPolicyRetryInput(
|
||
input: ExecuteChatAgentReplyInput,
|
||
): ExecuteChatAgentReplyInput {
|
||
return { ...input, directPolicyChecked: true };
|
||
}
|
||
|
||
export function App({
|
||
initialProjectPath: initialProjectPathOverride = '',
|
||
initialProjectManifest,
|
||
initialProjectKind = 'web',
|
||
orchestrationMode = 'professional-dag',
|
||
projectSupervisorOnly = false,
|
||
planningStartMode = false,
|
||
activeVersionId = null,
|
||
supervisorChatOnly = false,
|
||
initialSupervisorMessage = '',
|
||
initialSupervisorMessageClaimScope = '',
|
||
initialCreationType = null,
|
||
initialAttachments = [],
|
||
playRequest = null,
|
||
onPlayRequestHandled,
|
||
onManifestChange,
|
||
onPreviewChange,
|
||
onAgentRuntimeSummariesChange,
|
||
onAgentResultsChange,
|
||
}: AppProps = {}) {
|
||
const { setTitle: setWindowTitle } = useWindowChrome();
|
||
const [designAgentActive, setDesignAgentActive] = useState(planningStartMode);
|
||
const designAgentActiveRef = useRef(planningStartMode);
|
||
designAgentActiveRef.current = designAgentActive;
|
||
const [designAgentView, setDesignAgentView] = useState<DesignView | null>(
|
||
null,
|
||
);
|
||
const designAgentTurnRef = useRef<{
|
||
projectPath: string;
|
||
clientTurnId: string;
|
||
} | null>(null);
|
||
const designAgentEventSubscriptionReadyRef = useRef<Promise<void> | null>(
|
||
null,
|
||
);
|
||
const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>(
|
||
null,
|
||
);
|
||
const directCodexProductRuntime =
|
||
DIRECT_CODEX_PRODUCT_RUNTIME &&
|
||
projectSupervisorOnly &&
|
||
!supervisorChatOnly &&
|
||
!planningStartMode &&
|
||
!designAgentActive;
|
||
const [devMode] = useState(() =>
|
||
projectSupervisorOnly ? false : isDeveloperMode(),
|
||
);
|
||
const [initialProjectPath] = useState(
|
||
() => initialProjectPathOverride || readInitialProjectPath(),
|
||
);
|
||
const eagerSupervisorProject =
|
||
projectSupervisorOnly && Boolean(initialProjectPath);
|
||
const [projectPath, setProjectPath] = useState(initialProjectPath);
|
||
const [workspaceProjectKind, setWorkspaceProjectKind] =
|
||
useState<LocalProjectKind>(initialProjectKind);
|
||
const [localProject, setLocalProject] =
|
||
useState<InitLocalProjectResult | null>(() =>
|
||
eagerSupervisorProject
|
||
? {
|
||
projectPath: initialProjectPath,
|
||
manifestPath: `${initialProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`,
|
||
manifest: initialProjectManifest ?? seedManifest,
|
||
}
|
||
: null,
|
||
);
|
||
const localProjectPathRef = useRef<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (supervisorChatOnly) return;
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
const previousProjectPath = localProjectPathRef.current;
|
||
localProjectPathRef.current = nextProjectPath;
|
||
// 未绑定项目时无需触发插件宿主;这也避免启动空首页时产生无意义的 Tauri 调用。
|
||
if (!nextProjectPath && !previousProjectPath) return;
|
||
let active = true;
|
||
void setAgcPluginProjectPath(nextProjectPath)
|
||
.then(async () => {
|
||
if (active && nextProjectPath) {
|
||
await startAvailableAgcEditorPlugins(() => active);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
if (!active || !nextProjectPath) {
|
||
return;
|
||
}
|
||
setWorkspaceStatus(
|
||
`编辑器插件未就绪:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
});
|
||
return () => {
|
||
active = false;
|
||
if (nextProjectPath || previousProjectPath) {
|
||
void setAgcPluginProjectPath(null).catch(() => undefined);
|
||
}
|
||
localProjectPathRef.current = null;
|
||
};
|
||
}, [localProject?.projectPath, supervisorChatOnly]);
|
||
|
||
const manifestRefreshMountedRef = useRef(true);
|
||
const manifestRefreshStatesRef = useRef(
|
||
new Map<
|
||
string,
|
||
{
|
||
pending: boolean;
|
||
inFlight: Promise<void> | null;
|
||
}
|
||
>(),
|
||
);
|
||
const [manifest, setManifest] = useState<GameCreationAppManifest>(
|
||
initialProjectManifest ?? seedManifest,
|
||
);
|
||
const manifestRef = useRef(manifest);
|
||
manifestRef.current = manifest;
|
||
/**
|
||
* 跟随工作台壳的清单快照。
|
||
*
|
||
* 资源命令(改标签 / 改类型 / 重命名 / 删素材)由资源画布写入,写入后壳重读一次
|
||
* manifest、按 CAS 归并进 `currentProjectContext`,再把归并结果继续以这个 prop 传下来。
|
||
* 但它是 `useState` 的**初值**:壳里换了新清单不会再进来,于是聊天侧(`@` 选择器的
|
||
* 标签统计与候选、`@` 候选菜单)一直用挂载时那份旧清单 —— 用户改完标签,画布已经
|
||
* 更新,聊天侧却还是旧标签。
|
||
*
|
||
* 这里把壳那份快照补进聊天侧状态,判据只有两条:同一项目、内容确实变了
|
||
* (按内容的短路是必要的:聊天侧自己的写入会被壳原样回传,只比身份会让两边
|
||
* 无意义地互相推一轮)。壳那份快照始终由磁盘重读 + CAS 归并得到,因此不会把
|
||
* 聊天侧带到更旧的版本上。
|
||
*/
|
||
useEffect(() => {
|
||
const snapshot = initialProjectManifest;
|
||
const current = manifestRef.current;
|
||
if (!snapshot || snapshot === current) return;
|
||
if (snapshot.projectId !== current.projectId) return;
|
||
if (JSON.stringify(snapshot) === JSON.stringify(current)) return;
|
||
setManifest(snapshot);
|
||
}, [initialProjectManifest]);
|
||
const [projectStatus, setProjectStatus] = useState(
|
||
eagerSupervisorProject ? '已初始化' : '未初始化',
|
||
);
|
||
const [preview, setPreview] = useState<LocalPreviewResult | null>(null);
|
||
const [previewStatus, setPreviewStatus] = useState('未启动');
|
||
const initialSupervisorMessageLatchRef = useRef({
|
||
projectPath: initialProjectPath,
|
||
prompt: initialSupervisorMessage.trim(),
|
||
claimScope: initialSupervisorMessageClaimScope,
|
||
creationType: initialCreationType,
|
||
attachments: toDirectCodexTurnAttachments(initialAttachments),
|
||
});
|
||
const handledPlayRequestRef = useRef<string | null>(null);
|
||
|
||
function updateClientPreview(
|
||
nextPreview: LocalPreviewResult | null,
|
||
status: GameCreationAppPreviewStatus = nextPreview ? 'running' : 'stopped',
|
||
) {
|
||
setPreview(nextPreview);
|
||
onPreviewChange?.(
|
||
nextPreview
|
||
? {
|
||
status,
|
||
url: nextPreview.url,
|
||
port: nextPreview.port,
|
||
}
|
||
: { status },
|
||
);
|
||
}
|
||
|
||
const [chatInput, setChatInput] = useState(() =>
|
||
supervisorChatOnly && initialProjectPath
|
||
? readSupervisorChatDraft(initialProjectPath)
|
||
: '',
|
||
);
|
||
const [chatReferences, setChatReferences] = useState<ChatReference[]>([]);
|
||
/**
|
||
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
|
||
* 交给 `chat_with_game_creator_direct_codex` 的 `attachments`。附件只存在于前端状态,
|
||
* 提交后即清空——后端协议不变。
|
||
*/
|
||
const [chatAttachments, setChatAttachments] = useState<
|
||
DirectCodexTurnAttachment[]
|
||
>([]);
|
||
const [chatAttachmentNotice, setChatAttachmentNotice] = useState('');
|
||
/** 回合运行中再次发送的消息:FIFO 本地队列,当前回合结束后依次发出。 */
|
||
const [chatTurnQueue, setChatTurnQueue] = useState<QueuedChatTurn[]>([]);
|
||
const chatTurnQueueRef = useRef<QueuedChatTurn[]>([]);
|
||
chatTurnQueueRef.current = chatTurnQueue;
|
||
const [chatComposerNotice, setChatComposerNotice] = useState('');
|
||
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
|
||
useState(false);
|
||
const queuedChatTurnSequenceRef = useRef(0);
|
||
const [chatContent, setChatContent] = useState<DirectCodexUserContentPart[]>(
|
||
[],
|
||
);
|
||
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
||
/**
|
||
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
|
||
* 队列也属于刚结束的那条对话;留着会把 A 项目的附件路径带进 B 项目的下一个回合。
|
||
*/
|
||
useEffect(() => {
|
||
setChatAttachments([]);
|
||
setChatContent([]);
|
||
setChatAttachmentNotice('');
|
||
setChatComposerNotice('');
|
||
setChatTurnQueue([]);
|
||
chatTurnQueueRef.current = [];
|
||
}, [localProject?.projectPath]);
|
||
const [chatAgentBusy, setChatAgentBusy] = useState(false);
|
||
const directCodexConversationTurnSequenceRef = useRef(0);
|
||
/**
|
||
* DirectProject 聊天真相源:历史切片与运行态事件归并成同一份条目。
|
||
*
|
||
* 运行态只来自 Thread Manager(`subscribe` 的 bootstrap + `notify` 唤醒的 `consume`);
|
||
* 前端不再订阅 DirectRuntime 的回合进度事件,也不再各存一份回合流 / 工具卡片。
|
||
*/
|
||
const [directThreadChat, setDirectThreadChat] =
|
||
useState<DirectThreadChatState>(() => emptyDirectThreadChatState());
|
||
/** 最新回合是否在跑:历史里留下的半截回合一律按已结束渲染。 */
|
||
const directTurnRunning = directThreadChat.turnRunning;
|
||
/** 界面忙碌判定:本地正在跑这次 invoke,或订阅告诉还有一条回合没结束。 */
|
||
const supervisorChatBusy =
|
||
chatAgentBusy || (directCodexProductRuntime && directTurnRunning);
|
||
const chatAgentBusyRef = useRef(chatAgentBusy);
|
||
const directTurnRunningRef = useRef(directTurnRunning);
|
||
chatAgentBusyRef.current = chatAgentBusy;
|
||
directTurnRunningRef.current = directTurnRunning;
|
||
const previousDirectTurnRunningRef = useRef(directTurnRunning);
|
||
const directTurnCompletionPendingRef = useRef(false);
|
||
const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState<
|
||
string | null
|
||
>(null);
|
||
|
||
function createDirectCodexConversationTurnId() {
|
||
directCodexConversationTurnSequenceRef.current += 1;
|
||
let randomId = '';
|
||
try {
|
||
randomId = globalThis.crypto?.randomUUID?.().trim() ?? '';
|
||
} catch {
|
||
// The timestamp and in-page sequence remain unique enough for a local
|
||
// conversation append when WebView crypto is unavailable.
|
||
}
|
||
if (/^[a-z0-9][a-z0-9-]{5,159}$/iu.test(randomId)) {
|
||
return randomId;
|
||
}
|
||
return `${Date.now().toString(36)}-${directCodexConversationTurnSequenceRef.current.toString(36)}`;
|
||
}
|
||
|
||
/** 换项目 / 清空对话:聊天条目回到初始态(运行态与已加载历史一起清掉)。 */
|
||
function resetDirectThreadChat() {
|
||
setDirectThreadChat(emptyDirectThreadChatState());
|
||
}
|
||
|
||
const [projectSupervisorRuntime, setProjectSupervisorRuntime] =
|
||
useState<AgentRuntimeState | null>(null);
|
||
const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] =
|
||
useState<AgentRuntimeResponseStream | null>(null);
|
||
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
|
||
useState('');
|
||
const [designAgentTransientReply, setDesignAgentTransientReplyVisible] =
|
||
useState('');
|
||
const designAgentTransientReplyTargetRef = useRef('');
|
||
const designAgentVisibleReplyRef = useRef('');
|
||
const designAgentPendingViewRef = useRef<{
|
||
clientTurnId: string;
|
||
projectPath: string;
|
||
view: DesignView;
|
||
} | null>(null);
|
||
const [designAgentReasoning, setDesignAgentReasoning] = useState('');
|
||
const designAgentReasoningTurnRef = useRef<{
|
||
projectPath: string;
|
||
clientTurnId: string;
|
||
text: string;
|
||
} | null>(null);
|
||
|
||
function setDesignAgentTransientReplyTarget(next: string) {
|
||
designAgentTransientReplyTargetRef.current = next;
|
||
if (!next) {
|
||
designAgentVisibleReplyRef.current = '';
|
||
setDesignAgentTransientReplyVisible('');
|
||
}
|
||
}
|
||
|
||
function designAgentEventSubscriptionReady() {
|
||
// 业务动作只读取当前订阅代次;清理与下一次 effect 建立之间不能创建
|
||
// 一个没有订阅 effect 接管的悬挂 promise。
|
||
return designAgentEventSubscriptionReadyRef.current ?? Promise.resolve();
|
||
}
|
||
|
||
function createDesignAgentEventSubscriptionReady() {
|
||
if (!designAgentEventSubscriptionReadyRef.current) {
|
||
designAgentEventSubscriptionReadyRef.current = new Promise<void>(
|
||
(resolve) => {
|
||
designAgentEventSubscriptionResolveRef.current = resolve;
|
||
},
|
||
);
|
||
}
|
||
return designAgentEventSubscriptionReadyRef.current;
|
||
}
|
||
|
||
function resolveDesignAgentEventSubscriptionReady() {
|
||
designAgentEventSubscriptionResolveRef.current?.();
|
||
designAgentEventSubscriptionResolveRef.current = null;
|
||
}
|
||
|
||
useEffect(() => {
|
||
const timer = window.setInterval(() => {
|
||
const target = designAgentTransientReplyTargetRef.current;
|
||
setDesignAgentTransientReplyVisible((current) => {
|
||
if (!target) {
|
||
designAgentVisibleReplyRef.current = '';
|
||
return '';
|
||
}
|
||
const prefix = target.startsWith(current) ? current : '';
|
||
if (prefix === target) {
|
||
designAgentVisibleReplyRef.current = target;
|
||
return target;
|
||
}
|
||
const remaining = target.length - prefix.length;
|
||
const step = remaining > 160 ? 4 : remaining > 48 ? 2 : 1;
|
||
const next = target.slice(0, prefix.length + step);
|
||
designAgentVisibleReplyRef.current = next;
|
||
return next;
|
||
});
|
||
}, 50);
|
||
return () => window.clearInterval(timer);
|
||
}, []);
|
||
|
||
function designMessagesToChat(view: DesignView): ChatMessage[] {
|
||
const reasoningByMessageId = new Map<string, string[]>();
|
||
for (const entry of view.reasoningEntries ?? []) {
|
||
if (!entry.messageId) {
|
||
continue;
|
||
}
|
||
const texts = reasoningByMessageId.get(entry.messageId) ?? [];
|
||
texts.push(entry.text);
|
||
reasoningByMessageId.set(entry.messageId, texts);
|
||
}
|
||
const messages: ChatMessage[] = view.messages
|
||
.filter((message) => message.text.trim())
|
||
.map((message) => ({
|
||
role: message.role === 'user' ? 'user' : 'assistant',
|
||
text: message.text,
|
||
runtimeOwned: true,
|
||
messageId: message.id,
|
||
reasoningText: reasoningByMessageId.get(message.id)?.join('\n\n'),
|
||
updatedAt: Date.now(),
|
||
}));
|
||
const initialPrompt = initialSupervisorMessageLatchRef.current.prompt;
|
||
if (
|
||
initialPrompt &&
|
||
!messages.some(
|
||
(message) => message.role === 'user' && message.text === initialPrompt,
|
||
)
|
||
) {
|
||
messages.unshift({
|
||
role: 'user',
|
||
text: initialPrompt,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
});
|
||
}
|
||
return messages;
|
||
}
|
||
|
||
function applyDesignView(view: DesignView, projectPath: string) {
|
||
designAgentActiveRef.current = true;
|
||
setDesignAgentActive(true);
|
||
setDesignAgentView(view);
|
||
setProjectSupervisorRuntimeError(view.session.lastError ?? '');
|
||
setChatAgentBusy(view.running);
|
||
const conversation = designMessagesToChat(view);
|
||
setMessages(conversation);
|
||
savedConversationProjectPathRef.current = projectPath;
|
||
savedConversationCountRef.current = conversation.length;
|
||
latestMessagesRef.current = conversation;
|
||
}
|
||
|
||
function commitDesignAgentView(view: DesignView, projectPath: string) {
|
||
const pendingTurnId = designAgentPendingViewRef.current?.clientTurnId;
|
||
designAgentPendingViewRef.current = null;
|
||
applyDesignView(view, projectPath);
|
||
setDesignAgentReasoning('');
|
||
setDesignAgentTransientReplyTarget('');
|
||
if (designAgentTurnRef.current?.clientTurnId === pendingTurnId) {
|
||
designAgentTurnRef.current = null;
|
||
}
|
||
}
|
||
|
||
function applyDesignAgentViewAfterTransient(
|
||
view: DesignView,
|
||
projectPath: string,
|
||
clientTurnId: string,
|
||
) {
|
||
let target = designAgentTransientReplyTargetRef.current;
|
||
const tracked = designAgentTurnRef.current;
|
||
if (!target.trim() && !view.running) {
|
||
const latestAssistantText = [...view.messages]
|
||
.reverse()
|
||
.find((message) => message.role !== 'user' && message.text.trim())
|
||
?.text.trim();
|
||
if (latestAssistantText) {
|
||
setDesignAgentTransientReplyTarget(latestAssistantText);
|
||
target = latestAssistantText;
|
||
}
|
||
}
|
||
if (
|
||
!view.running &&
|
||
tracked?.clientTurnId === clientTurnId &&
|
||
target.trim() &&
|
||
designAgentVisibleReplyRef.current !== target
|
||
) {
|
||
designAgentPendingViewRef.current = {
|
||
clientTurnId,
|
||
projectPath,
|
||
view,
|
||
};
|
||
return;
|
||
}
|
||
commitDesignAgentView(view, projectPath);
|
||
}
|
||
|
||
useEffect(() => {
|
||
const timer = window.setInterval(() => {
|
||
const pending = designAgentPendingViewRef.current;
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
const target = designAgentTransientReplyTargetRef.current;
|
||
if (target && designAgentVisibleReplyRef.current !== target) {
|
||
return;
|
||
}
|
||
commitDesignAgentView(pending.view, pending.projectPath);
|
||
}, 50);
|
||
return () => window.clearInterval(timer);
|
||
// 收尾定时器只需注册一次;它读取 refs,避免随每次渲染重建。
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
async function hydrateDesignAgentSession(nextProjectPath: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath.trim()) {
|
||
return null;
|
||
}
|
||
const view = await invoke<DesignView | null>(
|
||
'hydrate_design_agent_session',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return null;
|
||
}
|
||
if (!view) {
|
||
return null;
|
||
}
|
||
applyDesignView(view, nextProjectPath);
|
||
return view;
|
||
}
|
||
|
||
async function executeDesignAgentTurn(
|
||
nextProjectPath: string,
|
||
input: DesignAgentInput,
|
||
clientTurnId = createAgentChatRunId('design-agent-turn'),
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectSupervisorRuntimeError('需要在 Tauri App 内运行。');
|
||
return;
|
||
}
|
||
designAgentTurnRef.current = {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
};
|
||
designAgentReasoningTurnRef.current = {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
text: '',
|
||
};
|
||
designAgentPendingViewRef.current = null;
|
||
await designAgentEventSubscriptionReady();
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
setDesignAgentTransientReplyTarget('');
|
||
setDesignAgentReasoning('');
|
||
try {
|
||
const view = await invoke<DesignView>('continue_design_agent_session', {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
input,
|
||
});
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
applyDesignAgentViewAfterTransient(view, nextProjectPath, clientTurnId);
|
||
} catch (error) {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
} finally {
|
||
if (!designAgentPendingViewRef.current) {
|
||
designAgentTurnRef.current = null;
|
||
setDesignAgentTransientReplyTarget('');
|
||
}
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
|
||
useState<string | null>(null);
|
||
const chatInputRef = useRef<HTMLDivElement | null>(null);
|
||
const supervisorChatMessagesRef = useRef<HTMLDivElement | null>(null);
|
||
const supervisorChatShouldFollowLatestRef = useRef(true);
|
||
const [assetStatus, setAssetStatus] = useState('未上传');
|
||
const [uploadedAssets, setUploadedAssets] = useState<
|
||
UploadLocalAssetResult[]
|
||
>([]);
|
||
const [assetLocalPath, setAssetLocalPath] = useState('assets/hero.png');
|
||
const [assetKind, setAssetKind] = useState('unknown');
|
||
const [assetMediaType, setAssetMediaType] = useState(
|
||
'application/octet-stream',
|
||
);
|
||
const [assetSourceKind, setAssetSourceKind] = useState('generated');
|
||
const [assetCanvasProjectId, setAssetCanvasProjectId] = useState('');
|
||
const [assetResourceId, setAssetResourceId] = useState('');
|
||
const [assetObjectId, setAssetObjectId] = useState('');
|
||
const [assetGenerationPrompt, setAssetGenerationPrompt] =
|
||
useState('首版核心美术素材');
|
||
const [canvasExportPath, setCanvasExportPath] = useState(
|
||
'/tmp/canvas-export.zip',
|
||
);
|
||
const [editorBaseUrl, setEditorBaseUrl] = useState('http://127.0.0.1:3000');
|
||
const [memoryScope, setMemoryScope] = useState<MemoryScope>('long');
|
||
const [memoryDraft, setMemoryDraft] = useState('');
|
||
const [memoryStatus, setMemoryStatus] = useState('未读取');
|
||
const [limitedCommandStatus, setLimitedCommandStatus] = useState('未运行');
|
||
const [limitedLocalCommands, setLimitedLocalCommands] = useState<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>([...GAME_CREATION_APP_LIMITED_RUN_COMMANDS]);
|
||
const [projectFiles, setProjectFiles] = useState<LocalProjectFileEntry[]>([]);
|
||
const [projectCheckpoints, setProjectCheckpoints] = useState<
|
||
LocalProjectCheckpointSummary[]
|
||
>([]);
|
||
const [filePath, setFilePath] = useState('game/index.html');
|
||
const [fileDraft, setFileDraft] = useState('');
|
||
const [fileStatus, setFileStatus] = useState('未读取');
|
||
const [agentRunTrace, setAgentRunTrace] =
|
||
useState<GameCreationAgentRunTrace | null>(null);
|
||
const [agentRunHistory, setAgentRunHistory] = useState<AgentRunHistoryItem[]>(
|
||
[],
|
||
);
|
||
const [agentRunHistoryFiles, setAgentRunHistoryFiles] = useState<
|
||
LocalProjectFileEntry[]
|
||
>([]);
|
||
const [agentRunHistoryVisibleCount, setAgentRunHistoryVisibleCount] =
|
||
useState(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
const [agentRunHistoryOverflowCount, setAgentRunHistoryOverflowCount] =
|
||
useState(0);
|
||
const [agentRunHistoryLoadingMore, setAgentRunHistoryLoadingMore] =
|
||
useState(false);
|
||
const [agentRunStatus, setAgentRunStatus] = useState('未运行');
|
||
const [agentRuntimeById, setAgentRuntimeById] = useState<
|
||
Record<string, AgentRuntimeState | undefined>
|
||
>({});
|
||
const agentRuntimeByIdRef = useRef(agentRuntimeById);
|
||
agentRuntimeByIdRef.current = agentRuntimeById;
|
||
const [professionalAgentResultsById, setProfessionalAgentResultsById] =
|
||
useState<Record<string, ProjectAgentResultSummary>>({});
|
||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||
const [llmConfigStatus, setLlmConfigStatus] =
|
||
useState<GameCreatorLlmConfigStatus | null>(null);
|
||
const [workspaceStatus, setWorkspaceStatus] = useState(
|
||
eagerSupervisorProject ? `已打开:${initialProjectPath}` : '请选择工作区',
|
||
);
|
||
const [selectedAgent, setSelectedAgent] = useState<AgentStatusCard | null>(
|
||
null,
|
||
);
|
||
const selectedAgentIdRef = useRef<string | null>(null);
|
||
selectedAgentIdRef.current = selectedAgent?.id ?? null;
|
||
const [agentConversationInput, setAgentConversationInput] = useState('');
|
||
const [agentConversationStatus, setAgentConversationStatus] =
|
||
useState('未选择 agent');
|
||
const [agentConversationMessages, setAgentConversationMessages] = useState<
|
||
LocalConversationMessageRecord[]
|
||
>([]);
|
||
const [agentConversationRuntime, setAgentConversationRuntime] =
|
||
useState<AgentRuntimeState | null>(null);
|
||
const [agentConversationSessionId, setAgentConversationSessionId] = useState<
|
||
string | null
|
||
>(null);
|
||
const [agentConversationRunSubmitMode, setAgentConversationRunSubmitMode] =
|
||
useState<AgentBackgroundSubmitMode>('steer');
|
||
const [agentConversationRuntimeError, setAgentConversationRuntimeError] =
|
||
useState('');
|
||
const [agentConversationVisibleCount, setAgentConversationVisibleCount] =
|
||
useState(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
const [agentConversationSaving, setAgentConversationSaving] = useState(false);
|
||
const [agentConversationBackgroundBusy, setAgentConversationBackgroundBusy] =
|
||
useState(false);
|
||
const [agentMemoryStatus, setAgentMemoryStatus] = useState('未读取');
|
||
const [agentMemoryContent, setAgentMemoryContent] = useState('');
|
||
const [messages, setMessages] = useState<ChatMessage[]>(
|
||
createDefaultChatMessages,
|
||
);
|
||
const [conversationVisibleCount, setConversationVisibleCount] = useState(
|
||
CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
);
|
||
const [directHistoryHasMore, setDirectHistoryHasMore] = useState(false);
|
||
const directHistoryOldestItemIdRef = useRef<string | null>(null);
|
||
const directHistoryLoadingRef = useRef(false);
|
||
const directHistoryAnchorGateRef = useRef<DirectHistoryAnchorGate | null>(
|
||
null,
|
||
);
|
||
const [pendingCommand, setPendingCommand] = useState<PendingCommand | null>(
|
||
null,
|
||
);
|
||
const [pendingUiConfirmation, setPendingUiConfirmation] =
|
||
useState<PendingUiConfirmation | null>(null);
|
||
const [pendingNonEmptyProjectCreate, setPendingNonEmptyProjectCreate] =
|
||
useState<{
|
||
projectPath: string;
|
||
announceToChat: boolean;
|
||
} | null>(null);
|
||
const [commandLog, setCommandLog] = useState<string[]>([
|
||
`${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`,
|
||
]);
|
||
const [projectLogStatus, setProjectLogStatus] = useState('未读取');
|
||
const [projectLogContent, setProjectLogContent] = useState('');
|
||
const [conversationWriteVersion, setConversationWriteVersion] = useState(0);
|
||
const savedConversationCountRef = useRef(
|
||
eagerSupervisorProject ? createDefaultChatMessages().length : 0,
|
||
);
|
||
const savedConversationProjectPathRef = useRef<string | null>(
|
||
eagerSupervisorProject ? initialProjectPath : null,
|
||
);
|
||
const projectConversationWriteConfirmedRef = useRef<string | null>(null);
|
||
const projectConversationWriteCancelledRef = useRef<{
|
||
projectPath: string;
|
||
messageCount: number;
|
||
} | null>(null);
|
||
const latestMessagesRef = useRef<ChatMessage[]>([]);
|
||
const conversationWriteInFlightRef = useRef(false);
|
||
const projectScopeVersionRef = useRef(0);
|
||
const projectSupervisorHistoryLoadVersionRef = useRef(0);
|
||
const projectSupervisorRuntimeResumeProjectPathRef = useRef<string | null>(
|
||
null,
|
||
);
|
||
const projectSupervisorSessionIdRef = useRef<string | null>(null);
|
||
projectSupervisorSessionIdRef.current = projectSupervisorSessionId;
|
||
const projectSupervisorRuntimeRef = useRef<AgentRuntimeState | null>(null);
|
||
projectSupervisorRuntimeRef.current = projectSupervisorRuntime;
|
||
const projectSupervisorExpectedRunIdRef = useRef<string | null>(null);
|
||
projectSupervisorExpectedRunIdRef.current = projectSupervisorExpectedRunId;
|
||
const projectSupervisorResponseStreamRef =
|
||
useRef<AgentRuntimeResponseStream | null>(null);
|
||
projectSupervisorResponseStreamRef.current = projectSupervisorResponseStream;
|
||
|
||
const refreshManifest = useCallback(
|
||
(nextProjectPath = localProjectPathRef.current ?? ''): Promise<void> => {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
const refreshStates = manifestRefreshStatesRef.current;
|
||
let refreshState = refreshStates.get(nextProjectPath);
|
||
if (!refreshState) {
|
||
refreshState = { pending: false, inFlight: null };
|
||
refreshStates.set(nextProjectPath, refreshState);
|
||
}
|
||
refreshState.pending = true;
|
||
if (refreshState.inFlight) {
|
||
return refreshState.inFlight;
|
||
}
|
||
|
||
const activeRefreshState = refreshState;
|
||
const refreshPromise = (async () => {
|
||
try {
|
||
while (activeRefreshState.pending) {
|
||
activeRefreshState.pending = false;
|
||
const projectScopeVersion = projectScopeVersionRef.current;
|
||
try {
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
manifestRefreshMountedRef.current &&
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectScopeVersionRef.current === projectScopeVersion
|
||
) {
|
||
setManifest(nextManifest);
|
||
}
|
||
} catch {
|
||
// Dev-only convenience; command errors are surfaced by the action that triggered them.
|
||
}
|
||
if (
|
||
!manifestRefreshMountedRef.current ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectScopeVersionRef.current !== projectScopeVersion
|
||
) {
|
||
activeRefreshState.pending = false;
|
||
}
|
||
}
|
||
} finally {
|
||
activeRefreshState.inFlight = null;
|
||
if (!activeRefreshState.pending) {
|
||
refreshStates.delete(nextProjectPath);
|
||
}
|
||
}
|
||
})();
|
||
activeRefreshState.inFlight = refreshPromise;
|
||
return refreshPromise;
|
||
},
|
||
[],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const refreshStates = manifestRefreshStatesRef.current;
|
||
manifestRefreshMountedRef.current = true;
|
||
return () => {
|
||
manifestRefreshMountedRef.current = false;
|
||
refreshStates.clear();
|
||
};
|
||
}, []);
|
||
const projectSupervisorRuntimeSyncingRef = useRef(new Set<string>());
|
||
const projectSupervisorRefreshConversationRef = useRef<
|
||
| ((
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
sessionId: string,
|
||
) => Promise<void>)
|
||
| null
|
||
>(null);
|
||
const executeChatAgentReplyRef = useRef<
|
||
(input: ExecuteChatAgentReplyInput) => Promise<void>
|
||
>(async () => undefined);
|
||
const agentConversationSavingRef = useRef(false);
|
||
const agentConversationBackgroundBusyRef = useRef(false);
|
||
const agentConversationLoadVersionRef = useRef(0);
|
||
const agentConversationSessionIdRef = useRef<string | null>(null);
|
||
agentConversationSessionIdRef.current = agentConversationSessionId;
|
||
const agentRuntimeResumeProjectPathRef = useRef<string | null>(null);
|
||
const agentRunHistoryLoadingMoreRef = useRef(false);
|
||
const initialProjectOpenedRef = useRef(false);
|
||
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
|
||
const queueRunLocalShortcutRef = useRef<() => void>(() => undefined);
|
||
const executeRunLocalRef = useRef<(announceToChat: boolean) => void>(
|
||
() => undefined,
|
||
);
|
||
|
||
function requestRuntimeConfigOpen() {
|
||
if (projectSupervisorOnly && !supervisorChatOnly) {
|
||
return;
|
||
}
|
||
setRuntimeConfigOpen(true);
|
||
}
|
||
|
||
const updateProjectSupervisorRuntime = useCallback(
|
||
(
|
||
runtime: AgentRuntimeState | null,
|
||
previous = projectSupervisorRuntimeRef.current,
|
||
) => {
|
||
const nextRuntime = runtime
|
||
? normalizeAgentRuntimeState(runtime, previous)
|
||
: null;
|
||
projectSupervisorRuntimeRef.current = nextRuntime;
|
||
setProjectSupervisorRuntime(nextRuntime);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const updateProjectSupervisorResponseStream = useCallback(
|
||
(
|
||
incoming: AgentRuntimeResponseStream | null | undefined,
|
||
runtime: AgentRuntimeState,
|
||
) => {
|
||
let nextStream = mergeProjectSupervisorResponseStream(
|
||
projectSupervisorResponseStreamRef.current,
|
||
incoming,
|
||
runtime,
|
||
);
|
||
const candidateStream = nextStream;
|
||
if (
|
||
candidateStream &&
|
||
latestMessagesRef.current.some(
|
||
(message) =>
|
||
message.runtimeOwned &&
|
||
message.role === 'assistant' &&
|
||
message.text === candidateStream.accumulatedText &&
|
||
typeof message.updatedAt === 'number' &&
|
||
message.updatedAt >= candidateStream.startedAt,
|
||
)
|
||
) {
|
||
// Keep the existing supervisor-chat behavior for restored history.
|
||
nextStream = null;
|
||
}
|
||
projectSupervisorResponseStreamRef.current = nextStream;
|
||
setProjectSupervisorResponseStream(nextStream);
|
||
},
|
||
[],
|
||
);
|
||
|
||
function resetProjectSupervisorState() {
|
||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = null;
|
||
projectSupervisorSessionIdRef.current = null;
|
||
projectSupervisorRuntimeRef.current = null;
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
projectSupervisorRuntimeSyncingRef.current.clear();
|
||
setProjectSupervisorSessionId(null);
|
||
setProjectSupervisorRuntime(null);
|
||
setProjectSupervisorExpectedRunId(null);
|
||
setProjectSupervisorResponseStream(null);
|
||
setProjectSupervisorRuntimeError('');
|
||
designAgentPendingViewRef.current = null;
|
||
designAgentReasoningTurnRef.current = null;
|
||
setDesignAgentReasoning('');
|
||
setDesignAgentActive(planningStartMode);
|
||
designAgentActiveRef.current = planningStartMode;
|
||
designAgentTurnRef.current = null;
|
||
setDesignAgentView(null);
|
||
}
|
||
|
||
function syncTerminalProjectSupervisorConversation(
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
if (!isAgentRuntimeTerminalState(runtime) || !runtime.sessionId) {
|
||
return;
|
||
}
|
||
const syncKey = `${projectPath}\n${runtime.sessionId}\n${runtime.runId}`;
|
||
const syncAlreadyStarted =
|
||
projectSupervisorRuntimeSyncingRef.current.has(syncKey);
|
||
if (syncAlreadyStarted) {
|
||
return;
|
||
}
|
||
const refreshConversation = projectSupervisorRefreshConversationRef.current;
|
||
if (!refreshConversation) {
|
||
return;
|
||
}
|
||
projectSupervisorRuntimeSyncingRef.current.add(syncKey);
|
||
void refreshConversation(invoke, projectPath, runtime.sessionId)
|
||
.then(() => {})
|
||
.catch((error) => {
|
||
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
|
||
if (
|
||
localProjectPathRef.current === projectPath &&
|
||
projectSupervisorSessionIdRef.current === runtime.sessionId
|
||
) {
|
||
setProjectSupervisorRuntimeError(
|
||
`项目总控 Agent 对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
useEscapeToClose(closeAgentConversation, selectedAgent !== null);
|
||
useEscapeToClose(cancelUiCommandConfirmation, pendingUiConfirmation !== null);
|
||
useEscapeToClose(
|
||
cancelProjectCreateInNonEmptyFolder,
|
||
pendingNonEmptyProjectCreate !== null,
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
pendingUiConfirmation?.projectPath &&
|
||
pendingUiConfirmation.projectPath !==
|
||
resolveCurrentUiConfirmationProjectPath()
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
}
|
||
// The callbacks intentionally read the latest project refs.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [localProject?.projectPath, projectPath]);
|
||
|
||
useEffect(() => {
|
||
if (!initialProjectPath || initialProjectOpenedRef.current) {
|
||
return;
|
||
}
|
||
initialProjectOpenedRef.current = true;
|
||
if (projectSupervisorOnly) {
|
||
if (directCodexProductRuntime) {
|
||
if (!isAbsoluteProjectPath(initialProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(initialProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
// This component is mounted inside the already-created project
|
||
// workbench. Hydrate localProject before the first direct turn so the
|
||
// direct runtime cannot silently create a second workspace.
|
||
void openWorkspace(
|
||
initialProjectPath,
|
||
false,
|
||
'open',
|
||
initialProjectKind,
|
||
);
|
||
return;
|
||
}
|
||
if (!isAbsoluteProjectPath(initialProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(initialProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
localProjectPathRef.current = initialProjectPath;
|
||
void loadProjectConversation(initialProjectPath).finally(() => {
|
||
if (
|
||
localProjectPathRef.current === initialProjectPath &&
|
||
!planningStartMode
|
||
) {
|
||
void refreshAgentRunTrace(initialProjectPath);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
void openWorkspace(
|
||
initialProjectPath,
|
||
false,
|
||
initialProjectKind !== 'web' ? 'open' : 'create',
|
||
initialProjectKind,
|
||
);
|
||
// Initial project opening is guarded by initialProjectOpenedRef.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [initialProjectPath, projectSupervisorOnly]);
|
||
|
||
useEffect(() => {
|
||
if (!directCodexProductRuntime || !supervisorChatBusy) {
|
||
return;
|
||
}
|
||
const timer = window.setInterval(() => {
|
||
void requestPlatformSessionRefresh().catch(() => {
|
||
// The active DirectProject turn will surface the original auth error;
|
||
// keepalive must not replace it with an unrelated background error.
|
||
});
|
||
}, DIRECT_CODEX_SESSION_KEEPALIVE_MS);
|
||
return () => window.clearInterval(timer);
|
||
}, [supervisorChatBusy, directCodexProductRuntime]);
|
||
|
||
/**
|
||
* DirectProject 聊天真相源。
|
||
*
|
||
* `subscribe` 的 bootstrap 就是此刻要处理的事件(游标已在队尾),此后只由
|
||
* `game-creator-direct-thread-notify` 唤醒 `consume`;前端不再订阅 DirectRuntime 的回合
|
||
* 进度事件,也不轮询"有没有回合在跑"。
|
||
*/
|
||
useEffect(() => {
|
||
if (!directCodexProductRuntime) {
|
||
return;
|
||
}
|
||
const projectPath = localProject?.projectPath ?? null;
|
||
const directInvoke = resolveTauriInvoke();
|
||
// 闸门先建(或复用首屏读取已经开好的那道):同一轮渲染里首屏会等订阅回执里的锚点。
|
||
const anchorGate = reuseOrOpenDirectHistoryAnchorGate(
|
||
directHistoryAnchorGateRef.current,
|
||
projectPath ?? '',
|
||
);
|
||
directHistoryAnchorGateRef.current = anchorGate;
|
||
if (!projectPath || !directInvoke || !canSubscribeTauriEvents()) {
|
||
// 订阅不可用:首屏退化成"取文件尾",不阻塞加载。
|
||
anchorGate.settle(null);
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let cleanup: (() => void) | null = null;
|
||
let subscriptionId: string | null = null;
|
||
let consuming = false;
|
||
let consumeAgain = false;
|
||
// 通知先于 subscribe 回执到达时,前端还不知道自己的 subscriptionId,没法立刻 consume。
|
||
// 记一笔欠账,拿到回执后立刻补一次,事件就不会卡在队伍里等下一次通知。
|
||
let notifyBeforeSubscription = false;
|
||
|
||
const bootstrap = async () => {
|
||
const result = await directInvoke<DirectThreadSubscriptionBootstrap>(
|
||
'subscribe_direct_project_thread',
|
||
{ projectPath },
|
||
);
|
||
if (disposed) {
|
||
anchorGate.settle(null);
|
||
return;
|
||
}
|
||
subscriptionId = result.subscriptionId;
|
||
// 首屏边界:回执里这一刻的最后一条已完成条目(含该条)。
|
||
anchorGate.settle(result.lastCompletedItemId ?? null);
|
||
setDirectThreadChat((state) =>
|
||
resolveDirectThreadBootstrap(state, result),
|
||
);
|
||
if (notifyBeforeSubscription) {
|
||
notifyBeforeSubscription = false;
|
||
void consume();
|
||
}
|
||
};
|
||
const consume = async () => {
|
||
if (!subscriptionId || disposed) return;
|
||
if (consuming) {
|
||
consumeAgain = true;
|
||
return;
|
||
}
|
||
consuming = true;
|
||
try {
|
||
do {
|
||
consumeAgain = false;
|
||
const result = await directInvoke<DirectThreadConsumeResult>(
|
||
'consume_direct_project_thread',
|
||
{ subscriptionId },
|
||
);
|
||
if (disposed) return;
|
||
setDirectThreadChat((state) =>
|
||
applyDirectThreadConsumeResult(state, result),
|
||
);
|
||
} while (consumeAgain && !disposed);
|
||
} catch (error) {
|
||
// 订阅被别人顶掉时重订一次;通知允许丢,下一次通知会再唤醒。
|
||
if (!disposed && String(error).includes('SUBSCRIPTION_EXPIRED')) {
|
||
subscriptionId = null;
|
||
try {
|
||
await bootstrap();
|
||
} catch {
|
||
/* 让下一次通知再试。 */
|
||
}
|
||
}
|
||
} finally {
|
||
consuming = false;
|
||
}
|
||
};
|
||
const setup = async () => {
|
||
try {
|
||
const unlisten = await subscribeTauriEvent<{ subscriptionId: string }>(
|
||
'game-creator-direct-thread-notify',
|
||
(event) => {
|
||
if (event.payload.subscriptionId === subscriptionId) {
|
||
void consume();
|
||
return;
|
||
}
|
||
if (!subscriptionId) notifyBeforeSubscription = true;
|
||
},
|
||
);
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
await bootstrap();
|
||
} catch {
|
||
// 历史仍可使用;订阅失败不伪造忙碌态,锚点缺失时首屏按文件尾取尾屏。
|
||
anchorGate.settle(null);
|
||
}
|
||
};
|
||
// 换项目就是换一份聊天:先回到初始态,再订阅新线程。
|
||
setDirectThreadChat(emptyDirectThreadChatState());
|
||
void setup();
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
// 首屏可能还在等这道闸门(例如切项目打断了订阅),不能让它永远等下去。
|
||
anchorGate.settle(null);
|
||
};
|
||
}, [directCodexProductRuntime, localProject?.projectPath]);
|
||
|
||
useEffect(() => {
|
||
if (projectSupervisorOnly && !directCodexProductRuntime) {
|
||
return;
|
||
}
|
||
if (!canSubscribeTauriEvents()) {
|
||
return;
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void subscribeTauriEvent<AgentProgressEvent>(
|
||
'game-creator-agent-progress',
|
||
(event) => {
|
||
if (event.payload.projectPath !== localProjectPathRef.current) {
|
||
return;
|
||
}
|
||
if (directCodexProductRuntime) {
|
||
// DirectProject 的进度已经从线程事件流进聊天条目,这条通用进度事件不再参与。
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: event.payload.message },
|
||
]);
|
||
},
|
||
)
|
||
.then((unlisten) => {
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch((error) => {
|
||
if (disposed) {
|
||
return;
|
||
}
|
||
setAgentRunStatus(
|
||
`实时状态不可用:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
};
|
||
}, [directCodexProductRuntime, projectSupervisorOnly]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!canSubscribeTauriEvents() ||
|
||
directCodexProductRuntime ||
|
||
designAgentActive
|
||
) {
|
||
return;
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void subscribeTauriEvent<GameCreatorAgentRuntimeUpdateEvent>(
|
||
'game-creator-agent-runtime-update',
|
||
(event) => {
|
||
const payload = event.payload;
|
||
if (payload.projectPath !== localProjectPathRef.current) {
|
||
return;
|
||
}
|
||
if (payload.manifestInvalidated) {
|
||
void refreshManifest(payload.projectPath);
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(payload.runtime);
|
||
if (payload.agentId === PROJECT_SUPERVISOR_AGENT_ID) {
|
||
const expectedSessionId = projectSupervisorSessionIdRef.current;
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
const expectedRunId = projectSupervisorExpectedRunIdRef.current;
|
||
const sameRun =
|
||
currentRuntime !== null &&
|
||
sameAgentRuntimeRun(nextRuntime, currentRuntime);
|
||
const isExpectedRun =
|
||
expectedRunId !== null && nextRuntime.runId === expectedRunId;
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.runId !== payload.runId ||
|
||
(expectedSessionId !== null &&
|
||
nextRuntime.sessionId !== expectedSessionId) ||
|
||
(currentRuntime !== null &&
|
||
((!sameRun && !isExpectedRun) ||
|
||
(sameRun && nextRuntime.updatedAt < currentRuntime.updatedAt)))
|
||
) {
|
||
return;
|
||
}
|
||
if (expectedSessionId === null && nextRuntime.sessionId) {
|
||
projectSupervisorSessionIdRef.current = nextRuntime.sessionId;
|
||
setProjectSupervisorSessionId(nextRuntime.sessionId);
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
if (isExpectedRun) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
updateProjectSupervisorResponseStream(
|
||
payload.runtime.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
if (invoke) {
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
payload.projectPath,
|
||
nextRuntime,
|
||
);
|
||
}
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
if (payload.agentId !== selectedAgentIdRef.current) {
|
||
return;
|
||
}
|
||
if (
|
||
agentConversationSessionIdRef.current !== null &&
|
||
payload.runtime.state.sessionId !==
|
||
agentConversationSessionIdRef.current
|
||
) {
|
||
return;
|
||
}
|
||
setAgentConversationRuntime((current) =>
|
||
normalizeAgentRuntimeState(nextRuntime, current),
|
||
);
|
||
setAgentConversationRuntimeError('');
|
||
},
|
||
)
|
||
.then((unlisten) => {
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch((error) => {
|
||
if (disposed) {
|
||
return;
|
||
}
|
||
setAgentConversationRuntimeError(
|
||
`Runtime 订阅不可用:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
};
|
||
// The subscription deliberately invokes the current render's helper; its
|
||
// mutable state is read through refs, so resubscribing for that helper is
|
||
// neither necessary nor safe.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
directCodexProductRuntime,
|
||
designAgentActive,
|
||
refreshManifest,
|
||
updateProjectSupervisorResponseStream,
|
||
updateProjectSupervisorRuntime,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
const ready = createDesignAgentEventSubscriptionReady();
|
||
if (!canSubscribeTauriEvents() || !designAgentActive) {
|
||
resolveDesignAgentEventSubscriptionReady();
|
||
return () => {
|
||
if (designAgentEventSubscriptionReadyRef.current === ready) {
|
||
designAgentEventSubscriptionReadyRef.current = null;
|
||
createDesignAgentEventSubscriptionReady();
|
||
}
|
||
};
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void subscribeTauriEvent<DesignEvent>('design-agent-update', (event) => {
|
||
const payload = event.payload;
|
||
const tracked = designAgentTurnRef.current;
|
||
if (
|
||
payload.projectPath !== localProjectPathRef.current ||
|
||
(tracked && payload.clientTurnId !== tracked.clientTurnId)
|
||
) {
|
||
return;
|
||
}
|
||
if (payload.kind === 'text' && payload.text != null) {
|
||
setDesignAgentTransientReplyTarget(payload.text);
|
||
}
|
||
if (payload.reasoningText != null) {
|
||
const reasoningTurn = designAgentReasoningTurnRef.current;
|
||
if (
|
||
reasoningTurn &&
|
||
reasoningTurn.projectPath === payload.projectPath &&
|
||
reasoningTurn.clientTurnId === payload.clientTurnId
|
||
) {
|
||
reasoningTurn.text = payload.reasoningText;
|
||
setDesignAgentReasoning(payload.reasoningText);
|
||
}
|
||
}
|
||
if (payload.kind === 'tool' && payload.text) {
|
||
setDesignAgentTransientReplyTarget(payload.text);
|
||
}
|
||
if (payload.view) {
|
||
applyDesignAgentViewAfterTransient(
|
||
payload.view,
|
||
payload.projectPath,
|
||
payload.clientTurnId,
|
||
);
|
||
}
|
||
})
|
||
.then((unlisten) => {
|
||
resolveDesignAgentEventSubscriptionReady();
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch(() => {
|
||
resolveDesignAgentEventSubscriptionReady();
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
resolveDesignAgentEventSubscriptionReady();
|
||
if (designAgentEventSubscriptionReadyRef.current === ready) {
|
||
designAgentEventSubscriptionReadyRef.current = null;
|
||
createDesignAgentEventSubscriptionReady();
|
||
}
|
||
};
|
||
// applyDesignView 读的是 refs 和当前项目路径,
|
||
// 把它写进依赖会在每轮回复时重订事件。
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [designAgentActive]);
|
||
|
||
useEffect(() => {
|
||
if (!canSubscribeTauriEvents()) {
|
||
return;
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void subscribeTauriEvent<GameCreatorManifestInvalidatedEvent>(
|
||
'game-creator-manifest-invalidated',
|
||
(event) => {
|
||
const activeProjectPath = localProjectPathRef.current;
|
||
if (
|
||
!activeProjectPath ||
|
||
!projectPathsMatchForInvalidation(
|
||
event.payload.projectPath,
|
||
activeProjectPath,
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
void refreshManifest(activeProjectPath);
|
||
},
|
||
)
|
||
.then((unlisten) => {
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch(() => {
|
||
// In-process Runtime events continue to carry the same invalidation signal.
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
};
|
||
}, [refreshManifest]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
const sessionId = projectSupervisorSessionId;
|
||
const trackedRunId =
|
||
projectSupervisorExpectedRunId ?? projectSupervisorRuntime?.runId ?? null;
|
||
if (
|
||
!invoke ||
|
||
directCodexProductRuntime ||
|
||
designAgentActive ||
|
||
!nextProjectPath ||
|
||
!sessionId ||
|
||
!trackedRunId ||
|
||
(!projectSupervisorRuntime && !projectSupervisorExpectedRunId) ||
|
||
(projectSupervisorRuntime !== null &&
|
||
isAgentRuntimeTerminalState(projectSupervisorRuntime) &&
|
||
!projectSupervisorExpectedRunId)
|
||
) {
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let inFlight = false;
|
||
const pollRuntime = async () => {
|
||
if (disposed || inFlight) {
|
||
return;
|
||
}
|
||
inFlight = true;
|
||
const runtimeBeforePoll = projectSupervisorRuntimeRef.current;
|
||
const responseStreamBeforePoll =
|
||
projectSupervisorResponseStreamRef.current;
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
if (
|
||
disposed ||
|
||
currentRuntime !== runtimeBeforePoll ||
|
||
projectSupervisorResponseStreamRef.current !==
|
||
responseStreamBeforePoll ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== trackedRunId ||
|
||
(currentRuntime !== null &&
|
||
sameAgentRuntimeRun(nextRuntime, currentRuntime) &&
|
||
nextRuntime.updatedAt < currentRuntime.updatedAt)
|
||
) {
|
||
return;
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
if (nextRuntime.runId === projectSupervisorExpectedRunIdRef.current) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
!disposed &&
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectSupervisorSessionIdRef.current === sessionId
|
||
) {
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
}
|
||
} finally {
|
||
inFlight = false;
|
||
}
|
||
};
|
||
void pollRuntime();
|
||
const timer = window.setInterval(() => {
|
||
void pollRuntime();
|
||
}, 750);
|
||
return () => {
|
||
disposed = true;
|
||
window.clearInterval(timer);
|
||
};
|
||
// Polling restarts only when the tracked runtime identity or phase changes.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
directCodexProductRuntime,
|
||
designAgentActive,
|
||
projectSupervisorSessionId,
|
||
projectSupervisorRuntime?.phase,
|
||
projectSupervisorRuntime?.runId,
|
||
projectSupervisorRuntime?.status,
|
||
projectSupervisorExpectedRunId,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
const supervisorRunId = projectSupervisorRuntime?.runId ?? null;
|
||
if (
|
||
!projectSupervisorOnly ||
|
||
directCodexProductRuntime ||
|
||
designAgentActive ||
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let inFlight = false;
|
||
const pollProfessionalRuntimes = async () => {
|
||
if (disposed || inFlight) {
|
||
return;
|
||
}
|
||
inFlight = true;
|
||
try {
|
||
const runtimes = await invoke<AgentRuntimeResult[]>(
|
||
'read_game_creator_agent_runtimes',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
disposed ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
const nextRuntimes = runtimes.map((runtimeResult) =>
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
const authoritativeProfessionalAgentIds = new Set(
|
||
nextRuntimes
|
||
.filter(
|
||
(runtime) =>
|
||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||
runtime.source,
|
||
) &&
|
||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||
runtime.parentRunId === supervisorRunId,
|
||
)
|
||
.map((runtime) => runtime.agentId),
|
||
);
|
||
setAgentRuntimeById((current) => {
|
||
const reconciled = { ...current };
|
||
for (const [key, runtime] of Object.entries(reconciled)) {
|
||
if (
|
||
runtime &&
|
||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||
runtime.source,
|
||
) &&
|
||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||
runtime.parentRunId === supervisorRunId &&
|
||
!authoritativeProfessionalAgentIds.has(runtime.agentId)
|
||
) {
|
||
delete reconciled[key];
|
||
}
|
||
}
|
||
return nextRuntimes.reduce(
|
||
(next, runtime) =>
|
||
mergeAgentRuntimeStateIntoMap(next, runtime, true),
|
||
reconciled,
|
||
);
|
||
});
|
||
} catch {
|
||
// Preserve the last truthful professional Runtime snapshot on transient read failures.
|
||
} finally {
|
||
inFlight = false;
|
||
}
|
||
};
|
||
void pollProfessionalRuntimes();
|
||
const timer = window.setInterval(() => {
|
||
void pollProfessionalRuntimes();
|
||
}, 1000);
|
||
return () => {
|
||
disposed = true;
|
||
window.clearInterval(timer);
|
||
};
|
||
// Track the stable projection fields; the Runtime object identity changes on every merge.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
directCodexProductRuntime,
|
||
designAgentActive,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime?.phase,
|
||
projectSupervisorRuntime?.runId,
|
||
projectSupervisorRuntime?.status,
|
||
]);
|
||
|
||
useLayoutEffect(() => {
|
||
if (
|
||
(!supervisorChatOnly &&
|
||
!(
|
||
projectSupervisorOnly &&
|
||
(directCodexProductRuntime || designAgentActive)
|
||
)) ||
|
||
!supervisorChatShouldFollowLatestRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const messageList = supervisorChatMessagesRef.current;
|
||
if (messageList) {
|
||
messageList.scrollTop = messageList.scrollHeight;
|
||
}
|
||
}, [
|
||
messages,
|
||
projectSupervisorResponseStream?.sequence,
|
||
projectSupervisorRuntime?.updatedAt,
|
||
projectSupervisorRuntimeError,
|
||
directThreadChat,
|
||
directCodexProductRuntime,
|
||
designAgentActive,
|
||
projectSupervisorOnly,
|
||
supervisorChatOnly,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
if (!supervisorChatOnly) {
|
||
return;
|
||
}
|
||
persistSupervisorChatDraft(initialProjectPath, chatInput);
|
||
}, [chatInput, initialProjectPath, supervisorChatOnly]);
|
||
|
||
useEffect(() => {
|
||
latestMessagesRef.current = messages;
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath;
|
||
if (!invoke || !nextProjectPath) {
|
||
return;
|
||
}
|
||
if (savedConversationProjectPathRef.current !== nextProjectPath) {
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = 0;
|
||
projectConversationWriteConfirmedRef.current = null;
|
||
projectConversationWriteCancelledRef.current = null;
|
||
}
|
||
// DirectProject history is written by Rust from raw app-server items.
|
||
// The browser only renders that projection and must not append chat rows.
|
||
if (projectSupervisorOnly && directCodexProductRuntime) {
|
||
// `messages` is only an optimistic UI projection in Direct mode; it is
|
||
// intentionally not proof of durability. Rust owns the raw response
|
||
// history, so this effect must not route these rows through the generic
|
||
// browser conversation writer.
|
||
savedConversationCountRef.current = messages.length;
|
||
return;
|
||
}
|
||
const start = savedConversationCountRef.current;
|
||
const pendingMessages = messages.slice(start);
|
||
if (pendingMessages.length === 0) {
|
||
return;
|
||
}
|
||
if (
|
||
projectSupervisorOnly &&
|
||
pendingMessages.every(
|
||
(message) =>
|
||
(message.runtimeOwned &&
|
||
!isPersistableDirectCodexConversationMessage(message)) ||
|
||
isTransientProjectOpenMessage(message, nextProjectPath),
|
||
)
|
||
) {
|
||
savedConversationCountRef.current = messages.length;
|
||
return;
|
||
}
|
||
if (conversationWriteInFlightRef.current) {
|
||
return;
|
||
}
|
||
if (pendingUiConfirmation) {
|
||
return;
|
||
}
|
||
if (
|
||
projectConversationWriteCancelledRef.current?.projectPath ===
|
||
nextProjectPath &&
|
||
projectConversationWriteCancelledRef.current.messageCount ===
|
||
messages.length
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
projectConversationWriteCancelledRef.current?.projectPath ===
|
||
nextProjectPath &&
|
||
projectConversationWriteCancelledRef.current.messageCount <
|
||
messages.length
|
||
) {
|
||
projectConversationWriteCancelledRef.current = null;
|
||
}
|
||
if (projectConversationWriteConfirmedRef.current !== nextProjectPath) {
|
||
void invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
)
|
||
.then((policyView) => {
|
||
if (
|
||
!policyView.policy.confirmCommands.includes('conversation.write')
|
||
) {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
return;
|
||
}
|
||
requestProjectPolicyConfirmation(
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
`写入 ${nextProjectPath} 的项目对话`,
|
||
() => {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
},
|
||
);
|
||
setWorkspaceStatus('等待确认保存项目对话');
|
||
})
|
||
.catch(() => {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
});
|
||
return;
|
||
}
|
||
conversationWriteInFlightRef.current = true;
|
||
void (async () => {
|
||
let wroteMessage = false;
|
||
for (const [index, message] of pendingMessages.entries()) {
|
||
if (
|
||
message.runtimeOwned &&
|
||
!isPersistableDirectCodexConversationMessage(message)
|
||
) {
|
||
savedConversationCountRef.current = start + index + 1;
|
||
continue;
|
||
}
|
||
if (isTransientProjectOpenMessage(message, nextProjectPath)) {
|
||
savedConversationCountRef.current = start + index + 1;
|
||
continue;
|
||
}
|
||
await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
...(message.messageId ? { messageId: message.messageId } : {}),
|
||
message: {
|
||
role: message.role === 'user' ? 'user' : 'assistant',
|
||
content: message.text,
|
||
agentId: null,
|
||
...(typeof message.updatedAt === 'number'
|
||
? { updatedAt: message.updatedAt }
|
||
: {}),
|
||
},
|
||
},
|
||
);
|
||
wroteMessage = true;
|
||
savedConversationCountRef.current = start + index + 1;
|
||
}
|
||
setWorkspaceStatus((current) =>
|
||
current.startsWith('项目对话保存失败') ||
|
||
current === '等待确认保存项目对话'
|
||
? `已打开:${nextProjectPath}`
|
||
: current,
|
||
);
|
||
if (wroteMessage) {
|
||
setCommandLog((current) => [...current, 'conversation.write']);
|
||
}
|
||
})()
|
||
.catch((error) => {
|
||
savedConversationCountRef.current = Math.min(
|
||
savedConversationCountRef.current,
|
||
start,
|
||
);
|
||
setWorkspaceStatus(
|
||
`项目对话保存失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`conversation.write.failed ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
})
|
||
.finally(() => {
|
||
conversationWriteInFlightRef.current = false;
|
||
if (
|
||
savedConversationCountRef.current < latestMessagesRef.current.length
|
||
) {
|
||
setConversationWriteVersion((current) => current + 1);
|
||
}
|
||
});
|
||
// Writes are driven by message and confirmation state; the confirmation helper
|
||
// intentionally observes the latest policy refs.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
messages,
|
||
conversationWriteVersion,
|
||
pendingUiConfirmation,
|
||
projectSupervisorOnly,
|
||
directCodexProductRuntime,
|
||
]);
|
||
|
||
function appendLocalPermissionLog(
|
||
projectPath: string | null,
|
||
event:
|
||
| 'permission.pending'
|
||
| 'permission.confirm'
|
||
| 'permission.cancel'
|
||
| 'command.auto',
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !projectPath) {
|
||
return;
|
||
}
|
||
let result: Promise<unknown>;
|
||
try {
|
||
result = Promise.resolve(
|
||
invoke('append_local_permission_log', {
|
||
projectPath,
|
||
event,
|
||
commandId,
|
||
}),
|
||
);
|
||
} catch (error) {
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.log.failed ${commandId}: ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
return;
|
||
}
|
||
void result.catch((error) => {
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.log.failed ${commandId}: ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
});
|
||
}
|
||
|
||
function resolvePermissionLogProjectPath(command: PendingCommand) {
|
||
if (command.id === 'project.create') {
|
||
return null;
|
||
}
|
||
return resolveChatProjectPath(localProject);
|
||
}
|
||
|
||
function resolveUiPermissionLogProjectPath(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
) {
|
||
if (commandId === 'project.create') {
|
||
return null;
|
||
}
|
||
if (needsInitializedChatProject(commandId)) {
|
||
return resolveChatProjectPath(localProject);
|
||
}
|
||
return projectPath;
|
||
}
|
||
|
||
function resolveCurrentUiConfirmationProjectPath() {
|
||
const openedProjectPath = localProjectPathRef.current?.trim();
|
||
if (openedProjectPath) {
|
||
return openedProjectPath;
|
||
}
|
||
const draftProjectPath = projectPath.trim();
|
||
if (
|
||
!draftProjectPath ||
|
||
!isAbsoluteProjectPath(draftProjectPath) ||
|
||
projectPathHasControlCharacter(draftProjectPath)
|
||
) {
|
||
return null;
|
||
}
|
||
return draftProjectPath;
|
||
}
|
||
|
||
function queuePendingCommand(command: PendingCommand) {
|
||
setPendingCommand(command);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.pending ${command.id}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.pending',
|
||
command.id,
|
||
);
|
||
}
|
||
|
||
function requestCommandConfirmation(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
detail: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
const permission = GAME_CREATION_APP_COMMANDS.find(
|
||
(command) => command.id === commandId,
|
||
)?.permission as GameCreationAppPermission | undefined;
|
||
|
||
if (permission === 'deny' || !permission) {
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
return;
|
||
}
|
||
if (permission === 'confirm') {
|
||
pendingUiConfirmationActionRef.current = onConfirm;
|
||
setPendingUiConfirmation({ commandId, detail, projectPath: null });
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.pending ${commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
resolveUiPermissionLogProjectPath(commandId),
|
||
'permission.pending',
|
||
commandId,
|
||
);
|
||
return;
|
||
}
|
||
onConfirm();
|
||
}
|
||
|
||
function requestProjectPolicyConfirmation(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string,
|
||
detail: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
pendingUiConfirmationActionRef.current = onConfirm;
|
||
setPendingUiConfirmation({ commandId, detail, projectPath });
|
||
setCommandLog((current) => [...current, `permission.pending ${commandId}`]);
|
||
appendLocalPermissionLog(projectPath, 'permission.pending', commandId);
|
||
}
|
||
|
||
function markProjectPolicyDenied(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
message: string,
|
||
) {
|
||
if (commandId.startsWith('project.') || commandId === 'task.list') {
|
||
setProjectStatus(message);
|
||
setWorkspaceStatus(message);
|
||
}
|
||
if (
|
||
commandId.startsWith('file.') ||
|
||
commandId === 'agent.trace_read' ||
|
||
commandId === 'project.index' ||
|
||
commandId === 'project.diff' ||
|
||
commandId === 'project.export_package' ||
|
||
commandId === 'project.export_list'
|
||
) {
|
||
setFileStatus(message);
|
||
}
|
||
if (commandId.startsWith('asset.') || commandId.startsWith('canvas.')) {
|
||
setAssetStatus(message);
|
||
}
|
||
if (commandId.startsWith('preview.')) {
|
||
setPreviewStatus(message);
|
||
setWorkspaceStatus(message);
|
||
}
|
||
if (commandId.startsWith('agent.')) {
|
||
setAgentRunStatus(message);
|
||
}
|
||
if (commandId.startsWith('memory.')) {
|
||
setMemoryStatus(message);
|
||
}
|
||
}
|
||
|
||
async function queueProjectPolicyConfirmationIfNeeded(
|
||
invoke: TauriInvoke,
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string,
|
||
detail: string,
|
||
readyMessage: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath },
|
||
);
|
||
if (policyView.policy.deniedCommands.includes(commandId)) {
|
||
const message = `项目权限策略拒绝执行:${commandId}`;
|
||
markProjectPolicyDenied(commandId, message);
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return true;
|
||
}
|
||
if (!policyView.policy.confirmCommands.includes(commandId)) {
|
||
return false;
|
||
}
|
||
requestProjectPolicyConfirmation(commandId, projectPath, detail, onConfirm);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: readyMessage },
|
||
]);
|
||
return true;
|
||
}
|
||
|
||
async function denyPendingCommandIfNeeded(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string | null,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !projectPath) {
|
||
return false;
|
||
}
|
||
try {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath },
|
||
);
|
||
if (!policyView.policy.deniedCommands.includes(commandId)) {
|
||
return false;
|
||
}
|
||
const message = `项目权限策略拒绝执行:${commandId}`;
|
||
markProjectPolicyDenied(commandId, message);
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function confirmUiCommand() {
|
||
const pending = pendingUiConfirmation;
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
if (
|
||
pending.projectPath &&
|
||
pending.projectPath !== resolveCurrentUiConfirmationProjectPath()
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
return;
|
||
}
|
||
const permissionProjectPath =
|
||
pending.projectPath ??
|
||
resolveUiPermissionLogProjectPath(pending.commandId);
|
||
if (
|
||
await denyPendingCommandIfNeeded(pending.commandId, permissionProjectPath)
|
||
) {
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
return;
|
||
}
|
||
const action = pendingUiConfirmationActionRef.current;
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.confirm ${pending.commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
permissionProjectPath,
|
||
'permission.confirm',
|
||
pending.commandId,
|
||
);
|
||
action?.();
|
||
}
|
||
|
||
function cancelUiCommandConfirmation() {
|
||
const pending = pendingUiConfirmation;
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
if (pending.commandId === 'conversation.write') {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (nextProjectPath) {
|
||
projectConversationWriteCancelledRef.current = {
|
||
projectPath: nextProjectPath,
|
||
messageCount: latestMessagesRef.current.length,
|
||
};
|
||
setWorkspaceStatus((current) =>
|
||
current === '等待确认保存项目对话'
|
||
? `已打开:${nextProjectPath}`
|
||
: current,
|
||
);
|
||
}
|
||
}
|
||
if (pending.commandId === 'conversation.read') {
|
||
if (pending.detail.includes('Agent 对话')) {
|
||
setAgentConversationStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 对话' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 对话' : current,
|
||
);
|
||
} else {
|
||
setWorkspaceStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目对话' : current,
|
||
);
|
||
}
|
||
}
|
||
if (pending.commandId === 'memory.read') {
|
||
setMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目记忆' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 私有记忆' : current,
|
||
);
|
||
if (!pending.detail.includes('Agent 私有记忆')) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消读取项目记忆。' },
|
||
]);
|
||
}
|
||
}
|
||
if (pending.commandId === 'memory.write') {
|
||
setMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消保存项目记忆' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消保存 Agent 私有记忆' : current,
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'file.list' ||
|
||
pending.commandId === 'file.read' ||
|
||
pending.commandId === 'agent.trace_read'
|
||
) {
|
||
setFileStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目文件' : current,
|
||
);
|
||
}
|
||
if (pending.commandId === 'agent.trace_read') {
|
||
setAgentRunStatus((current) =>
|
||
current === '等待确认' || current === '等待确认读取 Agent trace'
|
||
? '已取消读取 Agent trace'
|
||
: current,
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'project.status' ||
|
||
pending.commandId === 'task.list'
|
||
) {
|
||
const status =
|
||
pending.commandId === 'task.list'
|
||
? '已取消读取任务拆分'
|
||
: '已取消读取项目状态';
|
||
setProjectStatus(status);
|
||
setWorkspaceStatus(status);
|
||
}
|
||
if (pending.commandId === 'asset.list') {
|
||
setAssetStatus('已取消读取项目资产');
|
||
setWorkspaceStatus('已取消读取项目资产');
|
||
}
|
||
if (pending.commandId === 'asset.upload') {
|
||
setAssetStatus('已取消上传资产');
|
||
}
|
||
if (pending.commandId === 'canvas.project_open') {
|
||
setAssetStatus('已取消打开画板');
|
||
}
|
||
if (pending.commandId === 'canvas.project_sync') {
|
||
setAssetStatus('已取消同步画板项目');
|
||
}
|
||
if (pending.commandId === 'canvas.asset_import') {
|
||
setAssetStatus('已取消导入画板资产');
|
||
}
|
||
if (pending.commandId === 'canvas.asset_generate') {
|
||
setAssetStatus('已取消生成美术素材');
|
||
}
|
||
if (pending.commandId === 'canvas.export_import') {
|
||
setAssetStatus('已取消导入画板导出包');
|
||
}
|
||
if (
|
||
pending.commandId === 'preview.start' ||
|
||
pending.commandId === 'preview.open' ||
|
||
pending.commandId === 'preview.stop' ||
|
||
pending.commandId === 'preview.status'
|
||
) {
|
||
setPreviewStatus('已取消预览操作');
|
||
setWorkspaceStatus('已取消预览操作');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消预览操作' },
|
||
]);
|
||
}
|
||
if (
|
||
pending.commandId === 'agent.run_status' ||
|
||
pending.commandId === 'agent.audit'
|
||
) {
|
||
setAgentRunStatus(
|
||
pending.commandId === 'agent.audit'
|
||
? '已取消 Agent 审计'
|
||
: '已取消读取 Agent run 状态',
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'agent.resume' &&
|
||
pending.detail.includes('未完成的 Agent Runtime 任务')
|
||
) {
|
||
setAgentRunStatus('已取消恢复 Agent Runtime 任务');
|
||
}
|
||
if (
|
||
pending.commandId === 'project.index' ||
|
||
pending.commandId === 'project.diff' ||
|
||
pending.commandId === 'project.export_package'
|
||
) {
|
||
const status =
|
||
pending.commandId === 'project.index'
|
||
? '已取消刷新项目索引'
|
||
: pending.commandId === 'project.diff'
|
||
? '已取消对比项目 checkpoint'
|
||
: '已取消导出本地试玩包';
|
||
setFileStatus(status);
|
||
setWorkspaceStatus(status);
|
||
}
|
||
if (pending.commandId === 'project.create') {
|
||
setProjectStatus('已取消');
|
||
setWorkspaceStatus('已取消');
|
||
}
|
||
if (pending.commandId === 'file.write') {
|
||
setFileStatus('已取消保存项目文件');
|
||
}
|
||
if (pending.commandId === 'file.delete') {
|
||
setFileStatus('已取消删除项目文件');
|
||
}
|
||
if (pending.commandId === 'memory.write') {
|
||
setMemoryStatus('已取消写入项目记忆');
|
||
}
|
||
if (pending.commandId === 'memory.delete') {
|
||
setMemoryStatus('已取消删除项目记忆');
|
||
}
|
||
if (pending.commandId === 'asset.register') {
|
||
setAssetStatus('已取消登记资产');
|
||
}
|
||
if (pending.commandId === 'command.run_limited') {
|
||
setLimitedCommandStatus('已取消运行');
|
||
}
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${pending.commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
pending.projectPath ??
|
||
resolveUiPermissionLogProjectPath(pending.commandId),
|
||
'permission.cancel',
|
||
pending.commandId,
|
||
);
|
||
}
|
||
|
||
function handleRuntimeConfigOpen() {
|
||
setRuntimeConfigOpen(true);
|
||
}
|
||
|
||
function queueRunLocalShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'game.run_local' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行当前本地游戏。' },
|
||
]);
|
||
}
|
||
queueRunLocalShortcutRef.current = queueRunLocalShortcut;
|
||
executeRunLocalRef.current = executeRunLocal;
|
||
|
||
useEffect(() => {
|
||
const nextProjectPath = localProject?.projectPath;
|
||
if (
|
||
!projectSupervisorOnly ||
|
||
!playRequest ||
|
||
!nextProjectPath ||
|
||
playRequest.projectPath !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
const requestKey = `${playRequest.projectPath}\n${playRequest.requestId}`;
|
||
if (handledPlayRequestRef.current === requestKey) {
|
||
return;
|
||
}
|
||
handledPlayRequestRef.current = requestKey;
|
||
onPlayRequestHandled?.(playRequest.requestId);
|
||
void executeRunLocalRef.current(true);
|
||
}, [
|
||
localProject?.projectPath,
|
||
onPlayRequestHandled,
|
||
playRequest,
|
||
projectSupervisorOnly,
|
||
]);
|
||
|
||
function queueStaticSmokeShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'command.run_limited',
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行静态入口自检。' },
|
||
]);
|
||
}
|
||
|
||
function queueProjectCheckpointShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.checkpoint' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存当前项目快照。' },
|
||
]);
|
||
}
|
||
|
||
function queueProjectExportPackageShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.export_package' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备导出本地试玩包。' },
|
||
]);
|
||
}
|
||
|
||
function queuePreviewStartShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.start' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备启动本地预览。' },
|
||
]);
|
||
}
|
||
|
||
function queuePreviewOpenShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.open' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备打开当前本地预览。' },
|
||
]);
|
||
}
|
||
|
||
async function handleOpenLauncherWindow() {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请回到首页的项目组切换项目。' },
|
||
]);
|
||
}
|
||
|
||
async function handleRevealCurrentProjectDirectory() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内打开项目目录。' },
|
||
]);
|
||
return;
|
||
}
|
||
try {
|
||
await invoke('open_local_project_directory', {
|
||
projectPath: nextProjectPath,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已打开项目目录。' },
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function requireChatProjectForUserAction() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (nextProjectPath) {
|
||
return nextProjectPath;
|
||
}
|
||
setMemoryStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return null;
|
||
}
|
||
|
||
async function handleProjectInit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
requestCommandConfirmation(
|
||
'project.create',
|
||
`创建 ${projectPath}`,
|
||
() => void executeProjectCreate(projectPath, false),
|
||
);
|
||
}
|
||
|
||
async function resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
if (
|
||
projectSupervisorRuntimeResumeProjectPathRef.current === nextProjectPath
|
||
) {
|
||
return '';
|
||
}
|
||
try {
|
||
await invoke<AgentRuntimeResult[]>(
|
||
'resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
return '';
|
||
} catch (error) {
|
||
if (isMissingAgentRuntimeResumeCommandError(error)) {
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
return '';
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (
|
||
message.includes('项目权限策略要求用户确认:agent.resume') ||
|
||
message.includes('项目权限策略拒绝执行:agent.resume')
|
||
) {
|
||
return '';
|
||
}
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
const runtimeError = `项目总控 Agent 恢复失败:${message}`;
|
||
setProjectSupervisorRuntimeError(runtimeError);
|
||
return runtimeError;
|
||
}
|
||
}
|
||
|
||
async function readProjectSupervisorActiveSession(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
return readProjectSupervisorActiveSessionId(invoke, nextProjectPath);
|
||
}
|
||
|
||
async function refreshProjectSupervisorConversation(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
sessionId: string,
|
||
) {
|
||
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
|
||
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
|
||
const [projectConversation, supervisorConversation] = await Promise.all([
|
||
invoke<LocalConversationResult>('read_local_conversation', {
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
}),
|
||
invoke<LocalConversationResult>('read_local_conversation', {
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
}),
|
||
]);
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const conversationMessages = mergeProjectSupervisorConversation(
|
||
projectConversation.messages,
|
||
supervisorConversation.messages,
|
||
);
|
||
const transientResponse = projectSupervisorResponseStreamRef.current;
|
||
if (
|
||
conversationContainsProjectSupervisorResponseStream(
|
||
supervisorConversation.messages,
|
||
transientResponse,
|
||
)
|
||
) {
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
setProjectSupervisorResponseStream(null);
|
||
}
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setMessages((current) => {
|
||
// 不能整体替换:乐观插入、尚未落盘的用户消息会被冲掉(初始需求看不到就是这个原因)。
|
||
const nextMessages = mergeLoadedConversationWithPendingRuntimeMessages(
|
||
conversationMessages,
|
||
current,
|
||
);
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = nextMessages.length;
|
||
latestMessagesRef.current = nextMessages;
|
||
return nextMessages;
|
||
});
|
||
setProjectSupervisorRuntimeError('');
|
||
}
|
||
projectSupervisorRefreshConversationRef.current =
|
||
refreshProjectSupervisorConversation;
|
||
|
||
async function loadProjectConversation(
|
||
nextProjectPath: string,
|
||
skipPolicyConfirm = false,
|
||
mode: 'initial' | 'replace' = 'initial',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
return;
|
||
}
|
||
if (!skipPolicyConfirm) {
|
||
try {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('conversation.read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'conversation.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的项目对话历史`,
|
||
() => void loadProjectConversation(nextProjectPath, true, mode),
|
||
);
|
||
setWorkspaceStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch {
|
||
return;
|
||
}
|
||
}
|
||
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
|
||
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
|
||
try {
|
||
if (planningStartMode) {
|
||
try {
|
||
const design = await hydrateDesignAgentSession(nextProjectPath);
|
||
if (design) {
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
setWorkspaceStatus((workspaceStatus) =>
|
||
workspaceStatus === '等待确认'
|
||
? `已打开:${nextProjectPath}`
|
||
: workspaceStatus,
|
||
);
|
||
return;
|
||
}
|
||
designAgentActiveRef.current = true;
|
||
setDesignAgentActive(true);
|
||
setDesignAgentView(null);
|
||
projectSupervisorSessionIdRef.current = null;
|
||
setProjectSupervisorSessionId(null);
|
||
updateProjectSupervisorRuntime(null);
|
||
setMessages(createDefaultChatMessages());
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = 0;
|
||
latestMessagesRef.current = [];
|
||
setWorkspaceStatus((workspaceStatus) =>
|
||
workspaceStatus === '等待确认'
|
||
? `已打开:${nextProjectPath}`
|
||
: workspaceStatus,
|
||
);
|
||
return;
|
||
} catch (error) {
|
||
setProjectSupervisorRuntimeError(
|
||
`策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
const resumeError = directCodexProductRuntime
|
||
? ''
|
||
: await resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
if (directCodexProductRuntime) {
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
}
|
||
// Direct Codex owns its app-server conversation identity. The project
|
||
// workbench only hydrates the project conversation and must not inspect
|
||
// or create a legacy Supervisor Session while opening the project.
|
||
const sessionId = directCodexProductRuntime
|
||
? null
|
||
: await readProjectSupervisorActiveSession(invoke, nextProjectPath);
|
||
let runtimeError = '';
|
||
let loadedDirectHistoryHasMore = false;
|
||
let loadedDirectHistoryFirstItemId: string | null = null;
|
||
// 首屏连拉到的条目先暂存,待下方 staleness 守卫通过后再并入聊天 reducer:
|
||
// 迟到的切片属于已经切走的项目,不能在守卫之前就写进全局聊天状态。
|
||
let loadedDirectHistoryItems: DirectThreadItem[] = [];
|
||
// 首屏切片的新端边界只认订阅回执里的 `lastCompletedItemId`(含该条):比它更新的条目
|
||
// 只能来自运行态事件。打开项目时订阅 effect 还没跑,这里就先把闸门开好,订阅侧会复用
|
||
// 同一道闸门并 settle 它。同一道闸门只锚定一次:同一订阅下再读一次(例如重开同一个项目)
|
||
// 没有新回执可等,退回"按当前文件尾取尾屏",与 `/history` 手动重读一致。
|
||
let directHistoryThroughItemId: string | null = null;
|
||
if (directCodexProductRuntime && mode !== 'replace') {
|
||
const anchorGate = directHistoryAnchorGateToWaitFor(
|
||
directHistoryAnchorGateRef.current,
|
||
nextProjectPath,
|
||
);
|
||
if (anchorGate) {
|
||
directHistoryAnchorGateRef.current = anchorGate;
|
||
directHistoryThroughItemId = await anchorGate.anchor;
|
||
anchorGate.consumed = true;
|
||
}
|
||
}
|
||
const projectConversation = directCodexProductRuntime
|
||
? (() => {
|
||
// 首屏铺的就是这份视图的基线,"新回合"比较没有意义:判据退化为"这一页得有能渲染
|
||
// 的条目",所以基线取空,免得拿上一份对话的残留状态去比。首屏失败仍按原有语义
|
||
// 抛出,交给调用方的 catch 处理。
|
||
return readDirectHistoryPages({
|
||
existingEntries: [],
|
||
beforeItemId: null,
|
||
readSlice: (beforeItemId) =>
|
||
invoke<DirectThreadHistorySlice>(
|
||
'read_direct_project_history_slice',
|
||
beforeItemId
|
||
? {
|
||
projectPath: nextProjectPath,
|
||
beforeItemId,
|
||
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
}
|
||
: {
|
||
projectPath: nextProjectPath,
|
||
...(directHistoryThroughItemId
|
||
? { throughItemId: directHistoryThroughItemId }
|
||
: {}),
|
||
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
},
|
||
),
|
||
}).then((pages) => {
|
||
if (pages.error) {
|
||
throw pages.error;
|
||
}
|
||
loadedDirectHistoryHasMore = pages.hasMore;
|
||
loadedDirectHistoryFirstItemId = pages.firstItemId;
|
||
loadedDirectHistoryItems = pages.items;
|
||
return {
|
||
path: nextProjectPath,
|
||
agentId: null,
|
||
messages: [],
|
||
} satisfies LocalConversationResult;
|
||
});
|
||
})()
|
||
: invoke<LocalConversationResult>('read_local_conversation', {
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
});
|
||
const resolvedProjectConversation = await projectConversation;
|
||
// 首屏历史直接进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。
|
||
let supervisorConversation: LocalConversationResult | null = null;
|
||
let runtime: AgentRuntimeState | null = null;
|
||
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
|
||
if (sessionId) {
|
||
supervisorConversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
}
|
||
if (sessionId) {
|
||
try {
|
||
const runtimeResult = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
runtime = agentRuntimeStateFromResult(runtimeResult);
|
||
runtimeResponseStream = runtimeResult.responseStream ?? null;
|
||
} catch (error) {
|
||
runtimeError = error instanceof Error ? error.message : String(error);
|
||
}
|
||
}
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
const conversationMessages = mergeProjectSupervisorConversation(
|
||
resolvedProjectConversation.messages,
|
||
supervisorConversation?.messages ?? [],
|
||
);
|
||
if (
|
||
conversationContainsProjectSupervisorResponseStream(
|
||
supervisorConversation?.messages ?? [],
|
||
runtimeResponseStream,
|
||
)
|
||
) {
|
||
runtimeResponseStream = null;
|
||
}
|
||
projectSupervisorSessionIdRef.current = sessionId;
|
||
setProjectSupervisorSessionId(sessionId);
|
||
updateProjectSupervisorRuntime(runtime);
|
||
if (runtime) {
|
||
updateProjectSupervisorResponseStream(runtimeResponseStream, runtime);
|
||
} else {
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
setProjectSupervisorResponseStream(null);
|
||
}
|
||
setProjectSupervisorRuntimeError(runtimeError || resumeError);
|
||
if (directCodexProductRuntime) {
|
||
setDirectHistoryHasMore(loadedDirectHistoryHasMore);
|
||
directHistoryOldestItemIdRef.current = loadedDirectHistoryFirstItemId;
|
||
if (loadedDirectHistoryItems.length > 0) {
|
||
const items = loadedDirectHistoryItems;
|
||
// 首屏历史进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。
|
||
setDirectThreadChat((state) => mergeDirectHistoryItems(state, items));
|
||
}
|
||
}
|
||
setMessages((current) => {
|
||
// replace 分支同样不能丢掉尚未落盘的运行时消息(初始需求)。
|
||
const nextConversationMessages =
|
||
mergeLoadedConversationWithPendingRuntimeMessages(
|
||
conversationMessages,
|
||
current,
|
||
);
|
||
// 空对话(没有默认问候之后的新常态)同样应当接受回读结果。
|
||
const isEmptyConversation = current.length === 0;
|
||
const hasOnlyDefaultGreeting =
|
||
current.length === 1 &&
|
||
current[0]?.role === 'assistant' &&
|
||
current[0]?.text === '想做什么游戏?';
|
||
const hasOnlyOpenStatus =
|
||
current.length === 2 &&
|
||
current[0]?.role === 'assistant' &&
|
||
current[0]?.text === '想做什么游戏?' &&
|
||
current[1]?.role === 'assistant' &&
|
||
current[1]?.text === `已设置本地项目:${nextProjectPath}`;
|
||
if (
|
||
mode !== 'replace' &&
|
||
!isEmptyConversation &&
|
||
!hasOnlyDefaultGreeting &&
|
||
!hasOnlyOpenStatus
|
||
) {
|
||
return current;
|
||
}
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = nextConversationMessages.length;
|
||
latestMessagesRef.current = nextConversationMessages;
|
||
setWorkspaceStatus((workspaceStatus) => {
|
||
if (mode === 'replace') {
|
||
return `已读取项目对话历史:${conversationMessages.length} 条`;
|
||
}
|
||
return workspaceStatus === '等待确认'
|
||
? `已打开:${nextProjectPath}`
|
||
: workspaceStatus;
|
||
});
|
||
return nextConversationMessages;
|
||
});
|
||
} catch (error) {
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
setWorkspaceStatus((workspaceStatus) =>
|
||
workspaceStatus === '等待确认'
|
||
? `项目对话读取失败:${message}`
|
||
: workspaceStatus,
|
||
);
|
||
// Keep the default greeting when history is missing or blocked.
|
||
}
|
||
}
|
||
|
||
async function openWorkspace(
|
||
nextProjectPath: string,
|
||
announceToChat: boolean,
|
||
mode: 'create' | 'open' = 'create',
|
||
projectKind: LocalProjectKind = 'web',
|
||
) {
|
||
const trimmedProjectPath = nextProjectPath.trim();
|
||
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(trimmedProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setWorkspaceStatus('需要在 Tauri App 内运行');
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const projectScopeVersion = projectScopeVersionRef.current + 1;
|
||
projectScopeVersionRef.current = projectScopeVersion;
|
||
resetProjectSupervisorState();
|
||
resetDirectThreadChat();
|
||
updateClientPreview(null);
|
||
setPreviewStatus('未启动');
|
||
setWorkspaceStatus('正在打开');
|
||
setProjectStatus('正在初始化');
|
||
setSelectedAgent(null);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentConversationStatus('未选择 agent');
|
||
setAgentConversationSaving(false);
|
||
setAgentMemoryStatus('未读取');
|
||
setAgentMemoryContent('');
|
||
agentConversationSavingRef.current = false;
|
||
agentConversationLoadVersionRef.current += 1;
|
||
try {
|
||
const result =
|
||
mode === 'open'
|
||
? {
|
||
projectPath: trimmedProjectPath,
|
||
manifestPath: `${trimmedProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`,
|
||
manifest: await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: trimmedProjectPath },
|
||
),
|
||
}
|
||
: await invoke<InitLocalProjectResult>('init_local_game_project', {
|
||
projectPath: trimmedProjectPath,
|
||
projectId: createLocalProjectId(),
|
||
name: seedManifest.name,
|
||
});
|
||
if (projectScopeVersionRef.current !== projectScopeVersion) {
|
||
return;
|
||
}
|
||
const openedProjectPath = result.projectPath.trim();
|
||
if (
|
||
!openedProjectPath ||
|
||
!isAbsoluteProjectPath(openedProjectPath) ||
|
||
projectPathHasControlCharacter(openedProjectPath)
|
||
) {
|
||
const message = '本地项目路径无效';
|
||
setWorkspaceStatus(message);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const openedProject = { ...result, projectPath: openedProjectPath };
|
||
const conversationMessages = createDefaultChatMessages();
|
||
|
||
if (
|
||
pendingUiConfirmation?.projectPath &&
|
||
pendingUiConfirmation.projectPath !== openedProject.projectPath
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
}
|
||
localProjectPathRef.current = openedProject.projectPath;
|
||
setProjectPath(openedProject.projectPath);
|
||
setWorkspaceProjectKind(projectKind);
|
||
setLocalProject(openedProject);
|
||
if (supervisorChatOnly) {
|
||
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
|
||
}
|
||
setChatReferences([]);
|
||
setChatContent([]);
|
||
setManifest(openedProject.manifest);
|
||
setProjectFiles([]);
|
||
setProjectCheckpoints([]);
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRuntimeById({});
|
||
setMessages(conversationMessages);
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
savedConversationProjectPathRef.current = openedProject.projectPath;
|
||
savedConversationCountRef.current = conversationMessages.length;
|
||
writeRecentWorkspace(openedProject.projectPath);
|
||
setWorkspaceStatus(`已打开:${openedProject.projectPath}`);
|
||
setProjectStatus(mode === 'open' ? '已打开' : '已初始化');
|
||
appendLocalPermissionLog(
|
||
openedProject.projectPath,
|
||
'permission.confirm',
|
||
mode === 'open' ? 'project.status' : 'project.create',
|
||
);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已设置本地项目:${openedProject.projectPath}`,
|
||
},
|
||
]);
|
||
}
|
||
// Direct Codex must not resume or inspect legacy Supervisor work. Its
|
||
// own project conversation is hydrated above; the old run trace remains
|
||
// available only in the explicit legacy diagnostic surface.
|
||
void loadProjectConversation(openedProject.projectPath);
|
||
if (!directCodexProductRuntime) {
|
||
void refreshAgentRunTrace(openedProject.projectPath);
|
||
}
|
||
} catch (error) {
|
||
if (projectScopeVersionRef.current !== projectScopeVersion) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setWorkspaceStatus(message);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function openAgentConversation(
|
||
agent: AgentStatusCard,
|
||
skipConversationPolicyConfirm = false,
|
||
skipMemoryPolicyConfirm = false,
|
||
) {
|
||
const loadVersion = agentConversationLoadVersionRef.current + 1;
|
||
agentConversationLoadVersionRef.current = loadVersion;
|
||
setSelectedAgent(agent);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentMemoryContent('');
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
setAgentMemoryStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
setLlmConfigStatus(status);
|
||
}
|
||
} catch {
|
||
// Agent conversation history is still useful when config status is unavailable.
|
||
}
|
||
try {
|
||
if (
|
||
!skipConversationPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.read',
|
||
nextProjectPath,
|
||
`读取 ${agent.title} Agent 对话`,
|
||
'准备读取 Agent 对话。',
|
||
() =>
|
||
void openAgentConversation(agent, true, skipMemoryPolicyConfirm),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
setAgentMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentConversationStatus(message);
|
||
setAgentMemoryStatus(message);
|
||
return;
|
||
}
|
||
setAgentConversationStatus('正在读取');
|
||
setAgentMemoryStatus('正在读取');
|
||
try {
|
||
const result = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(result.messages);
|
||
setAgentConversationSessionId(result.sessionId ?? null);
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationRuntimeError(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus(
|
||
`已读取 ${result.messages.length} 条:${result.path}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'conversation.read']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
try {
|
||
if (
|
||
!skipMemoryPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${agent.title} Agent 私有记忆`,
|
||
'准备读取 Agent 私有记忆。',
|
||
() => void openAgentConversation(agent, true, true),
|
||
))
|
||
) {
|
||
setAgentMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
try {
|
||
const result = await invoke<LocalAgentMemoryResult>(
|
||
'read_local_agent_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
taskId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent(result.content);
|
||
setAgentMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '私有记忆为空',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.agent.read']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent('');
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
function closeAgentConversation() {
|
||
agentConversationLoadVersionRef.current += 1;
|
||
agentConversationSavingRef.current = false;
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setSelectedAgent(null);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentConversationStatus('未选择 agent');
|
||
setAgentConversationSaving(false);
|
||
setAgentConversationBackgroundBusy(false);
|
||
setAgentMemoryStatus('未读取');
|
||
setAgentMemoryContent('');
|
||
}
|
||
|
||
function prepareSuggestedToolCommandDraft(
|
||
toolCall: GameCreationAgentToolCallTrace,
|
||
) {
|
||
const commandDraft = commandDraftFromSuggestedToolCall(toolCall);
|
||
if (!commandDraft) {
|
||
return;
|
||
}
|
||
prepareChatCommandDraft(commandDraft);
|
||
closeAgentConversation();
|
||
}
|
||
|
||
function prepareChatCommandDraft(commandDraft: string) {
|
||
setChatInput(commandDraft);
|
||
setChatReferences([]);
|
||
setChatContent([]);
|
||
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
||
}
|
||
|
||
function handleChatComposerChange(draft: ChatComposerDraft) {
|
||
setChatInput(draft.text);
|
||
setChatReferences(draft.references);
|
||
setChatContent(draft.content ?? []);
|
||
}
|
||
|
||
useEffect(() => {
|
||
const handleResourceReferenceInsert = (event: Event) => {
|
||
const detail = (event as CustomEvent<ResourceReferenceInsertEventDetail>)
|
||
.detail;
|
||
if (!detail?.reference) return;
|
||
chatComposerRef.current?.insertReferences([detail.reference]);
|
||
chatComposerRef.current?.focus();
|
||
};
|
||
/**
|
||
* 画布拖拽的批量 @ 引用:与单条入口同一个消费点,只是一次把整批插进去。
|
||
*
|
||
* 逐条派发单条事件也能跑,但每次都会重建一次草稿并重新聚焦,而且插入顺序只能靠事件顺序兜着;
|
||
* 批次为空时静默返回(不开一次空事务)。
|
||
*/
|
||
const handleResourceReferenceInsertMany = (event: Event) => {
|
||
const detail = (
|
||
event as CustomEvent<ResourceReferenceInsertManyEventDetail>
|
||
).detail;
|
||
const references = detail?.references ?? [];
|
||
if (references.length === 0) return;
|
||
chatComposerRef.current?.insertReferences(references);
|
||
chatComposerRef.current?.focus();
|
||
};
|
||
window.addEventListener(
|
||
RESOURCE_REFERENCE_INSERT_EVENT,
|
||
handleResourceReferenceInsert,
|
||
);
|
||
window.addEventListener(
|
||
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
|
||
handleResourceReferenceInsertMany,
|
||
);
|
||
return () => {
|
||
window.removeEventListener(
|
||
RESOURCE_REFERENCE_INSERT_EVENT,
|
||
handleResourceReferenceInsert,
|
||
);
|
||
window.removeEventListener(
|
||
RESOURCE_REFERENCE_INSERT_MANY_EVENT,
|
||
handleResourceReferenceInsertMany,
|
||
);
|
||
};
|
||
}, []);
|
||
|
||
function prepareProjectAssetRegisterDraft(localPath: string) {
|
||
prepareChatCommandDraft(projectFileActionDrafts(localPath).assetCommand);
|
||
}
|
||
|
||
function prepareAgentEvidenceReadDraft(localPath: string) {
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
return;
|
||
}
|
||
prepareChatCommandDraft(projectFileActionDrafts(localPath).readCommand);
|
||
closeAgentConversation();
|
||
}
|
||
|
||
async function handlePickCanvasExportFileDraft() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内选择画板导出包。' },
|
||
]);
|
||
return;
|
||
}
|
||
try {
|
||
const selectedPath = await invoke<string | null>('pick_local_file');
|
||
if (!selectedPath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消选择画板导出包。' },
|
||
]);
|
||
return;
|
||
}
|
||
setCanvasExportPath(selectedPath);
|
||
prepareChatCommandDraft(`/import-canvas-export ${selectedPath} `);
|
||
setAssetStatus(`已选择画板导出包:${selectedPath}`);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已选择画板导出包:${selectedPath}` },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function saveAgentConversationMessage(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
`写入 ${agent.title} Agent 对话`,
|
||
'准备保存 Agent 对话。',
|
||
() => void saveAgentConversationMessage(agent, content, true),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationSavingRef.current = true;
|
||
setAgentConversationSaving(true);
|
||
setAgentConversationInput('');
|
||
setAgentConversationStatus('正在保存用户消息');
|
||
let savedUserResult: LocalConversationResult | null = null;
|
||
let stopStreamListen: (() => void) | null = null;
|
||
let streamListenDisposed = false;
|
||
let streamListenReady = false;
|
||
try {
|
||
savedUserResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'user',
|
||
content,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const savedUserMessages = savedUserResult.messages;
|
||
setAgentConversationMessages(savedUserMessages);
|
||
setAgentConversationStatus('正在连接 Agent LLM');
|
||
const streamRunId = createAgentChatRunId('agent-conversation');
|
||
if (canSubscribeTauriEvents()) {
|
||
try {
|
||
stopStreamListen =
|
||
await subscribeTauriEvent<GameCreatorRoleAgentChatStreamEvent>(
|
||
'game-creator-role-agent-chat-stream',
|
||
(event) => {
|
||
const payload = event.payload;
|
||
if (
|
||
payload.projectPath !== nextProjectPath ||
|
||
payload.agentId !== agent.id ||
|
||
payload.runId !== streamRunId ||
|
||
agentConversationLoadVersionRef.current !== saveVersion
|
||
) {
|
||
return;
|
||
}
|
||
if (payload.runtimeState) {
|
||
setAgentConversationRuntime((current) => {
|
||
const nextRuntime = normalizeAgentRuntimeState(
|
||
payload.runtimeState!,
|
||
current,
|
||
);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
return nextRuntime;
|
||
});
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
if (payload.status === 'started') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ?? 'Agent 已连接,正在等待回复',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'delta') {
|
||
const draftText =
|
||
payload.accumulatedText || payload.deltaText;
|
||
if (draftText) {
|
||
setAgentConversationMessages([
|
||
...savedUserMessages,
|
||
createLocalConversationDraftMessage(draftText),
|
||
]);
|
||
}
|
||
setAgentConversationStatus(
|
||
payload.finishReason
|
||
? `Agent 回复结束:${payload.finishReason}`
|
||
: '正在接收 Agent 回复',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'completed') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ?? 'Agent 回复完成,正在保存',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'failed') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ??
|
||
'Agent 流式回复失败,正在记录错误',
|
||
);
|
||
}
|
||
},
|
||
);
|
||
streamListenReady = true;
|
||
if (streamListenDisposed) {
|
||
stopStreamListen();
|
||
stopStreamListen = null;
|
||
}
|
||
} catch {
|
||
setAgentConversationStatus('实时状态不可用,正在使用普通回复模式');
|
||
}
|
||
}
|
||
const reply = streamListenReady
|
||
? await invoke<GameCreatorChatAgentReply>(
|
||
'chat_with_game_creator_role_agent_stream',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
prompt: content,
|
||
runId: streamRunId,
|
||
},
|
||
)
|
||
: await invoke<GameCreatorChatAgentReply>(
|
||
'chat_with_game_creator_role_agent',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
prompt: content,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
setAgentConversationRuntimeError(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus('正在保存 Agent 回复');
|
||
setAgentConversationMessages([
|
||
...savedUserMessages,
|
||
createLocalConversationDraftMessage(reply.replyText),
|
||
]);
|
||
const assistantResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'assistant',
|
||
content: reply.replyText,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(assistantResult.messages);
|
||
setAgentConversationStatus(
|
||
`已保存 ${assistantResult.messages.length} 条:${assistantResult.path}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'conversation.write']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
void captureAgentRuntimeError(error, agent.id);
|
||
if (savedUserResult) {
|
||
const message = `已保存用户消息;Agent 回复失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`;
|
||
try {
|
||
const errorResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'assistant',
|
||
content: message,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(errorResult.messages);
|
||
} catch {
|
||
setAgentConversationMessages([
|
||
...savedUserResult.messages,
|
||
{
|
||
schemaVersion: 'game-creator-conversation.v1',
|
||
role: 'assistant',
|
||
content: message,
|
||
agentId: null,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
}
|
||
setAgentConversationStatus(message);
|
||
} else {
|
||
setAgentConversationInput(content);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
} finally {
|
||
streamListenDisposed = true;
|
||
stopStreamListen?.();
|
||
agentConversationSavingRef.current = false;
|
||
setAgentConversationSaving(false);
|
||
}
|
||
}
|
||
|
||
async function startSelectedAgentBackgroundTask(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
submitMode = agentConversationRunSubmitMode,
|
||
) {
|
||
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const steerRuntime =
|
||
submitMode === 'steer'
|
||
? matchingAgentRuntimeForSteer(
|
||
[agentConversationRuntime],
|
||
agent.id,
|
||
agentConversationSessionId,
|
||
)
|
||
: null;
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
steerRuntime
|
||
? `向 ${agent.title} 当前 Run 追加指令`
|
||
: `启动 ${agent.title} 后台任务`,
|
||
steerRuntime
|
||
? '准备向当前 Agent Run 追加指令。'
|
||
: '准备启动 Agent 后台任务。',
|
||
() =>
|
||
void startSelectedAgentBackgroundTask(
|
||
agent,
|
||
content,
|
||
true,
|
||
submitMode,
|
||
),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
if (steerRuntime) {
|
||
setAgentConversationStatus(
|
||
`正在向当前 Run 追加指令:${steerRuntime.runId}`,
|
||
);
|
||
} else {
|
||
setAgentConversationInput('');
|
||
setAgentConversationStatus('正在启动 Agent 后台任务');
|
||
}
|
||
try {
|
||
let runtime: AgentRuntimeResult;
|
||
let successStatus: string;
|
||
if (steerRuntime) {
|
||
const steer = await invoke<AgentRuntimeSteerResult>(
|
||
'steer_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: steerRuntime.sessionId,
|
||
runId: steerRuntime.runId,
|
||
steerId: createAgentChatRunId('agent-steer'),
|
||
instruction: content,
|
||
},
|
||
);
|
||
runtime = steer.runtime;
|
||
successStatus = agentRuntimeSteerStatus(steer);
|
||
} else {
|
||
runtime = await invoke<AgentRuntimeResult>(
|
||
'start_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
task: content,
|
||
runId: createAgentChatRunId('agent-background-task'),
|
||
...(agentConversationSessionId
|
||
? { sessionId: agentConversationSessionId }
|
||
: {}),
|
||
},
|
||
);
|
||
successStatus = agentRuntimeStartStatus(runtime);
|
||
}
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationInput('');
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationSessionId(nextRuntime.sessionId);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
try {
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: nextRuntime.sessionId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationSessionId(
|
||
conversation.sessionId ?? nextRuntime.sessionId,
|
||
);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
setAgentConversationRuntimeError(
|
||
`对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus(successStatus);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
steerRuntime ? 'agent.runtime.steer' : 'agent.runtime.background_task',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationInput(content);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function cancelSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
) {
|
||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在取消 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'cancel_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus(agentRuntimeCancelStatus(nextRuntime, runId));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.background_task.cancel',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function retrySelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
) {
|
||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在重试 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
nextRunId: createAgentChatRunId('agent-background-retry'),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.background_task.retry',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function confirmSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
actionId: string,
|
||
) {
|
||
if (
|
||
!agent ||
|
||
!runId ||
|
||
!actionId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在确认并继续 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'confirm_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
actionId,
|
||
note: '开发者已确认待执行工具动作',
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.tool_confirmation.approved',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function rejectSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
actionId: string,
|
||
) {
|
||
if (
|
||
!agent ||
|
||
!runId ||
|
||
!actionId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在拒绝工具动作并继续 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'reject_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
actionId,
|
||
note: '开发者拒绝待执行工具动作',
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.tool_confirmation.rejected',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function answerSelectedAgentRuntimeUserInput(
|
||
agent: AgentStatusCard,
|
||
request: AgentRuntimeUserInputRequest,
|
||
responseId: string,
|
||
answers: Record<string, string>,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRequest = agentConversationRuntime?.userInputRequest;
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!responseId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
request.agentId !== agent.id ||
|
||
request.sessionId !== agentConversationSessionId ||
|
||
request.runId !== agentConversationRuntime?.runId ||
|
||
currentRequest?.requestId !== request.requestId ||
|
||
currentRequest?.actionId !== request.actionId
|
||
) {
|
||
setAgentConversationRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus('正在提交回答并继续当前 Run');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'answer_game_creator_agent_runtime_user_input',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId: request.runId,
|
||
actionId: request.actionId,
|
||
requestId: request.requestId,
|
||
responseId,
|
||
answers,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const runtimeState = agentRuntimeStateFromResult(
|
||
result,
|
||
agentConversationRuntime,
|
||
);
|
||
if (
|
||
runtimeState.agentId !== agent.id ||
|
||
runtimeState.sessionId !== request.sessionId ||
|
||
runtimeState.runId !== request.runId
|
||
) {
|
||
throw new Error('Agent 回答后 Runtime 身份不匹配');
|
||
}
|
||
setAgentConversationRuntime(runtimeState);
|
||
rememberAgentRuntimeState(runtimeState);
|
||
setAgentConversationSessionId(runtimeState.sessionId);
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: runtimeState.sessionId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationSessionId(
|
||
conversation.sessionId ?? runtimeState.sessionId,
|
||
);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus(agentRuntimeConversationStatus(runtimeState));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.user_input.answered',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentConversationRuntimeError(message);
|
||
setAgentConversationStatus(`回答提交失败:${message}`);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function saveSelectedAgentPrivateMemory(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentMemoryStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.deniedCommands.includes('memory.write')) {
|
||
const message = '项目权限策略拒绝执行:memory.write';
|
||
markProjectPolicyDenied('memory.write', message);
|
||
setAgentMemoryStatus(message);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'permission.deny memory.write',
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return;
|
||
}
|
||
if (policyView.policy.confirmCommands.includes('memory.write')) {
|
||
requestProjectPolicyConfirmation(
|
||
'memory.write',
|
||
nextProjectPath,
|
||
`写入 ${agent.title} Agent 私有记忆`,
|
||
() => void saveSelectedAgentPrivateMemory(agent, content, true),
|
||
);
|
||
setAgentMemoryStatus('等待确认');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存 Agent 私有记忆。' },
|
||
]);
|
||
return;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationSavingRef.current = true;
|
||
setAgentConversationSaving(true);
|
||
setAgentConversationInput('');
|
||
setAgentMemoryStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalAgentMemoryResult>(
|
||
'write_local_agent_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
taskId: agent.id,
|
||
content: appendMemoryContent(agentMemoryContent, content),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent(result.content);
|
||
setAgentMemoryStatus(`已追加私有记忆:${result.path}`);
|
||
setAgentConversationStatus(`已写入 ${agent.title} 私有记忆。`);
|
||
setCommandLog((current) => [...current, 'memory.agent.write']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationInput(content);
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationSavingRef.current = false;
|
||
setAgentConversationSaving(false);
|
||
}
|
||
}
|
||
|
||
function handleAgentConversationSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
void saveAgentConversationMessage(agent, content);
|
||
}
|
||
|
||
function handleAgentPrivateMemorySubmit() {
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
void saveSelectedAgentPrivateMemory(agent, content);
|
||
}
|
||
|
||
function handleAgentBackgroundTaskSubmit() {
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
void startSelectedAgentBackgroundTask(agent, content);
|
||
}
|
||
|
||
function showChatHelp() {
|
||
setCommandLog((current) => [...current, 'help.show']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `可用命令:\n${chatCommandHelp.join('\n')}`,
|
||
},
|
||
]);
|
||
}
|
||
|
||
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
const prompt = chatInput.trim();
|
||
const references = chatReferences;
|
||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if ((!prompt && references.length === 0) || chatAgentBusy) {
|
||
return;
|
||
}
|
||
|
||
setChatInput('');
|
||
setChatReferences([]);
|
||
setChatContent([]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: prompt,
|
||
runtimeOwned: !prompt.startsWith('/'),
|
||
},
|
||
]);
|
||
if (prompt === '/help') {
|
||
showChatHelp();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/config') {
|
||
handleRuntimeConfigOpen();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/capabilities' || prompt === '/能力') {
|
||
void executeAgentCapabilitiesChat();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/audit' || prompt === '/审计') {
|
||
void executeAgentAuditChat();
|
||
return;
|
||
}
|
||
|
||
const missingArgumentMessage = missingChatCommandArgumentMessage(prompt);
|
||
if (missingArgumentMessage) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: missingArgumentMessage },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/project ')) {
|
||
const nextProjectPath = prompt.slice('/project '.length).trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供本地项目绝对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '本地项目路径不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'project.create',
|
||
projectPath: nextProjectPath,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备初始化本地项目:${nextProjectPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/generate ') || prompt.startsWith('/draft ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const generationPrompt = prompt
|
||
.slice(
|
||
prompt.startsWith('/generate ')
|
||
? '/generate '.length
|
||
: '/draft '.length,
|
||
)
|
||
.trim();
|
||
if (!generationPrompt) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/generate 创作想法' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'game.generate_draft',
|
||
prompt: generationPrompt,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备生成本地游戏草案:${generationPrompt}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/status') {
|
||
void executeProjectStatus(true);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
handleProjectSummaryChatCommand({
|
||
agentRunHistory,
|
||
agentRunTrace,
|
||
agentStatusCards,
|
||
manifest,
|
||
prompt,
|
||
requireChatProjectForUserAction,
|
||
setMessages,
|
||
})
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/index') {
|
||
void queueOrExecuteProjectIndex();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/checkpoint') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.checkpoint' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存当前项目快照。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/checkpoints') {
|
||
void executeProjectCheckpoints(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/diff ')) {
|
||
const checkpointId = prompt.slice('/diff '.length).trim();
|
||
if (!checkpointId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/diff checkpoint-id' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCheckpointId(checkpointId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: 'checkpoint id 非法。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectDiff(checkpointId, true);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/restore ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const checkpointId = prompt.slice('/restore '.length).trim();
|
||
if (!checkpointId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/restore checkpoint-id' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCheckpointId(checkpointId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: 'checkpoint id 非法。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.restore', checkpointId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备回滚到 checkpoint:${checkpointId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/policy') {
|
||
void executeProjectPolicyRead(true);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
prompt.startsWith('/policy-deny ') ||
|
||
prompt.startsWith('/policy-allow ') ||
|
||
prompt.startsWith('/policy-confirm ') ||
|
||
prompt.startsWith('/policy-auto ')
|
||
) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const mode = prompt.startsWith('/policy-deny ')
|
||
? 'deny'
|
||
: prompt.startsWith('/policy-allow ')
|
||
? 'allow'
|
||
: prompt.startsWith('/policy-confirm ')
|
||
? 'confirm'
|
||
: 'auto';
|
||
const commandPrefix =
|
||
mode === 'deny'
|
||
? '/policy-deny '
|
||
: mode === 'allow'
|
||
? '/policy-allow '
|
||
: mode === 'confirm'
|
||
? '/policy-confirm '
|
||
: '/policy-auto ';
|
||
const commandId = prompt.slice(commandPrefix.length).trim();
|
||
if (!commandId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'deny' || mode === 'allow'
|
||
? '格式:/policy-deny file.write'
|
||
: '格式:/policy-confirm project.index',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isRegisteredGameCreationCommandId(commandId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `未知内置命令:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
(mode === 'confirm' || mode === 'auto') &&
|
||
!isProjectPolicyConfirmableCommandId(commandId)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void queueProjectPolicyMutation(commandId, mode);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
prompt.startsWith('/agent-policy-deny ') ||
|
||
prompt.startsWith('/agent-policy-allow ') ||
|
||
prompt.startsWith('/agent-policy-confirm ') ||
|
||
prompt.startsWith('/agent-policy-auto ')
|
||
) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const mode = prompt.startsWith('/agent-policy-deny ')
|
||
? 'deny'
|
||
: prompt.startsWith('/agent-policy-allow ')
|
||
? 'allow'
|
||
: prompt.startsWith('/agent-policy-confirm ')
|
||
? 'confirm'
|
||
: 'auto';
|
||
const commandPrefix =
|
||
mode === 'deny'
|
||
? '/agent-policy-deny '
|
||
: mode === 'allow'
|
||
? '/agent-policy-allow '
|
||
: mode === 'confirm'
|
||
? '/agent-policy-confirm '
|
||
: '/agent-policy-auto ';
|
||
const args = prompt.slice(commandPrefix.length).trim();
|
||
const [agentId = '', commandId = ''] = args.split(/\s+/, 2);
|
||
if (!agentId || !commandId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'deny' || mode === 'allow'
|
||
? '格式:/agent-policy-deny design-director file.read'
|
||
: '格式:/agent-policy-confirm design-director memory.write',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isRegisteredGameCreationCommandId(commandId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `未知内置命令:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
(mode === 'confirm' || mode === 'auto') &&
|
||
!isProjectPolicyConfirmableCommandId(commandId)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void queueAgentPolicyMutation(agentId, commandId, mode);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/llm-status') {
|
||
void executeLlmConfigStatus();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/llm-routes') {
|
||
void executeLlmRouteSummary();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/open-project' || prompt === '/show-project') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
void handleRevealCurrentProjectDirectory();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/switch-project') {
|
||
void handleOpenLauncherWindow();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/files') {
|
||
void executeProjectFiles(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/assets') {
|
||
void executeProjectAssets(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/credits') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectAssetCredits(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/art') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectVisualAssets(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/audio') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectAudioAssets(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/artifacts') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeCommonProjectArtifactReadDrafts(),
|
||
draftCommand: `/read ${commonProjectArtifactReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectArtifactReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run-artifacts') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const artifacts = agentRunTrace
|
||
? readableArtifactsFromAgentRunTrace(agentRunTrace)
|
||
: [];
|
||
const firstArtifact = artifacts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeRunArtifactReadDrafts(agentRunTrace),
|
||
draftCommand: firstArtifact
|
||
? `/read ${firstArtifact.path}`
|
||
: undefined,
|
||
draftCommandLabel: firstArtifact ? '读取首个 Run 产物' : undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/passes') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const artifacts = agentRunTrace
|
||
? readablePassArtifactsFromAgentRunTrace(agentRunTrace)
|
||
: [];
|
||
const firstArtifact = artifacts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentPassArtifactReadDrafts(agentRunTrace),
|
||
draftCommand: firstArtifact
|
||
? `/read ${firstArtifact.path}`
|
||
: undefined,
|
||
draftCommandLabel: firstArtifact ? '读取首个轮次产物' : undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/runs') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const firstHistory = agentRunHistory[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentRunHistoryReadDrafts(
|
||
agentRunTrace,
|
||
agentRunHistory,
|
||
),
|
||
draftCommand: firstHistory
|
||
? `/read ${firstHistory.path}`
|
||
: agentRunTrace
|
||
? '/trace'
|
||
: undefined,
|
||
draftCommandLabel: firstHistory
|
||
? '读取首个历史 Run'
|
||
: agentRunTrace
|
||
? '查看最近 Run'
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run-files') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentRunSupportFileReadDrafts(),
|
||
draftCommand: `/read ${commonAgentRunSupportReadDrafts[0].path}`,
|
||
draftCommandLabel: commonAgentRunSupportReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/internals') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectInternalReadDrafts(),
|
||
draftCommand: `/read ${commonProjectInternalReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectInternalReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/logs') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeCommonProjectLogReadDrafts(),
|
||
draftCommand: `/read ${commonProjectLogReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectLogReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/asset-register ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [
|
||
localPath,
|
||
kind = 'unknown',
|
||
mediaType = 'application/octet-stream',
|
||
] = prompt.slice('/asset-register '.length).trim().split(/\s+/);
|
||
if (!localPath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/asset-register assets/hero.png [kind] [mediaType]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '资产路径必须是项目内相对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'asset.register',
|
||
localPath,
|
||
kind: parseGameCreationAppAssetKind(kind, 'chat.asset-register.kind'),
|
||
mediaType,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备登记项目资产:${localPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/read ')) {
|
||
const relativePath = prompt.slice('/read '.length).trim();
|
||
if (!relativePath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/read game/index.html' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(relativePath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '文件路径必须是项目内相对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectFileReadChat(relativePath);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/tasks') {
|
||
void executeProjectTasks(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agents') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentStatusCardsForChat(
|
||
agentStatusCards,
|
||
llmConfigStatus,
|
||
),
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-conversations') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const drafts = agentConversationReadDraftsFromManifest(manifest);
|
||
const firstDraft = drafts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentConversationReadDrafts(manifest),
|
||
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
|
||
draftCommandLabel: firstDraft
|
||
? `读取${firstDraft.task.title}对话`
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-memories') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const drafts = agentMemoryReadDraftsFromManifest(manifest);
|
||
const firstDraft = drafts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentMemoryReadDrafts(manifest),
|
||
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
|
||
draftCommandLabel: firstDraft
|
||
? `读取${firstDraft.task.title}记忆`
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/trace' || prompt === '/loop') {
|
||
void executeAgentTraceChat();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-status') {
|
||
void executeAgentRunControl('status', undefined, true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-kill') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'agent.kill' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备标记最近 run 为 killed。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-retry') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'agent.retry' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备重新运行最近 run 的目标。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-resume' || prompt.startsWith('/agent-resume ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'agent.resume',
|
||
detail: prompt.slice('/agent-resume'.length).trim(),
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备恢复最近 run。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/history') {
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
void loadProjectConversation(nextProjectPath, false, 'replace');
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/commands' || prompt === '/limited-commands') {
|
||
void executeLimitedCommandList();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/smoke') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'command.run_limited',
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行静态入口自检。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'game.run_local' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行当前本地游戏。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/export') {
|
||
queueProjectExportPackageShortcut();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/exports') {
|
||
void executeProjectExportPackages(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.start' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备启动本地预览。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/open-preview') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.open' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备打开当前本地预览。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview-status') {
|
||
void executePreviewStatus(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview-stop') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
void executePreviewStop(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/memory' || prompt.startsWith('/memory ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const scope = parseOptionalMemoryScope(prompt.slice('/memory'.length));
|
||
if (!scope) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/memory [short|long|blackboard]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void executeMemoryReadChat(scope);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/remember ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const { scope, content } = parseRememberInput(
|
||
prompt.slice('/remember '.length),
|
||
);
|
||
if (!content) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供要追加的记忆内容。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'memory.write',
|
||
scope,
|
||
content,
|
||
mode: 'append',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备追加${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/memory-set ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const { scope, content } = parseRememberInput(
|
||
prompt.slice('/memory-set '.length),
|
||
);
|
||
if (!content) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供要保存的记忆内容。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'memory.write',
|
||
scope,
|
||
content,
|
||
mode: 'replace',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备覆盖保存${memoryScopeLabel(scope)}记忆。`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/forget-memory' || prompt.startsWith('/forget-memory ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const scope = parseOptionalMemoryScope(
|
||
prompt.slice('/forget-memory'.length),
|
||
);
|
||
if (!scope) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/forget-memory [short|long|blackboard]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'memory.delete', scope });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备删除${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/canvas ')) {
|
||
const canvasProjectId = prompt.slice('/canvas '.length).trim();
|
||
if (!canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供画板项目 ID。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'canvas.project_open', canvasProjectId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备打开画板项目:${canvasProjectId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/sync-canvas-project ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const canvasProjectId = prompt
|
||
.slice('/sync-canvas-project '.length)
|
||
.trim();
|
||
if (!canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供画板项目 ID。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'canvas.project_sync', canvasProjectId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备同步画板项目资源:${canvasProjectId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/generate-art ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const generationPrompt = prompt.slice('/generate-art '.length).trim();
|
||
if (!generationPrompt) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供美术生成提示词。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.asset_generate',
|
||
prompt: generationPrompt,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备生成首版美术素材。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/import-canvas-asset ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [localPath, canvasProjectId, canvasAssetId, kind, mediaType] =
|
||
prompt.slice('/import-canvas-asset '.length).trim().split(/\s+/);
|
||
if (!localPath || !canvasProjectId || !canvasAssetId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '画板资产路径必须是项目内相对路径。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
const canvasAssetObjectId = canvasAssetId.startsWith('object:')
|
||
? canvasAssetId.slice('object:'.length).trim()
|
||
: '';
|
||
if (canvasAssetId.startsWith('object:') && !canvasAssetObjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '请提供 object: 后面的资产对象 ID。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.asset_import',
|
||
localPath,
|
||
canvasProjectId,
|
||
canvasAssetId: canvasAssetObjectId ? '' : canvasAssetId,
|
||
canvasAssetObjectId,
|
||
kind: parseGameCreationAppAssetKind(
|
||
kind ?? 'unknown',
|
||
'chat.canvas-asset-import.kind',
|
||
),
|
||
mediaType: mediaType ?? 'application/octet-stream',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备导入画板资产:${localPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/import-canvas-export ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [exportPath, canvasProjectId] = prompt
|
||
.slice('/import-canvas-export '.length)
|
||
.trim()
|
||
.split(/\s+/);
|
||
if (!exportPath || !canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
!isAbsoluteProjectPath(exportPath) ||
|
||
projectPathHasControlCharacter(exportPath)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '画板导出 ZIP 路径必须是绝对路径。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.export_import',
|
||
exportPath,
|
||
canvasProjectId,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备导入画板导出包:${exportPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/')) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `未知命令:${prompt}。输入 /help 查看可用命令。`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
const clientTurnId = createDirectCodexConversationTurnId();
|
||
const userItem = chatComposerDraftToDirectCodexUserItem(
|
||
{ text: prompt, references, content: chatContent },
|
||
directCodexConversationMessageId(clientTurnId, 'user'),
|
||
);
|
||
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
|
||
}
|
||
|
||
async function executeLlmConfigStatus() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
setLlmConfigStatus(status);
|
||
setCommandLog((current) => [...current, 'llm.config_check']);
|
||
const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine);
|
||
const summary = status.configured
|
||
? `${formatLlmRouteEndpoint(status)}。`
|
||
: `官方智能服务未就绪;请登录或重新登录后重试。${formatCodexRuntimeCapabilities(status)},账号状态 ${
|
||
status.accountCredentialState === 'login_required'
|
||
? '需要登录'
|
||
: '暂不可用'
|
||
}。`;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [summary, ...agentLines].join('\n'),
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeLlmRouteSummary() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
setLlmConfigStatus(status);
|
||
setCommandLog((current) => [...current, 'llm.route_summary']);
|
||
const summary = summarizeAgentLlmRoutes(status);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeAgentCapabilitiesChat() {
|
||
const invoke = resolveTauriInvoke();
|
||
let capabilities: readonly GameCreationAgentCapabilityDescriptor[] =
|
||
GAME_CREATION_AGENT_CAPABILITIES;
|
||
if (invoke) {
|
||
try {
|
||
const nativeCapabilities = await invoke<
|
||
GameCreationAgentCapabilityDescriptor[]
|
||
>('get_game_creation_agent_capabilities');
|
||
if (nativeCapabilities.length > 0) {
|
||
capabilities = nativeCapabilities;
|
||
}
|
||
} catch {
|
||
capabilities = GAME_CREATION_AGENT_CAPABILITIES;
|
||
}
|
||
}
|
||
setCommandLog((current) => [...current, 'agent.capabilities']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeAgentCapabilities(capabilities) },
|
||
]);
|
||
}
|
||
|
||
async function resolveProjectSupervisorSessionForSubmission(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
const currentSessionId = projectSupervisorSessionIdRef.current;
|
||
if (currentSessionId) {
|
||
return currentSessionId;
|
||
}
|
||
const sessionId = await ensureProjectSupervisorActiveSessionId(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
if (!sessionId) {
|
||
throw new Error('项目总控 Agent active Session 不可用');
|
||
}
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return null;
|
||
}
|
||
projectSupervisorSessionIdRef.current = sessionId;
|
||
setProjectSupervisorSessionId(sessionId);
|
||
return sessionId;
|
||
}
|
||
|
||
async function refreshDirectProjectManifest(nextProjectPath: string) {
|
||
try {
|
||
await refreshManifest(nextProjectPath);
|
||
} catch {
|
||
// The direct turn result is still authoritative for the chat. A missing
|
||
// or legacy manifest must not hide the Codex reply.
|
||
}
|
||
}
|
||
|
||
async function executeChatAgentReply({
|
||
prompt,
|
||
clientTurnId: directConversationTurnId,
|
||
creationType,
|
||
attachments,
|
||
directPolicyChecked = false,
|
||
references,
|
||
userItem,
|
||
}: ExecuteChatAgentReplyInput) {
|
||
if (designAgentActiveRef.current) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectSupervisorRuntimeError('请先初始化本地项目');
|
||
return;
|
||
}
|
||
if (designAgentActiveRef.current) {
|
||
await executeDesignAgentTurn(
|
||
nextProjectPath,
|
||
{ type: 'message', text: prompt },
|
||
directConversationTurnId ?? createAgentChatRunId('design-agent-turn'),
|
||
);
|
||
return;
|
||
}
|
||
return;
|
||
}
|
||
// Product default: send the conversation directly to Codex app-server.
|
||
// The legacy Supervisor/harness path remains below for rollback and tests.
|
||
if (directCodexProductRuntime) {
|
||
const directInvoke = resolveTauriInvoke();
|
||
// Capture the project snapshot before any asynchronous policy/session work.
|
||
// `resolveChatProjectPath` only returns a path and TypeScript cannot infer
|
||
// that the source project is still non-null after an await; keeping the
|
||
// immutable snapshot also prevents a project switch from changing the
|
||
// projectId used by this turn halfway through submission.
|
||
const directProject = localProject;
|
||
const directProjectPath = resolveChatProjectPath(directProject);
|
||
const directProjectId = directProject?.manifest.projectId;
|
||
if (directProjectPath && directProjectId && directInvoke) {
|
||
const clientTurnId =
|
||
directConversationTurnId ?? createDirectCodexConversationTurnId();
|
||
const effectiveUserItem =
|
||
userItem ??
|
||
chatComposerDraftToDirectCodexUserItem(
|
||
{ text: prompt, references: references ?? [], content: [] },
|
||
directCodexConversationMessageId(clientTurnId, 'user'),
|
||
);
|
||
if (
|
||
!directPolicyChecked &&
|
||
projectConversationWriteConfirmedRef.current !== directProjectPath
|
||
) {
|
||
// 首轮的入参整体留一份给「确认后重跑」用,不在回调里重列字段。
|
||
const policyRetryInput = directCodexPolicyRetryInput({
|
||
prompt,
|
||
clientTurnId,
|
||
creationType,
|
||
attachments,
|
||
references,
|
||
userItem: effectiveUserItem,
|
||
});
|
||
try {
|
||
const policyPaused = await queueProjectPolicyConfirmationIfNeeded(
|
||
directInvoke,
|
||
'conversation.write',
|
||
directProjectPath,
|
||
'写入 DirectProject 对话历史',
|
||
'DirectProject 对话写入需要确认。',
|
||
() => {
|
||
projectConversationWriteConfirmedRef.current =
|
||
directProjectPath;
|
||
void executeChatAgentReply(policyRetryInput);
|
||
},
|
||
);
|
||
if (policyPaused) {
|
||
return;
|
||
}
|
||
projectConversationWriteConfirmedRef.current = directProjectPath;
|
||
} catch (error) {
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
setProjectSupervisorRuntimeError(
|
||
`DirectProject 对话权限检查失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
const directUserMessageId = directCodexConversationMessageId(
|
||
clientTurnId,
|
||
'user',
|
||
);
|
||
const directAssistantMessageId = directCodexConversationMessageId(
|
||
clientTurnId,
|
||
'assistant',
|
||
);
|
||
const appendDirectUserMessageIfMissing = (
|
||
current: ChatMessage[],
|
||
): ChatMessage[] => {
|
||
if (
|
||
current.some((message) => message.messageId === directUserMessageId)
|
||
) {
|
||
return current;
|
||
}
|
||
let optimisticIndex = -1;
|
||
for (let index = current.length - 1; index >= 0; index -= 1) {
|
||
const message = current[index];
|
||
if (
|
||
message?.role === 'user' &&
|
||
message.text === prompt &&
|
||
!message.messageId
|
||
) {
|
||
optimisticIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
if (optimisticIndex >= 0) {
|
||
return current.map((message, index) =>
|
||
index === optimisticIndex
|
||
? {
|
||
...message,
|
||
runtimeOwned: true,
|
||
messageId: directUserMessageId,
|
||
updatedAt: Date.now(),
|
||
}
|
||
: message,
|
||
);
|
||
}
|
||
return [
|
||
...current,
|
||
{
|
||
role: 'user' as const,
|
||
text: prompt,
|
||
runtimeOwned: true,
|
||
messageId: directUserMessageId,
|
||
updatedAt: Date.now(),
|
||
},
|
||
];
|
||
};
|
||
const appendDirectAssistantMessage = (
|
||
current: ChatMessage[],
|
||
text: string,
|
||
failed = false,
|
||
): ChatMessage[] => {
|
||
const messageId = failed
|
||
? `direct-codex:${clientTurnId}:failure`
|
||
: directAssistantMessageId;
|
||
const nextMessage: ChatMessage = {
|
||
role: 'assistant',
|
||
text,
|
||
runtimeOwned: true,
|
||
messageId,
|
||
updatedAt: Date.now(),
|
||
};
|
||
const withUser = appendDirectUserMessageIfMissing(current);
|
||
const existingIndex = withUser.findIndex(
|
||
(message) => message.messageId === messageId,
|
||
);
|
||
if (existingIndex < 0) {
|
||
return [...withUser, nextMessage];
|
||
}
|
||
return withUser.map((message, index) =>
|
||
index === existingIndex ? nextMessage : message,
|
||
);
|
||
};
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const directTurnInput: {
|
||
projectPath: string;
|
||
prompt: string;
|
||
clientTurnId: string;
|
||
creationType?: HomeCreationType;
|
||
attachments?: DirectCodexTurnAttachment[];
|
||
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
||
} = {
|
||
projectPath: directProjectPath,
|
||
prompt,
|
||
clientTurnId,
|
||
userItem: effectiveUserItem,
|
||
};
|
||
if (creationType) {
|
||
directTurnInput.creationType = creationType;
|
||
}
|
||
if (attachments?.length) {
|
||
directTurnInput.attachments = attachments;
|
||
}
|
||
directTurnInput.userItem = effectiveUserItem;
|
||
// 回复正文按条目身份从线程事件 / 历史切片进聊天,本地不再补一条,
|
||
// 因此这里只等回合跑完,不接返回值。
|
||
await withDirectCodexSessionRefresh(() => {
|
||
return directInvoke<string>(
|
||
'chat_with_game_creator_direct_codex',
|
||
directTurnInput,
|
||
);
|
||
});
|
||
// Rust already persisted the complete raw response items. Invalidate
|
||
// any history snapshot captured before the turn completed.
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||
}
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
await refreshDirectProjectManifest(directProjectPath);
|
||
}
|
||
} catch (error) {
|
||
if (
|
||
isDirectCodexTurnAlreadyRunningError(error) ||
|
||
isDirectCodexAnotherTurnRunningError(error)
|
||
) {
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
setProjectSupervisorRuntimeError(
|
||
'陶泥儿仍在处理上一条消息,可在输入盒点「终止」结束它,或等它结束后再发送。',
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
if (localProjectPathRef.current !== directProjectPath) {
|
||
return;
|
||
}
|
||
if (isDirectCodexTurnInterruptedError(error)) {
|
||
// 用户主动终止:不是失败,不写运行错误与诊断,只把回合标记成已终止。
|
||
setProjectSupervisorRuntimeError('');
|
||
setChatComposerNotice('已终止本次回合');
|
||
setMessages((current) =>
|
||
appendDirectAssistantMessage(current, '已终止本次回合。', true),
|
||
);
|
||
return;
|
||
}
|
||
void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID);
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
let persistedDetail = '';
|
||
const detailRef = message.match(
|
||
/详情:(\.agent\/runtime\/errors\/[^\s;]+)/,
|
||
)?.[1];
|
||
if (detailRef && directInvoke) {
|
||
try {
|
||
persistedDetail = await directInvoke<string>(
|
||
'read_agent_runtime_error_detail',
|
||
{
|
||
projectPath: directProjectPath,
|
||
detailRef,
|
||
},
|
||
);
|
||
} catch {
|
||
persistedDetail = '';
|
||
}
|
||
}
|
||
const visibleMessage = projectRuntimeVisibleError(
|
||
persistedDetail ? `${message}\n\n${persistedDetail}` : message,
|
||
'陶泥儿智能创作',
|
||
true,
|
||
);
|
||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
setProjectSupervisorRuntimeError(visibleMessage);
|
||
setMessages((current) =>
|
||
appendDirectAssistantMessage(current, visibleMessage, true),
|
||
);
|
||
}
|
||
} finally {
|
||
try {
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
await refreshManifest(directProjectPath);
|
||
}
|
||
} finally {
|
||
if (localProjectPathRef.current === directProjectPath) {
|
||
setChatAgentBusy(false);
|
||
setDirectCodexTurnCancelling(false);
|
||
chatAgentBusyRef.current = false;
|
||
// 本地 invoke 正常收尾仍可直接推进队列;恢复场景则由 turn.completed effect 推进。
|
||
dispatchNextQueuedChatTurn();
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
const message = directInvoke
|
||
? '当前项目尚未准备好,无法启动智能创作。'
|
||
: '需要在 Tauri App 内运行,无法启动智能创作。';
|
||
setProjectSupervisorRuntimeError(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: message,
|
||
runtimeOwned: true,
|
||
...(directConversationTurnId
|
||
? {
|
||
messageId: directCodexConversationMessageId(
|
||
directConversationTurnId,
|
||
'assistant',
|
||
),
|
||
}
|
||
: {}),
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const sessionId = await resolveProjectSupervisorSessionForSubmission(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
if (!sessionId || localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
|
||
const submissionRoute = resolveProjectSupervisorRuntimeSubmission({
|
||
workspaceProjectKind,
|
||
orchestrationMode,
|
||
supervisorChatOnly,
|
||
});
|
||
const submission = await submitProjectSupervisorRuntimeTask({
|
||
invoke,
|
||
projectPath: nextProjectPath,
|
||
sessionId,
|
||
prompt,
|
||
runtime: runtimeAtSubmission,
|
||
runProfile: submissionRoute.runProfile,
|
||
source: submissionRoute.source,
|
||
});
|
||
const runtimeResult = submission.runtimeResult;
|
||
const acceptedRunId = submission.acceptedRunId.trim();
|
||
if (!acceptedRunId) {
|
||
throw new Error('项目总控 Agent 请求未进入后台队列');
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${submission.mode} project-supervisor`,
|
||
]);
|
||
const runtime = agentRuntimeStateFromResult(
|
||
runtimeResult,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
runtime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
runtime.sessionId !== sessionId
|
||
) {
|
||
throw new Error('项目总控 Agent Runtime Session 身份不匹配');
|
||
}
|
||
const acceptedRuntimeReady = runtime.runId === acceptedRunId;
|
||
if (!acceptedRuntimeReady) {
|
||
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
|
||
setProjectSupervisorExpectedRunId(acceptedRunId);
|
||
}
|
||
if (acceptedRuntimeReady) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
updateProjectSupervisorRuntime(runtime);
|
||
updateProjectSupervisorResponseStream(
|
||
runtimeResult.responseStream,
|
||
runtime,
|
||
);
|
||
}
|
||
setProjectSupervisorRuntimeError('');
|
||
const refreshConversation =
|
||
projectSupervisorRefreshConversationRef.current;
|
||
if (refreshConversation) {
|
||
void refreshConversation(invoke, nextProjectPath, sessionId).catch(
|
||
(error) => {
|
||
if (
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectSupervisorSessionIdRef.current === sessionId
|
||
) {
|
||
setProjectSupervisorRuntimeError(
|
||
`项目总控 Agent 对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
if (acceptedRuntimeReady) {
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
runtime,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID);
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: message,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
executeChatAgentReplyRef.current = executeChatAgentReply;
|
||
|
||
useEffect(() => {
|
||
const latch = initialSupervisorMessageLatchRef.current;
|
||
if (
|
||
(!directCodexProductRuntime && !planningStartMode) ||
|
||
!latch.prompt ||
|
||
!localProject
|
||
) {
|
||
return;
|
||
}
|
||
if (localProject.projectPath !== latch.projectPath) {
|
||
// 不能在这里先"占用"这条初始消息:项目路径可能因为分隔符/大小写/时序先落到别的
|
||
// 路径上,一旦占用,真正匹配的项目就再也不会收到这条消息,用户的输入被静默丢掉。
|
||
// 这里只等待,占用留给下面真正要发送的那一步。
|
||
return;
|
||
}
|
||
if (
|
||
chatAgentBusy ||
|
||
!claimInitialSupervisorMessageForPage(latch.projectPath, latch.claimScope)
|
||
) {
|
||
return;
|
||
}
|
||
supervisorChatShouldFollowLatestRef.current = true;
|
||
const directConversationTurnId = directCodexProductRuntime
|
||
? createDirectCodexConversationTurnId()
|
||
: undefined;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: latch.prompt,
|
||
runtimeOwned: true,
|
||
...(directConversationTurnId
|
||
? {
|
||
messageId: directCodexConversationMessageId(
|
||
directConversationTurnId,
|
||
'user',
|
||
),
|
||
}
|
||
: {}),
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
void executeChatAgentReplyRef.current({
|
||
prompt: latch.prompt,
|
||
clientTurnId: directConversationTurnId,
|
||
creationType: latch.creationType,
|
||
attachments: latch.attachments,
|
||
});
|
||
}, [
|
||
chatAgentBusy,
|
||
directCodexProductRuntime,
|
||
initialCreationType,
|
||
initialSupervisorMessage,
|
||
localProject,
|
||
planningStartMode,
|
||
]);
|
||
|
||
async function handleProjectSupervisorToolAction(
|
||
decision: 'confirm' | 'reject',
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const runtime = projectSupervisorRuntimeRef.current;
|
||
const pendingAction = runtime?.pendingToolAction;
|
||
const sessionId = projectSupervisorSessionIdRef.current;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!runtime ||
|
||
!pendingAction?.actionId ||
|
||
!sessionId ||
|
||
chatAgentBusy
|
||
) {
|
||
return;
|
||
}
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const command =
|
||
decision === 'confirm'
|
||
? 'confirm_game_creator_agent_runtime_task'
|
||
: 'reject_game_creator_agent_runtime_task';
|
||
const result = await invoke<AgentRuntimeResult>(command, {
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: runtime.runId,
|
||
actionId: pendingAction.actionId,
|
||
note:
|
||
decision === 'confirm'
|
||
? '用户已确认待执行工具动作'
|
||
: '用户已拒绝待执行工具动作',
|
||
});
|
||
const nextRuntime = agentRuntimeStateFromResult(
|
||
result,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId ||
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== runtime.runId
|
||
) {
|
||
return;
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${decision} project-supervisor`,
|
||
]);
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectProfessionalAgentToolAction(
|
||
runtime: AgentRuntimeState,
|
||
decision: 'confirm' | 'reject',
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = agentRuntimeById[runtime.agentId];
|
||
const pendingAction = currentRuntime?.pendingToolAction;
|
||
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!currentRuntime ||
|
||
!pendingAction?.actionId ||
|
||
chatAgentBusy
|
||
) {
|
||
throw new Error('专业 Agent 待确认动作已变化,请等待状态刷新');
|
||
}
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
pendingAction.actionId !== runtime.pendingToolAction?.actionId ||
|
||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
currentRuntime.parentRunId !== supervisorRunId
|
||
) {
|
||
throw new Error('专业 Agent Runtime 身份已变化,请等待状态刷新');
|
||
}
|
||
setChatAgentBusy(true);
|
||
try {
|
||
const command =
|
||
decision === 'confirm'
|
||
? 'confirm_game_creator_agent_runtime_task'
|
||
: 'reject_game_creator_agent_runtime_task';
|
||
const result = await invoke<AgentRuntimeResult>(command, {
|
||
projectPath: nextProjectPath,
|
||
agentId: currentRuntime.agentId,
|
||
runId: currentRuntime.runId,
|
||
actionId: pendingAction.actionId,
|
||
note:
|
||
decision === 'confirm'
|
||
? '用户已确认专业 Agent 待执行工具动作'
|
||
: '用户已拒绝专业 Agent 待执行工具动作',
|
||
});
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== currentRuntime.agentId ||
|
||
nextRuntime.runId !== currentRuntime.runId
|
||
) {
|
||
throw new Error('专业 Agent 操作后 Runtime 身份不匹配');
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${decision} ${currentRuntime.agentId}`,
|
||
]);
|
||
} catch (error) {
|
||
throw new Error(
|
||
`专业 Agent 操作失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectProfessionalAgentRetry(
|
||
runtime: AgentRuntimeState,
|
||
): Promise<string> {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = agentRuntimeById[runtime.agentId];
|
||
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
|
||
throw new Error('专业 Agent 状态已变化,请等待刷新后重试');
|
||
}
|
||
const supervisorRuntime = projectSupervisorRuntimeRef.current;
|
||
if (!supervisorRuntime || isAgentRuntimeTerminalState(supervisorRuntime)) {
|
||
throw new Error('项目总控已停止,请先重试项目总控');
|
||
}
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
currentRuntime.parentRunId !== supervisorRunId ||
|
||
!(
|
||
currentRuntime.status === 'failed' || currentRuntime.phase === 'failed'
|
||
) ||
|
||
currentRuntime.pendingToolAction
|
||
) {
|
||
throw new Error('专业 Agent 当前状态不允许直接重试');
|
||
}
|
||
if (
|
||
projectSupervisorPendingRepairMatchesProfessional(
|
||
projectSupervisorRuntimeRef.current?.pendingToolAction,
|
||
currentRuntime,
|
||
)
|
||
) {
|
||
await handleProjectSupervisorToolAction('confirm');
|
||
return `已确认项目总控安排${projectProfessionalAgentLabel(
|
||
currentRuntime.agentId,
|
||
)}返工,正在同步新一轮状态`;
|
||
}
|
||
setChatAgentBusy(true);
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'confirm_retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: currentRuntime.agentId,
|
||
runId: currentRuntime.runId,
|
||
nextRunId: createAgentChatRunId('project-professional-retry'),
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的重试状态合并到当前界面';
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== currentRuntime.agentId ||
|
||
nextRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.parentRunId !== supervisorRunId
|
||
) {
|
||
throw new Error('专业 Agent 重试后的 Runtime 身份不匹配');
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.retry ${currentRuntime.agentId}`,
|
||
]);
|
||
return '重试请求已受理,正在同步新一轮状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`专业 Agent 重试失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectSupervisorRetry(
|
||
runtime: AgentRuntimeState,
|
||
): Promise<string> {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
|
||
throw new Error('项目总控状态已变化,请等待刷新后重试');
|
||
}
|
||
const needsReconciliation =
|
||
currentRuntime.status === 'needs-reconciliation' ||
|
||
currentRuntime.phase === 'needs-reconciliation';
|
||
if (needsReconciliation) {
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID
|
||
) {
|
||
throw new Error('项目总控待核对任务已变化,请等待刷新');
|
||
}
|
||
const previousRunId = currentRuntime.runId;
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'cancel_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: previousRunId,
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的取消状态合并到当前界面';
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.runId !== previousRunId ||
|
||
nextRuntime.sessionId !== currentRuntime.sessionId
|
||
) {
|
||
throw new Error('项目总控取消后的 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.cancel project-supervisor reconciliation',
|
||
]);
|
||
const queuePending = nextRuntime.taskQueue?.pending ?? 0;
|
||
if (
|
||
nextRuntime.status === 'cancelled' ||
|
||
nextRuntime.phase === 'cancelled'
|
||
) {
|
||
return queuePending > 0
|
||
? `旧任务已结束,队列中还有 ${queuePending} 个待处理任务,队列将继续处理`
|
||
: '旧任务已结束,当前队列为空,可重新启动项目总控';
|
||
}
|
||
return '已提交结束旧任务请求,正在同步取消状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`项目总控旧任务结束失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
const cancelledWithEmptyQueue =
|
||
(currentRuntime.status === 'cancelled' ||
|
||
currentRuntime.phase === 'cancelled') &&
|
||
(currentRuntime.taskQueue?.pending ?? 0) === 0;
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
!(
|
||
currentRuntime.status === 'failed' ||
|
||
currentRuntime.phase === 'failed' ||
|
||
cancelledWithEmptyQueue
|
||
) ||
|
||
currentRuntime.pendingToolAction
|
||
) {
|
||
throw new Error('项目总控当前状态不允许重试');
|
||
}
|
||
const previousRunId = currentRuntime.runId;
|
||
const nextRunId = createAgentChatRunId('project-supervisor-retry');
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'confirm_retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: previousRunId,
|
||
nextRunId,
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的重试状态合并到当前界面';
|
||
}
|
||
const inferredRunId = agentRuntimeStartedRunId(result, nextRunId);
|
||
const acceptedRunId =
|
||
result.acceptedRunId?.trim() ||
|
||
(inferredRunId === nextRunId ? nextRunId : '');
|
||
if (!acceptedRunId) {
|
||
throw new Error('项目总控重试请求未进入后台队列');
|
||
}
|
||
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
|
||
setProjectSupervisorExpectedRunId(acceptedRunId);
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (nextRuntime.runId === acceptedRunId) {
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.sessionId !== currentRuntime.sessionId
|
||
) {
|
||
throw new Error('项目总控重试后的 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.retry project-supervisor',
|
||
]);
|
||
return nextRuntime.runId === acceptedRunId
|
||
? '已在当前项目重新启动项目总控'
|
||
: '重试已受理,正在同步新一轮项目总控状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`项目总控重试失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectSupervisorUserInput(
|
||
request: AgentRuntimeUserInputRequest,
|
||
responseId: string,
|
||
answers: Record<string, string>,
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const runtime = projectSupervisorRuntimeRef.current;
|
||
const sessionId = projectSupervisorSessionIdRef.current;
|
||
const currentRequest = runtime?.userInputRequest;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!runtime ||
|
||
!sessionId ||
|
||
!responseId ||
|
||
chatAgentBusy
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
request.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
request.sessionId !== sessionId ||
|
||
request.runId !== runtime.runId ||
|
||
currentRequest?.requestId !== request.requestId ||
|
||
currentRequest?.actionId !== request.actionId
|
||
) {
|
||
setProjectSupervisorRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
|
||
return;
|
||
}
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'answer_game_creator_agent_runtime_user_input',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: request.runId,
|
||
actionId: request.actionId,
|
||
requestId: request.requestId,
|
||
responseId,
|
||
answers,
|
||
},
|
||
);
|
||
const nextRuntime = agentRuntimeStateFromResult(
|
||
result,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== request.runId
|
||
) {
|
||
throw new Error('项目总控 Agent 回答后 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
|
||
const refreshConversation =
|
||
projectSupervisorRefreshConversationRef.current;
|
||
if (refreshConversation) {
|
||
await refreshConversation(invoke, nextProjectPath, sessionId);
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.user_input.answered project-supervisor',
|
||
]);
|
||
setProjectSupervisorRuntimeError('');
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(`回答提交失败:${message}`);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function executeGameDraft(prompt: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setCommandLog((current) => [...current, 'game.generate_draft']);
|
||
setProjectStatus('正在调用 LLM');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: gameDraftStartedMessage() },
|
||
]);
|
||
const result = await invoke<GenerateLocalGameDraftResult>(
|
||
'generate_local_game_draft',
|
||
{ projectPath: nextProjectPath, prompt },
|
||
);
|
||
const generatedProjectPath = result.projectPath.trim();
|
||
if (
|
||
!generatedProjectPath ||
|
||
!isAbsoluteProjectPath(generatedProjectPath) ||
|
||
projectPathHasControlCharacter(generatedProjectPath)
|
||
) {
|
||
const message = '生成结果项目路径无效';
|
||
setProjectStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return;
|
||
}
|
||
setLocalProject({
|
||
projectPath: generatedProjectPath,
|
||
manifestPath: `${generatedProjectPath}/.agent/manifest.json`,
|
||
manifest: result.manifest,
|
||
});
|
||
setManifest(result.manifest);
|
||
setProjectStatus('已生成可试玩原型');
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'memory.write',
|
||
'file.write game/index.html',
|
||
]);
|
||
const completionSummary =
|
||
await refreshAgentRunTrace(generatedProjectPath);
|
||
|
||
try {
|
||
const previewResult = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: generatedProjectPath },
|
||
);
|
||
updateClientPreview(previewResult);
|
||
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
|
||
setCommandLog((current) => [...current, 'preview.start']);
|
||
void refreshManifest(generatedProjectPath);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
`已保存并在客户端运行视图启动预览:${previewResult.url}`,
|
||
completionSummary?.text,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n\n'),
|
||
draftCommand: completionSummary?.draftCommand,
|
||
},
|
||
]);
|
||
} catch (previewError) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
`已保存草案:${result.designPath}。预览启动失败:${
|
||
previewError instanceof Error
|
||
? previewError.message
|
||
: String(previewError)
|
||
}`,
|
||
completionSummary?.text,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n\n'),
|
||
draftCommand: completionSummary?.draftCommand,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
void refreshAgentRunTrace(nextProjectPath);
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: message,
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function markPendingCommandCanceled(command: PendingCommand) {
|
||
if (command.id === 'project.create') {
|
||
setWorkspaceStatus('已取消');
|
||
setProjectStatus('已取消');
|
||
return;
|
||
}
|
||
if (command.id === 'game.generate_draft') {
|
||
setProjectStatus('已取消生成游戏草案');
|
||
return;
|
||
}
|
||
if (
|
||
command.id === 'game.run_local' ||
|
||
command.id === 'command.run_limited'
|
||
) {
|
||
setLimitedCommandStatus('已取消运行');
|
||
return;
|
||
}
|
||
if (command.id === 'project.checkpoint') {
|
||
setFileStatus('已取消保存 checkpoint');
|
||
return;
|
||
}
|
||
if (command.id === 'project.export_package') {
|
||
setFileStatus('已取消导出本地试玩包');
|
||
return;
|
||
}
|
||
if (command.id === 'project.index') {
|
||
setFileStatus('已取消索引项目');
|
||
setWorkspaceStatus('已取消索引项目');
|
||
return;
|
||
}
|
||
if (command.id === 'project.restore') {
|
||
setFileStatus('已取消回滚项目');
|
||
return;
|
||
}
|
||
if (command.id === 'project.policy_write') {
|
||
setProjectStatus('已取消修改权限策略');
|
||
return;
|
||
}
|
||
if (command.id === 'preview.start' || command.id === 'preview.open') {
|
||
setPreviewStatus('已取消预览操作');
|
||
setWorkspaceStatus('已取消预览操作');
|
||
return;
|
||
}
|
||
if (
|
||
command.id === 'agent.kill' ||
|
||
command.id === 'agent.retry' ||
|
||
command.id === 'agent.resume'
|
||
) {
|
||
setAgentRunStatus('已取消 Agent run 操作');
|
||
return;
|
||
}
|
||
if (command.id === 'memory.write') {
|
||
setMemoryStatus('已取消写入项目记忆');
|
||
return;
|
||
}
|
||
if (command.id === 'memory.delete') {
|
||
setMemoryStatus('已取消删除项目记忆');
|
||
return;
|
||
}
|
||
if (command.id === 'asset.upload') {
|
||
setAssetStatus('已取消上传资产');
|
||
return;
|
||
}
|
||
if (command.id === 'asset.register') {
|
||
setAssetStatus('已取消登记资产');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.project_open') {
|
||
setAssetStatus('已取消打开画板');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.project_sync') {
|
||
setAssetStatus('已取消同步画板项目');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.asset_import') {
|
||
setAssetStatus('已取消导入画板资产');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.asset_generate') {
|
||
setAssetStatus('已取消生成美术素材');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.export_import') {
|
||
setAssetStatus('已取消导入画板导出包');
|
||
}
|
||
}
|
||
|
||
function handlePendingCommandCancel() {
|
||
const command = pendingCommand;
|
||
setPendingCommand(null);
|
||
if (command) {
|
||
markPendingCommandCanceled(command);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.cancel',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${command.id}`,
|
||
]);
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消。' },
|
||
]);
|
||
}
|
||
|
||
async function handlePendingCommandConfirm() {
|
||
const command = pendingCommand;
|
||
if (!command) {
|
||
return;
|
||
}
|
||
if (
|
||
needsInitializedChatProject(command.id) &&
|
||
!resolveChatProjectPath(localProject)
|
||
) {
|
||
setPendingCommand(null);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.cancel',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${command.id} missing-project`,
|
||
]);
|
||
requireChatProjectForUserAction();
|
||
return;
|
||
}
|
||
if (
|
||
await denyPendingCommandIfNeeded(
|
||
command.id,
|
||
resolvePermissionLogProjectPath(command),
|
||
)
|
||
) {
|
||
setPendingCommand(null);
|
||
return;
|
||
}
|
||
setPendingCommand(null);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.confirm',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.confirm ${command.id}`,
|
||
]);
|
||
if (command.id === 'asset.upload') {
|
||
void executeAssetUpload(command.file);
|
||
} else if (command.id === 'asset.register') {
|
||
void executeAssetRegister(
|
||
resolveChatProjectPath(localProject) ?? '',
|
||
command.localPath,
|
||
command.kind,
|
||
command.mediaType,
|
||
'generated',
|
||
'',
|
||
'',
|
||
'',
|
||
true,
|
||
);
|
||
} else if (command.id === 'game.run_local') {
|
||
void executeRunLocal(true);
|
||
} else if (command.id === 'command.run_limited') {
|
||
void executeLimitedCommand(command.commandId, true);
|
||
} else if (command.id === 'project.create') {
|
||
void executeProjectCreate(command.projectPath, true);
|
||
} else if (command.id === 'project.checkpoint') {
|
||
void executeProjectCheckpoint(true);
|
||
} else if (command.id === 'project.export_package') {
|
||
void executeProjectExportPackage(true);
|
||
} else if (command.id === 'project.index') {
|
||
void executeProjectIndex(true);
|
||
} else if (command.id === 'project.restore') {
|
||
void executeProjectRestore(command.checkpointId, true);
|
||
} else if (command.id === 'project.policy_write') {
|
||
void executeProjectPolicyWrite(command.policy, true);
|
||
} else if (command.id === 'preview.start') {
|
||
void executePreviewStart(true);
|
||
} else if (command.id === 'preview.open') {
|
||
void executePreviewOpen(true);
|
||
} else if (
|
||
command.id === 'agent.kill' ||
|
||
command.id === 'agent.retry' ||
|
||
command.id === 'agent.resume'
|
||
) {
|
||
void executeAgentRunControl(
|
||
command.id.slice('agent.'.length),
|
||
command.detail,
|
||
true,
|
||
);
|
||
} else if (command.id === 'memory.write') {
|
||
void executeMemoryWrite(
|
||
command.scope,
|
||
command.content,
|
||
command.mode ?? 'append',
|
||
);
|
||
} else if (command.id === 'memory.delete') {
|
||
void executeMemoryDeleteChat(command.scope);
|
||
} else if (command.id === 'canvas.project_open') {
|
||
void executeCanvasProjectOpen(command.canvasProjectId, true);
|
||
} else if (command.id === 'canvas.project_sync') {
|
||
void executeCanvasProjectSync(command.canvasProjectId, true);
|
||
} else if (command.id === 'canvas.asset_import') {
|
||
void executeCanvasAssetImport(
|
||
null,
|
||
command.localPath,
|
||
command.kind,
|
||
command.mediaType,
|
||
command.canvasProjectId,
|
||
command.canvasAssetId,
|
||
command.canvasAssetObjectId ?? '',
|
||
true,
|
||
);
|
||
} else if (command.id === 'canvas.asset_generate') {
|
||
void executeCanvasAssetGenerate(command.prompt, true);
|
||
} else if (command.id === 'canvas.export_import') {
|
||
void executeCanvasExportImport(
|
||
command.exportPath,
|
||
command.canvasProjectId,
|
||
true,
|
||
);
|
||
} else if (command.id === 'game.generate_draft') {
|
||
void executeGameDraft(command.prompt);
|
||
}
|
||
}
|
||
|
||
async function executeProjectCreate(
|
||
nextProjectPath: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const trimmedProjectPath = nextProjectPath.trim();
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
invoke &&
|
||
trimmedProjectPath &&
|
||
isAbsoluteProjectPath(trimmedProjectPath) &&
|
||
!projectPathHasControlCharacter(trimmedProjectPath)
|
||
) {
|
||
try {
|
||
const nonEmpty = await invoke<boolean>(
|
||
'is_local_project_directory_non_empty',
|
||
{ projectPath: trimmedProjectPath },
|
||
);
|
||
if (nonEmpty) {
|
||
setPendingNonEmptyProjectCreate({
|
||
projectPath: trimmedProjectPath,
|
||
announceToChat,
|
||
});
|
||
setWorkspaceStatus('目标文件夹不是空的');
|
||
setProjectStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch {
|
||
// ponytail: stale test doubles and older shells may miss this helper; init still validates.
|
||
}
|
||
}
|
||
await openWorkspace(nextProjectPath, announceToChat);
|
||
}
|
||
|
||
function cancelProjectCreateInNonEmptyFolder() {
|
||
const pendingCreate = pendingNonEmptyProjectCreate;
|
||
setPendingNonEmptyProjectCreate(null);
|
||
setWorkspaceStatus('已取消');
|
||
setProjectStatus('已取消');
|
||
if (pendingCreate?.announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已取消在非空文件夹中新建项目:${pendingCreate.projectPath}`,
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function confirmProjectCreateInNonEmptyFolder() {
|
||
const pendingCreate = pendingNonEmptyProjectCreate;
|
||
if (!pendingCreate) {
|
||
return;
|
||
}
|
||
setPendingNonEmptyProjectCreate(null);
|
||
void openWorkspace(pendingCreate.projectPath, pendingCreate.announceToChat);
|
||
}
|
||
|
||
async function executeProjectStatus(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.status',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的项目状态`,
|
||
'准备读取项目状态。',
|
||
() => void executeProjectStatus(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'project.status' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setProjectStatus('已读取状态');
|
||
setCommandLog((current) => [...current, 'project.status']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectStatus(nextManifest, nextProjectPath),
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectIndex(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectIndexResult>(
|
||
'build_local_project_index',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已索引 ${result.fileCount} 个文件`);
|
||
setCommandLog((current) => [...current, 'project.index']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectIndex(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function queueOrExecuteProjectIndex() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
void executeProjectIndex(true);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (result.policy.confirmCommands.includes('project.index')) {
|
||
queuePendingCommand({ id: 'project.index' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备刷新本地项目索引。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectIndex(true);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectCheckpoint(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectCheckpointResult>(
|
||
'create_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已保存 checkpoint:${result.checkpointId}`);
|
||
setCommandLog((current) => [...current, 'project.checkpoint']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectCheckpoint(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectExportPackage(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectExportPackageResult>(
|
||
'export_local_project_package',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已导出本地试玩包:${result.packageRelativePath}`);
|
||
setCommandLog((current) => [...current, 'project.export_package']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectExportPackage(result),
|
||
draftCommand: '/open-project',
|
||
draftCommandLabel: '显示目录',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectExportPackages(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.export_list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的本地试玩包`,
|
||
'准备列出本地试玩包。',
|
||
() => void executeProjectExportPackages(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectExportPackagesResult>(
|
||
'list_local_project_export_packages',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已列出 ${result.packages.length} 个本地试玩包`);
|
||
setCommandLog((current) => [...current, 'project.export_list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectExportPackages(result),
|
||
draftCommand:
|
||
result.packages.length > 0 ? '/open-project' : '/export',
|
||
draftCommandLabel:
|
||
result.packages.length > 0 ? '显示目录' : '导出试玩包',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectCheckpoints(
|
||
announceToChat: boolean,
|
||
skipListPolicyConfirm = false,
|
||
skipReadPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipListPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的 checkpoint`,
|
||
'准备列出项目 checkpoint。',
|
||
() => void executeProjectCheckpoints(true, true, false),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const manifestFiles = sortCheckpointManifestFiles(result.files);
|
||
const visibleFiles = manifestFiles.slice(0, 5);
|
||
if (
|
||
announceToChat &&
|
||
visibleFiles.length > 0 &&
|
||
!skipReadPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 checkpoint manifest`,
|
||
'准备读取 checkpoint manifest。',
|
||
() => void executeProjectCheckpoints(true, true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const checkpoints: LocalProjectCheckpointSummary[] = [];
|
||
for (const file of visibleFiles) {
|
||
const manifest = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: file.path,
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
checkpoints.push(checkpointSummaryFromManifest(file, manifest.content));
|
||
}
|
||
setProjectFiles(result.files);
|
||
setProjectCheckpoints(checkpoints);
|
||
setFileStatus(`已列出 ${manifestFiles.length} 个 checkpoint`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'file.list',
|
||
...(visibleFiles.length > 0 ? ['file.read checkpoint manifests'] : []),
|
||
]);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectCheckpoints(
|
||
checkpoints,
|
||
manifestFiles.length - visibleFiles.length,
|
||
),
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectDiff(
|
||
checkpointId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.diff',
|
||
nextProjectPath,
|
||
`对比 ${nextProjectPath} 的 checkpoint:${checkpointId}`,
|
||
'准备对比项目 checkpoint。',
|
||
() => void executeProjectDiff(checkpointId, true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectDiffResult>(
|
||
'diff_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath, checkpointId },
|
||
);
|
||
setFileStatus(`已对比 checkpoint:${result.checkpointId}`);
|
||
setCommandLog((current) => [...current, 'project.diff']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectDiff(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectRestore(
|
||
checkpointId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectRestoreResult>(
|
||
'restore_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath, checkpointId },
|
||
);
|
||
setFileStatus(
|
||
`已回滚 ${result.restoredCount} 个文件,删除 ${result.deletedCount} 个新增文件`,
|
||
);
|
||
setCommandLog((current) => [...current, 'project.restore']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已回滚 ${result.restoredCount} 个文件到 ${nextProjectPath},删除 ${result.deletedCount} 个新增文件:${result.checkpointId}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectPolicyRead(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectStatus('已读取权限策略');
|
||
setCommandLog((current) => [...current, 'project.policy_read']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectPolicy(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function queueProjectPolicyMutation(
|
||
commandId: string,
|
||
mode: 'deny' | 'allow' | 'confirm' | 'auto',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const alreadyDenied = result.policy.deniedCommands.includes(commandId);
|
||
const alreadyConfirmed =
|
||
result.policy.confirmCommands.includes(commandId);
|
||
if (mode === 'deny' && alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令已在拒绝列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'allow' && !alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令不在拒绝列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'confirm' && alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令已在确认列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'auto' && !alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令不在确认列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
const deniedCommands =
|
||
mode === 'deny'
|
||
? [...new Set([...result.policy.deniedCommands, commandId])]
|
||
: mode === 'allow'
|
||
? result.policy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: mode === 'confirm'
|
||
? result.policy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: result.policy.deniedCommands;
|
||
const confirmCommands =
|
||
mode === 'confirm'
|
||
? [...new Set([...result.policy.confirmCommands, commandId])]
|
||
: mode === 'deny'
|
||
? result.policy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: mode === 'auto'
|
||
? result.policy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: result.policy.confirmCommands;
|
||
queuePendingCommand({
|
||
id: 'project.policy_write',
|
||
policy: { ...result.policy, deniedCommands, confirmCommands },
|
||
});
|
||
const action =
|
||
mode === 'deny'
|
||
? '拒绝'
|
||
: mode === 'allow'
|
||
? '允许'
|
||
: mode === 'confirm'
|
||
? '确认'
|
||
: '自动执行';
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备${action}命令:${commandId}`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function queueAgentPolicyMutation(
|
||
agentId: string,
|
||
commandId: string,
|
||
mode: 'deny' | 'allow' | 'confirm' | 'auto',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const agentPolicies = { ...(result.policy.agentPolicies ?? {}) };
|
||
const agentPolicy = agentPolicies[agentId] ?? {
|
||
deniedCommands: [],
|
||
confirmCommands: [],
|
||
};
|
||
const alreadyDenied = agentPolicy.deniedCommands.includes(commandId);
|
||
const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId);
|
||
if (mode === 'deny' && alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'allow' && !alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'confirm' && alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'auto' && !alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
const deniedCommands =
|
||
mode === 'deny'
|
||
? [...new Set([...agentPolicy.deniedCommands, commandId])]
|
||
: mode === 'allow'
|
||
? agentPolicy.deniedCommands.filter((value) => value !== commandId)
|
||
: mode === 'confirm'
|
||
? agentPolicy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: agentPolicy.deniedCommands;
|
||
const confirmCommands =
|
||
mode === 'confirm'
|
||
? [...new Set([...agentPolicy.confirmCommands, commandId])]
|
||
: mode === 'deny'
|
||
? agentPolicy.confirmCommands.filter((value) => value !== commandId)
|
||
: mode === 'auto'
|
||
? agentPolicy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: agentPolicy.confirmCommands;
|
||
agentPolicies[agentId] = {
|
||
deniedCommands,
|
||
confirmCommands,
|
||
};
|
||
queuePendingCommand({
|
||
id: 'project.policy_write',
|
||
policy: {
|
||
...result.policy,
|
||
agentPolicies,
|
||
},
|
||
});
|
||
const action =
|
||
mode === 'deny'
|
||
? '拒绝'
|
||
: mode === 'allow'
|
||
? '允许'
|
||
: mode === 'confirm'
|
||
? '确认'
|
||
: '自动执行';
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备${action} Agent ${agentId} 命令:${commandId}`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectPolicyWrite(
|
||
policy: ProjectPermissionPolicy,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'write_project_permission_policy',
|
||
{ projectPath: nextProjectPath, policy },
|
||
);
|
||
setProjectStatus('已写入权限策略');
|
||
setCommandLog((current) => [...current, 'project.policy_write']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectPolicy(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectFiles(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setFileStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目文件`,
|
||
'准备列出项目文件。',
|
||
() => void executeProjectFiles(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectFiles(result.files);
|
||
setFileStatus(`已列出 ${result.files.length} 项`);
|
||
setCommandLog((current) => [...current, 'file.list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectFiles(result.files) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectAssets(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAssetStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'asset.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目资产`,
|
||
'准备列出项目资产。',
|
||
() => void executeProjectAssets(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'asset.list' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setAssetStatus(`已列出 ${nextManifest.assets.length} 个资产`);
|
||
setCommandLog((current) => [...current, 'asset.list']);
|
||
if (announceToChat) {
|
||
const firstReadableAsset = firstReadableProjectAssetPath(nextManifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectAssets(nextManifest),
|
||
draftCommand: firstReadableAsset
|
||
? `/read ${firstReadableAsset}`
|
||
: undefined,
|
||
draftCommandLabel: firstReadableAsset ? '读取首个资产' : undefined,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectFileReadChat(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setFileStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const commandId = isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read';
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
commandId,
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
'准备读取项目文件。',
|
||
() => void executeProjectFileReadChat(relativePath, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath, commandId },
|
||
);
|
||
setFilePath(result.path);
|
||
setFileDraft(result.content);
|
||
setFileStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.read']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectFileContent(result) },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectTasks(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'task.list',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的任务拆分`,
|
||
'准备读取任务拆分。',
|
||
() => void executeProjectTasks(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'task.list' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setProjectStatus('已读取任务拆分');
|
||
setCommandLog((current) => [...current, 'task.list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectTasks(nextManifest) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeAgentTraceChat(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
'准备读取 Agent run trace。',
|
||
() => void executeAgentTraceChat(true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
const trace = parseAgentRunTrace(result.content);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(formatAgentRunStatus(trace));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.trace_read',
|
||
'file.read .agent/run.latest.json',
|
||
]);
|
||
const traceSummary = summarizeAgentRunCompletionForChat(trace);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', ...traceSummary },
|
||
]);
|
||
} catch (error) {
|
||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||
const message = isMissingAgentRunTraceError(rawMessage)
|
||
? '暂无最近 Agent trace。先生成一次游戏草案后再查看。'
|
||
: rawMessage;
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeAgentRunControl(
|
||
action: string,
|
||
detail: string | undefined,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const commandId =
|
||
action === 'status'
|
||
? 'agent.run_status'
|
||
: (`agent.${action}` as GameCreationAppCommandDescriptor['id']);
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
commandId,
|
||
nextProjectPath,
|
||
action === 'status'
|
||
? `查看 ${nextProjectPath} 的最近 Agent run 状态`
|
||
: `执行 ${nextProjectPath} 的 ${commandId}`,
|
||
action === 'status'
|
||
? '准备查看 Agent run 状态。'
|
||
: '准备执行 Agent run 操作。',
|
||
() =>
|
||
void executeAgentRunControl(action, detail, announceToChat, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<AgentRunControlResult>('control_agent_run', {
|
||
projectPath: nextProjectPath,
|
||
action,
|
||
detail,
|
||
});
|
||
setAgentRunStatus(
|
||
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
commandId,
|
||
'file.write .agent/activity.jsonl',
|
||
'file.write .agent/output.jsonl',
|
||
'file.write .agent/context.bundle.json',
|
||
]);
|
||
if (announceToChat && action === 'status') {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'agent.run_status',
|
||
);
|
||
}
|
||
let traceReadQueued = false;
|
||
if (action === 'status') {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
() => void refreshAgentRunTrace(nextProjectPath),
|
||
);
|
||
traceReadQueued = true;
|
||
}
|
||
}
|
||
if (!traceReadQueued) {
|
||
await refreshAgentRunTrace(nextProjectPath);
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
result.message,
|
||
`run:${result.runId}`,
|
||
`状态:${result.status} / ${result.lifecycleStatus}`,
|
||
`下一步:${result.nextStep}`,
|
||
`事件:${result.activityPath}`,
|
||
`输出:${result.outputPath}`,
|
||
`上下文包:${result.contextBundlePath}`,
|
||
].join('\n'),
|
||
draftCommand: '/read .agent/output.jsonl',
|
||
draftCommandLabel: '读取 Run 输出',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||
const message = formatAgentRunControlError(action, rawMessage);
|
||
setAgentRunStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function queueAgentRunControlFromPanel(action: 'kill' | 'retry' | 'resume') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: `agent.${action}` });
|
||
setAgentRunStatus('等待确认');
|
||
}
|
||
|
||
async function executeAgentAuditChat(
|
||
confirmedPolicyCommands: GameCreationAppCommandDescriptor['id'][] = [],
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!confirmedPolicyCommands.includes('agent.audit') &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'agent.audit',
|
||
nextProjectPath,
|
||
`审计 ${nextProjectPath} 的 Agent 能力证据`,
|
||
'准备审计 Agent 能力证据。',
|
||
() => void executeAgentAuditChat(['agent.audit']),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const auditReadCommands: Array<GameCreationAppCommandDescriptor['id']> = [
|
||
'project.status',
|
||
'file.list',
|
||
'file.read',
|
||
'agent.trace_read',
|
||
];
|
||
const confirmCommandId = auditReadCommands.find(
|
||
(commandId) =>
|
||
policyView.policy.confirmCommands.includes(commandId) &&
|
||
!confirmedPolicyCommands.includes(commandId),
|
||
);
|
||
if (confirmCommandId) {
|
||
requestProjectPolicyConfirmation(
|
||
confirmCommandId,
|
||
nextProjectPath,
|
||
`审计 ${nextProjectPath} 需要读取 ${confirmCommandId}`,
|
||
() =>
|
||
void executeAgentAuditChat([
|
||
...confirmedPolicyCommands,
|
||
confirmCommandId,
|
||
]),
|
||
);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备确认审计读取:${confirmCommandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'agent.audit' },
|
||
);
|
||
const fileResult = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
let commandLogContent = '';
|
||
try {
|
||
const commandLogResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
commandLogContent = commandLogResult.content;
|
||
} catch {
|
||
commandLogContent = '';
|
||
}
|
||
let trace: GameCreationAgentRunTrace | null = null;
|
||
try {
|
||
const traceResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
trace = parseAgentRunTrace(traceResult.content);
|
||
} catch {
|
||
trace = null;
|
||
}
|
||
|
||
setManifest(nextManifest);
|
||
setProjectFiles(fileResult.files);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(
|
||
trace ? formatAgentRunStatus(trace) : '还没有最近一次 Agent run',
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.audit',
|
||
'project.status',
|
||
'file.list',
|
||
...(commandLogContent ? ['file.read .agent/logs/command.log'] : []),
|
||
...(trace ? ['agent.trace_read'] : []),
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentAudit(
|
||
nextManifest,
|
||
nextProjectPath,
|
||
fileResult.files,
|
||
trace,
|
||
commandLogContent,
|
||
),
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStart() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
|
||
requestCommandConfirmation(
|
||
'preview.start',
|
||
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
|
||
() => void executePreviewStart(false),
|
||
);
|
||
}
|
||
|
||
async function executePreviewStart(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setPreviewStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setPreviewStatus('正在启动');
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.start',
|
||
nextProjectPath,
|
||
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
|
||
'准备启动本地预览。',
|
||
() => void executePreviewStart(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
updateClientPreview(result);
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
void refreshManifest(nextProjectPath);
|
||
setCommandLog((current) => [...current, 'preview.start']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `预览已在客户端运行视图启动:${result.url}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executePreviewOpen(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (announceToChat && !nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
nextProjectPath &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.open',
|
||
nextProjectPath,
|
||
`打开 ${nextProjectPath} 的本地预览`,
|
||
'准备打开当前本地预览。',
|
||
() => void executePreviewOpen(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewStatus>(
|
||
'activate_local_game_preview',
|
||
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
|
||
);
|
||
if (
|
||
result.status === 'running' &&
|
||
result.url &&
|
||
result.port &&
|
||
result.root
|
||
) {
|
||
updateClientPreview({
|
||
url: result.url,
|
||
port: result.port,
|
||
root: result.root,
|
||
});
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
}
|
||
setCommandLog((current) => [...current, 'preview.open']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: result.url
|
||
? `已切换到客户端运行视图:${result.url}`
|
||
: '已切换到客户端运行视图。',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStatus() {
|
||
void executePreviewStatus(false);
|
||
}
|
||
|
||
async function executePreviewStatus(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (announceToChat && !nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
nextProjectPath &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.status',
|
||
nextProjectPath,
|
||
`查看 ${nextProjectPath} 的预览状态`,
|
||
'准备查看预览状态。',
|
||
() => void executePreviewStatus(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewStatus>(
|
||
'get_local_game_preview_status',
|
||
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
|
||
);
|
||
if (
|
||
result.status === 'running' &&
|
||
result.url &&
|
||
result.port &&
|
||
result.root
|
||
) {
|
||
updateClientPreview({
|
||
url: result.url,
|
||
port: result.port,
|
||
root: result.root,
|
||
});
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
} else {
|
||
updateClientPreview(null);
|
||
setPreviewStatus('未启动');
|
||
}
|
||
setCommandLog((current) => [...current, 'preview.status']);
|
||
if (announceToChat && nextProjectPath) {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'preview.status',
|
||
);
|
||
}
|
||
if (announceToChat) {
|
||
const previewRunning = result.status === 'running' && result.url;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: previewRunning ? `预览运行中:${result.url}` : '预览未启动。',
|
||
draftCommand: previewRunning ? '/open-preview' : '/preview',
|
||
draftCommandLabel: previewRunning
|
||
? '填入打开预览命令'
|
||
: '填入启动预览命令',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStop() {
|
||
void executePreviewStop(false);
|
||
}
|
||
|
||
async function executePreviewStop(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setPreviewStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.stop',
|
||
nextProjectPath,
|
||
`停止 ${nextProjectPath} 的本地预览`,
|
||
'准备停止本地预览。',
|
||
() => void executePreviewStop(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
await invoke<LocalPreviewStatus>('stop_local_game_preview', {
|
||
projectPath: nextProjectPath,
|
||
});
|
||
updateClientPreview(null);
|
||
setPreviewStatus('已停止');
|
||
setCommandLog((current) => [...current, 'preview.stop']);
|
||
if (announceToChat) {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'preview.stop',
|
||
);
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '预览已停止。' },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleMemoryRead(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在读取');
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('memory.read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void handleMemoryRead(true),
|
||
);
|
||
setMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'read_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope: memoryScope },
|
||
);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.read']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function validateMemoryPanelProjectPath() {
|
||
const nextProjectPath = projectPath.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setMemoryStatus('请提供本地项目绝对路径。');
|
||
return null;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setMemoryStatus('本地项目路径不能包含控制字符。');
|
||
return null;
|
||
}
|
||
return nextProjectPath;
|
||
}
|
||
|
||
async function handleMemoryWrite() {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'memory.write',
|
||
`保存 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void executeMemoryWritePanel(nextProjectPath),
|
||
);
|
||
}
|
||
|
||
async function executeMemoryWritePanel(nextProjectPath: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'write_local_game_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
scope: memoryScope,
|
||
content: memoryDraft,
|
||
},
|
||
);
|
||
setMemoryStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.write']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleMemoryDelete() {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'memory.delete',
|
||
`删除 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void executeMemoryDeletePanel(nextProjectPath),
|
||
);
|
||
}
|
||
|
||
async function executeMemoryDeletePanel(nextProjectPath: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在删除');
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'delete_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope: memoryScope },
|
||
);
|
||
setMemoryDraft('');
|
||
setMemoryStatus(`已删除:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.delete']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function executeMemoryReadChat(
|
||
scope: MemoryScope,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${memoryScopePath(scope)}`,
|
||
'准备读取项目记忆。',
|
||
() => void executeMemoryReadChat(scope, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'read_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope },
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.read']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: result.exists
|
||
? `${memoryScopeLabel(scope)}记忆:\n${result.content}`
|
||
: `${memoryScopeLabel(scope)}记忆为空。`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeMemoryWrite(
|
||
scope: MemoryScope,
|
||
content: string,
|
||
mode: MemoryWriteMode,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const currentMemory =
|
||
mode === 'append'
|
||
? await invoke<LocalGameMemoryResult>('read_local_game_memory', {
|
||
projectPath: nextProjectPath,
|
||
scope,
|
||
})
|
||
: null;
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'write_local_game_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
scope,
|
||
content:
|
||
mode === 'append'
|
||
? appendMemoryContent(currentMemory?.content ?? '', content)
|
||
: content,
|
||
},
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
...(mode === 'append' ? ['memory.read'] : []),
|
||
'memory.write',
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'append'
|
||
? `已追加${memoryScopeLabel(scope)}记忆。`
|
||
: `已保存${memoryScopeLabel(scope)}记忆。`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeMemoryDeleteChat(scope: MemoryScope) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'delete_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope },
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft('');
|
||
setMemoryStatus(`已删除:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.delete']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已删除${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function handleLimitedCommandRun(
|
||
command: GameCreationAppLimitedRunCommandDescriptor,
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
|
||
requestCommandConfirmation(
|
||
'command.run_limited',
|
||
`运行 ${command.title} 于 ${nextProjectPath}`,
|
||
() => void executeLimitedCommand(command.id, false),
|
||
);
|
||
}
|
||
|
||
async function refreshLimitedLocalCommands() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
setLimitedCommandStatus('正在读取内置命令');
|
||
try {
|
||
const commands = await invoke<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>('get_limited_local_commands');
|
||
setLimitedLocalCommands(
|
||
commands.length > 0
|
||
? commands
|
||
: [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS],
|
||
);
|
||
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
|
||
setCommandLog((current) => [...current, 'command.list_limited']);
|
||
} catch (error) {
|
||
setLimitedCommandStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
async function handleProjectLogRead(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectLogStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectLogStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
setProjectLogStatus(`正在读取 ${relativePath}`);
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${relativePath}`,
|
||
'准备读取项目日志。',
|
||
() => void handleProjectLogRead(relativePath, true),
|
||
))
|
||
) {
|
||
setProjectLogStatus('等待确认');
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
setProjectLogContent(result.content);
|
||
setProjectLogStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, `file.read ${result.path}`]);
|
||
} catch (error) {
|
||
setProjectLogContent('');
|
||
setProjectLogStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
async function executeLimitedCommandList() {
|
||
const invoke = resolveTauriInvoke();
|
||
let commands: GameCreationAppLimitedRunCommandDescriptor[] = [
|
||
...GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||
];
|
||
if (invoke) {
|
||
try {
|
||
const nativeCommands = await invoke<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>('get_limited_local_commands');
|
||
if (nativeCommands.length > 0) {
|
||
commands = nativeCommands;
|
||
}
|
||
} catch {
|
||
commands = [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS];
|
||
}
|
||
}
|
||
setLimitedLocalCommands(commands);
|
||
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
|
||
setCommandLog((current) => [...current, 'command.list_limited']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeLimitedLocalCommands(commands) },
|
||
]);
|
||
}
|
||
|
||
async function executeRunLocal(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setLimitedCommandStatus('正在运行');
|
||
try {
|
||
const smoke = directCodexProductRuntime
|
||
? null
|
||
: await invoke<LimitedLocalCommandResult>('run_limited_local_command', {
|
||
projectPath: nextProjectPath,
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
const previewResult = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
updateClientPreview(previewResult);
|
||
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
|
||
setLimitedCommandStatus(smoke?.output ?? '本地预览已启动');
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'game.run_local',
|
||
...(smoke ? [`command.run_limited ${smoke.commandId}`] : []),
|
||
'preview.start',
|
||
]);
|
||
void refreshManifest(nextProjectPath);
|
||
if (!directCodexProductRuntime) {
|
||
void refreshAgentRunTrace(nextProjectPath);
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `运行通过,已载入客户端运行视图:${previewResult.url}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setLimitedCommandStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeLimitedCommand(
|
||
commandId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
setLimitedCommandStatus('正在运行');
|
||
try {
|
||
const result = await invoke<LimitedLocalCommandResult>(
|
||
'run_limited_local_command',
|
||
{ projectPath: nextProjectPath, commandId },
|
||
);
|
||
const summary = `${result.output}\n日志:${result.logPath}`;
|
||
setLimitedCommandStatus(result.output);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`command.run_limited ${result.commandId}`,
|
||
]);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summary },
|
||
]);
|
||
}
|
||
void refreshManifest(nextProjectPath);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setLimitedCommandStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleFileList(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在列出');
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('file.list')) {
|
||
requestProjectPolicyConfirmation(
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目文件`,
|
||
() => void handleFileList(true),
|
||
);
|
||
setFileStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectFiles(result.files);
|
||
setFileStatus(`已列出 ${result.files.length} 项`);
|
||
setCommandLog((current) => [...current, 'file.list']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function validateFilePanelProjectPath() {
|
||
const nextProjectPath = projectPath.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setFileStatus('请提供本地项目绝对路径。');
|
||
return null;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setFileStatus('本地项目路径不能包含控制字符。');
|
||
return null;
|
||
}
|
||
return nextProjectPath;
|
||
}
|
||
|
||
function validateFilePanelPath() {
|
||
if (!isSafeProjectRelativePath(filePath)) {
|
||
setFileStatus('文件路径必须是项目内相对路径。');
|
||
return null;
|
||
}
|
||
return filePath.trim();
|
||
}
|
||
|
||
async function handleFileRead(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在读取');
|
||
try {
|
||
const commandId = isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read';
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes(commandId)) {
|
||
requestProjectPolicyConfirmation(
|
||
commandId,
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
() => void handleFileRead(true),
|
||
);
|
||
setFileStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId,
|
||
},
|
||
);
|
||
setFilePath(result.path);
|
||
setFileDraft(result.content);
|
||
setFileStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.read']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleFileWrite() {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'file.write',
|
||
`保存 ${nextProjectPath}/${relativePath}`,
|
||
() => void executeFileWrite(nextProjectPath, relativePath),
|
||
);
|
||
}
|
||
|
||
async function executeFileWrite(
|
||
nextProjectPath: string,
|
||
relativePath: string,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalProjectFileMutationResult>(
|
||
'write_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath, content: fileDraft },
|
||
);
|
||
setFileStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.write']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleFileDelete() {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'file.delete',
|
||
`删除 ${nextProjectPath}/${relativePath}`,
|
||
() => void executeFileDelete(nextProjectPath, relativePath),
|
||
);
|
||
}
|
||
|
||
async function executeFileDelete(
|
||
nextProjectPath: string,
|
||
relativePath: string,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在删除');
|
||
try {
|
||
const result = await invoke<LocalProjectFileMutationResult>(
|
||
'delete_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath },
|
||
);
|
||
setFileDraft('');
|
||
setFileStatus(result.deleted ? `已删除:${result.path}` : '文件不存在');
|
||
setCommandLog((current) => [...current, 'file.delete']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleAssetRegister() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const localPath = assetLocalPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
const resourceId = assetResourceId.trim();
|
||
const nextAssetObjectId = assetObjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setAssetStatus('资产路径必须是项目内相对路径。');
|
||
return;
|
||
}
|
||
if (assetSourceKind === 'canvas') {
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!resourceId && !nextAssetObjectId) {
|
||
setAssetStatus('请提供资源 ID 或资产对象 ID。');
|
||
return;
|
||
}
|
||
}
|
||
requestCommandConfirmation(
|
||
'asset.register',
|
||
`登记 ${nextProjectPath}/${localPath}`,
|
||
() =>
|
||
void executeAssetRegister(
|
||
nextProjectPath,
|
||
localPath,
|
||
parseGameCreationAppAssetKind(assetKind, 'ui.asset-register.kind'),
|
||
assetMediaType,
|
||
assetSourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
),
|
||
);
|
||
}
|
||
|
||
async function executeAssetRegister(
|
||
nextProjectPath: string,
|
||
localPath: string,
|
||
kind: GameCreationAppAssetKind,
|
||
mediaType: string,
|
||
sourceKind: string,
|
||
canvasProjectId: string,
|
||
resourceId: string,
|
||
nextAssetObjectId: string,
|
||
announceToChat = false,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在登记');
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'asset.register',
|
||
nextProjectPath,
|
||
`登记 ${nextProjectPath}/${localPath}`,
|
||
'准备登记项目资产。',
|
||
() =>
|
||
void executeAssetRegister(
|
||
nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
sourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
true,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'register_local_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
sourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId: nextAssetObjectId,
|
||
taskId: '',
|
||
prompt: '',
|
||
model: '',
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
setAssetStatus(`已登记:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'asset.register']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已登记资产:${result.localPath}` },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleCanvasAssetImport() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const localPath = assetLocalPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
const resourceId = assetResourceId.trim();
|
||
const nextAssetObjectId = assetObjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setAssetStatus('画板资产路径必须是项目内相对路径。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!resourceId && !nextAssetObjectId) {
|
||
setAssetStatus('请提供资源 ID 或资产对象 ID。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.asset_import',
|
||
`导入 ${nextProjectPath}/${localPath}`,
|
||
() =>
|
||
void executeCanvasAssetImport(
|
||
nextProjectPath,
|
||
localPath,
|
||
parseGameCreationAppAssetKind(
|
||
assetKind,
|
||
'ui.canvas-asset-import.kind',
|
||
),
|
||
assetMediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
false,
|
||
),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasAssetImport(
|
||
nextProjectPath: string | null,
|
||
localPath: string,
|
||
kind: GameCreationAppAssetKind,
|
||
mediaType: string,
|
||
canvasProjectId: string,
|
||
resourceId: string,
|
||
assetObjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const targetProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: nextProjectPath;
|
||
if (!targetProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.asset_import',
|
||
targetProjectPath,
|
||
`导入 ${targetProjectPath}/${localPath} 的画板来源资产`,
|
||
'准备导入画板资产。',
|
||
() =>
|
||
void executeCanvasAssetImport(
|
||
nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认导入画板资产');
|
||
return;
|
||
}
|
||
setAssetStatus('正在导入画板资产');
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'import_canvas_asset',
|
||
{
|
||
projectPath: targetProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId,
|
||
taskId: '',
|
||
prompt: '',
|
||
model: '',
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
const canvasSource = formatCanvasAssetSource({
|
||
canvasProjectId,
|
||
canvasAssetId: resourceId,
|
||
canvasAssetObjectId: assetObjectId,
|
||
});
|
||
setAssetStatus(`已导入画板资产 ${canvasSource}:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'canvas.asset_import']);
|
||
void refreshManifest(targetProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已导入画板资产 ${canvasSource}:${result.localPath}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasAssetGenerate() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const prompt = assetGenerationPrompt.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!prompt) {
|
||
setAssetStatus('请提供美术生成提示词。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.asset_generate',
|
||
`生成 ${nextProjectPath} 的首版美术素材`,
|
||
() => void executeCanvasAssetGenerate(prompt, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasAssetGenerate(
|
||
prompt: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath.trim();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.asset_generate',
|
||
nextProjectPath,
|
||
`生成 ${nextProjectPath} 的首版美术素材`,
|
||
'准备生成首版美术素材。',
|
||
() => void executeCanvasAssetGenerate(prompt, announceToChat, true),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认生成美术素材');
|
||
return;
|
||
}
|
||
setAssetStatus('正在生成美术素材');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '正在生成首版美术素材。' },
|
||
]);
|
||
}
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'generate_platform_art_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
prompt,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
setAssetStatus(`已生成美术素材:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'canvas.asset_generate']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已生成美术素材:${result.localPath}` },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasProjectSync() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.project_sync',
|
||
`同步画板项目资源:${canvasProjectId}`,
|
||
() => void executeCanvasProjectSync(canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasProjectSync(
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.project_sync',
|
||
nextProjectPath,
|
||
`同步 ${nextProjectPath} 的画板项目资源:${canvasProjectId}`,
|
||
'准备同步画板项目资源。',
|
||
() =>
|
||
void executeCanvasProjectSync(
|
||
canvasProjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认同步画板项目');
|
||
return;
|
||
}
|
||
setAssetStatus('正在同步画板项目资源');
|
||
const result = await invoke<SyncCanvasProjectAssetsResult>(
|
||
'sync_canvas_project_assets',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
canvasProjectId,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const byId = new Map(current.map((asset) => [asset.id, asset]));
|
||
for (const asset of result.assets) {
|
||
byId.set(asset.id, asset);
|
||
}
|
||
return [...byId.values()];
|
||
});
|
||
setAssetStatus(
|
||
`已同步画板项目:${result.importedCount} 个资产,${result.importRoot}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'canvas.project_sync']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已同步 ${result.importedCount} 个画板资产自 ${result.canvasProjectId}:${result.importRoot}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasExportImport() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const exportPath = canvasExportPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (
|
||
!isAbsoluteProjectPath(exportPath) ||
|
||
projectPathHasControlCharacter(exportPath)
|
||
) {
|
||
setAssetStatus('画板导出 ZIP 路径必须是绝对路径。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.export_import',
|
||
`导入画板导出包:${exportPath}`,
|
||
() => void executeCanvasExportImport(exportPath, canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasExportImport(
|
||
exportPath: string,
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.export_import',
|
||
nextProjectPath,
|
||
`导入 ${nextProjectPath} 的画板导出包:${exportPath}`,
|
||
'准备导入画板导出包。',
|
||
() =>
|
||
void executeCanvasExportImport(
|
||
exportPath,
|
||
canvasProjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认导入画板导出包');
|
||
return;
|
||
}
|
||
setAssetStatus('正在导入画板导出包');
|
||
const result = await invoke<ImportCanvasExportResult>(
|
||
'import_canvas_export',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
exportPath,
|
||
canvasProjectId,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const byId = new Map(current.map((asset) => [asset.id, asset]));
|
||
for (const asset of result.assets) {
|
||
byId.set(asset.id, asset);
|
||
}
|
||
return [...byId.values()];
|
||
});
|
||
setAssetStatus(
|
||
`已导入画板导出包:${result.importedCount} 个资产,${result.importRoot}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'canvas.export_import']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已导入 ${result.importedCount} 个画板资产自 ${canvasProjectId}:${result.importRoot}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleCanvasProjectOpen() {
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.project_open',
|
||
`打开本机画板项目 ${canvasProjectId}`,
|
||
() => void executeCanvasProjectOpen(canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasProjectOpen(
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在打开画板');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `正在打开画板:${canvasProjectId}` },
|
||
]);
|
||
}
|
||
try {
|
||
const result = await invoke<OpenCanvasProjectResult>(
|
||
'open_canvas_project',
|
||
{
|
||
canvasProjectId,
|
||
editorBaseUrl,
|
||
},
|
||
);
|
||
setAssetStatus(`已打开画板:${result.url}`);
|
||
setCommandLog((current) => [...current, 'canvas.project_open']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已打开画板:${result.url}`,
|
||
draftCommand: `/sync-canvas-project ${canvasProjectId}`,
|
||
draftCommandLabel: '同步此画板',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||
const file = event.currentTarget.files?.[0];
|
||
event.currentTarget.value = '';
|
||
if (!file) {
|
||
return;
|
||
}
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
|
||
queuePendingCommand({ id: 'asset.upload', file });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'user', text: `上传文件:${file.name}` },
|
||
{ role: 'assistant', text: `准备保存文件到本地项目:${file.name}` },
|
||
]);
|
||
}
|
||
|
||
async function executeAssetUpload(file: File) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在上传');
|
||
try {
|
||
const bytes = Array.from(new Uint8Array(await file.arrayBuffer()));
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'upload_local_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
fileName: file.name,
|
||
mediaType: file.type || 'application/octet-stream',
|
||
bytes,
|
||
},
|
||
);
|
||
setLocalProject({
|
||
projectPath: nextProjectPath,
|
||
manifestPath: result.manifestPath,
|
||
manifest,
|
||
});
|
||
setUploadedAssets((current) => [...current, result]);
|
||
setAssetStatus(`已上传:${result.localPath}`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'asset.upload',
|
||
'asset.register',
|
||
]);
|
||
void refreshManifest(nextProjectPath);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已保存文件:${result.localPath}` },
|
||
]);
|
||
} catch (error) {
|
||
setAssetStatus(error instanceof Error ? error.message : String(error));
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function loadAgentRunTraceFile(
|
||
relativePath: string,
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return null;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId: isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read',
|
||
},
|
||
);
|
||
const trace = parseAgentRunTrace(result.content);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(formatAgentRunStatus(trace));
|
||
setCommandLog((current) => [...current, `file.read ${relativePath}`]);
|
||
return summarizeAgentRunCompletionForChat(trace);
|
||
} catch (error) {
|
||
setAgentRunTrace(null);
|
||
setAgentRunStatus(
|
||
relativePath === '.agent/run.latest.json' &&
|
||
isMissingProjectFileError(error)
|
||
? '还没有最近一次 Agent run'
|
||
: error instanceof Error
|
||
? error.message
|
||
: String(error),
|
||
);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function handleAgentRunTraceFileOpen(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? '';
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
() => void handleAgentRunTraceFileOpen(relativePath, true),
|
||
);
|
||
setAgentRunStatus('等待确认读取 Agent trace');
|
||
return;
|
||
}
|
||
}
|
||
|
||
await loadAgentRunTraceFile(relativePath, nextProjectPath);
|
||
}
|
||
|
||
async function readAgentRunHistoryItems(
|
||
invoke: TauriInvoke,
|
||
runFiles: LocalProjectFileEntry[],
|
||
nextProjectPath: string,
|
||
) {
|
||
const history = await Promise.all(
|
||
runFiles.map(async (file) => {
|
||
try {
|
||
const traceResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: file.path,
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
return {
|
||
path: file.path,
|
||
size: file.size,
|
||
trace: parseAgentRunTrace(traceResult.content),
|
||
} satisfies AgentRunHistoryItem;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}),
|
||
);
|
||
return history
|
||
.filter((item): item is AgentRunHistoryItem => item !== null)
|
||
.sort((left, right) => right.trace.updatedAt - left.trace.updatedAt);
|
||
}
|
||
|
||
async function refreshAgentRunHistory(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRunHistoryOverflowCount(0);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const runFiles = result.files
|
||
.filter(
|
||
(file) =>
|
||
file.kind === 'file' &&
|
||
file.path.startsWith('.agent/runs/') &&
|
||
file.path.endsWith('.json'),
|
||
)
|
||
.sort(
|
||
(left, right) =>
|
||
(right.modifiedAt ?? 0) - (left.modifiedAt ?? 0) ||
|
||
right.path.localeCompare(left.path),
|
||
);
|
||
const runFilesToLoad = runFiles.slice(0, AGENT_RUN_HISTORY_MAX_COUNT);
|
||
const firstPage = await readAgentRunHistoryItems(
|
||
invoke,
|
||
runFilesToLoad.slice(0, AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT),
|
||
nextProjectPath,
|
||
);
|
||
setAgentRunHistory(firstPage);
|
||
setAgentRunHistoryFiles(runFilesToLoad);
|
||
setAgentRunHistoryOverflowCount(
|
||
Math.max(0, runFiles.length - runFilesToLoad.length),
|
||
);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
} catch {
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRunHistoryOverflowCount(0);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
}
|
||
}
|
||
|
||
function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) {
|
||
if (!runtime) {
|
||
return;
|
||
}
|
||
const next = mergeAgentRuntimeStateIntoMap(
|
||
agentRuntimeByIdRef.current,
|
||
runtime,
|
||
false,
|
||
);
|
||
agentRuntimeByIdRef.current = next;
|
||
setAgentRuntimeById(next);
|
||
}
|
||
|
||
async function refreshAgentRuntimes(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentRuntimeById({});
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
return;
|
||
}
|
||
if (directCodexProductRuntime) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
setAgentRuntimeById({});
|
||
return;
|
||
}
|
||
try {
|
||
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
|
||
try {
|
||
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
|
||
'resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
for (const runtimeResult of resumedRuntimes) {
|
||
rememberAgentRuntimeState(
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
if (isMissingAgentRuntimeResumeCommandError(error)) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
} else if (
|
||
message.includes('项目权限策略要求用户确认:agent.resume')
|
||
) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
requestProjectPolicyConfirmation(
|
||
'agent.resume',
|
||
nextProjectPath,
|
||
`恢复 ${nextProjectPath} 中未完成的 Agent Runtime 任务`,
|
||
() => {
|
||
void invoke<AgentRuntimeResult[]>(
|
||
'confirm_resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
)
|
||
.then((resumedRuntimes) => {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
for (const runtimeResult of resumedRuntimes) {
|
||
rememberAgentRuntimeState(
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
}
|
||
setAgentRunStatus('已确认恢复 Agent Runtime 任务');
|
||
})
|
||
.catch((resumeError) => {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
setAgentRunStatus(
|
||
`Agent Runtime 恢复失败:${
|
||
resumeError instanceof Error
|
||
? resumeError.message
|
||
: String(resumeError)
|
||
}`,
|
||
);
|
||
});
|
||
},
|
||
);
|
||
setAgentRunStatus('等待确认恢复 Agent Runtime 任务');
|
||
} else if (message.includes('项目权限策略拒绝执行:agent.resume')) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
markProjectPolicyDenied('agent.resume', message);
|
||
} else {
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
setAgentRunStatus(`Agent Runtime 恢复失败:${message}`);
|
||
}
|
||
}
|
||
}
|
||
const runtimes = await invoke<AgentRuntimeResult[]>(
|
||
'read_game_creator_agent_runtimes',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const nextRuntimes: AgentRuntimeState[] = [];
|
||
for (const runtimeResult of runtimes) {
|
||
nextRuntimes.push(agentRuntimeStateFromResult(runtimeResult));
|
||
}
|
||
const supervisorRuntimeIndex = nextRuntimes.findIndex(
|
||
(runtime) => runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID,
|
||
);
|
||
const persistedSupervisorRuntime =
|
||
supervisorRuntimeIndex >= 0
|
||
? nextRuntimes[supervisorRuntimeIndex]!
|
||
: null;
|
||
if (projectSupervisorOnly && persistedSupervisorRuntime) {
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
if (
|
||
!currentRuntime ||
|
||
persistedSupervisorRuntime.updatedAt >= currentRuntime.updatedAt
|
||
) {
|
||
if (persistedSupervisorRuntime.sessionId) {
|
||
projectSupervisorSessionIdRef.current =
|
||
persistedSupervisorRuntime.sessionId;
|
||
setProjectSupervisorSessionId(persistedSupervisorRuntime.sessionId);
|
||
}
|
||
updateProjectSupervisorRuntime(persistedSupervisorRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
runtimes[supervisorRuntimeIndex]?.responseStream,
|
||
persistedSupervisorRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
}
|
||
}
|
||
setAgentRuntimeById((current) =>
|
||
nextRuntimes.reduce(
|
||
(next, runtime) => mergeAgentRuntimeStateIntoMap(next, runtime, true),
|
||
current,
|
||
),
|
||
);
|
||
} catch {
|
||
if (localProjectPathRef.current === nextProjectPath) {
|
||
setAgentRuntimeById({});
|
||
}
|
||
}
|
||
}
|
||
|
||
async function refreshAgentRunTrace(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
await refreshAgentRunHistory(nextProjectPath);
|
||
return null;
|
||
}
|
||
const summary = await loadAgentRunTraceFile(
|
||
'.agent/run.latest.json',
|
||
nextProjectPath,
|
||
);
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
await refreshAgentRunHistory(nextProjectPath);
|
||
return summary;
|
||
}
|
||
|
||
async function handleAgentRunTraceRefresh(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
() => void handleAgentRunTraceRefresh(true),
|
||
);
|
||
setAgentRunStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
await refreshAgentRunTrace(nextProjectPath);
|
||
} catch (error) {
|
||
setAgentRunStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function scheduleReadyAgentTasks(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
policyView.policy.confirmCommands.includes('agent.schedule_ready')
|
||
) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.schedule_ready',
|
||
nextProjectPath,
|
||
`调度 ${nextProjectPath} 中已 Ready 的 Agent 任务`,
|
||
() => void scheduleReadyAgentTasks(true),
|
||
);
|
||
setAgentRunStatus('等待确认调度 Ready 任务');
|
||
return;
|
||
}
|
||
}
|
||
setAgentRunStatus('正在调度 Ready 任务');
|
||
const scheduledRuntimes = await invoke<AgentRuntimeResult[]>(
|
||
'schedule_game_creator_agent_ready_tasks',
|
||
{ projectPath: nextProjectPath, limit: 16 },
|
||
);
|
||
for (const runtimeResult of scheduledRuntimes) {
|
||
rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult));
|
||
}
|
||
await refreshManifest(nextProjectPath);
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
const message =
|
||
scheduledRuntimes.length > 0
|
||
? `已调度 ${scheduledRuntimes.length} 个 Ready 任务。`
|
||
: '没有可调度的 Ready 任务。';
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
const agentStatusCards = deriveAgentStatusCards(
|
||
manifest,
|
||
agentRunTrace ?? agentRunHistory[0]?.trace ?? null,
|
||
agentRuntimeById,
|
||
);
|
||
const professionalResultCandidates = taskRowsFromManifest(manifest).map(
|
||
(task) => ({
|
||
agentId: agentConversationId(task),
|
||
label: projectProfessionalAgentLabel(agentConversationId(task)),
|
||
runtimeUpdatedAt:
|
||
agentRuntimeById[agentConversationId(task)]?.updatedAt ?? 0,
|
||
}),
|
||
);
|
||
const professionalResultCandidateKey = professionalResultCandidates
|
||
.map((candidate) => `${candidate.agentId}:${candidate.runtimeUpdatedAt}`)
|
||
.join('|');
|
||
useEffect(() => {
|
||
const nextProjectPath = localProject?.projectPath;
|
||
if (!projectSupervisorOnly || !nextProjectPath || !onManifestChange) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
void (async () => {
|
||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||
const before = await invoke<LocalGameProjectRevisionStatus>(
|
||
'get_local_game_project_revision',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const currentManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const after = await invoke<LocalGameProjectRevisionStatus>(
|
||
'get_local_game_project_revision',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (before.revision !== after.revision) {
|
||
continue;
|
||
}
|
||
if (!cancelled) {
|
||
onManifestChange(nextProjectPath, currentManifest, {
|
||
projectId: currentManifest.projectId,
|
||
revision: after.revision,
|
||
source: 'supervisor',
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
})().catch(() => undefined);
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [
|
||
localProject?.projectPath,
|
||
manifest,
|
||
onManifestChange,
|
||
projectSupervisorOnly,
|
||
]);
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
if (
|
||
!projectSupervisorOnly ||
|
||
directCodexProductRuntime ||
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
professionalResultCandidates.length === 0
|
||
) {
|
||
setProfessionalAgentResultsById({});
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
void Promise.all(
|
||
professionalResultCandidates.map(async (candidate) => {
|
||
try {
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: candidate.agentId,
|
||
},
|
||
);
|
||
const finalMessage = conversation.messages
|
||
.slice()
|
||
.reverse()
|
||
.find(
|
||
(message) =>
|
||
message.role === 'assistant' &&
|
||
message.content.trim() &&
|
||
isAgentFinalizationMessageId(message.messageId),
|
||
);
|
||
if (!finalMessage) {
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'missing' as const,
|
||
};
|
||
}
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'loaded' as const,
|
||
result: {
|
||
agentId: candidate.agentId,
|
||
runId: finalMessage.messageId!,
|
||
label: candidate.label,
|
||
title: `${candidate.label} 文本回执`,
|
||
content: finalMessage.content.trim(),
|
||
updatedAt: finalMessage.updatedAt,
|
||
} satisfies ProjectAgentResultSummary,
|
||
};
|
||
} catch {
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'failed' as const,
|
||
};
|
||
}
|
||
}),
|
||
).then((results) => {
|
||
if (disposed || localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
setProfessionalAgentResultsById((current) => {
|
||
const candidateAgentIds = new Set(
|
||
professionalResultCandidates.map((candidate) => candidate.agentId),
|
||
);
|
||
const next = Object.fromEntries(
|
||
Object.entries(current).filter(([agentId]) =>
|
||
candidateAgentIds.has(agentId),
|
||
),
|
||
);
|
||
for (const outcome of results) {
|
||
if (outcome.status === 'loaded') {
|
||
next[outcome.agentId] = outcome.result;
|
||
} else if (outcome.status === 'missing') {
|
||
delete next[outcome.agentId];
|
||
}
|
||
}
|
||
return next;
|
||
});
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
};
|
||
// The semantic candidate key replaces the freshly allocated candidates array.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
directCodexProductRuntime,
|
||
localProject?.projectPath,
|
||
professionalResultCandidateKey,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime?.runId,
|
||
]);
|
||
useEffect(() => {
|
||
if (!projectSupervisorOnly || !onAgentRuntimeSummariesChange) {
|
||
return;
|
||
}
|
||
onAgentRuntimeSummariesChange(
|
||
projectAgentRuntimeSummaries(
|
||
manifest,
|
||
projectSupervisorRuntime,
|
||
agentRuntimeById,
|
||
),
|
||
);
|
||
}, [
|
||
agentRuntimeById,
|
||
manifest,
|
||
onAgentRuntimeSummariesChange,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime,
|
||
]);
|
||
useEffect(() => {
|
||
if (!projectSupervisorOnly || !onAgentResultsChange) {
|
||
return;
|
||
}
|
||
onAgentResultsChange(Object.values(professionalAgentResultsById));
|
||
}, [
|
||
onAgentResultsChange,
|
||
professionalAgentResultsById,
|
||
projectSupervisorOnly,
|
||
]);
|
||
const mainProjectSummary = localProject
|
||
? summarizeMainProjectHeader(manifest, agentStatusCards)
|
||
: null;
|
||
const visibleAgentRunHistory = agentRunHistory.slice(
|
||
0,
|
||
agentRunHistoryVisibleCount,
|
||
);
|
||
const visibleMainProjectFiles = projectFiles
|
||
.filter((file) => file.kind === 'file' && !file.path.startsWith('.agent/'))
|
||
.slice(0, 8);
|
||
const visibleMainProjectAssets = manifest.assets
|
||
.filter(
|
||
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
|
||
)
|
||
.slice(0, 8);
|
||
const chatProjectAssets = manifest.assets.filter(
|
||
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
|
||
);
|
||
// `@` 面板「当前版本素材」的版本来源:版本列表来自 manifest,
|
||
// 当前版本由工作台壳(`WorkspaceLauncherShell`)持有的那一份状态给出,
|
||
// 传 `null` 表示回退到 manifest 中最新的版本。
|
||
const chatProjectVersions = manifest.versions ?? [];
|
||
const chatActiveVersionId = activeVersionId;
|
||
const visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5);
|
||
const currentProjectTitle = localProject
|
||
? manifest.name.trim() || projectNameFromPath(localProject.projectPath)
|
||
: seedManifest.name;
|
||
useEffect(() => {
|
||
if (!supervisorChatOnly) {
|
||
return;
|
||
}
|
||
setWindowTitle(localProject ? currentProjectTitle : undefined);
|
||
return () => setWindowTitle(undefined);
|
||
}, [currentProjectTitle, localProject, setWindowTitle, supervisorChatOnly]);
|
||
const agentRunHistoryOmittedCount =
|
||
Math.max(0, agentRunHistoryFiles.length - agentRunHistoryVisibleCount) +
|
||
agentRunHistoryOverflowCount;
|
||
const canShowMoreAgentRunHistory =
|
||
agentRunHistoryVisibleCount < agentRunHistoryFiles.length;
|
||
|
||
async function showMoreAgentRunHistory() {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke || agentRunHistoryLoadingMoreRef.current) {
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const nextVisibleCount = Math.min(
|
||
agentRunHistoryVisibleCount + AGENT_RUN_HISTORY_VISIBLE_STEP,
|
||
agentRunHistoryFiles.length,
|
||
);
|
||
const nextFiles = agentRunHistoryFiles.slice(
|
||
agentRunHistoryVisibleCount,
|
||
nextVisibleCount,
|
||
);
|
||
agentRunHistoryLoadingMoreRef.current = true;
|
||
setAgentRunHistoryLoadingMore(true);
|
||
try {
|
||
const nextItems = await readAgentRunHistoryItems(
|
||
invoke,
|
||
nextFiles,
|
||
nextProjectPath,
|
||
);
|
||
setAgentRunHistory((current) =>
|
||
[...current, ...nextItems].sort(
|
||
(left, right) => right.trace.updatedAt - left.trace.updatedAt,
|
||
),
|
||
);
|
||
setAgentRunHistoryVisibleCount(nextVisibleCount);
|
||
} finally {
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
setAgentRunHistoryLoadingMore(false);
|
||
}
|
||
}
|
||
|
||
function handleAgentRunHistoryScroll(event: UIEvent<HTMLElement>) {
|
||
if (!canShowMoreAgentRunHistory) {
|
||
return;
|
||
}
|
||
const target = event.currentTarget;
|
||
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 24) {
|
||
void showMoreAgentRunHistory();
|
||
}
|
||
}
|
||
|
||
const visibleMessages = latestVisibleItems(
|
||
messages,
|
||
conversationVisibleCount,
|
||
);
|
||
const hiddenConversationCount = Math.max(
|
||
0,
|
||
messages.length - visibleMessages.length,
|
||
);
|
||
const hasEarlierConversationMessages =
|
||
hiddenConversationCount > 0 ||
|
||
(directCodexProductRuntime && directHistoryHasMore);
|
||
const projectSupervisorTransientReply =
|
||
projectSupervisorResponseStream?.accumulatedText.trim() ?? '';
|
||
const projectSupervisorNeedsUserInput = agentRuntimeNeedsUserInput(
|
||
projectSupervisorRuntime,
|
||
);
|
||
const projectSupervisorHasConversationControls = Boolean(
|
||
projectSupervisorRuntime?.pendingToolAction ||
|
||
projectSupervisorRuntime?.userInputRequest ||
|
||
projectSupervisorNeedsUserInput,
|
||
);
|
||
const visibleAgentConversationMessages = latestVisibleItems(
|
||
agentConversationMessages,
|
||
agentConversationVisibleCount,
|
||
);
|
||
const hiddenAgentConversationCount = Math.max(
|
||
0,
|
||
agentConversationMessages.length - visibleAgentConversationMessages.length,
|
||
);
|
||
const selectedAgentLlmStatus = selectedAgent
|
||
? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent)
|
||
: null;
|
||
const selectedAgentLlmWarning = selectedAgent
|
||
? formatAgentLlmConfigWarning(llmConfigStatus, selectedAgent)
|
||
: null;
|
||
const selectedAgentNeedsUserInput = agentRuntimeNeedsUserInput(
|
||
agentConversationRuntime,
|
||
);
|
||
const selectedAgentSteerRuntime = selectedAgent
|
||
? matchingAgentRuntimeForSteer(
|
||
[agentConversationRuntime],
|
||
selectedAgent.id,
|
||
agentConversationSessionId,
|
||
)
|
||
: null;
|
||
|
||
async function showEarlierConversationMessages() {
|
||
if (directCodexProductRuntime && directHistoryHasMore) {
|
||
const invoke = resolveTauriInvoke();
|
||
const projectPath = localProject?.projectPath;
|
||
if (invoke && projectPath && !directHistoryLoadingRef.current) {
|
||
directHistoryLoadingRef.current = true;
|
||
try {
|
||
// 一次点击连拉,直到出现新回合或取不动为止:一屏全是工具卡片 / 思考文本时,
|
||
// 单发一页会让用户点了"显示更早"却看不到任何变化。
|
||
const pages = await readDirectHistoryPages({
|
||
existingEntries: selectDirectChatEntries(directThreadChat),
|
||
beforeItemId: directHistoryOldestItemIdRef.current,
|
||
readSlice: (beforeItemId) =>
|
||
invoke<DirectThreadHistorySlice>(
|
||
'read_direct_project_history_slice',
|
||
beforeItemId
|
||
? {
|
||
projectPath,
|
||
beforeItemId,
|
||
limit: CONVERSATION_VISIBLE_STEP,
|
||
}
|
||
: { projectPath, limit: CONVERSATION_VISIBLE_STEP },
|
||
),
|
||
});
|
||
if (localProjectPathRef.current !== projectPath) {
|
||
return;
|
||
}
|
||
// 连拉到的页一次性进聊天 reducer:与运行态同形、同身份,重复读取只补不重。
|
||
setDirectThreadChat((state) =>
|
||
mergeDirectHistoryItems(state, pages.items),
|
||
);
|
||
// 首页或中途读取失败时保留旧值:失败页没有可靠的 hasMore,不能因为
|
||
// 一次瞬时 IO 抖动把「显示更早」按钮永久收掉。
|
||
if (!pages.error) {
|
||
setDirectHistoryHasMore(pages.hasMore);
|
||
}
|
||
directHistoryOldestItemIdRef.current =
|
||
pages.firstItemId ?? directHistoryOldestItemIdRef.current;
|
||
if (pages.error) {
|
||
// 已经取到的页照常进视图,失败只走下面同一个报错出口。
|
||
throw pages.error;
|
||
}
|
||
} catch (error) {
|
||
setWorkspaceStatus(
|
||
`读取更早的对话历史失败:${error instanceof Error ? error.message : String(error)}`,
|
||
);
|
||
} finally {
|
||
directHistoryLoadingRef.current = false;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
setConversationVisibleCount((current) =>
|
||
Math.min(messages.length, current + CONVERSATION_VISIBLE_STEP),
|
||
);
|
||
}
|
||
|
||
function showEarlierAgentConversationMessages() {
|
||
setAgentConversationVisibleCount((current) =>
|
||
Math.min(
|
||
agentConversationMessages.length,
|
||
current + CONVERSATION_VISIBLE_STEP,
|
||
),
|
||
);
|
||
}
|
||
|
||
function handleConversationScroll(event: UIEvent<HTMLElement>) {
|
||
if (!hasEarlierConversationMessages) {
|
||
return;
|
||
}
|
||
if (event.currentTarget.scrollTop <= 24) {
|
||
showEarlierConversationMessages();
|
||
}
|
||
}
|
||
|
||
function handleSupervisorChatScroll(event: UIEvent<HTMLDivElement>) {
|
||
handleConversationScroll(event);
|
||
const messageList = event.currentTarget;
|
||
const distanceFromBottom =
|
||
messageList.scrollHeight -
|
||
messageList.scrollTop -
|
||
messageList.clientHeight;
|
||
supervisorChatShouldFollowLatestRef.current =
|
||
distanceFromBottom <= AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD;
|
||
}
|
||
|
||
function handleAgentConversationScroll(event: UIEvent<HTMLElement>) {
|
||
if (hiddenAgentConversationCount === 0) {
|
||
return;
|
||
}
|
||
if (event.currentTarget.scrollTop <= 24) {
|
||
showEarlierAgentConversationMessages();
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!selectedAgent) {
|
||
return;
|
||
}
|
||
const refreshedAgent = agentStatusCards.find(
|
||
(agent) => agent.id === selectedAgent.id,
|
||
);
|
||
if (!refreshedAgent) {
|
||
closeAgentConversation();
|
||
return;
|
||
}
|
||
if (!sameAgentStatusCard(selectedAgent, refreshedAgent)) {
|
||
setSelectedAgent(refreshedAgent);
|
||
}
|
||
}, [agentStatusCards, selectedAgent]);
|
||
|
||
/**
|
||
* 发起一轮 direct-codex 对话回合:提交与队列出队共用同一条路径,避免两条入口的
|
||
* 消息落盘/回合 id/附件参数走样。
|
||
*/
|
||
function startDirectCodexConversationTurn(input: {
|
||
prompt: string;
|
||
attachments?: DirectCodexTurnAttachment[];
|
||
references?: ChatReference[];
|
||
content?: DirectCodexUserContentPart[];
|
||
}) {
|
||
const clientTurnId = createDirectCodexConversationTurnId();
|
||
supervisorChatShouldFollowLatestRef.current = true;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: input.prompt,
|
||
runtimeOwned: true,
|
||
messageId: directCodexConversationMessageId(clientTurnId, 'user'),
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
void executeChatAgentReply({
|
||
prompt: input.prompt,
|
||
clientTurnId,
|
||
attachments: input.attachments?.length ? input.attachments : undefined,
|
||
references: input.references,
|
||
userItem: chatComposerDraftToDirectCodexUserItem(
|
||
{
|
||
text: input.prompt,
|
||
references: input.references ?? [],
|
||
content: input.content ?? [],
|
||
},
|
||
directCodexConversationMessageId(clientTurnId, 'user'),
|
||
),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 输入盒上传本地文件:复用首页建项目那条 `upload_local_asset` 链路把文件写进项目,
|
||
* 再以**项目相对路径**生成回合附件(绝对路径会被 Rust 侧附件规则判为失败)。
|
||
*/
|
||
async function handleChatComposerUploadFiles(files: readonly File[]) {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke || !nextProjectPath) {
|
||
setChatAttachmentNotice('需要先打开本地项目,才能上传文件');
|
||
return;
|
||
}
|
||
const remaining = MAX_CHAT_COMPOSER_ATTACHMENTS - chatAttachments.length;
|
||
const accepted = files.slice(0, Math.max(remaining, 0));
|
||
if (accepted.length === 0) {
|
||
setChatAttachmentNotice(
|
||
`最多同时携带 ${MAX_CHAT_COMPOSER_ATTACHMENTS} 个附件,请先移除已有附件`,
|
||
);
|
||
return;
|
||
}
|
||
setChatAttachmentNotice('正在上传文件');
|
||
try {
|
||
const imported = await uploadLocalFilesAsAttachments(
|
||
invoke,
|
||
nextProjectPath,
|
||
accepted,
|
||
);
|
||
const attachments = toDirectCodexTurnAttachments(imported);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
setChatAttachments((current) =>
|
||
[...current, ...attachments].slice(0, MAX_CHAT_COMPOSER_ATTACHMENTS),
|
||
);
|
||
const failed = attachments.filter(
|
||
(attachment) => attachment.status === 'failed',
|
||
);
|
||
setChatAttachmentNotice(
|
||
failed.length > 0
|
||
? `${failed.length} 个文件未能上传:${failed[0]?.name ?? ''}`
|
||
: `已上传 ${attachments.length} 个文件,将在下次发送时作为本轮附件`,
|
||
);
|
||
void refreshManifest(nextProjectPath);
|
||
} catch (error) {
|
||
if (localProjectPathRef.current === nextProjectPath) {
|
||
setChatAttachmentNotice(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function removeChatComposerAttachment(index: number) {
|
||
setChatAttachments((current) =>
|
||
current.filter((_, currentIndex) => currentIndex !== index),
|
||
);
|
||
}
|
||
|
||
/** 回合运行中再次发送:进本地 FIFO 队列;队列满时拒绝并保留草稿,不静默丢消息。 */
|
||
function enqueueChatTurnForRunningTurn(input: {
|
||
prompt: string;
|
||
attachments: DirectCodexTurnAttachment[];
|
||
references: ChatReference[];
|
||
content: DirectCodexUserContentPart[];
|
||
}): boolean {
|
||
if (isChatTurnQueueFull(chatTurnQueueRef.current)) {
|
||
setChatComposerNotice(chatQueueFullNotice());
|
||
return false;
|
||
}
|
||
queuedChatTurnSequenceRef.current += 1;
|
||
const turn = createQueuedChatTurn({
|
||
id: `queued-chat-turn-${Date.now()}-${queuedChatTurnSequenceRef.current}`,
|
||
prompt: input.prompt,
|
||
attachments: input.attachments,
|
||
references: input.references,
|
||
content: input.content,
|
||
createdAt: Date.now(),
|
||
});
|
||
const nextQueue = enqueueChatTurn(chatTurnQueueRef.current, turn);
|
||
chatTurnQueueRef.current = nextQueue;
|
||
setChatTurnQueue(nextQueue);
|
||
setChatComposerNotice('已加入发送队列,当前回合结束后自动发送');
|
||
return true;
|
||
}
|
||
|
||
function cancelQueuedChatTurn(id: string) {
|
||
const nextQueue = removeQueuedChatTurn(chatTurnQueueRef.current, id);
|
||
chatTurnQueueRef.current = nextQueue;
|
||
setChatTurnQueue(nextQueue);
|
||
if (nextQueue.length === 0) {
|
||
setChatComposerNotice('');
|
||
}
|
||
}
|
||
|
||
/** 队首出队并立即发出:只在 DirectProject 收到 `turn.completed` 后调用。 */
|
||
function dispatchNextQueuedChatTurn() {
|
||
if (chatAgentBusyRef.current || directTurnRunningRef.current) {
|
||
return;
|
||
}
|
||
directTurnCompletionPendingRef.current = false;
|
||
const { next, rest } = dequeueChatTurn(chatTurnQueueRef.current);
|
||
if (!next) {
|
||
return;
|
||
}
|
||
chatTurnQueueRef.current = rest;
|
||
setChatTurnQueue(rest);
|
||
if (rest.length === 0) {
|
||
setChatComposerNotice('');
|
||
}
|
||
startDirectCodexConversationTurn({
|
||
prompt: next.prompt,
|
||
attachments: next.attachments,
|
||
references: next.references,
|
||
content: next.content,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 回合事件可能先于本地 invoke 的 finally 到达,因此先记 pending,等本地忙碌态复位后再出队。
|
||
*/
|
||
useEffect(() => {
|
||
if (!directCodexProductRuntime) {
|
||
directTurnCompletionPendingRef.current = false;
|
||
previousDirectTurnRunningRef.current = directTurnRunning;
|
||
return;
|
||
}
|
||
const wasRunning = previousDirectTurnRunningRef.current;
|
||
previousDirectTurnRunningRef.current = directTurnRunning;
|
||
if (wasRunning && !directTurnRunning) {
|
||
directTurnCompletionPendingRef.current = true;
|
||
}
|
||
if (
|
||
directTurnCompletionPendingRef.current &&
|
||
!chatAgentBusy &&
|
||
!directTurnRunning
|
||
) {
|
||
directTurnCompletionPendingRef.current = false;
|
||
dispatchNextQueuedChatTurn();
|
||
}
|
||
// 队列出队函数读取本轮 render 的项目输入;这里只由三个生命周期信号触发。
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [chatAgentBusy, directCodexProductRuntime, directTurnRunning]);
|
||
|
||
/** 终止当前 direct-codex 回合:只取消这一轮,UI 由回合的 finally 复位。 */
|
||
async function handleCancelDirectCodexTurn() {
|
||
if (directCodexTurnCancelling) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const directProjectPath = resolveChatProjectPath(localProject);
|
||
// 繁忙判据与「终止」按钮的可见条件一致:本地 invoke 正在跑,或订阅说还有一条回合没结束。
|
||
// 只认 `turn.started` 推导出来的 `directTurnRunning` 会漏掉"刚提交、事件还没到"的窗口。
|
||
if (!invoke || !directProjectPath || !supervisorChatBusy) {
|
||
setProjectSupervisorRuntimeError('当前没有正在运行的回合,无法终止。');
|
||
return;
|
||
}
|
||
setDirectCodexTurnCancelling(true);
|
||
setChatComposerNotice('正在终止当前回合');
|
||
try {
|
||
const result = await invoke<DirectTurnCancelView>(
|
||
'cancel_direct_codex_turn',
|
||
{ projectPath: directProjectPath },
|
||
);
|
||
const message = result?.message?.trim();
|
||
if (result?.outcome === 'released') {
|
||
// 这一轮已经没有人替它收尾(执行进程已退出 / 从没进执行器),Rust 侧强制释放了
|
||
// 守卫并补了终态事件;这里按同一个收口函数同步把界面复位,不等 IPC 通知。
|
||
// 时刻取宿主观测到的这一刻:终止返回就是这一轮的终态,原生随后补的事件若先到,
|
||
// 收口已经是冻结值,不会被抬高,也不会复活成"永远运行中"。
|
||
// 但取消回包可能晚于新回合的开始:先核对身份(clientTurnId 对应的本轮用户条目),
|
||
// 只收口确实是这一轮的那一次,避免在新回合的回调里把旧轮的时间盖上来。
|
||
const cancelledUserItemId = result.clientTurnId
|
||
? directCodexConversationMessageId(result.clientTurnId, 'user')
|
||
: '';
|
||
setDirectThreadChat((state) =>
|
||
directThreadTurnMatchesUser(state, cancelledUserItemId)
|
||
? finishDirectThreadTurn(state, Date.now())
|
||
: state,
|
||
);
|
||
setChatAgentBusy(false);
|
||
setProjectSupervisorRuntimeError('');
|
||
setChatComposerNotice(
|
||
message ?? '已结束这一轮占用,可以直接重新发送消息',
|
||
);
|
||
return;
|
||
}
|
||
if (message) {
|
||
setChatComposerNotice(message);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectSupervisorRuntimeError(`终止失败:${message}`);
|
||
setChatComposerNotice('');
|
||
} finally {
|
||
setDirectCodexTurnCancelling(false);
|
||
}
|
||
}
|
||
|
||
function handleProjectSupervisorOnlySubmit(
|
||
event: FormEvent<HTMLFormElement>,
|
||
) {
|
||
event.preventDefault();
|
||
const prompt = chatInput.trim();
|
||
const references = chatReferences;
|
||
const pendingAttachments = chatAttachments;
|
||
if (
|
||
!directCodexProductRuntime &&
|
||
supervisorChatOnly &&
|
||
prompt.startsWith('/')
|
||
) {
|
||
void handleChatSubmit(event);
|
||
return;
|
||
}
|
||
if (
|
||
!directCodexProductRuntime &&
|
||
agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)
|
||
) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (
|
||
!prompt &&
|
||
references.length === 0 &&
|
||
chatContent.length === 0 &&
|
||
pendingAttachments.length === 0
|
||
) {
|
||
return;
|
||
}
|
||
if (supervisorChatBusy) {
|
||
// 回合运行中再次发送:direct-codex 面板把消息放进本地 FIFO 队列,当前回合结束后
|
||
// 依次发出;其它面板保持原有"运行中不接受新输入"的行为。
|
||
if (directCodexProductRuntime) {
|
||
const enqueued = enqueueChatTurnForRunningTurn({
|
||
prompt,
|
||
attachments: pendingAttachments,
|
||
references,
|
||
content: chatContent,
|
||
});
|
||
if (enqueued) {
|
||
setChatInput('');
|
||
setChatContent([]);
|
||
setChatReferences([]);
|
||
setChatAttachments([]);
|
||
setChatAttachmentNotice('');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (directCodexProductRuntime && prompt === '/history') {
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
setChatInput('');
|
||
setChatReferences([]);
|
||
setChatContent([]);
|
||
void loadProjectConversation(nextProjectPath, false, 'replace');
|
||
return;
|
||
}
|
||
if (supervisorChatOnly || directCodexProductRuntime || designAgentActive) {
|
||
supervisorChatShouldFollowLatestRef.current = true;
|
||
}
|
||
if (directCodexProductRuntime) {
|
||
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
|
||
setChatInput('');
|
||
setChatReferences([]);
|
||
setChatAttachments([]);
|
||
setChatContent([]);
|
||
setChatAttachmentNotice('');
|
||
setChatComposerNotice('');
|
||
startDirectCodexConversationTurn({
|
||
prompt,
|
||
attachments: pendingAttachments,
|
||
references,
|
||
content: chatContent,
|
||
});
|
||
return;
|
||
}
|
||
setChatInput('');
|
||
setChatReferences([]);
|
||
setChatContent([]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: prompt,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
void executeChatAgentReply({ prompt, references });
|
||
}
|
||
|
||
const visibleProfessionalAgentCards = agentStatusCards.filter(
|
||
(agent) =>
|
||
agent.id !== PROJECT_SUPERVISOR_AGENT_ID &&
|
||
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
|
||
);
|
||
const useDesignAgentSurface = Boolean(designAgentView) || designAgentActive;
|
||
|
||
if (projectSupervisorOnly && supervisorChatOnly) {
|
||
const supervisorProjectPath =
|
||
localProject?.projectPath || initialProjectPath || projectPath;
|
||
return (
|
||
<SupervisorChatOnlyView
|
||
activeVersionId={chatActiveVersionId}
|
||
chatAgentBusy={chatAgentBusy}
|
||
chatInput={chatInput}
|
||
chatReferences={chatReferences}
|
||
composerRef={chatComposerRef}
|
||
chatProjectAssets={chatProjectAssets}
|
||
messagesRef={supervisorChatMessagesRef}
|
||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||
onChatInputChange={handleChatComposerChange}
|
||
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
|
||
onConfirmConfirmation={confirmUiCommand}
|
||
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
|
||
onScroll={handleSupervisorChatScroll}
|
||
onShowEarlierMessages={showEarlierConversationMessages}
|
||
hasEarlierConversationMessages={hasEarlierConversationMessages}
|
||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||
onToolAction={handleProjectSupervisorToolAction}
|
||
onUserInput={handleProjectSupervisorUserInput}
|
||
pendingConfirmation={pendingUiConfirmation}
|
||
pendingCommand={pendingCommand}
|
||
onCancelPendingCommand={handlePendingCommandCancel}
|
||
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
|
||
projectPath={supervisorProjectPath}
|
||
runtime={projectSupervisorRuntime}
|
||
runtimeConfigOpen={runtimeConfigOpen}
|
||
runtimeError={projectSupervisorRuntimeError}
|
||
transientReply={
|
||
designAgentActive
|
||
? designAgentTransientReply
|
||
: projectSupervisorTransientReply
|
||
}
|
||
hasConversationControls={projectSupervisorHasConversationControls}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
needsUserInput={projectSupervisorNeedsUserInput}
|
||
visibleMessages={visibleMessages}
|
||
workspaceStatus={workspaceStatus}
|
||
expectedRunId={projectSupervisorExpectedRunId}
|
||
versions={chatProjectVersions}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (projectSupervisorOnly) {
|
||
return (
|
||
<ProjectSupervisorView
|
||
initialSupervisorMessage={initialSupervisorMessage}
|
||
activeVersionId={chatActiveVersionId}
|
||
attachments={chatAttachments}
|
||
attachmentNotice={chatAttachmentNotice}
|
||
chatInput={chatInput}
|
||
chatReferences={chatReferences}
|
||
composerNotice={chatComposerNotice}
|
||
onCancelQueuedTurn={cancelQueuedChatTurn}
|
||
onCancelTurn={() => void handleCancelDirectCodexTurn()}
|
||
onRemoveAttachment={removeChatComposerAttachment}
|
||
onUploadFiles={(files) => void handleChatComposerUploadFiles(files)}
|
||
queuedTurns={chatTurnQueue}
|
||
turnCancelling={directCodexTurnCancelling}
|
||
composerRef={chatComposerRef}
|
||
chatProjectAssets={chatProjectAssets}
|
||
directCodex={directCodexProductRuntime}
|
||
directTurnRunning={directCodexProductRuntime && directTurnRunning}
|
||
directTurnStartedAt={directThreadChat.turnStartedAt}
|
||
directEntries={
|
||
directCodexProductRuntime
|
||
? selectDirectChatEntries(directThreadChat)
|
||
: []
|
||
}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
hasEarlierConversationMessages={hasEarlierConversationMessages}
|
||
messagesRef={supervisorChatMessagesRef}
|
||
needsUserInput={
|
||
directCodexProductRuntime ? false : projectSupervisorNeedsUserInput
|
||
}
|
||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||
onCancelPendingCommand={handlePendingCommandCancel}
|
||
onChatInputChange={handleChatComposerChange}
|
||
onConfirmConfirmation={confirmUiCommand}
|
||
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
|
||
onScroll={handleSupervisorChatScroll}
|
||
onShowEarlierMessages={showEarlierConversationMessages}
|
||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||
pendingConfirmation={
|
||
directCodexProductRuntime ? null : pendingUiConfirmation
|
||
}
|
||
pendingCommand={directCodexProductRuntime ? pendingCommand : null}
|
||
projectPath={localProject?.projectPath ?? projectPath}
|
||
conversationMessages={messages}
|
||
transientReply={
|
||
designAgentActive
|
||
? designAgentTransientReply
|
||
: directCodexProductRuntime
|
||
? ''
|
||
: projectSupervisorTransientReply
|
||
}
|
||
showDesignReasoning={designAgentActive}
|
||
designReasoning={designAgentReasoning}
|
||
designReasoningEntries={
|
||
useDesignAgentSurface ? (designAgentView?.reasoningEntries ?? []) : []
|
||
}
|
||
visibleMessages={visibleMessages}
|
||
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
|
||
showProfessionalCollaboration={
|
||
!directCodexProductRuntime && orchestrationMode === 'professional-dag'
|
||
}
|
||
workspaceStatus={workspaceStatus}
|
||
designView={useDesignAgentSurface ? designAgentView : null}
|
||
onDesignApprove={
|
||
useDesignAgentSurface
|
||
? (requestId, approved) => {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const invoke = resolveTauriInvoke();
|
||
if (!nextProjectPath || !invoke) {
|
||
return;
|
||
}
|
||
const clientTurnId = createAgentChatRunId('design-agent-turn');
|
||
designAgentTurnRef.current = {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
};
|
||
designAgentReasoningTurnRef.current = {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
text: '',
|
||
};
|
||
designAgentPendingViewRef.current = null;
|
||
setDesignAgentTransientReplyTarget('');
|
||
setDesignAgentReasoning('');
|
||
setChatAgentBusy(true);
|
||
void designAgentEventSubscriptionReady()
|
||
.then(() =>
|
||
invoke<DesignView>('decide_design_phase', {
|
||
projectPath: nextProjectPath,
|
||
clientTurnId,
|
||
requestId,
|
||
approved,
|
||
}),
|
||
)
|
||
.then((view) => {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
designAgentTurnRef.current?.projectPath !==
|
||
nextProjectPath ||
|
||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
|
||
) {
|
||
return;
|
||
}
|
||
applyDesignAgentViewAfterTransient(
|
||
view,
|
||
nextProjectPath,
|
||
clientTurnId,
|
||
);
|
||
})
|
||
.catch((error) => {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
designAgentTurnRef.current?.projectPath !==
|
||
nextProjectPath ||
|
||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
|
||
) {
|
||
return;
|
||
}
|
||
setProjectSupervisorRuntimeError(String(error));
|
||
})
|
||
.finally(() => {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
designAgentTurnRef.current?.projectPath !==
|
||
nextProjectPath ||
|
||
designAgentTurnRef.current?.clientTurnId !== clientTurnId
|
||
) {
|
||
return;
|
||
}
|
||
if (!designAgentPendingViewRef.current) {
|
||
designAgentTurnRef.current = null;
|
||
setDesignAgentTransientReplyTarget('');
|
||
}
|
||
setChatAgentBusy(false);
|
||
});
|
||
}
|
||
: undefined
|
||
}
|
||
onDesignClarify={
|
||
useDesignAgentSurface
|
||
? (question: DesignClarificationRequest, optionIndex, text) => {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
void executeDesignAgentTurn(nextProjectPath, {
|
||
type: 'clarification',
|
||
requestId: question.requestId,
|
||
optionIndex,
|
||
text: text.trim() ? text : null,
|
||
});
|
||
}
|
||
: undefined
|
||
}
|
||
onDesignRetry={
|
||
useDesignAgentSurface
|
||
? () => {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
void executeDesignAgentTurn(nextProjectPath, { type: 'retry' });
|
||
}
|
||
: undefined
|
||
}
|
||
runtime={projectSupervisorRuntime}
|
||
error={projectSupervisorRuntimeError}
|
||
runtimeByAgentId={agentRuntimeById}
|
||
controlBusy={
|
||
directCodexProductRuntime ? supervisorChatBusy : chatAgentBusy
|
||
}
|
||
readOnly={directCodexProductRuntime}
|
||
versions={chatProjectVersions}
|
||
professionalResultsByAgentId={professionalAgentResultsById}
|
||
onToolAction={handleProjectSupervisorToolAction}
|
||
onSupervisorRetry={handleProjectSupervisorRetry}
|
||
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
|
||
onProfessionalRetry={handleProjectProfessionalAgentRetry}
|
||
onUserInput={handleProjectSupervisorUserInput}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<main
|
||
className={`app-shell ${devMode ? 'app-shell--dev' : 'app-shell--user'}`}
|
||
>
|
||
<ProjectWorkspaceChatPane
|
||
activeVersionId={chatActiveVersionId}
|
||
agentRunStatus={agentRunStatus}
|
||
agentRuntimeById={agentRuntimeById}
|
||
agentStatusCards={agentStatusCards}
|
||
cancelProjectCreateInNonEmptyFolder={
|
||
cancelProjectCreateInNonEmptyFolder
|
||
}
|
||
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
||
chatAgentBusy={chatAgentBusy}
|
||
chatInput={chatInput}
|
||
chatReferences={chatReferences}
|
||
composerRef={chatComposerRef}
|
||
chatProjectAssets={chatProjectAssets}
|
||
chatInputRef={chatInputRef}
|
||
confirmProjectCreateInNonEmptyFolder={
|
||
confirmProjectCreateInNonEmptyFolder
|
||
}
|
||
confirmUiCommand={confirmUiCommand}
|
||
currentProjectTitle={currentProjectTitle}
|
||
devMode={devMode}
|
||
executeAgentAuditChat={executeAgentAuditChat}
|
||
executeAgentCapabilitiesChat={executeAgentCapabilitiesChat}
|
||
executeAgentRunControl={executeAgentRunControl}
|
||
executeAgentTraceChat={executeAgentTraceChat}
|
||
executeLimitedCommandList={executeLimitedCommandList}
|
||
executeLlmConfigStatus={executeLlmConfigStatus}
|
||
executeMemoryReadChat={executeMemoryReadChat}
|
||
executePreviewStatus={executePreviewStatus}
|
||
executePreviewStop={executePreviewStop}
|
||
executeProjectAssets={executeProjectAssets}
|
||
executeProjectCheckpoints={executeProjectCheckpoints}
|
||
executeProjectDiff={executeProjectDiff}
|
||
executeProjectFileReadChat={executeProjectFileReadChat}
|
||
executeProjectFiles={executeProjectFiles}
|
||
executeProjectPolicyRead={executeProjectPolicyRead}
|
||
executeProjectStatus={executeProjectStatus}
|
||
executeProjectTasks={executeProjectTasks}
|
||
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
|
||
handleAssetUpload={handleAssetUpload}
|
||
handleChatSubmit={handleChatSubmit}
|
||
handleConversationScroll={handleConversationScroll}
|
||
handleOpenLauncherWindow={handleOpenLauncherWindow}
|
||
handlePendingCommandCancel={handlePendingCommandCancel}
|
||
handlePendingCommandConfirm={handlePendingCommandConfirm}
|
||
handlePickCanvasExportFileDraft={handlePickCanvasExportFileDraft}
|
||
handleProjectProfessionalAgentRetry={
|
||
handleProjectProfessionalAgentRetry
|
||
}
|
||
handleProjectProfessionalAgentToolAction={
|
||
handleProjectProfessionalAgentToolAction
|
||
}
|
||
handleProjectSupervisorRetry={handleProjectSupervisorRetry}
|
||
handleProjectSupervisorToolAction={handleProjectSupervisorToolAction}
|
||
handleProjectSupervisorUserInput={handleProjectSupervisorUserInput}
|
||
handleRevealCurrentProjectDirectory={
|
||
handleRevealCurrentProjectDirectory
|
||
}
|
||
handleRuntimeConfigOpen={handleRuntimeConfigOpen}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
hasEarlierConversationMessages={hasEarlierConversationMessages}
|
||
llmConfigStatus={llmConfigStatus}
|
||
loadProjectConversation={loadProjectConversation}
|
||
localProject={localProject}
|
||
mainProjectSummary={mainProjectSummary}
|
||
openAgentConversation={openAgentConversation}
|
||
pendingCommand={pendingCommand}
|
||
pendingNonEmptyProjectCreate={pendingNonEmptyProjectCreate}
|
||
pendingUiConfirmation={pendingUiConfirmation}
|
||
prepareChatCommandDraft={prepareChatCommandDraft}
|
||
prepareProjectAssetRegisterDraft={prepareProjectAssetRegisterDraft}
|
||
previewStatus={previewStatus}
|
||
professionalAgentResultsById={professionalAgentResultsById}
|
||
projectPath={projectPath}
|
||
projectSupervisorNeedsUserInput={projectSupervisorNeedsUserInput}
|
||
projectSupervisorRuntime={projectSupervisorRuntime}
|
||
projectSupervisorRuntimeError={projectSupervisorRuntimeError}
|
||
projectSupervisorTransientReply={projectSupervisorTransientReply}
|
||
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
|
||
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
|
||
queuePendingCommand={queuePendingCommand}
|
||
queuePreviewOpenShortcut={queuePreviewOpenShortcut}
|
||
queuePreviewStartShortcut={queuePreviewStartShortcut}
|
||
queueProjectCheckpointShortcut={queueProjectCheckpointShortcut}
|
||
queueProjectExportPackageShortcut={queueProjectExportPackageShortcut}
|
||
queueRunLocalShortcut={queueRunLocalShortcut}
|
||
queueStaticSmokeShortcut={queueStaticSmokeShortcut}
|
||
scheduleReadyAgentTasks={scheduleReadyAgentTasks}
|
||
onChatComposerChange={handleChatComposerChange}
|
||
showChatHelp={showChatHelp}
|
||
showEarlierConversationMessages={showEarlierConversationMessages}
|
||
visibleMainProjectAssets={visibleMainProjectAssets}
|
||
versions={chatProjectVersions}
|
||
visibleMainProjectCheckpoints={visibleMainProjectCheckpoints}
|
||
visibleMainProjectFiles={visibleMainProjectFiles}
|
||
visibleMessages={visibleMessages}
|
||
workspaceStatus={workspaceStatus}
|
||
/>
|
||
|
||
{runtimeConfigOpen ? (
|
||
<RuntimeConfigDialog
|
||
projectPath={localProject?.projectPath}
|
||
onClose={() => setRuntimeConfigOpen(false)}
|
||
onLog={(entry) => setCommandLog((current) => [...current, entry])}
|
||
/>
|
||
) : null}
|
||
|
||
{selectedAgent ? (
|
||
<AgentConversationOverlay
|
||
agentConversationBackgroundBusy={agentConversationBackgroundBusy}
|
||
agentConversationInput={agentConversationInput}
|
||
agentConversationMessages={agentConversationMessages}
|
||
agentConversationRunSubmitMode={agentConversationRunSubmitMode}
|
||
agentConversationRuntime={agentConversationRuntime}
|
||
agentConversationRuntimeError={agentConversationRuntimeError}
|
||
agentConversationSaving={agentConversationSaving}
|
||
agentConversationStatus={agentConversationStatus}
|
||
agentMemoryContent={agentMemoryContent}
|
||
agentMemoryStatus={agentMemoryStatus}
|
||
answerSelectedAgentRuntimeUserInput={
|
||
answerSelectedAgentRuntimeUserInput
|
||
}
|
||
cancelSelectedAgentRuntimeTask={cancelSelectedAgentRuntimeTask}
|
||
closeAgentConversation={closeAgentConversation}
|
||
confirmSelectedAgentRuntimeTask={confirmSelectedAgentRuntimeTask}
|
||
executeLlmConfigStatus={executeLlmConfigStatus}
|
||
handleAgentBackgroundTaskSubmit={handleAgentBackgroundTaskSubmit}
|
||
handleAgentConversationScroll={handleAgentConversationScroll}
|
||
handleAgentConversationSubmit={handleAgentConversationSubmit}
|
||
handleAgentPrivateMemorySubmit={handleAgentPrivateMemorySubmit}
|
||
hiddenAgentConversationCount={hiddenAgentConversationCount}
|
||
openAgentConversation={openAgentConversation}
|
||
prepareAgentEvidenceReadDraft={prepareAgentEvidenceReadDraft}
|
||
prepareSuggestedToolCommandDraft={prepareSuggestedToolCommandDraft}
|
||
rejectSelectedAgentRuntimeTask={rejectSelectedAgentRuntimeTask}
|
||
retrySelectedAgentRuntimeTask={retrySelectedAgentRuntimeTask}
|
||
selectedAgent={selectedAgent}
|
||
selectedAgentLlmStatus={selectedAgentLlmStatus}
|
||
selectedAgentLlmWarning={selectedAgentLlmWarning}
|
||
selectedAgentNeedsUserInput={selectedAgentNeedsUserInput}
|
||
selectedAgentSteerRuntime={selectedAgentSteerRuntime}
|
||
setAgentConversationInput={setAgentConversationInput}
|
||
setAgentConversationRunSubmitMode={setAgentConversationRunSubmitMode}
|
||
setRuntimeConfigOpen={setRuntimeConfigOpen}
|
||
showEarlierAgentConversationMessages={
|
||
showEarlierAgentConversationMessages
|
||
}
|
||
visibleAgentConversationMessages={visibleAgentConversationMessages}
|
||
/>
|
||
) : null}
|
||
|
||
{devMode ? (
|
||
<section className="developer-pane" aria-label="开发环境">
|
||
<DeveloperRuntimePanels
|
||
agentRunHistory={agentRunHistory}
|
||
agentRunHistoryLoadingMore={agentRunHistoryLoadingMore}
|
||
agentRunHistoryOmittedCount={agentRunHistoryOmittedCount}
|
||
agentRunStatus={agentRunStatus}
|
||
agentRunTrace={agentRunTrace}
|
||
canShowMoreAgentRunHistory={canShowMoreAgentRunHistory}
|
||
handleAgentRunHistoryScroll={handleAgentRunHistoryScroll}
|
||
handleAgentRunTraceFileOpen={handleAgentRunTraceFileOpen}
|
||
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
|
||
manifest={manifest}
|
||
prepareChatCommandDraft={prepareChatCommandDraft}
|
||
refreshManifest={refreshManifest}
|
||
showMoreAgentRunHistory={showMoreAgentRunHistory}
|
||
visibleAgentRunHistory={visibleAgentRunHistory}
|
||
/>
|
||
<DeveloperProjectPanels
|
||
assetCanvasProjectId={assetCanvasProjectId}
|
||
assetGenerationPrompt={assetGenerationPrompt}
|
||
assetKind={assetKind}
|
||
assetLocalPath={assetLocalPath}
|
||
assetMediaType={assetMediaType}
|
||
assetObjectId={assetObjectId}
|
||
assetResourceId={assetResourceId}
|
||
assetSourceKind={assetSourceKind}
|
||
assetStatus={assetStatus}
|
||
canvasExportPath={canvasExportPath}
|
||
commandLog={commandLog}
|
||
editorBaseUrl={editorBaseUrl}
|
||
fileDraft={fileDraft}
|
||
filePath={filePath}
|
||
fileStatus={fileStatus}
|
||
handleAssetRegister={handleAssetRegister}
|
||
handleCanvasAssetGenerate={handleCanvasAssetGenerate}
|
||
handleCanvasAssetImport={handleCanvasAssetImport}
|
||
handleCanvasExportImport={handleCanvasExportImport}
|
||
handleCanvasProjectOpen={handleCanvasProjectOpen}
|
||
handleCanvasProjectSync={handleCanvasProjectSync}
|
||
handleFileDelete={handleFileDelete}
|
||
handleFileList={handleFileList}
|
||
handleFileRead={handleFileRead}
|
||
handleFileWrite={handleFileWrite}
|
||
handleLimitedCommandRun={handleLimitedCommandRun}
|
||
handleMemoryDelete={handleMemoryDelete}
|
||
handleMemoryRead={handleMemoryRead}
|
||
handleMemoryWrite={handleMemoryWrite}
|
||
handlePreviewStart={handlePreviewStart}
|
||
handlePreviewStatus={handlePreviewStatus}
|
||
handlePreviewStop={handlePreviewStop}
|
||
handleProjectInit={handleProjectInit}
|
||
handleProjectLogRead={handleProjectLogRead}
|
||
limitedCommandStatus={limitedCommandStatus}
|
||
limitedLocalCommands={limitedLocalCommands}
|
||
localProject={localProject}
|
||
memoryDraft={memoryDraft}
|
||
memoryScope={memoryScope}
|
||
memoryStatus={memoryStatus}
|
||
preview={preview}
|
||
previewStatus={previewStatus}
|
||
projectFiles={projectFiles}
|
||
projectLogContent={projectLogContent}
|
||
projectLogStatus={projectLogStatus}
|
||
projectPath={projectPath}
|
||
projectStatus={projectStatus}
|
||
refreshLimitedLocalCommands={refreshLimitedLocalCommands}
|
||
setAssetCanvasProjectId={setAssetCanvasProjectId}
|
||
setAssetGenerationPrompt={setAssetGenerationPrompt}
|
||
setAssetKind={setAssetKind}
|
||
setAssetLocalPath={setAssetLocalPath}
|
||
setAssetMediaType={setAssetMediaType}
|
||
setAssetObjectId={setAssetObjectId}
|
||
setAssetResourceId={setAssetResourceId}
|
||
setAssetSourceKind={setAssetSourceKind}
|
||
setCanvasExportPath={setCanvasExportPath}
|
||
setEditorBaseUrl={setEditorBaseUrl}
|
||
setFileDraft={setFileDraft}
|
||
setFilePath={setFilePath}
|
||
setMemoryDraft={setMemoryDraft}
|
||
setMemoryScope={setMemoryScope}
|
||
setProjectPath={setProjectPath}
|
||
uploadedAssets={uploadedAssets}
|
||
/>
|
||
</section>
|
||
) : null}
|
||
</main>
|
||
);
|
||
}
|