11132 lines
354 KiB
TypeScript
11132 lines
354 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 GameCreationAppCommandDescriptor,
|
||
type GameCreationAppLimitedRunCommandDescriptor,
|
||
type GameCreationAppManifest,
|
||
type GameCreationAppPermission,
|
||
type GameCreationAppPreviewState,
|
||
type GameCreationAppPreviewStatus,
|
||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import {
|
||
AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD,
|
||
AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT,
|
||
AGENT_RUN_HISTORY_MAX_COUNT,
|
||
AGENT_RUN_HISTORY_VISIBLE_STEP,
|
||
CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
CONVERSATION_VISIBLE_STEP,
|
||
defaultProjectPath,
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
seedManifest,
|
||
} from './app/constants';
|
||
import { useEscapeToClose } from './app/dialogs';
|
||
import { resolveTauriInvoke } from './app/tauri';
|
||
import type {
|
||
AgentBackgroundSubmitMode,
|
||
AgentProgressEvent,
|
||
AgentRunControlResult,
|
||
AgentRunHistoryItem,
|
||
AgentRuntimeResponseStream,
|
||
AgentRuntimeResult,
|
||
AgentRuntimeState,
|
||
AgentRuntimeSteerResult,
|
||
AgentRuntimeUserInputRequest,
|
||
AgentStatusCard,
|
||
ChatMessage,
|
||
GameCreatorAgentRuntimeUpdateEvent,
|
||
GameCreatorChatAgentReply,
|
||
GameCreatorLlmConfigStatus,
|
||
GameCreatorRoleAgentChatStreamEvent,
|
||
GenerateLocalGameDraftResult,
|
||
ImportCanvasExportResult,
|
||
InitLocalProjectResult,
|
||
LimitedLocalCommandResult,
|
||
ListLocalProjectFilesResult,
|
||
LocalAgentMemoryResult,
|
||
LocalConversationMessageRecord,
|
||
LocalConversationResult,
|
||
LocalGameMemoryResult,
|
||
LocalGameProjectRevisionStatus,
|
||
LocalPreviewResult,
|
||
LocalPreviewStatus,
|
||
LocalProjectCheckpointResult,
|
||
LocalProjectCheckpointSummary,
|
||
LocalProjectDiffResult,
|
||
LocalProjectDirectoryStatus,
|
||
LocalProjectExportPackageResult,
|
||
LocalProjectExportPackagesResult,
|
||
LocalProjectFileEntry,
|
||
LocalProjectFileMutationResult,
|
||
LocalProjectFileResult,
|
||
LocalProjectIndexResult,
|
||
LocalProjectRestoreResult,
|
||
MemoryScope,
|
||
MemoryWriteMode,
|
||
OpenCanvasProjectResult,
|
||
PendingCommand,
|
||
PendingUiConfirmation,
|
||
ProjectPermissionPolicy,
|
||
ProjectPermissionPolicyView,
|
||
SyncCanvasProjectAssetsResult,
|
||
TauriInvoke,
|
||
UploadLocalAssetResult,
|
||
} from './app/types';
|
||
import {
|
||
agentConversationId,
|
||
agentRuntimeCancelStatus,
|
||
agentRuntimeConversationStatus,
|
||
agentRuntimeNeedsUserInput,
|
||
agentRuntimeStartedRunId,
|
||
agentRuntimeStartStatus,
|
||
agentRuntimeStateFromResult,
|
||
agentRuntimeSteerStatus,
|
||
conversationContainsProjectSupervisorResponseStream,
|
||
createAgentChatRunId,
|
||
createDefaultChatMessages,
|
||
createLocalConversationDraftMessage,
|
||
ensureProjectSupervisorActiveSessionId,
|
||
isAgentFinalizationMessageId,
|
||
isAgentRuntimeTerminalState,
|
||
isMissingAgentRuntimeResumeCommandError,
|
||
isRuntimeConfigMissingError,
|
||
matchingAgentRuntimeForSteer,
|
||
mergeAgentRuntimeStateIntoMap,
|
||
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
||
mergeProjectSupervisorConversation,
|
||
mergeProjectSupervisorResponseStream,
|
||
normalizeAgentRuntimeState,
|
||
projectNameFromPath,
|
||
projectProfessionalAgentLabel,
|
||
projectSupervisorPendingRepairMatchesProfessional,
|
||
projectSupervisorResponseStreamIdentity,
|
||
readProjectSupervisorActiveSessionId,
|
||
sameAgentRuntimeRun,
|
||
submitProjectSupervisorRuntimeTask,
|
||
taskRowsFromManifest,
|
||
} from './features/agent-runtime';
|
||
import {
|
||
isDeveloperMode,
|
||
isTransientProjectOpenMessage,
|
||
latestVisibleItems,
|
||
persistSupervisorChatDraft,
|
||
readInitialProjectPath,
|
||
readSupervisorChatDraft,
|
||
type WorkspaceLauncherProps,
|
||
writeRecentWorkspace,
|
||
} from './features/app-shell/model';
|
||
import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
|
||
import {
|
||
agentConversationReadDraftsFromManifest,
|
||
agentMemoryReadDraftsFromManifest,
|
||
commandDraftFromSuggestedToolCall,
|
||
deriveAgentStatusCards,
|
||
formatAgentDialogLlmStatus,
|
||
formatAgentLlmConfigWarning,
|
||
formatAgentRunControlError,
|
||
formatLlmAgentStatusLine,
|
||
formatLlmRouteEndpoint,
|
||
isMissingAgentRunTraceError,
|
||
projectAgentRuntimeSummaries,
|
||
readablePassArtifactsFromAgentRunTrace,
|
||
sameAgentStatusCard,
|
||
summarizeAgentAudit,
|
||
summarizeAgentCapabilities,
|
||
summarizeAgentConversationReadDrafts,
|
||
summarizeAgentLlmRoutes,
|
||
summarizeAgentMemoryReadDrafts,
|
||
summarizeAgentPassArtifactReadDrafts,
|
||
summarizeAgentRunCompletionForChat,
|
||
summarizeAgentRunHistoryReadDrafts,
|
||
summarizeAgentStatusCardsForChat,
|
||
summarizeLimitedLocalCommands,
|
||
summarizeRunArtifactReadDrafts,
|
||
} from './features/project-summary/agentPresentation';
|
||
import {
|
||
chatCommandHelp,
|
||
checkpointSummaryFromManifest,
|
||
commonAgentRunSupportReadDrafts,
|
||
commonProjectArtifactReadDrafts,
|
||
commonProjectInternalReadDrafts,
|
||
commonProjectLogReadDrafts,
|
||
firstReadableProjectAssetPath,
|
||
formatAgentRunStatus,
|
||
formatCanvasAssetSource,
|
||
isAbsoluteProjectPath,
|
||
isSafeProjectRelativePath,
|
||
missingChatCommandArgumentMessage,
|
||
projectFileActionDrafts,
|
||
projectPathHasControlCharacter,
|
||
readableArtifactsFromAgentRunTrace,
|
||
sortCheckpointManifestFiles,
|
||
summarizeAgentRunSupportFileReadDrafts,
|
||
summarizeCommonProjectArtifactReadDrafts,
|
||
summarizeCommonProjectLogReadDrafts,
|
||
summarizeMainProjectHeader,
|
||
summarizeProjectAssetCredits,
|
||
summarizeProjectAssets,
|
||
summarizeProjectAudioAssets,
|
||
summarizeProjectCheckpoint,
|
||
summarizeProjectCheckpoints,
|
||
summarizeProjectDiff,
|
||
summarizeProjectExportPackage,
|
||
summarizeProjectExportPackages,
|
||
summarizeProjectFileContent,
|
||
summarizeProjectFiles,
|
||
summarizeProjectIndex,
|
||
summarizeProjectInternalReadDrafts,
|
||
summarizeProjectPolicy,
|
||
summarizeProjectStatus,
|
||
summarizeProjectTasks,
|
||
summarizeProjectVisualAssets,
|
||
} from './features/project-summary/projectSummary';
|
||
import { AgentConversationOverlay } from './features/project-workspace/AgentConversationOverlay';
|
||
import {
|
||
gameDraftStartedMessage,
|
||
isMissingProjectFileError,
|
||
parseAgentRunTrace,
|
||
} from './features/project-workspace/agentRunTrace';
|
||
import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
|
||
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
|
||
import { resolveEmbeddedPreviewUrl } from './features/project-workspace/LocalGamePreviewFrame';
|
||
import {
|
||
appendMemoryContent,
|
||
memoryScopeLabel,
|
||
memoryScopePath,
|
||
parseOptionalMemoryScope,
|
||
parseRememberInput,
|
||
} from './features/project-workspace/memoryCommands';
|
||
import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation';
|
||
import {
|
||
isAgentTraceFilePath,
|
||
isProjectPolicyConfirmableCommandId,
|
||
isRegisteredGameCreationCommandId,
|
||
isSafeCanvasProjectId,
|
||
isSafeCheckpointId,
|
||
needsInitializedChatProject,
|
||
resolveChatProjectPath,
|
||
resolvePendingCommandProjectPath,
|
||
} from './features/project-workspace/projectCommandPolicy';
|
||
import { handleProjectSummaryChatCommand } from './features/project-workspace/projectSummaryCommands';
|
||
import { ProjectSupervisorView } from './features/project-workspace/ProjectSupervisorView';
|
||
import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane';
|
||
import {
|
||
buildGameChatProgressEvidence,
|
||
collectGameChatResultImages,
|
||
formatGameChatStageRecord,
|
||
gameChatFinalReplyMessages,
|
||
gameChatRuntimeEventMessages,
|
||
mergeGameChatFinalReplyMessagesIntoHistory,
|
||
mergeGameChatRuntimeEventMessagesIntoHistory,
|
||
SupervisorChatOnlyView,
|
||
} from './features/project-workspace/SupervisorChatOnlyView';
|
||
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
|
||
import {
|
||
type ProjectAgentResultSummary,
|
||
type ProjectAgentRuntimeSummary,
|
||
} from './view/project-development';
|
||
|
||
const initialSupervisorMessageClaimsByPage = new WeakMap<Window, Set<string>>();
|
||
const LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
|
||
'genarrative.game-chat.auto-preview-authorization.v1';
|
||
const GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
|
||
'genarrative.game-chat.auto-preview-authorization.v2';
|
||
|
||
type GameChatAutoPreviewAuthorization = {
|
||
afterRevision: number;
|
||
afterValidatedAt: number;
|
||
authorizationId: string;
|
||
projectPath: string;
|
||
runId: string;
|
||
};
|
||
|
||
type GameChatPlayableRevision = {
|
||
runId: string;
|
||
revision: number;
|
||
validatedAt: number;
|
||
};
|
||
|
||
type GameChatPreviewValidationCandidate = GameChatPlayableRevision & {
|
||
eventOrder: number;
|
||
playable: boolean;
|
||
};
|
||
|
||
const GAME_CHAT_STAGE_TASK_IDS = [
|
||
'design-director',
|
||
'art-director',
|
||
'art-asset-plan',
|
||
'code-director',
|
||
'code-prototype',
|
||
'preview-readiness',
|
||
'preview-playtest',
|
||
] as const;
|
||
|
||
function gameChatManifestHasTerminalStageTasks(
|
||
manifest: GameCreationAppManifest | null,
|
||
) {
|
||
if (!manifest) {
|
||
return false;
|
||
}
|
||
return GAME_CHAT_STAGE_TASK_IDS.every((taskId) => {
|
||
const status = manifest.tasks.find((task) => task.id === taskId)?.status;
|
||
return status === 'completed' || status === 'failed';
|
||
});
|
||
}
|
||
|
||
function gameChatRuntimeHasTerminalOutcome(runtime: AgentRuntimeState) {
|
||
return (
|
||
['completed', 'failed', 'cancelled'].includes(runtime.status) ||
|
||
['completed', 'failed', 'cancelled'].includes(runtime.phase)
|
||
);
|
||
}
|
||
|
||
function mergeGameChatHydratedConversationMessages(
|
||
historyMessages: ChatMessage[],
|
||
currentMessages: ChatMessage[],
|
||
) {
|
||
const mergedMessages = mergeGameChatRuntimeEventMessagesIntoHistory(
|
||
mergeGameChatFinalReplyMessagesIntoHistory(
|
||
mergeGameChatRuntimeResponseMessagesIntoHistory(
|
||
historyMessages,
|
||
currentMessages,
|
||
),
|
||
currentMessages,
|
||
),
|
||
currentMessages,
|
||
);
|
||
const historyMessageReferences = new Set(historyMessages);
|
||
const pendingMessages = mergedMessages
|
||
.filter((message) => !historyMessageReferences.has(message))
|
||
.sort((left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0));
|
||
// Conversation persistence uses a positional cursor. Keep durable history
|
||
// as one prefix so newly observed replies cannot sort ahead of that cursor
|
||
// and be skipped during hydration.
|
||
return [...historyMessages, ...pendingMessages];
|
||
}
|
||
|
||
function gameChatPlayableRevisionIsAfterAuthorization(
|
||
revision: GameChatPlayableRevision,
|
||
authorization: GameChatAutoPreviewAuthorization,
|
||
) {
|
||
return (
|
||
revision.revision > authorization.afterRevision ||
|
||
(revision.revision === authorization.afterRevision &&
|
||
revision.validatedAt > authorization.afterValidatedAt)
|
||
);
|
||
}
|
||
|
||
export function latestGameChatPlayableRevision(
|
||
runtime: AgentRuntimeState | null,
|
||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||
): GameChatPlayableRevision | null {
|
||
if (!runtime?.runId) {
|
||
return null;
|
||
}
|
||
const playtestChildren = new Map<string, AgentRuntimeState>();
|
||
for (const child of Object.values(runtimeByAgentId)) {
|
||
if (
|
||
child?.agentId !== 'preview-playtest' ||
|
||
child.taskId !== 'preview-playtest' ||
|
||
child.source !== 'agent-ready-task-scheduler' ||
|
||
child.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
child.parentRunId !== runtime.runId
|
||
) {
|
||
continue;
|
||
}
|
||
playtestChildren.set(
|
||
`${child.agentId}\n${child.sessionId}\n${child.runId}`,
|
||
child,
|
||
);
|
||
}
|
||
let latest: GameChatPreviewValidationCandidate | null = null;
|
||
let eventOrder = 0;
|
||
for (const child of playtestChildren.values()) {
|
||
for (const event of child.recentEvents ?? []) {
|
||
eventOrder += 1;
|
||
if (
|
||
event.agentId !== child.agentId ||
|
||
event.taskId !== child.taskId ||
|
||
event.sessionId !== child.sessionId ||
|
||
event.runId !== child.runId ||
|
||
event.eventType !== 'observation' ||
|
||
!Number.isSafeInteger(event.updatedAt) ||
|
||
event.updatedAt < 0 ||
|
||
!event.summary.startsWith('preview.validate:') ||
|
||
!event.detail?.trim().startsWith('{')
|
||
) {
|
||
continue;
|
||
}
|
||
try {
|
||
const detail = JSON.parse(event.detail) as {
|
||
passed?: unknown;
|
||
playtestPassed?: unknown;
|
||
revision?: unknown;
|
||
};
|
||
const playable =
|
||
event.summary.startsWith('preview.validate:ok') &&
|
||
detail.passed === true &&
|
||
detail.playtestPassed === true;
|
||
if (
|
||
typeof detail.passed !== 'boolean' ||
|
||
typeof detail.revision !== 'number' ||
|
||
!Number.isSafeInteger(detail.revision) ||
|
||
detail.revision <= 0
|
||
) {
|
||
continue;
|
||
}
|
||
const shouldReplace =
|
||
!latest ||
|
||
detail.revision > latest.revision ||
|
||
(detail.revision === latest.revision &&
|
||
event.updatedAt > latest.validatedAt) ||
|
||
(detail.revision === latest.revision &&
|
||
event.updatedAt === latest.validatedAt &&
|
||
((latest.playable && !playable) ||
|
||
(latest.playable === playable &&
|
||
eventOrder > latest.eventOrder)));
|
||
if (!shouldReplace) {
|
||
continue;
|
||
}
|
||
latest = {
|
||
eventOrder,
|
||
playable,
|
||
runId: runtime.runId,
|
||
revision: detail.revision,
|
||
validatedAt: event.updatedAt,
|
||
};
|
||
} catch {
|
||
// Ignore malformed or truncated public evidence and wait for a valid revision.
|
||
}
|
||
}
|
||
}
|
||
return latest?.playable
|
||
? {
|
||
runId: latest.runId,
|
||
revision: latest.revision,
|
||
validatedAt: latest.validatedAt,
|
||
}
|
||
: null;
|
||
}
|
||
|
||
function readStoredGameChatAutoPreviewAuthorization(): GameChatAutoPreviewAuthorization | null {
|
||
try {
|
||
window.localStorage.removeItem(
|
||
LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
|
||
);
|
||
const raw = window.localStorage.getItem(
|
||
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
|
||
);
|
||
if (!raw) {
|
||
return null;
|
||
}
|
||
const parsed = JSON.parse(raw) as Partial<GameChatAutoPreviewAuthorization>;
|
||
const projectPath = parsed.projectPath?.trim() ?? '';
|
||
const runId = parsed.runId?.trim() ?? '';
|
||
const authorizationId = parsed.authorizationId?.trim() ?? '';
|
||
const afterRevision = parsed.afterRevision;
|
||
const afterValidatedAt = parsed.afterValidatedAt;
|
||
if (
|
||
!projectPath ||
|
||
!runId ||
|
||
!authorizationId ||
|
||
typeof afterRevision !== 'number' ||
|
||
!Number.isSafeInteger(afterRevision) ||
|
||
afterRevision < 0 ||
|
||
typeof afterValidatedAt !== 'number' ||
|
||
!Number.isSafeInteger(afterValidatedAt) ||
|
||
afterValidatedAt < 0 ||
|
||
!isAbsoluteProjectPath(projectPath) ||
|
||
projectPathHasControlCharacter(projectPath) ||
|
||
projectPathHasControlCharacter(authorizationId) ||
|
||
projectPathHasControlCharacter(runId)
|
||
) {
|
||
window.localStorage.removeItem(
|
||
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
|
||
);
|
||
return null;
|
||
}
|
||
return {
|
||
afterRevision,
|
||
afterValidatedAt,
|
||
authorizationId,
|
||
projectPath,
|
||
runId,
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function storeGameChatAutoPreviewAuthorization(
|
||
authorization: GameChatAutoPreviewAuthorization | null,
|
||
) {
|
||
try {
|
||
if (authorization) {
|
||
window.localStorage.setItem(
|
||
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
|
||
JSON.stringify(authorization),
|
||
);
|
||
} else {
|
||
window.localStorage.removeItem(
|
||
GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY,
|
||
);
|
||
}
|
||
} catch {
|
||
// A volatile in-memory authorization remains sufficient when storage is unavailable.
|
||
}
|
||
}
|
||
|
||
function claimInitialSupervisorMessageForPage(projectPath: string) {
|
||
let claimedProjectPaths = initialSupervisorMessageClaimsByPage.get(window);
|
||
if (!claimedProjectPaths) {
|
||
claimedProjectPaths = new Set<string>();
|
||
initialSupervisorMessageClaimsByPage.set(window, claimedProjectPaths);
|
||
}
|
||
if (claimedProjectPaths.has(projectPath)) {
|
||
return false;
|
||
}
|
||
claimedProjectPaths.add(projectPath);
|
||
return true;
|
||
}
|
||
|
||
export function consumeInitialGameChatMessage(
|
||
searchParams: URLSearchParams,
|
||
location: Pick<Location, 'pathname' | 'hash'>,
|
||
replaceUrl: (url: string) => void,
|
||
) {
|
||
const message = searchParams.get('initialMessage')?.trim() ?? '';
|
||
if (!message) {
|
||
return '';
|
||
}
|
||
searchParams.delete('initialMessage');
|
||
const remainingSearch = searchParams.toString();
|
||
replaceUrl(
|
||
`${location.pathname}${remainingSearch ? `?${remainingSearch}` : ''}${location.hash}`,
|
||
);
|
||
return message;
|
||
}
|
||
|
||
export { AuthenticatedClient } from './app/AuthenticatedClient';
|
||
export type { PendingCommand } from './app/types';
|
||
export {
|
||
deriveAgentStatusCards,
|
||
summarizeAgentAudit,
|
||
summarizeAgentRunTrace,
|
||
} from './features/project-summary/agentPresentation';
|
||
export { isAbsoluteProjectPath } from './features/project-summary/projectSummary';
|
||
export {
|
||
needsInitializedChatProject,
|
||
parseRememberInput,
|
||
pendingCommandDetail,
|
||
resolveChatProjectPath,
|
||
resolvePendingCommandProjectPath,
|
||
};
|
||
|
||
export function WorkspaceLauncher(props: WorkspaceLauncherProps) {
|
||
return <WorkspaceLauncherShell {...props} ProjectSupervisor={App} />;
|
||
}
|
||
|
||
export function GameChatReleaseApp({
|
||
initialProjectPath = '',
|
||
initialSupervisorMessage = '',
|
||
}: {
|
||
initialProjectPath?: string;
|
||
initialSupervisorMessage?: string;
|
||
}) {
|
||
return (
|
||
<App
|
||
initialProjectPath={initialProjectPath}
|
||
projectSupervisorOnly
|
||
gameChatOnly
|
||
initialSupervisorMessage={initialSupervisorMessage}
|
||
/>
|
||
);
|
||
}
|
||
|
||
type AppProps = {
|
||
initialProjectPath?: string;
|
||
initialProjectManifest?: GameCreationAppManifest;
|
||
projectSupervisorOnly?: boolean;
|
||
supervisorChatOnly?: boolean;
|
||
gameChatOnly?: boolean;
|
||
initialSupervisorMessage?: string;
|
||
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
|
||
onAgentRuntimeSummariesChange?: (
|
||
summaries: ProjectAgentRuntimeSummary[],
|
||
) => void;
|
||
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
|
||
};
|
||
|
||
export function App({
|
||
initialProjectPath: initialProjectPathOverride = '',
|
||
initialProjectManifest,
|
||
projectSupervisorOnly = false,
|
||
supervisorChatOnly = false,
|
||
gameChatOnly = false,
|
||
initialSupervisorMessage = '',
|
||
onPreviewChange,
|
||
onAgentRuntimeSummariesChange,
|
||
onAgentResultsChange,
|
||
}: AppProps = {}) {
|
||
const [devMode] = useState(() =>
|
||
projectSupervisorOnly ? false : isDeveloperMode(),
|
||
);
|
||
const [initialProjectPath] = useState(
|
||
() => initialProjectPathOverride || readInitialProjectPath(),
|
||
);
|
||
const eagerSupervisorProject =
|
||
projectSupervisorOnly && Boolean(initialProjectPath) && !gameChatOnly;
|
||
const [projectPath, setProjectPath] = useState(
|
||
initialProjectPath || defaultProjectPath,
|
||
);
|
||
const [localProject, setLocalProject] =
|
||
useState<InitLocalProjectResult | null>(() =>
|
||
eagerSupervisorProject
|
||
? {
|
||
projectPath: initialProjectPath,
|
||
manifestPath: `${initialProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`,
|
||
manifest: initialProjectManifest ?? seedManifest,
|
||
}
|
||
: null,
|
||
);
|
||
const localProjectPathRef = useRef<string | null>(null);
|
||
localProjectPathRef.current = localProject?.projectPath ?? null;
|
||
const [manifest, setManifest] = useState<GameCreationAppManifest>(
|
||
initialProjectManifest ?? seedManifest,
|
||
);
|
||
const [projectStatus, setProjectStatus] = useState(
|
||
eagerSupervisorProject ? '已初始化' : '未初始化',
|
||
);
|
||
const [preview, setPreview] = useState<LocalPreviewResult | null>(null);
|
||
const gameChatPreviewRef = useRef<LocalPreviewResult | null>(null);
|
||
gameChatPreviewRef.current = preview;
|
||
const [gameChatPreviewRevision, setGameChatPreviewRevision] = useState<
|
||
number | null
|
||
>(null);
|
||
const gameChatPreviewRevisionRef = useRef<number | null>(null);
|
||
gameChatPreviewRevisionRef.current = gameChatPreviewRevision;
|
||
const [previewStatus, setPreviewStatus] = useState('未启动');
|
||
const [gameChatProjectSelectionBusy, setGameChatProjectSelectionBusy] =
|
||
useState(false);
|
||
const gameChatProjectSelectionVersionRef = useRef(0);
|
||
const gameChatAutoPreviewAuthorizationRef =
|
||
useRef<GameChatAutoPreviewAuthorization | null>(
|
||
readStoredGameChatAutoPreviewAuthorization(),
|
||
);
|
||
const gameChatAutoPreviewAttemptedRef = useRef(new Set<string>());
|
||
const gameChatObservedRunKeysRef = useRef(new Set<string>());
|
||
const gameChatArchivedRunKeysRef = useRef(new Set<string>());
|
||
const gameChatPendingStageRuntimesRef = useRef(
|
||
new Map<string, AgentRuntimeState>(),
|
||
);
|
||
const gameChatCommittedResponseStreamKeysRef = useRef(new Set<string>());
|
||
const initialSupervisorMessageLatchRef = useRef({
|
||
projectPath: initialProjectPath,
|
||
prompt: initialSupervisorMessage.trim(),
|
||
});
|
||
|
||
function updateClientPreview(
|
||
nextPreview: LocalPreviewResult | null,
|
||
status: GameCreationAppPreviewStatus = nextPreview ? 'running' : 'stopped',
|
||
) {
|
||
setPreview(nextPreview);
|
||
onPreviewChange?.(
|
||
nextPreview
|
||
? {
|
||
status,
|
||
url: nextPreview.url,
|
||
port: nextPreview.port,
|
||
}
|
||
: { status },
|
||
);
|
||
}
|
||
const [chatInput, setChatInput] = useState(() =>
|
||
(supervisorChatOnly || gameChatOnly) && initialProjectPath
|
||
? readSupervisorChatDraft(initialProjectPath)
|
||
: '',
|
||
);
|
||
const [chatAgentBusy, setChatAgentBusy] = useState(false);
|
||
const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState<
|
||
string | null
|
||
>(null);
|
||
const [projectSupervisorRuntime, setProjectSupervisorRuntime] =
|
||
useState<AgentRuntimeState | null>(null);
|
||
const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] =
|
||
useState<AgentRuntimeResponseStream | null>(null);
|
||
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
|
||
useState('');
|
||
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
|
||
useState<string | null>(null);
|
||
const chatInputRef = useRef<HTMLInputElement | null>(null);
|
||
const supervisorChatMessagesRef = useRef<HTMLDivElement | null>(null);
|
||
const supervisorChatShouldFollowLatestRef = useRef(true);
|
||
const [assetStatus, setAssetStatus] = useState('未上传');
|
||
const [uploadedAssets, setUploadedAssets] = useState<
|
||
UploadLocalAssetResult[]
|
||
>([]);
|
||
const [assetLocalPath, setAssetLocalPath] = useState('assets/hero.png');
|
||
const [assetKind, setAssetKind] = useState('asset');
|
||
const [assetMediaType, setAssetMediaType] = useState(
|
||
'application/octet-stream',
|
||
);
|
||
const [assetSourceKind, setAssetSourceKind] = useState('generated');
|
||
const [assetCanvasProjectId, setAssetCanvasProjectId] = useState('');
|
||
const [assetResourceId, setAssetResourceId] = useState('');
|
||
const [assetObjectId, setAssetObjectId] = useState('');
|
||
const [assetGenerationPrompt, setAssetGenerationPrompt] =
|
||
useState('首版核心美术素材');
|
||
const [canvasExportPath, setCanvasExportPath] = useState(
|
||
'/tmp/canvas-export.zip',
|
||
);
|
||
const [editorBaseUrl, setEditorBaseUrl] = useState('http://127.0.0.1:3000');
|
||
const [memoryScope, setMemoryScope] = useState<MemoryScope>('long');
|
||
const [memoryDraft, setMemoryDraft] = useState('');
|
||
const [memoryStatus, setMemoryStatus] = useState('未读取');
|
||
const [limitedCommandStatus, setLimitedCommandStatus] = useState('未运行');
|
||
const [limitedLocalCommands, setLimitedLocalCommands] = useState<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>([...GAME_CREATION_APP_LIMITED_RUN_COMMANDS]);
|
||
const [projectFiles, setProjectFiles] = useState<LocalProjectFileEntry[]>([]);
|
||
const [projectCheckpoints, setProjectCheckpoints] = useState<
|
||
LocalProjectCheckpointSummary[]
|
||
>([]);
|
||
const [filePath, setFilePath] = useState('game/index.html');
|
||
const [fileDraft, setFileDraft] = useState('');
|
||
const [fileStatus, setFileStatus] = useState('未读取');
|
||
const [agentRunTrace, setAgentRunTrace] =
|
||
useState<GameCreationAgentRunTrace | null>(null);
|
||
const [agentRunHistory, setAgentRunHistory] = useState<AgentRunHistoryItem[]>(
|
||
[],
|
||
);
|
||
const [agentRunHistoryFiles, setAgentRunHistoryFiles] = useState<
|
||
LocalProjectFileEntry[]
|
||
>([]);
|
||
const [agentRunHistoryVisibleCount, setAgentRunHistoryVisibleCount] =
|
||
useState(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
const [agentRunHistoryOverflowCount, setAgentRunHistoryOverflowCount] =
|
||
useState(0);
|
||
const [agentRunHistoryLoadingMore, setAgentRunHistoryLoadingMore] =
|
||
useState(false);
|
||
const [agentRunStatus, setAgentRunStatus] = useState('未运行');
|
||
const [agentRuntimeById, setAgentRuntimeById] = useState<
|
||
Record<string, AgentRuntimeState | undefined>
|
||
>({});
|
||
const agentRuntimeByIdRef = useRef(agentRuntimeById);
|
||
agentRuntimeByIdRef.current = agentRuntimeById;
|
||
const [professionalAgentResultsById, setProfessionalAgentResultsById] =
|
||
useState<Record<string, ProjectAgentResultSummary>>({});
|
||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||
const [llmConfigStatus, setLlmConfigStatus] =
|
||
useState<GameCreatorLlmConfigStatus | null>(null);
|
||
const [workspaceStatus, setWorkspaceStatus] = useState(
|
||
eagerSupervisorProject ? `已打开:${initialProjectPath}` : '请选择工作区',
|
||
);
|
||
const [selectedAgent, setSelectedAgent] = useState<AgentStatusCard | null>(
|
||
null,
|
||
);
|
||
const selectedAgentIdRef = useRef<string | null>(null);
|
||
selectedAgentIdRef.current = selectedAgent?.id ?? null;
|
||
const [agentConversationInput, setAgentConversationInput] = useState('');
|
||
const [agentConversationStatus, setAgentConversationStatus] =
|
||
useState('未选择 agent');
|
||
const [agentConversationMessages, setAgentConversationMessages] = useState<
|
||
LocalConversationMessageRecord[]
|
||
>([]);
|
||
const [agentConversationRuntime, setAgentConversationRuntime] =
|
||
useState<AgentRuntimeState | null>(null);
|
||
const [agentConversationSessionId, setAgentConversationSessionId] = useState<
|
||
string | null
|
||
>(null);
|
||
const [agentConversationRunSubmitMode, setAgentConversationRunSubmitMode] =
|
||
useState<AgentBackgroundSubmitMode>('steer');
|
||
const [agentConversationRuntimeError, setAgentConversationRuntimeError] =
|
||
useState('');
|
||
const [agentConversationVisibleCount, setAgentConversationVisibleCount] =
|
||
useState(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
const [agentConversationSaving, setAgentConversationSaving] = useState(false);
|
||
const [agentConversationBackgroundBusy, setAgentConversationBackgroundBusy] =
|
||
useState(false);
|
||
const [agentMemoryStatus, setAgentMemoryStatus] = useState('未读取');
|
||
const [agentMemoryContent, setAgentMemoryContent] = useState('');
|
||
const [messages, setMessages] = useState<ChatMessage[]>(
|
||
createDefaultChatMessages,
|
||
);
|
||
const [conversationVisibleCount, setConversationVisibleCount] = useState(
|
||
CONVERSATION_INITIAL_VISIBLE_COUNT,
|
||
);
|
||
const [pendingCommand, setPendingCommand] = useState<PendingCommand | null>(
|
||
null,
|
||
);
|
||
const [pendingUiConfirmation, setPendingUiConfirmation] =
|
||
useState<PendingUiConfirmation | null>(null);
|
||
const [pendingNonEmptyProjectCreate, setPendingNonEmptyProjectCreate] =
|
||
useState<{
|
||
projectPath: string;
|
||
announceToChat: boolean;
|
||
} | null>(null);
|
||
const [commandLog, setCommandLog] = useState<string[]>([
|
||
`${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`,
|
||
]);
|
||
const [projectLogStatus, setProjectLogStatus] = useState('未读取');
|
||
const [projectLogContent, setProjectLogContent] = useState('');
|
||
const [conversationWriteVersion, setConversationWriteVersion] = useState(0);
|
||
const savedConversationCountRef = useRef(
|
||
eagerSupervisorProject ? createDefaultChatMessages().length : 0,
|
||
);
|
||
const savedConversationProjectPathRef = useRef<string | null>(
|
||
eagerSupervisorProject ? initialProjectPath : null,
|
||
);
|
||
const projectConversationWriteConfirmedRef = useRef<string | null>(null);
|
||
const projectConversationWriteCancelledRef = useRef<{
|
||
projectPath: string;
|
||
messageCount: number;
|
||
} | null>(null);
|
||
const latestMessagesRef = useRef<ChatMessage[]>([]);
|
||
const conversationWriteInFlightRef = useRef(false);
|
||
const projectScopeVersionRef = useRef(0);
|
||
const projectSupervisorHistoryLoadVersionRef = useRef(0);
|
||
const projectSupervisorRuntimeResumeProjectPathRef = useRef<string | null>(
|
||
null,
|
||
);
|
||
const projectSupervisorSessionIdRef = useRef<string | null>(null);
|
||
projectSupervisorSessionIdRef.current = projectSupervisorSessionId;
|
||
const projectSupervisorRuntimeRef = useRef<AgentRuntimeState | null>(null);
|
||
projectSupervisorRuntimeRef.current = projectSupervisorRuntime;
|
||
const projectSupervisorExpectedRunIdRef = useRef<string | null>(null);
|
||
projectSupervisorExpectedRunIdRef.current = projectSupervisorExpectedRunId;
|
||
const projectSupervisorResponseStreamRef =
|
||
useRef<AgentRuntimeResponseStream | null>(null);
|
||
projectSupervisorResponseStreamRef.current = projectSupervisorResponseStream;
|
||
const projectSupervisorRuntimeSyncingRef = useRef(new Set<string>());
|
||
const projectSupervisorRefreshConversationRef = useRef<
|
||
| ((
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
sessionId: string,
|
||
runId?: string,
|
||
) => Promise<void>)
|
||
| null
|
||
>(null);
|
||
const executeChatAgentReplyRef = useRef<(prompt: string) => Promise<void>>(
|
||
async () => undefined,
|
||
);
|
||
const agentConversationSavingRef = useRef(false);
|
||
const agentConversationBackgroundBusyRef = useRef(false);
|
||
const agentConversationLoadVersionRef = useRef(0);
|
||
const agentConversationSessionIdRef = useRef<string | null>(null);
|
||
agentConversationSessionIdRef.current = agentConversationSessionId;
|
||
const agentRuntimeResumeProjectPathRef = useRef<string | null>(null);
|
||
const agentRunHistoryLoadingMoreRef = useRef(false);
|
||
const initialProjectOpenedRef = useRef(false);
|
||
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
|
||
|
||
function requestRuntimeConfigOpen() {
|
||
if (projectSupervisorOnly && !supervisorChatOnly && !gameChatOnly) {
|
||
return;
|
||
}
|
||
setRuntimeConfigOpen(true);
|
||
}
|
||
|
||
function setGameChatAutoPreviewAuthorization(
|
||
authorization: GameChatAutoPreviewAuthorization | null,
|
||
) {
|
||
gameChatAutoPreviewAuthorizationRef.current = authorization;
|
||
storeGameChatAutoPreviewAuthorization(authorization);
|
||
}
|
||
|
||
const updateProjectSupervisorRuntime = useCallback(
|
||
(
|
||
runtime: AgentRuntimeState | null,
|
||
previous = projectSupervisorRuntimeRef.current,
|
||
) => {
|
||
const nextRuntime = runtime
|
||
? normalizeAgentRuntimeState(runtime, previous)
|
||
: null;
|
||
const nextProjectPath = localProjectPathRef.current;
|
||
if (
|
||
gameChatOnly &&
|
||
nextProjectPath &&
|
||
nextRuntime?.runId &&
|
||
!isAgentRuntimeTerminalState(nextRuntime)
|
||
) {
|
||
gameChatObservedRunKeysRef.current.add(
|
||
`${nextProjectPath}\n${nextRuntime.runId}`,
|
||
);
|
||
}
|
||
projectSupervisorRuntimeRef.current = nextRuntime;
|
||
setProjectSupervisorRuntime(nextRuntime);
|
||
},
|
||
[gameChatOnly],
|
||
);
|
||
|
||
const updateProjectSupervisorResponseStream = useCallback(
|
||
(
|
||
incoming: AgentRuntimeResponseStream | null | undefined,
|
||
runtime: AgentRuntimeState,
|
||
) => {
|
||
let nextStream = mergeProjectSupervisorResponseStream(
|
||
projectSupervisorResponseStreamRef.current,
|
||
incoming,
|
||
runtime,
|
||
);
|
||
const candidateStream = nextStream;
|
||
if (candidateStream?.status === 'ready' && gameChatOnly) {
|
||
const text = candidateStream.accumulatedText.trim();
|
||
const responseKey =
|
||
projectSupervisorResponseStreamIdentity(candidateStream);
|
||
const messageId = `runtime-response:${responseKey}`;
|
||
const alreadyCommitted =
|
||
gameChatCommittedResponseStreamKeysRef.current.has(responseKey) ||
|
||
latestMessagesRef.current.some(
|
||
(message) => message.messageId === messageId,
|
||
);
|
||
if (text && !alreadyCommitted) {
|
||
gameChatCommittedResponseStreamKeysRef.current.add(responseKey);
|
||
const nextMessage: ChatMessage = {
|
||
role: 'assistant',
|
||
text: candidateStream.accumulatedText,
|
||
messageId,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
updatedAt: candidateStream.updatedAt,
|
||
runtimeOwned: true,
|
||
};
|
||
setMessages((current) => {
|
||
if (current.some((message) => message.messageId === messageId)) {
|
||
latestMessagesRef.current = current;
|
||
return current;
|
||
}
|
||
const nextMessages = [...current, nextMessage];
|
||
latestMessagesRef.current = nextMessages;
|
||
return nextMessages;
|
||
});
|
||
}
|
||
// Ready text is now a normal chat message; transientReply must not show
|
||
// the same response a second time while polling/event delivery catches up.
|
||
nextStream = null;
|
||
} else if (
|
||
candidateStream &&
|
||
latestMessagesRef.current.some(
|
||
(message) =>
|
||
message.runtimeOwned &&
|
||
message.role === 'assistant' &&
|
||
message.text === candidateStream.accumulatedText &&
|
||
typeof message.updatedAt === 'number' &&
|
||
message.updatedAt >= candidateStream.startedAt,
|
||
)
|
||
) {
|
||
// Keep the existing supervisor-chat behavior for restored history.
|
||
nextStream = null;
|
||
}
|
||
projectSupervisorResponseStreamRef.current = nextStream;
|
||
setProjectSupervisorResponseStream(nextStream);
|
||
},
|
||
[gameChatOnly],
|
||
);
|
||
|
||
function resetProjectSupervisorState() {
|
||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = null;
|
||
projectSupervisorSessionIdRef.current = null;
|
||
projectSupervisorRuntimeRef.current = null;
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
gameChatPendingStageRuntimesRef.current.clear();
|
||
gameChatCommittedResponseStreamKeysRef.current.clear();
|
||
projectSupervisorRuntimeSyncingRef.current.clear();
|
||
setProjectSupervisorSessionId(null);
|
||
setProjectSupervisorRuntime(null);
|
||
setProjectSupervisorExpectedRunId(null);
|
||
setProjectSupervisorResponseStream(null);
|
||
setProjectSupervisorRuntimeError('');
|
||
}
|
||
|
||
function syncTerminalProjectSupervisorConversation(
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
if (!isAgentRuntimeTerminalState(runtime) || !runtime.sessionId) {
|
||
return;
|
||
}
|
||
const syncKey = `${projectPath}\n${runtime.sessionId}\n${runtime.runId}`;
|
||
if (projectSupervisorRuntimeSyncingRef.current.has(syncKey)) {
|
||
return;
|
||
}
|
||
const refreshConversation = projectSupervisorRefreshConversationRef.current;
|
||
if (!refreshConversation) {
|
||
return;
|
||
}
|
||
projectSupervisorRuntimeSyncingRef.current.add(syncKey);
|
||
void refreshConversation(
|
||
invoke,
|
||
projectPath,
|
||
runtime.sessionId,
|
||
runtime.runId,
|
||
).catch((error) => {
|
||
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
|
||
if (
|
||
localProjectPathRef.current === projectPath &&
|
||
projectSupervisorSessionIdRef.current === runtime.sessionId
|
||
) {
|
||
setProjectSupervisorRuntimeError(
|
||
`项目总控 Agent 对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
function flushPendingGameChatStageRecords() {
|
||
if (!gameChatOnly || !gameChatManifestHasTerminalStageTasks(manifest)) {
|
||
return;
|
||
}
|
||
for (const [
|
||
archiveKey,
|
||
pendingRuntime,
|
||
] of gameChatPendingStageRuntimesRef.current) {
|
||
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
|
||
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
|
||
continue;
|
||
}
|
||
const progress = buildGameChatProgressEvidence(
|
||
pendingRuntime,
|
||
agentRuntimeById,
|
||
manifest,
|
||
);
|
||
if (!progress) {
|
||
continue;
|
||
}
|
||
const text = formatGameChatStageRecord(
|
||
pendingRuntime,
|
||
progress,
|
||
collectGameChatResultImages(manifest),
|
||
);
|
||
gameChatArchivedRunKeysRef.current.add(archiveKey);
|
||
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
|
||
setMessages((current) => {
|
||
if (
|
||
current.some(
|
||
(message) => message.role === 'assistant' && message.text === text,
|
||
)
|
||
) {
|
||
return current;
|
||
}
|
||
// Conversation hydration can update the saved cursor in the same
|
||
// turn as this append. Clamp it to the pre-append list so the new
|
||
// stage record remains visible to the persistence effect.
|
||
const projectPath = archiveKey.split('\n', 1)[0];
|
||
if (savedConversationProjectPathRef.current === projectPath) {
|
||
savedConversationCountRef.current = Math.min(
|
||
savedConversationCountRef.current,
|
||
current.length,
|
||
);
|
||
}
|
||
return [...current, { role: 'assistant', text }];
|
||
});
|
||
}
|
||
}
|
||
|
||
function appendGameChatRuntimeEventMessages(
|
||
nextProjectPath: string,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
const eventMessages = gameChatRuntimeEventMessages(
|
||
runtime,
|
||
agentRuntimeById,
|
||
);
|
||
if (eventMessages.length === 0) {
|
||
return;
|
||
}
|
||
setMessages((current) => {
|
||
const currentMessageIds = new Set(
|
||
current
|
||
.map((message) => message.messageId?.trim())
|
||
.filter((messageId): messageId is string => Boolean(messageId)),
|
||
);
|
||
const missingMessages = eventMessages.filter(
|
||
(message) =>
|
||
message.messageId && !currentMessageIds.has(message.messageId),
|
||
);
|
||
if (missingMessages.length === 0) {
|
||
return current;
|
||
}
|
||
if (savedConversationProjectPathRef.current === nextProjectPath) {
|
||
savedConversationCountRef.current = Math.min(
|
||
savedConversationCountRef.current,
|
||
current.length,
|
||
);
|
||
}
|
||
return [...current, ...missingMessages];
|
||
});
|
||
}
|
||
|
||
const appendGameChatFinalReplyMessages = useCallback(
|
||
(nextProjectPath: string, runtimeResults: AgentRuntimeResult[]) => {
|
||
if (!gameChatOnly || runtimeResults.length === 0) {
|
||
return;
|
||
}
|
||
// A professional Runtime is only part of the active game-chat turn when
|
||
// it was delegated by the current Project Supervisor run. This prevents
|
||
// a stale child Runtime (or a different app mode) from leaking into the
|
||
// project transcript after a restart.
|
||
const supervisorRunId =
|
||
projectSupervisorRuntimeRef.current?.runId ??
|
||
runtimeResults.find(
|
||
(result) => result.state.agentId === PROJECT_SUPERVISOR_AGENT_ID,
|
||
)?.state.runId;
|
||
if (!supervisorRunId) {
|
||
return;
|
||
}
|
||
const messages = runtimeResults.flatMap((result) => {
|
||
const runtime = agentRuntimeStateFromResult(result);
|
||
if (
|
||
runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
runtime.parentRunId !== supervisorRunId
|
||
) {
|
||
return [];
|
||
}
|
||
return gameChatFinalReplyMessages([result.responseStream]);
|
||
});
|
||
if (messages.length === 0) {
|
||
return;
|
||
}
|
||
setMessages((current) => {
|
||
const currentMessageIds = new Set(
|
||
current
|
||
.map((message) => message.messageId?.trim())
|
||
.filter((messageId): messageId is string => Boolean(messageId)),
|
||
);
|
||
const missingMessages = messages.filter(
|
||
(message) =>
|
||
message.messageId && !currentMessageIds.has(message.messageId),
|
||
);
|
||
if (missingMessages.length === 0) {
|
||
return current;
|
||
}
|
||
if (savedConversationProjectPathRef.current === nextProjectPath) {
|
||
savedConversationCountRef.current = Math.min(
|
||
savedConversationCountRef.current,
|
||
current.length,
|
||
);
|
||
}
|
||
const orderedMissingMessages = [...missingMessages].sort(
|
||
(left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0),
|
||
);
|
||
const nextMessages = [...current, ...orderedMissingMessages];
|
||
latestMessagesRef.current = nextMessages;
|
||
return nextMessages;
|
||
});
|
||
},
|
||
[gameChatOnly],
|
||
);
|
||
|
||
function appendGameChatStageRecord(
|
||
nextProjectPath: string,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
if (!gameChatOnly || !gameChatRuntimeHasTerminalOutcome(runtime)) {
|
||
return;
|
||
}
|
||
const archiveKey = `${nextProjectPath}\n${runtime.runId}`;
|
||
// A restored terminal runtime may be the first runtime snapshot observed
|
||
// after the app opens. Do not require a prior non-terminal event: the
|
||
// durable runtime/manifest pair is sufficient evidence for the record.
|
||
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
|
||
return;
|
||
}
|
||
gameChatPendingStageRuntimesRef.current.set(archiveKey, runtime);
|
||
flushPendingGameChatStageRecords();
|
||
}
|
||
|
||
useEffect(() => {
|
||
const nextProjectPath = localProject?.projectPath;
|
||
const runtime = projectSupervisorRuntime;
|
||
if (gameChatOnly && nextProjectPath && runtime) {
|
||
appendGameChatRuntimeEventMessages(nextProjectPath, runtime);
|
||
}
|
||
if (
|
||
gameChatOnly &&
|
||
nextProjectPath &&
|
||
runtime &&
|
||
gameChatRuntimeHasTerminalOutcome(runtime)
|
||
) {
|
||
// Hydration can restore a terminal root run without delivering a live
|
||
// runtime-update event. Feed that snapshot through the same deferred
|
||
// archive path used by live terminal updates. A run that was already
|
||
// observed in a non-terminal state is archived by its terminal
|
||
// conversation refresh instead, avoiding a hydration race with that
|
||
// refresh's saved-message cursor.
|
||
const archiveKey = `${nextProjectPath}\n${runtime.runId}`;
|
||
const refreshKey = `${nextProjectPath}\n${runtime.sessionId}\n${runtime.runId}`;
|
||
if (
|
||
!gameChatObservedRunKeysRef.current.has(archiveKey) &&
|
||
!projectSupervisorRuntimeSyncingRef.current.has(refreshKey)
|
||
) {
|
||
appendGameChatStageRecord(nextProjectPath, runtime);
|
||
}
|
||
}
|
||
flushPendingGameChatStageRecords();
|
||
// The terminal Runtime can arrive before the durable manifest refresh.
|
||
// Retry when either projection changes, but archive each run only once.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
agentRuntimeById,
|
||
gameChatOnly,
|
||
localProject?.projectPath,
|
||
manifest,
|
||
projectSupervisorRuntime,
|
||
]);
|
||
|
||
useEscapeToClose(closeAgentConversation, selectedAgent !== null);
|
||
useEscapeToClose(cancelUiCommandConfirmation, pendingUiConfirmation !== null);
|
||
useEscapeToClose(
|
||
cancelProjectCreateInNonEmptyFolder,
|
||
pendingNonEmptyProjectCreate !== null,
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
pendingUiConfirmation?.projectPath &&
|
||
pendingUiConfirmation.projectPath !==
|
||
resolveCurrentUiConfirmationProjectPath()
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
}
|
||
// The callbacks intentionally read the latest project refs.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [localProject?.projectPath, projectPath]);
|
||
|
||
useEffect(() => {
|
||
if (!initialProjectPath || initialProjectOpenedRef.current) {
|
||
return;
|
||
}
|
||
initialProjectOpenedRef.current = true;
|
||
if (projectSupervisorOnly) {
|
||
if (gameChatOnly) {
|
||
void openGameChatProjectPath(initialProjectPath);
|
||
return;
|
||
}
|
||
if (!isAbsoluteProjectPath(initialProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(initialProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
localProjectPathRef.current = initialProjectPath;
|
||
void loadProjectConversation(initialProjectPath).finally(() => {
|
||
if (localProjectPathRef.current === initialProjectPath) {
|
||
void refreshAgentRunTrace(initialProjectPath);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
void openWorkspace(initialProjectPath, false);
|
||
// Initial project opening is guarded by initialProjectOpenedRef.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [gameChatOnly, initialProjectPath, projectSupervisorOnly]);
|
||
|
||
useEffect(() => {
|
||
if (projectSupervisorOnly) {
|
||
return;
|
||
}
|
||
const listen = window.__TAURI__?.event?.listen;
|
||
if (!listen) {
|
||
return;
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void listen<AgentProgressEvent>('game-creator-agent-progress', (event) => {
|
||
if (event.payload.projectPath !== localProjectPathRef.current) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: event.payload.message },
|
||
]);
|
||
})
|
||
.then((unlisten) => {
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch((error) => {
|
||
if (disposed) {
|
||
return;
|
||
}
|
||
setAgentRunStatus(
|
||
`实时状态不可用:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
};
|
||
}, [projectSupervisorOnly]);
|
||
|
||
useEffect(() => {
|
||
const listen = window.__TAURI__?.event?.listen;
|
||
const invoke = resolveTauriInvoke();
|
||
if (!listen) {
|
||
return;
|
||
}
|
||
let cleanup: (() => void) | null = null;
|
||
let disposed = false;
|
||
void listen<GameCreatorAgentRuntimeUpdateEvent>(
|
||
'game-creator-agent-runtime-update',
|
||
(event) => {
|
||
const payload = event.payload;
|
||
if (payload.projectPath !== localProjectPathRef.current) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(payload.runtime);
|
||
if (gameChatOnly && payload.agentId !== PROJECT_SUPERVISOR_AGENT_ID) {
|
||
appendGameChatFinalReplyMessages(payload.projectPath, [
|
||
payload.runtime,
|
||
]);
|
||
}
|
||
if (payload.agentId === PROJECT_SUPERVISOR_AGENT_ID) {
|
||
const expectedSessionId = projectSupervisorSessionIdRef.current;
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
const expectedRunId = projectSupervisorExpectedRunIdRef.current;
|
||
const sameRun =
|
||
currentRuntime !== null &&
|
||
sameAgentRuntimeRun(nextRuntime, currentRuntime);
|
||
const isExpectedRun =
|
||
expectedRunId !== null && nextRuntime.runId === expectedRunId;
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.runId !== payload.runId ||
|
||
(expectedSessionId !== null &&
|
||
nextRuntime.sessionId !== expectedSessionId) ||
|
||
(currentRuntime !== null &&
|
||
((!sameRun && !isExpectedRun) ||
|
||
(sameRun && nextRuntime.updatedAt < currentRuntime.updatedAt)))
|
||
) {
|
||
return;
|
||
}
|
||
if (expectedSessionId === null && nextRuntime.sessionId) {
|
||
projectSupervisorSessionIdRef.current = nextRuntime.sessionId;
|
||
setProjectSupervisorSessionId(nextRuntime.sessionId);
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
if (isExpectedRun) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
updateProjectSupervisorResponseStream(
|
||
payload.runtime.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
if (invoke) {
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
payload.projectPath,
|
||
nextRuntime,
|
||
);
|
||
}
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
if (payload.agentId !== selectedAgentIdRef.current) {
|
||
return;
|
||
}
|
||
if (
|
||
agentConversationSessionIdRef.current !== null &&
|
||
payload.runtime.state.sessionId !==
|
||
agentConversationSessionIdRef.current
|
||
) {
|
||
return;
|
||
}
|
||
setAgentConversationRuntime((current) =>
|
||
normalizeAgentRuntimeState(nextRuntime, current),
|
||
);
|
||
setAgentConversationRuntimeError('');
|
||
},
|
||
)
|
||
.then((unlisten) => {
|
||
if (disposed) {
|
||
unlisten();
|
||
return;
|
||
}
|
||
cleanup = unlisten;
|
||
})
|
||
.catch((error) => {
|
||
if (disposed) {
|
||
return;
|
||
}
|
||
setAgentConversationRuntimeError(
|
||
`Runtime 订阅不可用:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
cleanup?.();
|
||
};
|
||
}, [
|
||
appendGameChatFinalReplyMessages,
|
||
gameChatOnly,
|
||
updateProjectSupervisorResponseStream,
|
||
updateProjectSupervisorRuntime,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
const sessionId = projectSupervisorSessionId;
|
||
const trackedRunId =
|
||
projectSupervisorExpectedRunId ?? projectSupervisorRuntime?.runId ?? null;
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!sessionId ||
|
||
!trackedRunId ||
|
||
(!projectSupervisorRuntime && !projectSupervisorExpectedRunId) ||
|
||
(projectSupervisorRuntime !== null &&
|
||
isAgentRuntimeTerminalState(projectSupervisorRuntime) &&
|
||
!projectSupervisorExpectedRunId)
|
||
) {
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let inFlight = false;
|
||
const pollRuntime = async () => {
|
||
if (disposed || inFlight) {
|
||
return;
|
||
}
|
||
inFlight = true;
|
||
const runtimeBeforePoll = projectSupervisorRuntimeRef.current;
|
||
const responseStreamBeforePoll =
|
||
projectSupervisorResponseStreamRef.current;
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
if (
|
||
disposed ||
|
||
currentRuntime !== runtimeBeforePoll ||
|
||
projectSupervisorResponseStreamRef.current !==
|
||
responseStreamBeforePoll ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== trackedRunId ||
|
||
(currentRuntime !== null &&
|
||
sameAgentRuntimeRun(nextRuntime, currentRuntime) &&
|
||
nextRuntime.updatedAt < currentRuntime.updatedAt)
|
||
) {
|
||
return;
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
if (nextRuntime.runId === projectSupervisorExpectedRunIdRef.current) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
!disposed &&
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectSupervisorSessionIdRef.current === sessionId
|
||
) {
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
}
|
||
} finally {
|
||
inFlight = false;
|
||
}
|
||
};
|
||
void pollRuntime();
|
||
const timer = window.setInterval(() => {
|
||
void pollRuntime();
|
||
}, 750);
|
||
return () => {
|
||
disposed = true;
|
||
window.clearInterval(timer);
|
||
};
|
||
// Polling restarts only when the tracked runtime identity or phase changes.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
projectSupervisorSessionId,
|
||
projectSupervisorRuntime?.phase,
|
||
projectSupervisorRuntime?.runId,
|
||
projectSupervisorRuntime?.status,
|
||
projectSupervisorExpectedRunId,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
const supervisorRunId = projectSupervisorRuntime?.runId ?? null;
|
||
if (
|
||
!projectSupervisorOnly ||
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let inFlight = false;
|
||
const pollProfessionalRuntimes = async () => {
|
||
if (disposed || inFlight) {
|
||
return;
|
||
}
|
||
inFlight = true;
|
||
try {
|
||
const runtimes = await invoke<AgentRuntimeResult[]>(
|
||
'read_game_creator_agent_runtimes',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
disposed ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
if (gameChatOnly) {
|
||
appendGameChatFinalReplyMessages(nextProjectPath, runtimes);
|
||
}
|
||
const nextRuntimes = runtimes.map((runtimeResult) =>
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
const authoritativeProfessionalAgentIds = new Set(
|
||
nextRuntimes
|
||
.filter(
|
||
(runtime) =>
|
||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||
runtime.source,
|
||
) &&
|
||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||
runtime.parentRunId === supervisorRunId,
|
||
)
|
||
.map((runtime) => runtime.agentId),
|
||
);
|
||
setAgentRuntimeById((current) => {
|
||
const reconciled = { ...current };
|
||
for (const [key, runtime] of Object.entries(reconciled)) {
|
||
if (
|
||
runtime &&
|
||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||
runtime.source,
|
||
) &&
|
||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||
runtime.parentRunId === supervisorRunId &&
|
||
!authoritativeProfessionalAgentIds.has(runtime.agentId)
|
||
) {
|
||
delete reconciled[key];
|
||
}
|
||
}
|
||
return nextRuntimes.reduce(
|
||
(next, runtime) =>
|
||
mergeAgentRuntimeStateIntoMap(next, runtime, true),
|
||
reconciled,
|
||
);
|
||
});
|
||
} catch {
|
||
// Preserve the last truthful professional Runtime snapshot on transient read failures.
|
||
} finally {
|
||
inFlight = false;
|
||
}
|
||
};
|
||
void pollProfessionalRuntimes();
|
||
const timer = window.setInterval(() => {
|
||
void pollProfessionalRuntimes();
|
||
}, 1000);
|
||
return () => {
|
||
disposed = true;
|
||
window.clearInterval(timer);
|
||
};
|
||
// Track the stable projection fields; the Runtime object identity changes on every merge.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime?.phase,
|
||
projectSupervisorRuntime?.runId,
|
||
projectSupervisorRuntime?.status,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
if (!gameChatOnly || !invoke || !nextProjectPath) {
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
let inFlight = false;
|
||
let attemptedRunId: string | null = null;
|
||
let attemptedAuthorizationId: string | null = null;
|
||
const authorizationMatches = (
|
||
expected: GameChatAutoPreviewAuthorization,
|
||
) => {
|
||
const current = gameChatAutoPreviewAuthorizationRef.current;
|
||
return (
|
||
current?.authorizationId === expected.authorizationId &&
|
||
current.projectPath === expected.projectPath &&
|
||
current.runId === expected.runId
|
||
);
|
||
};
|
||
const attemptIsCurrent = (
|
||
runId: string,
|
||
authorization: GameChatAutoPreviewAuthorization,
|
||
) =>
|
||
!disposed &&
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectSupervisorRuntimeRef.current?.runId === runId &&
|
||
authorizationMatches(authorization);
|
||
const clearMatchingAuthorization = (
|
||
authorization: GameChatAutoPreviewAuthorization,
|
||
) => {
|
||
if (authorizationMatches(authorization)) {
|
||
setGameChatAutoPreviewAuthorization(null);
|
||
}
|
||
};
|
||
const readCurrentProjectRevision = async () => {
|
||
const result = await invoke<LocalGameProjectRevisionStatus>(
|
||
'get_local_game_project_revision',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (!Number.isSafeInteger(result.revision) || result.revision < 0) {
|
||
throw new Error('本地游戏项目 revision 无效');
|
||
}
|
||
return result.revision;
|
||
};
|
||
const stopStaleStartedPreview = async (
|
||
startedPreview: LocalPreviewResult,
|
||
) => {
|
||
try {
|
||
await invoke<boolean>('stop_local_game_preview_if_matches', {
|
||
projectPath: nextProjectPath,
|
||
expectedPreview: startedPreview,
|
||
});
|
||
} catch {
|
||
// A newer preview identity or a closed project already owns the visible state.
|
||
}
|
||
};
|
||
const syncPreview = async () => {
|
||
if (disposed || inFlight) {
|
||
return;
|
||
}
|
||
inFlight = true;
|
||
try {
|
||
const status = await invoke<LocalPreviewStatus>(
|
||
'get_local_game_preview_status',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (disposed || localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const currentSupervisor = projectSupervisorRuntimeRef.current;
|
||
let playableRevision = latestGameChatPlayableRevision(
|
||
currentSupervisor,
|
||
agentRuntimeByIdRef.current,
|
||
);
|
||
if (playableRevision) {
|
||
const currentRevision = await readCurrentProjectRevision();
|
||
if (
|
||
disposed ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !==
|
||
playableRevision.runId
|
||
) {
|
||
return;
|
||
}
|
||
if (currentRevision !== playableRevision.revision) {
|
||
playableRevision = null;
|
||
}
|
||
}
|
||
const runningPreview =
|
||
status.status === 'running' &&
|
||
status.url &&
|
||
status.port &&
|
||
status.root &&
|
||
resolveEmbeddedPreviewUrl({
|
||
status: status.status,
|
||
url: status.url,
|
||
})
|
||
? {
|
||
url: status.url,
|
||
port: status.port,
|
||
root: status.root,
|
||
}
|
||
: null;
|
||
if (runningPreview) {
|
||
updateClientPreview(runningPreview);
|
||
setPreviewStatus(`运行中:127.0.0.1:${runningPreview.port}`);
|
||
if (
|
||
playableRevision &&
|
||
playableRevision.revision >
|
||
(gameChatPreviewRevisionRef.current ?? 0)
|
||
) {
|
||
gameChatPreviewRevisionRef.current = playableRevision.revision;
|
||
setGameChatPreviewRevision(playableRevision.revision);
|
||
}
|
||
const runningAuthorization =
|
||
gameChatAutoPreviewAuthorizationRef.current;
|
||
if (
|
||
playableRevision &&
|
||
runningAuthorization?.projectPath === nextProjectPath &&
|
||
runningAuthorization.runId === playableRevision.runId &&
|
||
gameChatPlayableRevisionIsAfterAuthorization(
|
||
playableRevision,
|
||
runningAuthorization,
|
||
)
|
||
) {
|
||
clearMatchingAuthorization(runningAuthorization);
|
||
}
|
||
return;
|
||
}
|
||
const hadRunningPreview = Boolean(gameChatPreviewRef.current);
|
||
updateClientPreview(null);
|
||
if (hadRunningPreview) {
|
||
setPreviewStatus('未启动');
|
||
}
|
||
const authorization = gameChatAutoPreviewAuthorizationRef.current;
|
||
if (
|
||
!playableRevision ||
|
||
authorization?.projectPath !== nextProjectPath ||
|
||
authorization.runId !== playableRevision.runId ||
|
||
!gameChatPlayableRevisionIsAfterAuthorization(
|
||
playableRevision,
|
||
authorization,
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
const autoPreviewRunId = playableRevision.runId;
|
||
attemptedRunId = autoPreviewRunId;
|
||
attemptedAuthorizationId = authorization.authorizationId;
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (!attemptIsCurrent(autoPreviewRunId, authorization)) {
|
||
return;
|
||
}
|
||
setManifest(nextManifest);
|
||
const attemptKey = `${nextProjectPath}\n${autoPreviewRunId}\nauthorization:${authorization.authorizationId}\nrevision:${playableRevision.revision}\nvalidatedAt:${playableRevision.validatedAt}`;
|
||
if (gameChatAutoPreviewAttemptedRef.current.has(attemptKey)) {
|
||
return;
|
||
}
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (!attemptIsCurrent(autoPreviewRunId, authorization)) {
|
||
return;
|
||
}
|
||
const previewDenied =
|
||
policyView.policy.deniedCommands.includes('preview.start') ||
|
||
Object.values(policyView.policy.agentPolicies ?? {}).some((policy) =>
|
||
policy.deniedCommands.includes('preview.start'),
|
||
);
|
||
if (previewDenied) {
|
||
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
|
||
clearMatchingAuthorization(authorization);
|
||
const message = '项目权限策略拒绝执行:preview.start';
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'permission.deny preview.start',
|
||
]);
|
||
setPreviewStatus(message);
|
||
return;
|
||
}
|
||
const revisionBeforeStart = await readCurrentProjectRevision();
|
||
if (
|
||
!attemptIsCurrent(autoPreviewRunId, authorization) ||
|
||
revisionBeforeStart !== playableRevision.revision
|
||
) {
|
||
return;
|
||
}
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'permission.confirm',
|
||
'preview.start',
|
||
);
|
||
let startedPreview: LocalPreviewResult;
|
||
try {
|
||
startedPreview = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
expectedRevision: playableRevision.revision,
|
||
},
|
||
);
|
||
} catch (error) {
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
const retryWithNewEvidence =
|
||
message.startsWith('项目正在被其他写操作占用:') ||
|
||
message.startsWith('本地游戏项目已在验证后发生变化');
|
||
if (!retryWithNewEvidence) {
|
||
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
|
||
clearMatchingAuthorization(authorization);
|
||
}
|
||
throw error;
|
||
}
|
||
let revisionAfterStart: number;
|
||
try {
|
||
revisionAfterStart = await readCurrentProjectRevision();
|
||
} catch (error) {
|
||
await stopStaleStartedPreview(startedPreview);
|
||
throw error;
|
||
}
|
||
if (
|
||
!attemptIsCurrent(autoPreviewRunId, authorization) ||
|
||
revisionAfterStart !== playableRevision.revision
|
||
) {
|
||
await stopStaleStartedPreview(startedPreview);
|
||
return;
|
||
}
|
||
gameChatAutoPreviewAttemptedRef.current.add(attemptKey);
|
||
clearMatchingAuthorization(authorization);
|
||
updateClientPreview(startedPreview);
|
||
if (
|
||
playableRevision &&
|
||
playableRevision.revision > (gameChatPreviewRevisionRef.current ?? 0)
|
||
) {
|
||
gameChatPreviewRevisionRef.current = playableRevision.revision;
|
||
setGameChatPreviewRevision(playableRevision.revision);
|
||
}
|
||
setPreviewStatus(`运行中:127.0.0.1:${startedPreview.port}`);
|
||
setCommandLog((current) => [...current, 'preview.start']);
|
||
} catch (error) {
|
||
if (
|
||
!disposed &&
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
(!attemptedRunId ||
|
||
projectSupervisorRuntimeRef.current?.runId === attemptedRunId) &&
|
||
(!attemptedAuthorizationId ||
|
||
!gameChatAutoPreviewAuthorizationRef.current ||
|
||
gameChatAutoPreviewAuthorizationRef.current.authorizationId ===
|
||
attemptedAuthorizationId)
|
||
) {
|
||
setPreviewStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
} finally {
|
||
inFlight = false;
|
||
}
|
||
};
|
||
void syncPreview();
|
||
const timer = window.setInterval(() => {
|
||
void syncPreview();
|
||
}, 1000);
|
||
return () => {
|
||
disposed = true;
|
||
window.clearInterval(timer);
|
||
};
|
||
// The interval reads the latest Runtime through refs and is recreated only for identity changes.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
gameChatOnly,
|
||
localProject?.projectPath,
|
||
projectSupervisorRuntime?.runId,
|
||
]);
|
||
|
||
useLayoutEffect(() => {
|
||
if (
|
||
(!supervisorChatOnly && !gameChatOnly) ||
|
||
!supervisorChatShouldFollowLatestRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const messageList = supervisorChatMessagesRef.current;
|
||
if (messageList) {
|
||
messageList.scrollTop = messageList.scrollHeight;
|
||
}
|
||
}, [
|
||
messages,
|
||
projectSupervisorResponseStream?.sequence,
|
||
projectSupervisorRuntime?.updatedAt,
|
||
projectSupervisorRuntimeError,
|
||
gameChatOnly,
|
||
supervisorChatOnly,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
if (!supervisorChatOnly && !gameChatOnly) {
|
||
return;
|
||
}
|
||
const draftProjectPath = gameChatOnly
|
||
? (localProject?.projectPath ?? initialProjectPath)
|
||
: initialProjectPath;
|
||
persistSupervisorChatDraft(draftProjectPath, chatInput);
|
||
}, [
|
||
chatInput,
|
||
gameChatOnly,
|
||
initialProjectPath,
|
||
localProject?.projectPath,
|
||
supervisorChatOnly,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
latestMessagesRef.current = messages;
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath;
|
||
if (!invoke || !nextProjectPath) {
|
||
return;
|
||
}
|
||
if (savedConversationProjectPathRef.current !== nextProjectPath) {
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = 0;
|
||
projectConversationWriteConfirmedRef.current = null;
|
||
projectConversationWriteCancelledRef.current = null;
|
||
}
|
||
const start = savedConversationCountRef.current;
|
||
const pendingMessages = messages.slice(start);
|
||
if (pendingMessages.length === 0) {
|
||
return;
|
||
}
|
||
if (
|
||
projectSupervisorOnly &&
|
||
pendingMessages.every(
|
||
(message) =>
|
||
message.runtimeOwned ||
|
||
isTransientProjectOpenMessage(message, nextProjectPath),
|
||
)
|
||
) {
|
||
savedConversationCountRef.current = messages.length;
|
||
return;
|
||
}
|
||
if (conversationWriteInFlightRef.current) {
|
||
return;
|
||
}
|
||
if (pendingUiConfirmation) {
|
||
return;
|
||
}
|
||
if (
|
||
projectConversationWriteCancelledRef.current?.projectPath ===
|
||
nextProjectPath &&
|
||
projectConversationWriteCancelledRef.current.messageCount ===
|
||
messages.length
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
projectConversationWriteCancelledRef.current?.projectPath ===
|
||
nextProjectPath &&
|
||
projectConversationWriteCancelledRef.current.messageCount <
|
||
messages.length
|
||
) {
|
||
projectConversationWriteCancelledRef.current = null;
|
||
}
|
||
if (projectConversationWriteConfirmedRef.current !== nextProjectPath) {
|
||
void invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
)
|
||
.then((policyView) => {
|
||
if (
|
||
!policyView.policy.confirmCommands.includes('conversation.write')
|
||
) {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
return;
|
||
}
|
||
requestProjectPolicyConfirmation(
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
`写入 ${nextProjectPath} 的项目对话`,
|
||
() => {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
},
|
||
);
|
||
setWorkspaceStatus('等待确认保存项目对话');
|
||
})
|
||
.catch(() => {
|
||
projectConversationWriteConfirmedRef.current = nextProjectPath;
|
||
setConversationWriteVersion((current) => current + 1);
|
||
});
|
||
return;
|
||
}
|
||
conversationWriteInFlightRef.current = true;
|
||
void (async () => {
|
||
let wroteMessage = false;
|
||
for (const [index, message] of pendingMessages.entries()) {
|
||
if (message.runtimeOwned) {
|
||
savedConversationCountRef.current = start + index + 1;
|
||
continue;
|
||
}
|
||
if (isTransientProjectOpenMessage(message, nextProjectPath)) {
|
||
savedConversationCountRef.current = start + index + 1;
|
||
continue;
|
||
}
|
||
await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
...(message.messageId ? { messageId: message.messageId } : {}),
|
||
message: {
|
||
role: message.role,
|
||
content: message.text,
|
||
agentId: null,
|
||
...(typeof message.updatedAt === 'number'
|
||
? { updatedAt: message.updatedAt }
|
||
: {}),
|
||
},
|
||
},
|
||
);
|
||
wroteMessage = true;
|
||
savedConversationCountRef.current = start + index + 1;
|
||
}
|
||
setWorkspaceStatus((current) =>
|
||
current.startsWith('项目对话保存失败') ||
|
||
current === '等待确认保存项目对话'
|
||
? `已打开:${nextProjectPath}`
|
||
: current,
|
||
);
|
||
if (wroteMessage) {
|
||
setCommandLog((current) => [...current, 'conversation.write']);
|
||
}
|
||
})()
|
||
.catch((error) => {
|
||
savedConversationCountRef.current = Math.min(
|
||
savedConversationCountRef.current,
|
||
start,
|
||
);
|
||
setWorkspaceStatus(
|
||
`项目对话保存失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`conversation.write.failed ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
})
|
||
.finally(() => {
|
||
conversationWriteInFlightRef.current = false;
|
||
if (
|
||
savedConversationCountRef.current < latestMessagesRef.current.length
|
||
) {
|
||
setConversationWriteVersion((current) => current + 1);
|
||
}
|
||
});
|
||
// Writes are driven by message and confirmation state; the confirmation helper
|
||
// intentionally observes the latest policy refs.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
messages,
|
||
conversationWriteVersion,
|
||
pendingUiConfirmation,
|
||
projectSupervisorOnly,
|
||
]);
|
||
|
||
function appendLocalPermissionLog(
|
||
projectPath: string | null,
|
||
event:
|
||
| 'permission.pending'
|
||
| 'permission.confirm'
|
||
| 'permission.cancel'
|
||
| 'command.auto',
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !projectPath) {
|
||
return;
|
||
}
|
||
let result: Promise<unknown>;
|
||
try {
|
||
result = Promise.resolve(
|
||
invoke('append_local_permission_log', {
|
||
projectPath,
|
||
event,
|
||
commandId,
|
||
}),
|
||
);
|
||
} catch (error) {
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.log.failed ${commandId}: ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
return;
|
||
}
|
||
void result.catch((error) => {
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.log.failed ${commandId}: ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
]);
|
||
});
|
||
}
|
||
|
||
function resolvePermissionLogProjectPath(command: PendingCommand) {
|
||
if (command.id === 'project.create') {
|
||
return null;
|
||
}
|
||
return resolveChatProjectPath(localProject);
|
||
}
|
||
|
||
function resolveUiPermissionLogProjectPath(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
) {
|
||
if (commandId === 'project.create') {
|
||
return null;
|
||
}
|
||
if (needsInitializedChatProject(commandId)) {
|
||
return resolveChatProjectPath(localProject);
|
||
}
|
||
return projectPath;
|
||
}
|
||
|
||
function resolveCurrentUiConfirmationProjectPath() {
|
||
const openedProjectPath = localProjectPathRef.current?.trim();
|
||
if (openedProjectPath) {
|
||
return openedProjectPath;
|
||
}
|
||
const draftProjectPath = projectPath.trim();
|
||
if (
|
||
!draftProjectPath ||
|
||
!isAbsoluteProjectPath(draftProjectPath) ||
|
||
projectPathHasControlCharacter(draftProjectPath)
|
||
) {
|
||
return null;
|
||
}
|
||
return draftProjectPath;
|
||
}
|
||
|
||
function queuePendingCommand(command: PendingCommand) {
|
||
setPendingCommand(command);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.pending ${command.id}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.pending',
|
||
command.id,
|
||
);
|
||
}
|
||
|
||
function requestCommandConfirmation(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
detail: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
const permission = GAME_CREATION_APP_COMMANDS.find(
|
||
(command) => command.id === commandId,
|
||
)?.permission as GameCreationAppPermission | undefined;
|
||
|
||
if (permission === 'deny' || !permission) {
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
return;
|
||
}
|
||
if (permission === 'confirm') {
|
||
pendingUiConfirmationActionRef.current = onConfirm;
|
||
setPendingUiConfirmation({ commandId, detail, projectPath: null });
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.pending ${commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
resolveUiPermissionLogProjectPath(commandId),
|
||
'permission.pending',
|
||
commandId,
|
||
);
|
||
return;
|
||
}
|
||
onConfirm();
|
||
}
|
||
|
||
function requestProjectPolicyConfirmation(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string,
|
||
detail: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
pendingUiConfirmationActionRef.current = onConfirm;
|
||
setPendingUiConfirmation({ commandId, detail, projectPath });
|
||
setCommandLog((current) => [...current, `permission.pending ${commandId}`]);
|
||
appendLocalPermissionLog(projectPath, 'permission.pending', commandId);
|
||
}
|
||
|
||
function markProjectPolicyDenied(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
message: string,
|
||
) {
|
||
if (commandId.startsWith('project.') || commandId === 'task.list') {
|
||
setProjectStatus(message);
|
||
setWorkspaceStatus(message);
|
||
}
|
||
if (
|
||
commandId.startsWith('file.') ||
|
||
commandId === 'agent.trace_read' ||
|
||
commandId === 'project.index' ||
|
||
commandId === 'project.diff' ||
|
||
commandId === 'project.export_package' ||
|
||
commandId === 'project.export_list'
|
||
) {
|
||
setFileStatus(message);
|
||
}
|
||
if (commandId.startsWith('asset.') || commandId.startsWith('canvas.')) {
|
||
setAssetStatus(message);
|
||
}
|
||
if (commandId.startsWith('preview.')) {
|
||
setPreviewStatus(message);
|
||
setWorkspaceStatus(message);
|
||
}
|
||
if (commandId.startsWith('agent.')) {
|
||
setAgentRunStatus(message);
|
||
}
|
||
if (commandId.startsWith('memory.')) {
|
||
setMemoryStatus(message);
|
||
}
|
||
}
|
||
|
||
async function queueProjectPolicyConfirmationIfNeeded(
|
||
invoke: TauriInvoke,
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string,
|
||
detail: string,
|
||
readyMessage: string,
|
||
onConfirm: () => void,
|
||
) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath },
|
||
);
|
||
if (policyView.policy.deniedCommands.includes(commandId)) {
|
||
const message = `项目权限策略拒绝执行:${commandId}`;
|
||
markProjectPolicyDenied(commandId, message);
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return true;
|
||
}
|
||
if (!policyView.policy.confirmCommands.includes(commandId)) {
|
||
return false;
|
||
}
|
||
requestProjectPolicyConfirmation(commandId, projectPath, detail, onConfirm);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: readyMessage },
|
||
]);
|
||
return true;
|
||
}
|
||
|
||
async function denyPendingCommandIfNeeded(
|
||
commandId: GameCreationAppCommandDescriptor['id'],
|
||
projectPath: string | null,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !projectPath) {
|
||
return false;
|
||
}
|
||
try {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath },
|
||
);
|
||
if (!policyView.policy.deniedCommands.includes(commandId)) {
|
||
return false;
|
||
}
|
||
const message = `项目权限策略拒绝执行:${commandId}`;
|
||
markProjectPolicyDenied(commandId, message);
|
||
setCommandLog((current) => [...current, `permission.deny ${commandId}`]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function confirmUiCommand() {
|
||
const pending = pendingUiConfirmation;
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
if (
|
||
pending.projectPath &&
|
||
pending.projectPath !== resolveCurrentUiConfirmationProjectPath()
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
return;
|
||
}
|
||
const permissionProjectPath =
|
||
pending.projectPath ??
|
||
resolveUiPermissionLogProjectPath(pending.commandId);
|
||
if (
|
||
await denyPendingCommandIfNeeded(pending.commandId, permissionProjectPath)
|
||
) {
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
return;
|
||
}
|
||
const action = pendingUiConfirmationActionRef.current;
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.confirm ${pending.commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
permissionProjectPath,
|
||
'permission.confirm',
|
||
pending.commandId,
|
||
);
|
||
action?.();
|
||
}
|
||
|
||
function cancelUiCommandConfirmation() {
|
||
const pending = pendingUiConfirmation;
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
if (pending.commandId === 'conversation.write') {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (nextProjectPath) {
|
||
projectConversationWriteCancelledRef.current = {
|
||
projectPath: nextProjectPath,
|
||
messageCount: latestMessagesRef.current.length,
|
||
};
|
||
setWorkspaceStatus((current) =>
|
||
current === '等待确认保存项目对话'
|
||
? `已打开:${nextProjectPath}`
|
||
: current,
|
||
);
|
||
}
|
||
}
|
||
if (pending.commandId === 'conversation.read') {
|
||
if (pending.detail.includes('Agent 对话')) {
|
||
setAgentConversationStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 对话' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 对话' : current,
|
||
);
|
||
} else {
|
||
setWorkspaceStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目对话' : current,
|
||
);
|
||
}
|
||
}
|
||
if (pending.commandId === 'memory.read') {
|
||
setMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目记忆' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消读取 Agent 私有记忆' : current,
|
||
);
|
||
if (!pending.detail.includes('Agent 私有记忆')) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消读取项目记忆。' },
|
||
]);
|
||
}
|
||
}
|
||
if (pending.commandId === 'memory.write') {
|
||
setMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消保存项目记忆' : current,
|
||
);
|
||
setAgentMemoryStatus((current) =>
|
||
current === '等待确认' ? '已取消保存 Agent 私有记忆' : current,
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'file.list' ||
|
||
pending.commandId === 'file.read' ||
|
||
pending.commandId === 'agent.trace_read'
|
||
) {
|
||
setFileStatus((current) =>
|
||
current === '等待确认' ? '已取消读取项目文件' : current,
|
||
);
|
||
}
|
||
if (pending.commandId === 'agent.trace_read') {
|
||
setAgentRunStatus((current) =>
|
||
current === '等待确认' || current === '等待确认读取 Agent trace'
|
||
? '已取消读取 Agent trace'
|
||
: current,
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'project.status' ||
|
||
pending.commandId === 'task.list'
|
||
) {
|
||
const status =
|
||
pending.commandId === 'task.list'
|
||
? '已取消读取任务拆分'
|
||
: '已取消读取项目状态';
|
||
setProjectStatus(status);
|
||
setWorkspaceStatus(status);
|
||
}
|
||
if (pending.commandId === 'asset.list') {
|
||
setAssetStatus('已取消读取项目资产');
|
||
setWorkspaceStatus('已取消读取项目资产');
|
||
}
|
||
if (pending.commandId === 'asset.upload') {
|
||
setAssetStatus('已取消上传资产');
|
||
}
|
||
if (pending.commandId === 'canvas.project_open') {
|
||
setAssetStatus('已取消打开画板');
|
||
}
|
||
if (pending.commandId === 'canvas.project_sync') {
|
||
setAssetStatus('已取消同步画板项目');
|
||
}
|
||
if (pending.commandId === 'canvas.asset_import') {
|
||
setAssetStatus('已取消导入画板资产');
|
||
}
|
||
if (pending.commandId === 'canvas.asset_generate') {
|
||
setAssetStatus('已取消生成美术素材');
|
||
}
|
||
if (pending.commandId === 'canvas.export_import') {
|
||
setAssetStatus('已取消导入画板导出包');
|
||
}
|
||
if (
|
||
pending.commandId === 'preview.start' ||
|
||
pending.commandId === 'preview.open' ||
|
||
pending.commandId === 'preview.stop' ||
|
||
pending.commandId === 'preview.status'
|
||
) {
|
||
setPreviewStatus('已取消预览操作');
|
||
setWorkspaceStatus('已取消预览操作');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消预览操作' },
|
||
]);
|
||
}
|
||
if (
|
||
pending.commandId === 'agent.run_status' ||
|
||
pending.commandId === 'agent.audit'
|
||
) {
|
||
setAgentRunStatus(
|
||
pending.commandId === 'agent.audit'
|
||
? '已取消 Agent 审计'
|
||
: '已取消读取 Agent run 状态',
|
||
);
|
||
}
|
||
if (
|
||
pending.commandId === 'agent.resume' &&
|
||
pending.detail.includes('未完成的 Agent Runtime 任务')
|
||
) {
|
||
setAgentRunStatus('已取消恢复 Agent Runtime 任务');
|
||
}
|
||
if (
|
||
pending.commandId === 'project.index' ||
|
||
pending.commandId === 'project.diff' ||
|
||
pending.commandId === 'project.export_package'
|
||
) {
|
||
const status =
|
||
pending.commandId === 'project.index'
|
||
? '已取消刷新项目索引'
|
||
: pending.commandId === 'project.diff'
|
||
? '已取消对比项目 checkpoint'
|
||
: '已取消导出本地试玩包';
|
||
setFileStatus(status);
|
||
setWorkspaceStatus(status);
|
||
}
|
||
if (pending.commandId === 'project.create') {
|
||
setProjectStatus('已取消');
|
||
setWorkspaceStatus('已取消');
|
||
}
|
||
if (pending.commandId === 'file.write') {
|
||
setFileStatus('已取消保存项目文件');
|
||
}
|
||
if (pending.commandId === 'file.delete') {
|
||
setFileStatus('已取消删除项目文件');
|
||
}
|
||
if (pending.commandId === 'memory.write') {
|
||
setMemoryStatus('已取消写入项目记忆');
|
||
}
|
||
if (pending.commandId === 'memory.delete') {
|
||
setMemoryStatus('已取消删除项目记忆');
|
||
}
|
||
if (pending.commandId === 'asset.register') {
|
||
setAssetStatus('已取消登记资产');
|
||
}
|
||
if (pending.commandId === 'command.run_limited') {
|
||
setLimitedCommandStatus('已取消运行');
|
||
}
|
||
pendingUiConfirmationActionRef.current = null;
|
||
setPendingUiConfirmation(null);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${pending.commandId}`,
|
||
]);
|
||
appendLocalPermissionLog(
|
||
pending.projectPath ??
|
||
resolveUiPermissionLogProjectPath(pending.commandId),
|
||
'permission.cancel',
|
||
pending.commandId,
|
||
);
|
||
}
|
||
|
||
function handleRuntimeConfigOpen() {
|
||
setRuntimeConfigOpen(true);
|
||
}
|
||
|
||
function queueRunLocalShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'game.run_local' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行当前本地游戏。' },
|
||
]);
|
||
}
|
||
|
||
function queueStaticSmokeShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'command.run_limited',
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行静态入口自检。' },
|
||
]);
|
||
}
|
||
|
||
function queueProjectCheckpointShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.checkpoint' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存当前项目快照。' },
|
||
]);
|
||
}
|
||
|
||
function queueProjectExportPackageShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.export_package' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备导出本地试玩包。' },
|
||
]);
|
||
}
|
||
|
||
function queuePreviewStartShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.start' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备启动本地预览。' },
|
||
]);
|
||
}
|
||
|
||
function queuePreviewOpenShortcut() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.open' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备打开当前本地预览。' },
|
||
]);
|
||
}
|
||
|
||
async function handleOpenLauncherWindow() {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请回到首页的项目组切换项目。' },
|
||
]);
|
||
}
|
||
|
||
async function handleRevealCurrentProjectDirectory() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内打开项目目录。' },
|
||
]);
|
||
return;
|
||
}
|
||
try {
|
||
await invoke('open_local_project_directory', {
|
||
projectPath: nextProjectPath,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已打开项目目录。' },
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function requireChatProjectForUserAction() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (nextProjectPath) {
|
||
return nextProjectPath;
|
||
}
|
||
setMemoryStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return null;
|
||
}
|
||
|
||
async function handleProjectInit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
requestCommandConfirmation(
|
||
'project.create',
|
||
`创建 ${projectPath}`,
|
||
() => void executeProjectCreate(projectPath, false),
|
||
);
|
||
}
|
||
|
||
async function resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
if (
|
||
projectSupervisorRuntimeResumeProjectPathRef.current === nextProjectPath
|
||
) {
|
||
return '';
|
||
}
|
||
try {
|
||
await invoke<AgentRuntimeResult[]>(
|
||
'resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
return '';
|
||
} catch (error) {
|
||
if (isMissingAgentRuntimeResumeCommandError(error)) {
|
||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
return '';
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (
|
||
message.includes('项目权限策略要求用户确认:agent.resume') ||
|
||
message.includes('项目权限策略拒绝执行:agent.resume')
|
||
) {
|
||
return '';
|
||
}
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
const runtimeError = `项目总控 Agent 恢复失败:${message}`;
|
||
setProjectSupervisorRuntimeError(runtimeError);
|
||
return runtimeError;
|
||
}
|
||
}
|
||
|
||
async function readProjectSupervisorActiveSession(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
return readProjectSupervisorActiveSessionId(invoke, nextProjectPath);
|
||
}
|
||
|
||
async function refreshProjectSupervisorConversation(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
sessionId: string,
|
||
runId?: string,
|
||
) {
|
||
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
|
||
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
|
||
const [projectConversation, supervisorConversation] = await Promise.all([
|
||
invoke<LocalConversationResult>('read_local_conversation', {
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
}),
|
||
invoke<LocalConversationResult>('read_local_conversation', {
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
}),
|
||
]);
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const conversationMessages = mergeProjectSupervisorConversation(
|
||
projectConversation.messages,
|
||
supervisorConversation.messages,
|
||
);
|
||
const transientResponse = projectSupervisorResponseStreamRef.current;
|
||
if (
|
||
!gameChatOnly &&
|
||
conversationContainsProjectSupervisorResponseStream(
|
||
supervisorConversation.messages,
|
||
transientResponse,
|
||
)
|
||
) {
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
setProjectSupervisorResponseStream(null);
|
||
}
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setMessages((current) => {
|
||
// React may apply this hydration update after a ready stream callback
|
||
// that was queued in the same turn. Read `current` inside the updater
|
||
// so a committed game-chat response cannot be overwritten by stale
|
||
// `latestMessagesRef` state captured before that callback ran.
|
||
const nextMessages = gameChatOnly
|
||
? mergeGameChatHydratedConversationMessages(
|
||
conversationMessages,
|
||
current,
|
||
)
|
||
: conversationMessages;
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = gameChatOnly
|
||
? conversationMessages.length
|
||
: nextMessages.length;
|
||
latestMessagesRef.current = nextMessages;
|
||
return nextMessages;
|
||
});
|
||
const terminalRuntime = projectSupervisorRuntimeRef.current;
|
||
if (
|
||
runId &&
|
||
terminalRuntime?.runId === runId &&
|
||
isAgentRuntimeTerminalState(terminalRuntime)
|
||
) {
|
||
appendGameChatStageRecord(nextProjectPath, terminalRuntime);
|
||
}
|
||
setProjectSupervisorRuntimeError('');
|
||
}
|
||
projectSupervisorRefreshConversationRef.current =
|
||
refreshProjectSupervisorConversation;
|
||
|
||
async function loadProjectConversation(
|
||
nextProjectPath: string,
|
||
skipPolicyConfirm = false,
|
||
mode: 'initial' | 'replace' = 'initial',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
return;
|
||
}
|
||
if (!skipPolicyConfirm) {
|
||
try {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('conversation.read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'conversation.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的项目对话历史`,
|
||
() => void loadProjectConversation(nextProjectPath, true, mode),
|
||
);
|
||
setWorkspaceStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch {
|
||
return;
|
||
}
|
||
}
|
||
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
|
||
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
|
||
try {
|
||
const resumeError = await resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
const sessionId = await readProjectSupervisorActiveSession(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
const projectConversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: null,
|
||
},
|
||
);
|
||
let supervisorConversation: LocalConversationResult | null = null;
|
||
let runtime: AgentRuntimeState | null = null;
|
||
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
|
||
let runtimeError = '';
|
||
if (sessionId) {
|
||
supervisorConversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
try {
|
||
const runtimeResult = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
},
|
||
);
|
||
runtime = agentRuntimeStateFromResult(runtimeResult);
|
||
runtimeResponseStream = runtimeResult.responseStream ?? null;
|
||
} catch (error) {
|
||
runtimeError = error instanceof Error ? error.message : String(error);
|
||
}
|
||
}
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
const conversationMessages = mergeProjectSupervisorConversation(
|
||
projectConversation.messages,
|
||
supervisorConversation?.messages ?? [],
|
||
);
|
||
if (
|
||
!gameChatOnly &&
|
||
conversationContainsProjectSupervisorResponseStream(
|
||
supervisorConversation?.messages ?? [],
|
||
runtimeResponseStream,
|
||
)
|
||
) {
|
||
runtimeResponseStream = null;
|
||
}
|
||
projectSupervisorSessionIdRef.current = sessionId;
|
||
setProjectSupervisorSessionId(sessionId);
|
||
updateProjectSupervisorRuntime(runtime);
|
||
if (runtime) {
|
||
updateProjectSupervisorResponseStream(runtimeResponseStream, runtime);
|
||
} else {
|
||
projectSupervisorResponseStreamRef.current = null;
|
||
setProjectSupervisorResponseStream(null);
|
||
}
|
||
setProjectSupervisorRuntimeError(runtimeError || resumeError);
|
||
setMessages((current) => {
|
||
const nextConversationMessages = gameChatOnly
|
||
? mergeGameChatHydratedConversationMessages(
|
||
conversationMessages,
|
||
current,
|
||
)
|
||
: conversationMessages;
|
||
const hasOnlyDefaultGreeting =
|
||
current.length === 1 &&
|
||
current[0]?.role === 'assistant' &&
|
||
current[0]?.text === '想做什么游戏?';
|
||
const hasOnlyOpenStatus =
|
||
current.length === 2 &&
|
||
current[0]?.role === 'assistant' &&
|
||
current[0]?.text === '想做什么游戏?' &&
|
||
current[1]?.role === 'assistant' &&
|
||
current[1]?.text === `已设置本地项目:${nextProjectPath}`;
|
||
if (
|
||
mode !== 'replace' &&
|
||
!hasOnlyDefaultGreeting &&
|
||
!hasOnlyOpenStatus
|
||
) {
|
||
return current;
|
||
}
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
savedConversationProjectPathRef.current = nextProjectPath;
|
||
savedConversationCountRef.current = gameChatOnly
|
||
? conversationMessages.length
|
||
: nextConversationMessages.length;
|
||
latestMessagesRef.current = nextConversationMessages;
|
||
setWorkspaceStatus((workspaceStatus) => {
|
||
if (mode === 'replace') {
|
||
return `已读取项目对话历史:${conversationMessages.length} 条`;
|
||
}
|
||
return workspaceStatus === '等待确认'
|
||
? `已打开:${nextProjectPath}`
|
||
: workspaceStatus;
|
||
});
|
||
return nextConversationMessages;
|
||
});
|
||
} catch (error) {
|
||
if (
|
||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||
localProjectPathRef.current !== nextProjectPath
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
setWorkspaceStatus((workspaceStatus) =>
|
||
workspaceStatus === '等待确认'
|
||
? `项目对话读取失败:${message}`
|
||
: workspaceStatus,
|
||
);
|
||
// Keep the default greeting when history is missing or blocked.
|
||
}
|
||
}
|
||
|
||
async function openWorkspace(
|
||
nextProjectPath: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const trimmedProjectPath = nextProjectPath.trim();
|
||
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(trimmedProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setWorkspaceStatus('需要在 Tauri App 内运行');
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const projectScopeVersion = projectScopeVersionRef.current + 1;
|
||
projectScopeVersionRef.current = projectScopeVersion;
|
||
resetProjectSupervisorState();
|
||
if (
|
||
gameChatAutoPreviewAuthorizationRef.current?.projectPath !==
|
||
nextProjectPath
|
||
) {
|
||
setGameChatAutoPreviewAuthorization(null);
|
||
}
|
||
updateClientPreview(null);
|
||
gameChatPreviewRevisionRef.current = null;
|
||
setGameChatPreviewRevision(null);
|
||
setPreviewStatus('未启动');
|
||
setWorkspaceStatus('正在打开');
|
||
setProjectStatus('正在初始化');
|
||
setSelectedAgent(null);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentConversationStatus('未选择 agent');
|
||
setAgentConversationSaving(false);
|
||
setAgentMemoryStatus('未读取');
|
||
setAgentMemoryContent('');
|
||
agentConversationSavingRef.current = false;
|
||
agentConversationLoadVersionRef.current += 1;
|
||
try {
|
||
const result = await invoke<InitLocalProjectResult>(
|
||
'init_local_game_project',
|
||
{
|
||
projectPath: trimmedProjectPath,
|
||
projectId: seedManifest.projectId,
|
||
name: gameChatOnly
|
||
? projectNameFromPath(trimmedProjectPath)
|
||
: seedManifest.name,
|
||
},
|
||
);
|
||
if (projectScopeVersionRef.current !== projectScopeVersion) {
|
||
return;
|
||
}
|
||
const openedProjectPath = result.projectPath.trim();
|
||
if (
|
||
!openedProjectPath ||
|
||
!isAbsoluteProjectPath(openedProjectPath) ||
|
||
projectPathHasControlCharacter(openedProjectPath)
|
||
) {
|
||
const message = '本地项目路径无效';
|
||
setWorkspaceStatus(message);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const openedProject = { ...result, projectPath: openedProjectPath };
|
||
const conversationMessages = createDefaultChatMessages();
|
||
|
||
if (
|
||
pendingUiConfirmation?.projectPath &&
|
||
pendingUiConfirmation.projectPath !== openedProject.projectPath
|
||
) {
|
||
cancelUiCommandConfirmation();
|
||
}
|
||
localProjectPathRef.current = openedProject.projectPath;
|
||
setProjectPath(openedProject.projectPath);
|
||
setLocalProject(openedProject);
|
||
if (gameChatOnly) {
|
||
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
|
||
}
|
||
setManifest(openedProject.manifest);
|
||
setProjectFiles([]);
|
||
setProjectCheckpoints([]);
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRuntimeById({});
|
||
setMessages(conversationMessages);
|
||
setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
savedConversationProjectPathRef.current = openedProject.projectPath;
|
||
savedConversationCountRef.current = conversationMessages.length;
|
||
writeRecentWorkspace(openedProject.projectPath);
|
||
setWorkspaceStatus(`已打开:${openedProject.projectPath}`);
|
||
setProjectStatus('已初始化');
|
||
appendLocalPermissionLog(
|
||
openedProject.projectPath,
|
||
'permission.confirm',
|
||
'project.create',
|
||
);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已设置本地项目:${openedProject.projectPath}`,
|
||
},
|
||
]);
|
||
}
|
||
void loadProjectConversation(openedProject.projectPath);
|
||
void refreshAgentRunTrace(openedProject.projectPath);
|
||
} catch (error) {
|
||
if (projectScopeVersionRef.current !== projectScopeVersion) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setWorkspaceStatus(message);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function openGameChatProjectPath(
|
||
nextProjectPath: string,
|
||
requestedSelectionVersion?: number,
|
||
) {
|
||
const selectionVersion =
|
||
requestedSelectionVersion ??
|
||
gameChatProjectSelectionVersionRef.current + 1;
|
||
gameChatProjectSelectionVersionRef.current = selectionVersion;
|
||
setPendingNonEmptyProjectCreate(null);
|
||
const trimmedProjectPath = nextProjectPath.trim();
|
||
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
||
setWorkspaceStatus('请提供工作区绝对路径');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(trimmedProjectPath)) {
|
||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setWorkspaceStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
setGameChatProjectSelectionBusy(true);
|
||
try {
|
||
const directoryStatus = await invoke<LocalProjectDirectoryStatus>(
|
||
'inspect_local_project_directory',
|
||
{ projectPath: trimmedProjectPath },
|
||
);
|
||
if (gameChatProjectSelectionVersionRef.current !== selectionVersion) {
|
||
return;
|
||
}
|
||
if (directoryStatus.exists && !directoryStatus.isDirectory) {
|
||
setWorkspaceStatus('项目路径不是文件夹');
|
||
return;
|
||
}
|
||
if (directoryStatus.isGameCreatorProject) {
|
||
await openWorkspace(trimmedProjectPath, false);
|
||
return;
|
||
}
|
||
await executeProjectCreate(trimmedProjectPath, false, selectionVersion);
|
||
} catch (error) {
|
||
setWorkspaceStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
if (gameChatProjectSelectionVersionRef.current === selectionVersion) {
|
||
setGameChatProjectSelectionBusy(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleGameChatProjectPick() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || gameChatProjectSelectionBusy) {
|
||
return;
|
||
}
|
||
const selectionVersion = gameChatProjectSelectionVersionRef.current + 1;
|
||
gameChatProjectSelectionVersionRef.current = selectionVersion;
|
||
setPendingNonEmptyProjectCreate(null);
|
||
setGameChatProjectSelectionBusy(true);
|
||
try {
|
||
const selectedPath = await invoke<string | null>(
|
||
'pick_local_project_directory',
|
||
);
|
||
if (gameChatProjectSelectionVersionRef.current !== selectionVersion) {
|
||
return;
|
||
}
|
||
if (!selectedPath) {
|
||
setWorkspaceStatus('已取消');
|
||
return;
|
||
}
|
||
await openGameChatProjectPath(selectedPath, selectionVersion);
|
||
} catch (error) {
|
||
setWorkspaceStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
if (gameChatProjectSelectionVersionRef.current === selectionVersion) {
|
||
setGameChatProjectSelectionBusy(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function openAgentConversation(
|
||
agent: AgentStatusCard,
|
||
skipConversationPolicyConfirm = false,
|
||
skipMemoryPolicyConfirm = false,
|
||
) {
|
||
const loadVersion = agentConversationLoadVersionRef.current + 1;
|
||
agentConversationLoadVersionRef.current = loadVersion;
|
||
setSelectedAgent(agent);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentMemoryContent('');
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
setAgentMemoryStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
setLlmConfigStatus(status);
|
||
}
|
||
} catch {
|
||
// Agent conversation history is still useful when config status is unavailable.
|
||
}
|
||
try {
|
||
if (
|
||
!skipConversationPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.read',
|
||
nextProjectPath,
|
||
`读取 ${agent.title} Agent 对话`,
|
||
'准备读取 Agent 对话。',
|
||
() =>
|
||
void openAgentConversation(agent, true, skipMemoryPolicyConfirm),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
setAgentMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentConversationStatus(message);
|
||
setAgentMemoryStatus(message);
|
||
return;
|
||
}
|
||
setAgentConversationStatus('正在读取');
|
||
setAgentMemoryStatus('正在读取');
|
||
try {
|
||
const result = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(result.messages);
|
||
setAgentConversationSessionId(result.sessionId ?? null);
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationRuntimeError(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus(
|
||
`已读取 ${result.messages.length} 条:${result.path}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'conversation.read']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
try {
|
||
if (
|
||
!skipMemoryPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${agent.title} Agent 私有记忆`,
|
||
'准备读取 Agent 私有记忆。',
|
||
() => void openAgentConversation(agent, true, true),
|
||
))
|
||
) {
|
||
setAgentMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
try {
|
||
const result = await invoke<LocalAgentMemoryResult>(
|
||
'read_local_agent_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
taskId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent(result.content);
|
||
setAgentMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '私有记忆为空',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.agent.read']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== loadVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent('');
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
function closeAgentConversation() {
|
||
agentConversationLoadVersionRef.current += 1;
|
||
agentConversationSavingRef.current = false;
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setSelectedAgent(null);
|
||
setAgentConversationInput('');
|
||
setAgentConversationMessages([]);
|
||
setAgentConversationRuntime(null);
|
||
setAgentConversationSessionId(null);
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
|
||
setAgentConversationStatus('未选择 agent');
|
||
setAgentConversationSaving(false);
|
||
setAgentConversationBackgroundBusy(false);
|
||
setAgentMemoryStatus('未读取');
|
||
setAgentMemoryContent('');
|
||
}
|
||
|
||
function prepareSuggestedToolCommandDraft(
|
||
toolCall: GameCreationAgentToolCallTrace,
|
||
) {
|
||
const commandDraft = commandDraftFromSuggestedToolCall(toolCall);
|
||
if (!commandDraft) {
|
||
return;
|
||
}
|
||
prepareChatCommandDraft(commandDraft);
|
||
closeAgentConversation();
|
||
}
|
||
|
||
function prepareChatCommandDraft(commandDraft: string) {
|
||
setChatInput(commandDraft);
|
||
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
||
}
|
||
|
||
function prepareProjectAssetRegisterDraft(localPath: string) {
|
||
prepareChatCommandDraft(projectFileActionDrafts(localPath).assetCommand);
|
||
}
|
||
|
||
function prepareAgentEvidenceReadDraft(localPath: string) {
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
return;
|
||
}
|
||
prepareChatCommandDraft(projectFileActionDrafts(localPath).readCommand);
|
||
closeAgentConversation();
|
||
}
|
||
|
||
async function handlePickCanvasExportFileDraft() {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内选择画板导出包。' },
|
||
]);
|
||
return;
|
||
}
|
||
try {
|
||
const selectedPath = await invoke<string | null>('pick_local_file');
|
||
if (!selectedPath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消选择画板导出包。' },
|
||
]);
|
||
return;
|
||
}
|
||
setCanvasExportPath(selectedPath);
|
||
prepareChatCommandDraft(`/import-canvas-export ${selectedPath} `);
|
||
setAssetStatus(`已选择画板导出包:${selectedPath}`);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已选择画板导出包:${selectedPath}` },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function saveAgentConversationMessage(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
`写入 ${agent.title} Agent 对话`,
|
||
'准备保存 Agent 对话。',
|
||
() => void saveAgentConversationMessage(agent, content, true),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationSavingRef.current = true;
|
||
setAgentConversationSaving(true);
|
||
setAgentConversationInput('');
|
||
setAgentConversationStatus('正在保存用户消息');
|
||
let savedUserResult: LocalConversationResult | null = null;
|
||
let stopStreamListen: (() => void) | null = null;
|
||
let streamListenDisposed = false;
|
||
let streamListenReady = false;
|
||
try {
|
||
savedUserResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'user',
|
||
content,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const savedUserMessages = savedUserResult.messages;
|
||
setAgentConversationMessages(savedUserMessages);
|
||
setAgentConversationStatus('正在连接 Agent LLM');
|
||
const streamRunId = createAgentChatRunId('agent-conversation');
|
||
const listen = window.__TAURI__?.event?.listen;
|
||
if (listen) {
|
||
try {
|
||
stopStreamListen = await listen<GameCreatorRoleAgentChatStreamEvent>(
|
||
'game-creator-role-agent-chat-stream',
|
||
(event) => {
|
||
const payload = event.payload;
|
||
if (
|
||
payload.projectPath !== nextProjectPath ||
|
||
payload.agentId !== agent.id ||
|
||
payload.runId !== streamRunId ||
|
||
agentConversationLoadVersionRef.current !== saveVersion
|
||
) {
|
||
return;
|
||
}
|
||
if (payload.runtimeState) {
|
||
setAgentConversationRuntime((current) => {
|
||
const nextRuntime = normalizeAgentRuntimeState(
|
||
payload.runtimeState!,
|
||
current,
|
||
);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
return nextRuntime;
|
||
});
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
if (payload.status === 'started') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ?? 'Agent 已连接,正在等待回复',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'delta') {
|
||
const draftText = payload.accumulatedText || payload.deltaText;
|
||
if (draftText) {
|
||
setAgentConversationMessages([
|
||
...savedUserMessages,
|
||
createLocalConversationDraftMessage(draftText),
|
||
]);
|
||
}
|
||
setAgentConversationStatus(
|
||
payload.finishReason
|
||
? `Agent 回复结束:${payload.finishReason}`
|
||
: '正在接收 Agent 回复',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'completed') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ?? 'Agent 回复完成,正在保存',
|
||
);
|
||
return;
|
||
}
|
||
if (payload.status === 'failed') {
|
||
setAgentConversationStatus(
|
||
payload.runtimeSummary ?? 'Agent 流式回复失败,正在记录错误',
|
||
);
|
||
}
|
||
},
|
||
);
|
||
streamListenReady = true;
|
||
if (streamListenDisposed) {
|
||
stopStreamListen();
|
||
stopStreamListen = null;
|
||
}
|
||
} catch {
|
||
setAgentConversationStatus('实时状态不可用,正在使用普通回复模式');
|
||
}
|
||
}
|
||
const reply = streamListenReady
|
||
? await invoke<GameCreatorChatAgentReply>(
|
||
'chat_with_game_creator_role_agent_stream',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
prompt: content,
|
||
runId: streamRunId,
|
||
},
|
||
)
|
||
: await invoke<GameCreatorChatAgentReply>(
|
||
'chat_with_game_creator_role_agent',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
prompt: content,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'read_game_creator_agent_runtime',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
}
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
setAgentConversationRuntimeError(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus('正在保存 Agent 回复');
|
||
setAgentConversationMessages([
|
||
...savedUserMessages,
|
||
createLocalConversationDraftMessage(reply.replyText),
|
||
]);
|
||
const assistantResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'assistant',
|
||
content: reply.replyText,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(assistantResult.messages);
|
||
setAgentConversationStatus(
|
||
`已保存 ${assistantResult.messages.length} 条:${assistantResult.path}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'conversation.write']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
if (savedUserResult) {
|
||
const message = `已保存用户消息;Agent 回复失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`;
|
||
try {
|
||
const errorResult = await invoke<LocalConversationResult>(
|
||
'append_local_conversation_message',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
message: {
|
||
role: 'assistant',
|
||
content: message,
|
||
agentId: null,
|
||
},
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(errorResult.messages);
|
||
} catch {
|
||
setAgentConversationMessages([
|
||
...savedUserResult.messages,
|
||
{
|
||
schemaVersion: 'game-creator-conversation.v1',
|
||
role: 'assistant',
|
||
content: message,
|
||
agentId: null,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
}
|
||
setAgentConversationStatus(message);
|
||
} else {
|
||
setAgentConversationInput(content);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
} finally {
|
||
streamListenDisposed = true;
|
||
stopStreamListen?.();
|
||
agentConversationSavingRef.current = false;
|
||
setAgentConversationSaving(false);
|
||
}
|
||
}
|
||
|
||
async function startSelectedAgentBackgroundTask(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
submitMode = agentConversationRunSubmitMode,
|
||
) {
|
||
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const steerRuntime =
|
||
submitMode === 'steer'
|
||
? matchingAgentRuntimeForSteer(
|
||
[agentConversationRuntime],
|
||
agent.id,
|
||
agentConversationSessionId,
|
||
)
|
||
: null;
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'conversation.write',
|
||
nextProjectPath,
|
||
steerRuntime
|
||
? `向 ${agent.title} 当前 Run 追加指令`
|
||
: `启动 ${agent.title} 后台任务`,
|
||
steerRuntime
|
||
? '准备向当前 Agent Run 追加指令。'
|
||
: '准备启动 Agent 后台任务。',
|
||
() =>
|
||
void startSelectedAgentBackgroundTask(
|
||
agent,
|
||
content,
|
||
true,
|
||
submitMode,
|
||
),
|
||
))
|
||
) {
|
||
setAgentConversationStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
if (steerRuntime) {
|
||
setAgentConversationStatus(
|
||
`正在向当前 Run 追加指令:${steerRuntime.runId}`,
|
||
);
|
||
} else {
|
||
setAgentConversationInput('');
|
||
setAgentConversationStatus('正在启动 Agent 后台任务');
|
||
}
|
||
try {
|
||
let runtime: AgentRuntimeResult;
|
||
let successStatus: string;
|
||
if (steerRuntime) {
|
||
const steer = await invoke<AgentRuntimeSteerResult>(
|
||
'steer_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: steerRuntime.sessionId,
|
||
runId: steerRuntime.runId,
|
||
steerId: createAgentChatRunId('agent-steer'),
|
||
instruction: content,
|
||
},
|
||
);
|
||
runtime = steer.runtime;
|
||
successStatus = agentRuntimeSteerStatus(steer);
|
||
} else {
|
||
runtime = await invoke<AgentRuntimeResult>(
|
||
'start_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
task: content,
|
||
runId: createAgentChatRunId('agent-background-task'),
|
||
...(agentConversationSessionId
|
||
? { sessionId: agentConversationSessionId }
|
||
: {}),
|
||
},
|
||
);
|
||
successStatus = agentRuntimeStartStatus(runtime);
|
||
}
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationInput('');
|
||
setAgentConversationRunSubmitMode('steer');
|
||
setAgentConversationSessionId(nextRuntime.sessionId);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
try {
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: nextRuntime.sessionId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationSessionId(
|
||
conversation.sessionId ?? nextRuntime.sessionId,
|
||
);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||
setAgentConversationRuntimeError(
|
||
`对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
}
|
||
setAgentConversationStatus(successStatus);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
steerRuntime ? 'agent.runtime.steer' : 'agent.runtime.background_task',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationInput(content);
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function cancelSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
) {
|
||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在取消 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'cancel_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus(agentRuntimeCancelStatus(nextRuntime, runId));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.background_task.cancel',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function retrySelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
) {
|
||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在重试 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
nextRunId: createAgentChatRunId('agent-background-retry'),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.background_task.retry',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function confirmSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
actionId: string,
|
||
) {
|
||
if (
|
||
!agent ||
|
||
!runId ||
|
||
!actionId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在确认并继续 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'confirm_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
actionId,
|
||
note: '开发者已确认待执行工具动作',
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.tool_confirmation.approved',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function rejectSelectedAgentRuntimeTask(
|
||
agent: AgentStatusCard,
|
||
runId: string,
|
||
actionId: string,
|
||
) {
|
||
if (
|
||
!agent ||
|
||
!runId ||
|
||
!actionId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentConversationStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||
if (llmWarning) {
|
||
setAgentConversationStatus(llmWarning);
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationStatus('正在拒绝工具动作并继续 Agent 后台任务');
|
||
try {
|
||
const runtime = await invoke<AgentRuntimeResult>(
|
||
'reject_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId,
|
||
actionId,
|
||
note: '开发者拒绝待执行工具动作',
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||
setAgentConversationRuntime(nextRuntime);
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setAgentConversationRuntimeError('');
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.tool_confirmation.rejected',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function answerSelectedAgentRuntimeUserInput(
|
||
agent: AgentStatusCard,
|
||
request: AgentRuntimeUserInputRequest,
|
||
responseId: string,
|
||
answers: Record<string, string>,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRequest = agentConversationRuntime?.userInputRequest;
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!responseId ||
|
||
agentConversationBackgroundBusyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
request.agentId !== agent.id ||
|
||
request.sessionId !== agentConversationSessionId ||
|
||
request.runId !== agentConversationRuntime?.runId ||
|
||
currentRequest?.requestId !== request.requestId ||
|
||
currentRequest?.actionId !== request.actionId
|
||
) {
|
||
setAgentConversationRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
agentConversationBackgroundBusyRef.current = true;
|
||
setAgentConversationBackgroundBusy(true);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus('正在提交回答并继续当前 Run');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'answer_game_creator_agent_runtime_user_input',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
runId: request.runId,
|
||
actionId: request.actionId,
|
||
requestId: request.requestId,
|
||
responseId,
|
||
answers,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const runtimeState = agentRuntimeStateFromResult(
|
||
result,
|
||
agentConversationRuntime,
|
||
);
|
||
if (
|
||
runtimeState.agentId !== agent.id ||
|
||
runtimeState.sessionId !== request.sessionId ||
|
||
runtimeState.runId !== request.runId
|
||
) {
|
||
throw new Error('Agent 回答后 Runtime 身份不匹配');
|
||
}
|
||
setAgentConversationRuntime(runtimeState);
|
||
rememberAgentRuntimeState(runtimeState);
|
||
setAgentConversationSessionId(runtimeState.sessionId);
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: agent.id,
|
||
sessionId: runtimeState.sessionId,
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationMessages(conversation.messages);
|
||
setAgentConversationSessionId(
|
||
conversation.sessionId ?? runtimeState.sessionId,
|
||
);
|
||
setAgentConversationRuntimeError('');
|
||
setAgentConversationStatus(agentRuntimeConversationStatus(runtimeState));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.user_input.answered',
|
||
]);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentConversationRuntimeError(message);
|
||
setAgentConversationStatus(`回答提交失败:${message}`);
|
||
} finally {
|
||
agentConversationBackgroundBusyRef.current = false;
|
||
setAgentConversationBackgroundBusy(false);
|
||
}
|
||
}
|
||
|
||
async function saveSelectedAgentPrivateMemory(
|
||
agent: AgentStatusCard,
|
||
content: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke) {
|
||
setAgentMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentMemoryStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const saveVersion = agentConversationLoadVersionRef.current;
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.deniedCommands.includes('memory.write')) {
|
||
const message = '项目权限策略拒绝执行:memory.write';
|
||
markProjectPolicyDenied('memory.write', message);
|
||
setAgentMemoryStatus(message);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'permission.deny memory.write',
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return;
|
||
}
|
||
if (policyView.policy.confirmCommands.includes('memory.write')) {
|
||
requestProjectPolicyConfirmation(
|
||
'memory.write',
|
||
nextProjectPath,
|
||
`写入 ${agent.title} Agent 私有记忆`,
|
||
() => void saveSelectedAgentPrivateMemory(agent, content, true),
|
||
);
|
||
setAgentMemoryStatus('等待确认');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存 Agent 私有记忆。' },
|
||
]);
|
||
return;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
return;
|
||
}
|
||
agentConversationSavingRef.current = true;
|
||
setAgentConversationSaving(true);
|
||
setAgentConversationInput('');
|
||
setAgentMemoryStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalAgentMemoryResult>(
|
||
'write_local_agent_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
taskId: agent.id,
|
||
content: appendMemoryContent(agentMemoryContent, content),
|
||
},
|
||
);
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentMemoryContent(result.content);
|
||
setAgentMemoryStatus(`已追加私有记忆:${result.path}`);
|
||
setAgentConversationStatus(`已写入 ${agent.title} 私有记忆。`);
|
||
setCommandLog((current) => [...current, 'memory.agent.write']);
|
||
} catch (error) {
|
||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||
return;
|
||
}
|
||
setAgentConversationInput(content);
|
||
setAgentMemoryStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
} finally {
|
||
agentConversationSavingRef.current = false;
|
||
setAgentConversationSaving(false);
|
||
}
|
||
}
|
||
|
||
function handleAgentConversationSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
void saveAgentConversationMessage(agent, content);
|
||
}
|
||
|
||
function handleAgentPrivateMemorySubmit() {
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (!agent || !content || agentConversationSavingRef.current) {
|
||
return;
|
||
}
|
||
void saveSelectedAgentPrivateMemory(agent, content);
|
||
}
|
||
|
||
function handleAgentBackgroundTaskSubmit() {
|
||
const agent = selectedAgent;
|
||
const content = agentConversationInput.trim();
|
||
if (agentRuntimeNeedsUserInput(agentConversationRuntime)) {
|
||
setAgentConversationStatus('请先回答 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!agent || !content || agentConversationBackgroundBusyRef.current) {
|
||
return;
|
||
}
|
||
void startSelectedAgentBackgroundTask(agent, content);
|
||
}
|
||
|
||
function showChatHelp() {
|
||
setCommandLog((current) => [...current, 'help.show']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `可用命令:\n${chatCommandHelp.join('\n')}`,
|
||
},
|
||
]);
|
||
}
|
||
|
||
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
const prompt = chatInput.trim();
|
||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!prompt || chatAgentBusy) {
|
||
return;
|
||
}
|
||
|
||
setChatInput('');
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: prompt,
|
||
runtimeOwned: !prompt.startsWith('/'),
|
||
},
|
||
]);
|
||
if (prompt === '/help') {
|
||
showChatHelp();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/config') {
|
||
handleRuntimeConfigOpen();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/capabilities' || prompt === '/能力') {
|
||
void executeAgentCapabilitiesChat();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/audit' || prompt === '/审计') {
|
||
void executeAgentAuditChat();
|
||
return;
|
||
}
|
||
|
||
const missingArgumentMessage = missingChatCommandArgumentMessage(prompt);
|
||
if (missingArgumentMessage) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: missingArgumentMessage },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/project ')) {
|
||
const nextProjectPath = prompt.slice('/project '.length).trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供本地项目绝对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '本地项目路径不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'project.create',
|
||
projectPath: nextProjectPath,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备初始化本地项目:${nextProjectPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/generate ') || prompt.startsWith('/draft ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const generationPrompt = prompt
|
||
.slice(
|
||
prompt.startsWith('/generate ')
|
||
? '/generate '.length
|
||
: '/draft '.length,
|
||
)
|
||
.trim();
|
||
if (!generationPrompt) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/generate 创作想法' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'game.generate_draft',
|
||
prompt: generationPrompt,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备生成本地游戏草案:${generationPrompt}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/status') {
|
||
void executeProjectStatus(true);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
handleProjectSummaryChatCommand({
|
||
agentRunHistory,
|
||
agentRunTrace,
|
||
agentStatusCards,
|
||
manifest,
|
||
prompt,
|
||
requireChatProjectForUserAction,
|
||
setMessages,
|
||
})
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/index') {
|
||
void queueOrExecuteProjectIndex();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/checkpoint') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.checkpoint' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备保存当前项目快照。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/checkpoints') {
|
||
void executeProjectCheckpoints(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/diff ')) {
|
||
const checkpointId = prompt.slice('/diff '.length).trim();
|
||
if (!checkpointId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/diff checkpoint-id' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCheckpointId(checkpointId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: 'checkpoint id 非法。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectDiff(checkpointId, true);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/restore ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const checkpointId = prompt.slice('/restore '.length).trim();
|
||
if (!checkpointId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/restore checkpoint-id' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCheckpointId(checkpointId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: 'checkpoint id 非法。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'project.restore', checkpointId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备回滚到 checkpoint:${checkpointId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/policy') {
|
||
void executeProjectPolicyRead(true);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
prompt.startsWith('/policy-deny ') ||
|
||
prompt.startsWith('/policy-allow ') ||
|
||
prompt.startsWith('/policy-confirm ') ||
|
||
prompt.startsWith('/policy-auto ')
|
||
) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const mode = prompt.startsWith('/policy-deny ')
|
||
? 'deny'
|
||
: prompt.startsWith('/policy-allow ')
|
||
? 'allow'
|
||
: prompt.startsWith('/policy-confirm ')
|
||
? 'confirm'
|
||
: 'auto';
|
||
const commandPrefix =
|
||
mode === 'deny'
|
||
? '/policy-deny '
|
||
: mode === 'allow'
|
||
? '/policy-allow '
|
||
: mode === 'confirm'
|
||
? '/policy-confirm '
|
||
: '/policy-auto ';
|
||
const commandId = prompt.slice(commandPrefix.length).trim();
|
||
if (!commandId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'deny' || mode === 'allow'
|
||
? '格式:/policy-deny file.write'
|
||
: '格式:/policy-confirm project.index',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isRegisteredGameCreationCommandId(commandId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `未知内置命令:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
(mode === 'confirm' || mode === 'auto') &&
|
||
!isProjectPolicyConfirmableCommandId(commandId)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void queueProjectPolicyMutation(commandId, mode);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
prompt.startsWith('/agent-policy-deny ') ||
|
||
prompt.startsWith('/agent-policy-allow ') ||
|
||
prompt.startsWith('/agent-policy-confirm ') ||
|
||
prompt.startsWith('/agent-policy-auto ')
|
||
) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const mode = prompt.startsWith('/agent-policy-deny ')
|
||
? 'deny'
|
||
: prompt.startsWith('/agent-policy-allow ')
|
||
? 'allow'
|
||
: prompt.startsWith('/agent-policy-confirm ')
|
||
? 'confirm'
|
||
: 'auto';
|
||
const commandPrefix =
|
||
mode === 'deny'
|
||
? '/agent-policy-deny '
|
||
: mode === 'allow'
|
||
? '/agent-policy-allow '
|
||
: mode === 'confirm'
|
||
? '/agent-policy-confirm '
|
||
: '/agent-policy-auto ';
|
||
const args = prompt.slice(commandPrefix.length).trim();
|
||
const [agentId = '', commandId = ''] = args.split(/\s+/, 2);
|
||
if (!agentId || !commandId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'deny' || mode === 'allow'
|
||
? '格式:/agent-policy-deny design-director file.read'
|
||
: '格式:/agent-policy-confirm design-director memory.write',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isRegisteredGameCreationCommandId(commandId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `未知内置命令:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
(mode === 'confirm' || mode === 'auto') &&
|
||
!isProjectPolicyConfirmableCommandId(commandId)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void queueAgentPolicyMutation(agentId, commandId, mode);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/llm-status') {
|
||
void executeLlmConfigStatus();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/llm-routes') {
|
||
void executeLlmRouteSummary();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/open-project' || prompt === '/show-project') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
void handleRevealCurrentProjectDirectory();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/switch-project') {
|
||
void handleOpenLauncherWindow();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/files') {
|
||
void executeProjectFiles(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/assets') {
|
||
void executeProjectAssets(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/credits') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectAssetCredits(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/art') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectVisualAssets(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/audio') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const summary = summarizeProjectAudioAssets(manifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/artifacts') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeCommonProjectArtifactReadDrafts(),
|
||
draftCommand: `/read ${commonProjectArtifactReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectArtifactReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run-artifacts') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const artifacts = agentRunTrace
|
||
? readableArtifactsFromAgentRunTrace(agentRunTrace)
|
||
: [];
|
||
const firstArtifact = artifacts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeRunArtifactReadDrafts(agentRunTrace),
|
||
draftCommand: firstArtifact
|
||
? `/read ${firstArtifact.path}`
|
||
: undefined,
|
||
draftCommandLabel: firstArtifact ? '读取首个 Run 产物' : undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/passes') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const artifacts = agentRunTrace
|
||
? readablePassArtifactsFromAgentRunTrace(agentRunTrace)
|
||
: [];
|
||
const firstArtifact = artifacts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentPassArtifactReadDrafts(agentRunTrace),
|
||
draftCommand: firstArtifact
|
||
? `/read ${firstArtifact.path}`
|
||
: undefined,
|
||
draftCommandLabel: firstArtifact ? '读取首个轮次产物' : undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/runs') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const firstHistory = agentRunHistory[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentRunHistoryReadDrafts(
|
||
agentRunTrace,
|
||
agentRunHistory,
|
||
),
|
||
draftCommand: firstHistory
|
||
? `/read ${firstHistory.path}`
|
||
: agentRunTrace
|
||
? '/trace'
|
||
: undefined,
|
||
draftCommandLabel: firstHistory
|
||
? '读取首个历史 Run'
|
||
: agentRunTrace
|
||
? '查看最近 Run'
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run-files') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentRunSupportFileReadDrafts(),
|
||
draftCommand: `/read ${commonAgentRunSupportReadDrafts[0].path}`,
|
||
draftCommandLabel: commonAgentRunSupportReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/internals') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectInternalReadDrafts(),
|
||
draftCommand: `/read ${commonProjectInternalReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectInternalReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/logs') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeCommonProjectLogReadDrafts(),
|
||
draftCommand: `/read ${commonProjectLogReadDrafts[0].path}`,
|
||
draftCommandLabel: commonProjectLogReadDrafts[0].label,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/asset-register ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [
|
||
localPath,
|
||
kind = 'asset',
|
||
mediaType = 'application/octet-stream',
|
||
] = prompt.slice('/asset-register '.length).trim().split(/\s+/);
|
||
if (!localPath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/asset-register assets/hero.png [kind] [mediaType]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '资产路径必须是项目内相对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'asset.register', localPath, kind, mediaType });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备登记项目资产:${localPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/read ')) {
|
||
const relativePath = prompt.slice('/read '.length).trim();
|
||
if (!relativePath) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '格式:/read game/index.html' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(relativePath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '文件路径必须是项目内相对路径。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectFileReadChat(relativePath);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/tasks') {
|
||
void executeProjectTasks(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agents') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentStatusCardsForChat(
|
||
agentStatusCards,
|
||
llmConfigStatus,
|
||
),
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-conversations') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const drafts = agentConversationReadDraftsFromManifest(manifest);
|
||
const firstDraft = drafts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentConversationReadDrafts(manifest),
|
||
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
|
||
draftCommandLabel: firstDraft
|
||
? `读取${firstDraft.task.title}对话`
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-memories') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const drafts = agentMemoryReadDraftsFromManifest(manifest);
|
||
const firstDraft = drafts[0] ?? null;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentMemoryReadDrafts(manifest),
|
||
draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined,
|
||
draftCommandLabel: firstDraft
|
||
? `读取${firstDraft.task.title}记忆`
|
||
: undefined,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/trace' || prompt === '/loop') {
|
||
void executeAgentTraceChat();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-status') {
|
||
void executeAgentRunControl('status', undefined, true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-kill') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'agent.kill' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备标记最近 run 为 killed。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-retry') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'agent.retry' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备重新运行最近 run 的目标。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/agent-resume' || prompt.startsWith('/agent-resume ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'agent.resume',
|
||
detail: prompt.slice('/agent-resume'.length).trim(),
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备恢复最近 run。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/history') {
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
void loadProjectConversation(nextProjectPath, false, 'replace');
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/commands' || prompt === '/limited-commands') {
|
||
void executeLimitedCommandList();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/smoke') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'command.run_limited',
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行静态入口自检。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/run') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'game.run_local' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备运行当前本地游戏。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/export') {
|
||
queueProjectExportPackageShortcut();
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/exports') {
|
||
void executeProjectExportPackages(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.start' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备启动本地预览。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/open-preview') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'preview.open' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备打开当前本地预览。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview-status') {
|
||
void executePreviewStatus(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/preview-stop') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
void executePreviewStop(true);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/memory' || prompt.startsWith('/memory ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const scope = parseOptionalMemoryScope(prompt.slice('/memory'.length));
|
||
if (!scope) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/memory [short|long|blackboard]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
void executeMemoryReadChat(scope);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/remember ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const { scope, content } = parseRememberInput(
|
||
prompt.slice('/remember '.length),
|
||
);
|
||
if (!content) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供要追加的记忆内容。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'memory.write',
|
||
scope,
|
||
content,
|
||
mode: 'append',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备追加${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/memory-set ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const { scope, content } = parseRememberInput(
|
||
prompt.slice('/memory-set '.length),
|
||
);
|
||
if (!content) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供要保存的记忆内容。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'memory.write',
|
||
scope,
|
||
content,
|
||
mode: 'replace',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备覆盖保存${memoryScopeLabel(scope)}记忆。`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt === '/forget-memory' || prompt.startsWith('/forget-memory ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const scope = parseOptionalMemoryScope(
|
||
prompt.slice('/forget-memory'.length),
|
||
);
|
||
if (!scope) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/forget-memory [short|long|blackboard]',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'memory.delete', scope });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备删除${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/canvas ')) {
|
||
const canvasProjectId = prompt.slice('/canvas '.length).trim();
|
||
if (!canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供画板项目 ID。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'canvas.project_open', canvasProjectId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备打开画板项目:${canvasProjectId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/sync-canvas-project ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const canvasProjectId = prompt
|
||
.slice('/sync-canvas-project '.length)
|
||
.trim();
|
||
if (!canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供画板项目 ID。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: 'canvas.project_sync', canvasProjectId });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备同步画板项目资源:${canvasProjectId}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/generate-art ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const generationPrompt = prompt.slice('/generate-art '.length).trim();
|
||
if (!generationPrompt) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请提供美术生成提示词。' },
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.asset_generate',
|
||
prompt: generationPrompt,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备生成首版美术素材。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/import-canvas-asset ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [localPath, canvasProjectId, canvasAssetId, kind, mediaType] =
|
||
prompt.slice('/import-canvas-asset '.length).trim().split(/\s+/);
|
||
if (!localPath || !canvasProjectId || !canvasAssetId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '画板资产路径必须是项目内相对路径。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
const canvasAssetObjectId = canvasAssetId.startsWith('object:')
|
||
? canvasAssetId.slice('object:'.length).trim()
|
||
: '';
|
||
if (canvasAssetId.startsWith('object:') && !canvasAssetObjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '请提供 object: 后面的资产对象 ID。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.asset_import',
|
||
localPath,
|
||
canvasProjectId,
|
||
canvasAssetId: canvasAssetObjectId ? '' : canvasAssetId,
|
||
canvasAssetObjectId,
|
||
kind: kind ?? 'asset',
|
||
mediaType: mediaType ?? 'application/octet-stream',
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备导入画板资产:${localPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/import-canvas-export ')) {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
const [exportPath, canvasProjectId] = prompt
|
||
.slice('/import-canvas-export '.length)
|
||
.trim()
|
||
.split(/\s+/);
|
||
if (!exportPath || !canvasProjectId) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '画板项目 ID 不能包含控制字符。' },
|
||
]);
|
||
return;
|
||
}
|
||
if (
|
||
!isAbsoluteProjectPath(exportPath) ||
|
||
projectPathHasControlCharacter(exportPath)
|
||
) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: '画板导出 ZIP 路径必须是绝对路径。',
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
queuePendingCommand({
|
||
id: 'canvas.export_import',
|
||
exportPath,
|
||
canvasProjectId,
|
||
});
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备导入画板导出包:${exportPath}` },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (prompt.startsWith('/')) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `未知命令:${prompt}。输入 /help 查看可用命令。`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
|
||
void executeChatAgentReply(prompt);
|
||
}
|
||
|
||
async function executeLlmConfigStatus() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
setLlmConfigStatus(status);
|
||
setCommandLog((current) => [...current, 'llm.config_check']);
|
||
const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine);
|
||
const summary = status.configured
|
||
? `LLM 已配置:${formatLlmRouteEndpoint(status)}。`
|
||
: `LLM 未就绪:${status.error ?? '配置不完整'}。${
|
||
status.reasoningEffort ? `推理 ${status.reasoningEffort},` : ''
|
||
}联网检索 ${status.webSearchEnabled ? '开启' : '关闭'},API Key:${
|
||
status.apiKeyPresent ? '已读取' : '未读取'
|
||
}。`;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [summary, ...agentLines].join('\n'),
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeLlmRouteSummary() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const status = await invoke<GameCreatorLlmConfigStatus>(
|
||
'check_game_creator_llm_config',
|
||
);
|
||
setLlmConfigStatus(status);
|
||
setCommandLog((current) => [...current, 'llm.route_summary']);
|
||
const summary = summarizeAgentLlmRoutes(status);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summary.text,
|
||
draftCommand: summary.draftCommand,
|
||
draftCommandLabel: summary.draftCommandLabel,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setLlmConfigStatus(null);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeAgentCapabilitiesChat() {
|
||
const invoke = resolveTauriInvoke();
|
||
let capabilities: readonly GameCreationAgentCapabilityDescriptor[] =
|
||
GAME_CREATION_AGENT_CAPABILITIES;
|
||
if (invoke) {
|
||
try {
|
||
const nativeCapabilities = await invoke<
|
||
GameCreationAgentCapabilityDescriptor[]
|
||
>('get_game_creation_agent_capabilities');
|
||
if (nativeCapabilities.length > 0) {
|
||
capabilities = nativeCapabilities;
|
||
}
|
||
} catch {
|
||
capabilities = GAME_CREATION_AGENT_CAPABILITIES;
|
||
}
|
||
}
|
||
setCommandLog((current) => [...current, 'agent.capabilities']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeAgentCapabilities(capabilities) },
|
||
]);
|
||
}
|
||
|
||
async function resolveProjectSupervisorSessionForSubmission(
|
||
invoke: TauriInvoke,
|
||
nextProjectPath: string,
|
||
) {
|
||
const currentSessionId = projectSupervisorSessionIdRef.current;
|
||
if (currentSessionId) {
|
||
return currentSessionId;
|
||
}
|
||
const sessionId = await ensureProjectSupervisorActiveSessionId(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
if (!sessionId) {
|
||
throw new Error('项目总控 Agent active Session 不可用');
|
||
}
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return null;
|
||
}
|
||
projectSupervisorSessionIdRef.current = sessionId;
|
||
setProjectSupervisorSessionId(sessionId);
|
||
return sessionId;
|
||
}
|
||
|
||
async function executeChatAgentReply(prompt: string) {
|
||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const sessionId = await resolveProjectSupervisorSessionForSubmission(
|
||
invoke,
|
||
nextProjectPath,
|
||
);
|
||
if (!sessionId || localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const submissionRunProfile =
|
||
supervisorChatOnly && !gameChatOnly
|
||
? 'standard'
|
||
: 'autonomous-game-build';
|
||
const runtimeAtSubmission = projectSupervisorRuntimeRef.current;
|
||
const steerRuntime = gameChatOnly
|
||
? matchingAgentRuntimeForSteer(
|
||
[runtimeAtSubmission],
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
submissionRunProfile,
|
||
'project-supervisor-game-chat',
|
||
)
|
||
: null;
|
||
let autoPreviewAfterRevision = 0;
|
||
let autoPreviewAfterValidatedAt = 0;
|
||
if (steerRuntime) {
|
||
const playableAtSubmission = latestGameChatPlayableRevision(
|
||
runtimeAtSubmission,
|
||
agentRuntimeByIdRef.current,
|
||
);
|
||
autoPreviewAfterRevision = Math.max(
|
||
gameChatPreviewRevisionRef.current ?? 0,
|
||
playableAtSubmission?.revision ?? 0,
|
||
);
|
||
autoPreviewAfterValidatedAt = playableAtSubmission?.validatedAt ?? 0;
|
||
try {
|
||
const revisionStatus = await invoke<LocalGameProjectRevisionStatus>(
|
||
'get_local_game_project_revision',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
Number.isSafeInteger(revisionStatus.revision) &&
|
||
revisionStatus.revision >= 0
|
||
) {
|
||
autoPreviewAfterRevision = Math.max(
|
||
autoPreviewAfterRevision,
|
||
revisionStatus.revision,
|
||
);
|
||
}
|
||
} catch {
|
||
// The durable evidence cursor still prevents consuming an older validation.
|
||
}
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
const submission = await submitProjectSupervisorRuntimeTask({
|
||
invoke,
|
||
projectPath: nextProjectPath,
|
||
sessionId,
|
||
prompt,
|
||
runtime: runtimeAtSubmission,
|
||
runProfile: submissionRunProfile,
|
||
...(gameChatOnly ? { source: 'project-supervisor-game-chat' } : {}),
|
||
});
|
||
const runtimeResult = submission.runtimeResult;
|
||
const acceptedRunId = submission.acceptedRunId.trim();
|
||
if (!acceptedRunId) {
|
||
throw new Error('项目总控 Agent 请求未进入后台队列');
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${submission.mode} project-supervisor`,
|
||
]);
|
||
const runtime = agentRuntimeStateFromResult(
|
||
runtimeResult,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
runtime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
runtime.sessionId !== sessionId
|
||
) {
|
||
throw new Error('项目总控 Agent Runtime Session 身份不匹配');
|
||
}
|
||
const acceptedRuntimeReady = runtime.runId === acceptedRunId;
|
||
if (!acceptedRuntimeReady) {
|
||
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
|
||
setProjectSupervisorExpectedRunId(acceptedRunId);
|
||
}
|
||
if (gameChatOnly) {
|
||
gameChatObservedRunKeysRef.current.add(
|
||
`${nextProjectPath}\n${acceptedRunId}`,
|
||
);
|
||
setGameChatAutoPreviewAuthorization({
|
||
afterRevision:
|
||
submission.mode === 'steer' ? autoPreviewAfterRevision : 0,
|
||
afterValidatedAt:
|
||
submission.mode === 'steer' ? autoPreviewAfterValidatedAt : 0,
|
||
authorizationId: createAgentChatRunId('game-chat-preview-auth'),
|
||
projectPath: nextProjectPath,
|
||
runId: acceptedRunId,
|
||
});
|
||
}
|
||
if (acceptedRuntimeReady) {
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
updateProjectSupervisorRuntime(runtime);
|
||
updateProjectSupervisorResponseStream(
|
||
runtimeResult.responseStream,
|
||
runtime,
|
||
);
|
||
}
|
||
setProjectSupervisorRuntimeError('');
|
||
const refreshConversation =
|
||
projectSupervisorRefreshConversationRef.current;
|
||
if (refreshConversation) {
|
||
void refreshConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
sessionId,
|
||
acceptedRunId,
|
||
).catch((error) => {
|
||
if (
|
||
localProjectPathRef.current === nextProjectPath &&
|
||
projectSupervisorSessionIdRef.current === sessionId
|
||
) {
|
||
setProjectSupervisorRuntimeError(
|
||
`项目总控 Agent 对话刷新失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
if (acceptedRuntimeReady) {
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
runtime,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: message,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
executeChatAgentReplyRef.current = executeChatAgentReply;
|
||
|
||
useEffect(() => {
|
||
const latch = initialSupervisorMessageLatchRef.current;
|
||
if (!gameChatOnly || !latch.prompt || !localProject) {
|
||
return;
|
||
}
|
||
if (localProject.projectPath !== latch.projectPath) {
|
||
claimInitialSupervisorMessageForPage(latch.projectPath);
|
||
return;
|
||
}
|
||
if (
|
||
chatAgentBusy ||
|
||
!claimInitialSupervisorMessageForPage(latch.projectPath)
|
||
) {
|
||
return;
|
||
}
|
||
supervisorChatShouldFollowLatestRef.current = true;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: latch.prompt,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
void executeChatAgentReplyRef.current(latch.prompt);
|
||
}, [chatAgentBusy, gameChatOnly, initialSupervisorMessage, localProject]);
|
||
|
||
async function handleProjectSupervisorToolAction(
|
||
decision: 'confirm' | 'reject',
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const runtime = projectSupervisorRuntimeRef.current;
|
||
const pendingAction = runtime?.pendingToolAction;
|
||
const sessionId = projectSupervisorSessionIdRef.current;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!runtime ||
|
||
!pendingAction?.actionId ||
|
||
!sessionId ||
|
||
chatAgentBusy
|
||
) {
|
||
return;
|
||
}
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const command =
|
||
decision === 'confirm'
|
||
? 'confirm_game_creator_agent_runtime_task'
|
||
: 'reject_game_creator_agent_runtime_task';
|
||
const result = await invoke<AgentRuntimeResult>(command, {
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: runtime.runId,
|
||
actionId: pendingAction.actionId,
|
||
note:
|
||
decision === 'confirm'
|
||
? '用户已确认待执行工具动作'
|
||
: '用户已拒绝待执行工具动作',
|
||
});
|
||
const nextRuntime = agentRuntimeStateFromResult(
|
||
result,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId ||
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== runtime.runId
|
||
) {
|
||
return;
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${decision} project-supervisor`,
|
||
]);
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(message);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectProfessionalAgentToolAction(
|
||
runtime: AgentRuntimeState,
|
||
decision: 'confirm' | 'reject',
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = agentRuntimeById[runtime.agentId];
|
||
const pendingAction = currentRuntime?.pendingToolAction;
|
||
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!currentRuntime ||
|
||
!pendingAction?.actionId ||
|
||
chatAgentBusy
|
||
) {
|
||
throw new Error('专业 Agent 待确认动作已变化,请等待状态刷新');
|
||
}
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
pendingAction.actionId !== runtime.pendingToolAction?.actionId ||
|
||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
currentRuntime.parentRunId !== supervisorRunId
|
||
) {
|
||
throw new Error('专业 Agent Runtime 身份已变化,请等待状态刷新');
|
||
}
|
||
setChatAgentBusy(true);
|
||
try {
|
||
const command =
|
||
decision === 'confirm'
|
||
? 'confirm_game_creator_agent_runtime_task'
|
||
: 'reject_game_creator_agent_runtime_task';
|
||
const result = await invoke<AgentRuntimeResult>(command, {
|
||
projectPath: nextProjectPath,
|
||
agentId: currentRuntime.agentId,
|
||
runId: currentRuntime.runId,
|
||
actionId: pendingAction.actionId,
|
||
note:
|
||
decision === 'confirm'
|
||
? '用户已确认专业 Agent 待执行工具动作'
|
||
: '用户已拒绝专业 Agent 待执行工具动作',
|
||
});
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return;
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== currentRuntime.agentId ||
|
||
nextRuntime.runId !== currentRuntime.runId
|
||
) {
|
||
throw new Error('专业 Agent 操作后 Runtime 身份不匹配');
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.${decision} ${currentRuntime.agentId}`,
|
||
]);
|
||
} catch (error) {
|
||
throw new Error(
|
||
`专业 Agent 操作失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectProfessionalAgentRetry(
|
||
runtime: AgentRuntimeState,
|
||
): Promise<string> {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = agentRuntimeById[runtime.agentId];
|
||
const supervisorRunId = projectSupervisorRuntimeRef.current?.runId;
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
|
||
throw new Error('专业 Agent 状态已变化,请等待刷新后重试');
|
||
}
|
||
const supervisorRuntime = projectSupervisorRuntimeRef.current;
|
||
if (!supervisorRuntime || isAgentRuntimeTerminalState(supervisorRuntime)) {
|
||
throw new Error('项目总控已停止,请先重试项目总控');
|
||
}
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
currentRuntime.parentRunId !== supervisorRunId ||
|
||
!(
|
||
currentRuntime.status === 'failed' || currentRuntime.phase === 'failed'
|
||
) ||
|
||
currentRuntime.pendingToolAction
|
||
) {
|
||
throw new Error('专业 Agent 当前状态不允许直接重试');
|
||
}
|
||
if (
|
||
projectSupervisorPendingRepairMatchesProfessional(
|
||
projectSupervisorRuntimeRef.current?.pendingToolAction,
|
||
currentRuntime,
|
||
)
|
||
) {
|
||
await handleProjectSupervisorToolAction('confirm');
|
||
return `已确认项目总控安排${projectProfessionalAgentLabel(
|
||
currentRuntime.agentId,
|
||
)}返工,正在同步新一轮状态`;
|
||
}
|
||
setChatAgentBusy(true);
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'confirm_retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: currentRuntime.agentId,
|
||
runId: currentRuntime.runId,
|
||
nextRunId: createAgentChatRunId('project-professional-retry'),
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== supervisorRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的重试状态合并到当前界面';
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== currentRuntime.agentId ||
|
||
nextRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.parentRunId !== supervisorRunId
|
||
) {
|
||
throw new Error('专业 Agent 重试后的 Runtime 身份不匹配');
|
||
}
|
||
rememberAgentRuntimeState(nextRuntime);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`agent.runtime.retry ${currentRuntime.agentId}`,
|
||
]);
|
||
return '重试请求已受理,正在同步新一轮状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`专业 Agent 重试失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectSupervisorRetry(
|
||
runtime: AgentRuntimeState,
|
||
): Promise<string> {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) {
|
||
throw new Error('项目总控状态已变化,请等待刷新后重试');
|
||
}
|
||
const needsReconciliation =
|
||
currentRuntime.status === 'needs-reconciliation' ||
|
||
currentRuntime.phase === 'needs-reconciliation';
|
||
if (needsReconciliation) {
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID
|
||
) {
|
||
throw new Error('项目总控待核对任务已变化,请等待刷新');
|
||
}
|
||
const previousRunId = currentRuntime.runId;
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'cancel_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: previousRunId,
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的取消状态合并到当前界面';
|
||
}
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.runId !== previousRunId ||
|
||
nextRuntime.sessionId !== currentRuntime.sessionId
|
||
) {
|
||
throw new Error('项目总控取消后的 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.cancel project-supervisor reconciliation',
|
||
]);
|
||
const queuePending = nextRuntime.taskQueue?.pending ?? 0;
|
||
if (
|
||
nextRuntime.status === 'cancelled' ||
|
||
nextRuntime.phase === 'cancelled'
|
||
) {
|
||
return queuePending > 0
|
||
? `旧任务已结束,队列中还有 ${queuePending} 个待处理任务,队列将继续处理`
|
||
: '旧任务已结束,当前队列为空,可重新启动项目总控';
|
||
}
|
||
return '已提交结束旧任务请求,正在同步取消状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`项目总控旧任务结束失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
const cancelledWithEmptyQueue =
|
||
(currentRuntime.status === 'cancelled' ||
|
||
currentRuntime.phase === 'cancelled') &&
|
||
(currentRuntime.taskQueue?.pending ?? 0) === 0;
|
||
if (
|
||
currentRuntime.runId !== runtime.runId ||
|
||
currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
!(
|
||
currentRuntime.status === 'failed' ||
|
||
currentRuntime.phase === 'failed' ||
|
||
cancelledWithEmptyQueue
|
||
) ||
|
||
currentRuntime.pendingToolAction
|
||
) {
|
||
throw new Error('项目总控当前状态不允许重试');
|
||
}
|
||
const previousRunId = currentRuntime.runId;
|
||
const nextRunId = createAgentChatRunId('project-supervisor-retry');
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'confirm_retry_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: previousRunId,
|
||
nextRunId,
|
||
},
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorRuntimeRef.current?.runId !== previousRunId
|
||
) {
|
||
return '项目已切换,未把旧项目的重试状态合并到当前界面';
|
||
}
|
||
const inferredRunId = agentRuntimeStartedRunId(result, nextRunId);
|
||
const acceptedRunId =
|
||
result.acceptedRunId?.trim() ||
|
||
(inferredRunId === nextRunId ? nextRunId : '');
|
||
if (!acceptedRunId) {
|
||
throw new Error('项目总控重试请求未进入后台队列');
|
||
}
|
||
projectSupervisorExpectedRunIdRef.current = acceptedRunId;
|
||
setProjectSupervisorExpectedRunId(acceptedRunId);
|
||
const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime);
|
||
if (nextRuntime.runId === acceptedRunId) {
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.sessionId !== currentRuntime.sessionId
|
||
) {
|
||
throw new Error('项目总控重试后的 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
result.responseStream,
|
||
nextRuntime,
|
||
);
|
||
projectSupervisorExpectedRunIdRef.current = null;
|
||
setProjectSupervisorExpectedRunId(null);
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.retry project-supervisor',
|
||
]);
|
||
return nextRuntime.runId === acceptedRunId
|
||
? '已在当前项目重新启动项目总控'
|
||
: '重试已受理,正在同步新一轮项目总控状态';
|
||
} catch (error) {
|
||
throw new Error(
|
||
`项目总控重试失败:${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`,
|
||
);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function handleProjectSupervisorUserInput(
|
||
request: AgentRuntimeUserInputRequest,
|
||
responseId: string,
|
||
answers: Record<string, string>,
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
const runtime = projectSupervisorRuntimeRef.current;
|
||
const sessionId = projectSupervisorSessionIdRef.current;
|
||
const currentRequest = runtime?.userInputRequest;
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
!runtime ||
|
||
!sessionId ||
|
||
!responseId ||
|
||
chatAgentBusy
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
request.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
request.sessionId !== sessionId ||
|
||
request.runId !== runtime.runId ||
|
||
currentRequest?.requestId !== request.requestId ||
|
||
currentRequest?.actionId !== request.actionId
|
||
) {
|
||
setProjectSupervisorRuntimeError('待回答请求已变更,请刷新 Runtime 状态');
|
||
return;
|
||
}
|
||
setChatAgentBusy(true);
|
||
setProjectSupervisorRuntimeError('');
|
||
try {
|
||
const result = await invoke<AgentRuntimeResult>(
|
||
'answer_game_creator_agent_runtime_user_input',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
runId: request.runId,
|
||
actionId: request.actionId,
|
||
requestId: request.requestId,
|
||
responseId,
|
||
answers,
|
||
},
|
||
);
|
||
const nextRuntime = agentRuntimeStateFromResult(
|
||
result,
|
||
projectSupervisorRuntimeRef.current,
|
||
);
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
if (
|
||
nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
nextRuntime.sessionId !== sessionId ||
|
||
nextRuntime.runId !== request.runId
|
||
) {
|
||
throw new Error('项目总控 Agent 回答后 Runtime 身份不匹配');
|
||
}
|
||
updateProjectSupervisorRuntime(nextRuntime);
|
||
updateProjectSupervisorResponseStream(result.responseStream, nextRuntime);
|
||
const refreshConversation =
|
||
projectSupervisorRefreshConversationRef.current;
|
||
if (refreshConversation) {
|
||
await refreshConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
sessionId,
|
||
request.runId,
|
||
);
|
||
}
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.runtime.user_input.answered project-supervisor',
|
||
]);
|
||
setProjectSupervisorRuntimeError('');
|
||
syncTerminalProjectSupervisorConversation(
|
||
invoke,
|
||
nextProjectPath,
|
||
nextRuntime,
|
||
);
|
||
} catch (error) {
|
||
if (
|
||
localProjectPathRef.current !== nextProjectPath ||
|
||
projectSupervisorSessionIdRef.current !== sessionId
|
||
) {
|
||
return;
|
||
}
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setProjectSupervisorRuntimeError(`回答提交失败:${message}`);
|
||
} finally {
|
||
setChatAgentBusy(false);
|
||
}
|
||
}
|
||
|
||
async function executeGameDraft(prompt: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setCommandLog((current) => [...current, 'game.generate_draft']);
|
||
setProjectStatus('正在调用 LLM');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: gameDraftStartedMessage() },
|
||
]);
|
||
const result = await invoke<GenerateLocalGameDraftResult>(
|
||
'generate_local_game_draft',
|
||
{ projectPath: nextProjectPath, prompt },
|
||
);
|
||
const generatedProjectPath = result.projectPath.trim();
|
||
if (
|
||
!generatedProjectPath ||
|
||
!isAbsoluteProjectPath(generatedProjectPath) ||
|
||
projectPathHasControlCharacter(generatedProjectPath)
|
||
) {
|
||
const message = '生成结果项目路径无效';
|
||
setProjectStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
return;
|
||
}
|
||
setLocalProject({
|
||
projectPath: generatedProjectPath,
|
||
manifestPath: `${generatedProjectPath}/.agent/manifest.json`,
|
||
manifest: result.manifest,
|
||
});
|
||
setManifest(result.manifest);
|
||
setProjectStatus('已生成可试玩原型');
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'memory.write',
|
||
'file.write game/index.html',
|
||
]);
|
||
const completionSummary =
|
||
await refreshAgentRunTrace(generatedProjectPath);
|
||
|
||
try {
|
||
const previewResult = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: generatedProjectPath },
|
||
);
|
||
updateClientPreview(previewResult);
|
||
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
|
||
setCommandLog((current) => [...current, 'preview.start']);
|
||
void refreshManifest(generatedProjectPath);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
`已保存并在客户端运行视图启动预览:${previewResult.url}`,
|
||
completionSummary?.text,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n\n'),
|
||
draftCommand: completionSummary?.draftCommand,
|
||
},
|
||
]);
|
||
} catch (previewError) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
`已保存草案:${result.designPath}。预览启动失败:${
|
||
previewError instanceof Error
|
||
? previewError.message
|
||
: String(previewError)
|
||
}`,
|
||
completionSummary?.text,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n\n'),
|
||
draftCommand: completionSummary?.draftCommand,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
void refreshAgentRunTrace(nextProjectPath);
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: message,
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function markPendingCommandCanceled(command: PendingCommand) {
|
||
if (command.id === 'project.create') {
|
||
setWorkspaceStatus('已取消');
|
||
setProjectStatus('已取消');
|
||
return;
|
||
}
|
||
if (command.id === 'game.generate_draft') {
|
||
setProjectStatus('已取消生成游戏草案');
|
||
return;
|
||
}
|
||
if (
|
||
command.id === 'game.run_local' ||
|
||
command.id === 'command.run_limited'
|
||
) {
|
||
setLimitedCommandStatus('已取消运行');
|
||
return;
|
||
}
|
||
if (command.id === 'project.checkpoint') {
|
||
setFileStatus('已取消保存 checkpoint');
|
||
return;
|
||
}
|
||
if (command.id === 'project.export_package') {
|
||
setFileStatus('已取消导出本地试玩包');
|
||
return;
|
||
}
|
||
if (command.id === 'project.index') {
|
||
setFileStatus('已取消索引项目');
|
||
setWorkspaceStatus('已取消索引项目');
|
||
return;
|
||
}
|
||
if (command.id === 'project.restore') {
|
||
setFileStatus('已取消回滚项目');
|
||
return;
|
||
}
|
||
if (command.id === 'project.policy_write') {
|
||
setProjectStatus('已取消修改权限策略');
|
||
return;
|
||
}
|
||
if (command.id === 'preview.start' || command.id === 'preview.open') {
|
||
setPreviewStatus('已取消预览操作');
|
||
setWorkspaceStatus('已取消预览操作');
|
||
return;
|
||
}
|
||
if (
|
||
command.id === 'agent.kill' ||
|
||
command.id === 'agent.retry' ||
|
||
command.id === 'agent.resume'
|
||
) {
|
||
setAgentRunStatus('已取消 Agent run 操作');
|
||
return;
|
||
}
|
||
if (command.id === 'memory.write') {
|
||
setMemoryStatus('已取消写入项目记忆');
|
||
return;
|
||
}
|
||
if (command.id === 'memory.delete') {
|
||
setMemoryStatus('已取消删除项目记忆');
|
||
return;
|
||
}
|
||
if (command.id === 'asset.upload') {
|
||
setAssetStatus('已取消上传资产');
|
||
return;
|
||
}
|
||
if (command.id === 'asset.register') {
|
||
setAssetStatus('已取消登记资产');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.project_open') {
|
||
setAssetStatus('已取消打开画板');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.project_sync') {
|
||
setAssetStatus('已取消同步画板项目');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.asset_import') {
|
||
setAssetStatus('已取消导入画板资产');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.asset_generate') {
|
||
setAssetStatus('已取消生成美术素材');
|
||
return;
|
||
}
|
||
if (command.id === 'canvas.export_import') {
|
||
setAssetStatus('已取消导入画板导出包');
|
||
}
|
||
}
|
||
|
||
function handlePendingCommandCancel() {
|
||
const command = pendingCommand;
|
||
setPendingCommand(null);
|
||
if (command) {
|
||
markPendingCommandCanceled(command);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.cancel',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${command.id}`,
|
||
]);
|
||
}
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '已取消。' },
|
||
]);
|
||
}
|
||
|
||
async function handlePendingCommandConfirm() {
|
||
const command = pendingCommand;
|
||
if (!command) {
|
||
return;
|
||
}
|
||
if (
|
||
needsInitializedChatProject(command.id) &&
|
||
!resolveChatProjectPath(localProject)
|
||
) {
|
||
setPendingCommand(null);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.cancel',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.cancel ${command.id} missing-project`,
|
||
]);
|
||
requireChatProjectForUserAction();
|
||
return;
|
||
}
|
||
if (
|
||
await denyPendingCommandIfNeeded(
|
||
command.id,
|
||
resolvePermissionLogProjectPath(command),
|
||
)
|
||
) {
|
||
setPendingCommand(null);
|
||
return;
|
||
}
|
||
setPendingCommand(null);
|
||
appendLocalPermissionLog(
|
||
resolvePermissionLogProjectPath(command),
|
||
'permission.confirm',
|
||
command.id,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`permission.confirm ${command.id}`,
|
||
]);
|
||
if (command.id === 'asset.upload') {
|
||
void executeAssetUpload(command.file);
|
||
} else if (command.id === 'asset.register') {
|
||
void executeAssetRegister(
|
||
resolveChatProjectPath(localProject) ?? '',
|
||
command.localPath,
|
||
command.kind,
|
||
command.mediaType,
|
||
'generated',
|
||
'',
|
||
'',
|
||
'',
|
||
true,
|
||
);
|
||
} else if (command.id === 'game.run_local') {
|
||
void executeRunLocal(true);
|
||
} else if (command.id === 'command.run_limited') {
|
||
void executeLimitedCommand(command.commandId, true);
|
||
} else if (command.id === 'project.create') {
|
||
void executeProjectCreate(command.projectPath, true);
|
||
} else if (command.id === 'project.checkpoint') {
|
||
void executeProjectCheckpoint(true);
|
||
} else if (command.id === 'project.export_package') {
|
||
void executeProjectExportPackage(true);
|
||
} else if (command.id === 'project.index') {
|
||
void executeProjectIndex(true);
|
||
} else if (command.id === 'project.restore') {
|
||
void executeProjectRestore(command.checkpointId, true);
|
||
} else if (command.id === 'project.policy_write') {
|
||
void executeProjectPolicyWrite(command.policy, true);
|
||
} else if (command.id === 'preview.start') {
|
||
void executePreviewStart(true);
|
||
} else if (command.id === 'preview.open') {
|
||
void executePreviewOpen(true);
|
||
} else if (
|
||
command.id === 'agent.kill' ||
|
||
command.id === 'agent.retry' ||
|
||
command.id === 'agent.resume'
|
||
) {
|
||
void executeAgentRunControl(
|
||
command.id.slice('agent.'.length),
|
||
command.detail,
|
||
true,
|
||
);
|
||
} else if (command.id === 'memory.write') {
|
||
void executeMemoryWrite(
|
||
command.scope,
|
||
command.content,
|
||
command.mode ?? 'append',
|
||
);
|
||
} else if (command.id === 'memory.delete') {
|
||
void executeMemoryDeleteChat(command.scope);
|
||
} else if (command.id === 'canvas.project_open') {
|
||
void executeCanvasProjectOpen(command.canvasProjectId, true);
|
||
} else if (command.id === 'canvas.project_sync') {
|
||
void executeCanvasProjectSync(command.canvasProjectId, true);
|
||
} else if (command.id === 'canvas.asset_import') {
|
||
void executeCanvasAssetImport(
|
||
null,
|
||
command.localPath,
|
||
command.kind,
|
||
command.mediaType,
|
||
command.canvasProjectId,
|
||
command.canvasAssetId,
|
||
command.canvasAssetObjectId ?? '',
|
||
true,
|
||
);
|
||
} else if (command.id === 'canvas.asset_generate') {
|
||
void executeCanvasAssetGenerate(command.prompt, true);
|
||
} else if (command.id === 'canvas.export_import') {
|
||
void executeCanvasExportImport(
|
||
command.exportPath,
|
||
command.canvasProjectId,
|
||
true,
|
||
);
|
||
} else if (command.id === 'game.generate_draft') {
|
||
void executeGameDraft(command.prompt);
|
||
}
|
||
}
|
||
|
||
async function executeProjectCreate(
|
||
nextProjectPath: string,
|
||
announceToChat: boolean,
|
||
expectedGameChatSelectionVersion?: number,
|
||
) {
|
||
const trimmedProjectPath = nextProjectPath.trim();
|
||
const invoke = resolveTauriInvoke();
|
||
if (
|
||
invoke &&
|
||
trimmedProjectPath &&
|
||
isAbsoluteProjectPath(trimmedProjectPath) &&
|
||
!projectPathHasControlCharacter(trimmedProjectPath)
|
||
) {
|
||
try {
|
||
const nonEmpty = await invoke<boolean>(
|
||
'is_local_project_directory_non_empty',
|
||
{ projectPath: trimmedProjectPath },
|
||
);
|
||
if (
|
||
expectedGameChatSelectionVersion !== undefined &&
|
||
gameChatProjectSelectionVersionRef.current !==
|
||
expectedGameChatSelectionVersion
|
||
) {
|
||
return;
|
||
}
|
||
if (nonEmpty) {
|
||
setPendingNonEmptyProjectCreate({
|
||
projectPath: trimmedProjectPath,
|
||
announceToChat,
|
||
});
|
||
setWorkspaceStatus('目标文件夹不是空的');
|
||
setProjectStatus('等待确认');
|
||
return;
|
||
}
|
||
} catch {
|
||
// ponytail: stale test doubles and older shells may miss this helper; init still validates.
|
||
}
|
||
}
|
||
if (
|
||
expectedGameChatSelectionVersion !== undefined &&
|
||
gameChatProjectSelectionVersionRef.current !==
|
||
expectedGameChatSelectionVersion
|
||
) {
|
||
return;
|
||
}
|
||
await openWorkspace(nextProjectPath, announceToChat);
|
||
}
|
||
|
||
function cancelProjectCreateInNonEmptyFolder() {
|
||
const pendingCreate = pendingNonEmptyProjectCreate;
|
||
setPendingNonEmptyProjectCreate(null);
|
||
setWorkspaceStatus('已取消');
|
||
setProjectStatus('已取消');
|
||
if (pendingCreate?.announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已取消在非空文件夹中新建项目:${pendingCreate.projectPath}`,
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
function confirmProjectCreateInNonEmptyFolder() {
|
||
const pendingCreate = pendingNonEmptyProjectCreate;
|
||
if (!pendingCreate) {
|
||
return;
|
||
}
|
||
setPendingNonEmptyProjectCreate(null);
|
||
void openWorkspace(pendingCreate.projectPath, pendingCreate.announceToChat);
|
||
}
|
||
|
||
async function executeProjectStatus(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.status',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的项目状态`,
|
||
'准备读取项目状态。',
|
||
() => void executeProjectStatus(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'project.status' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setProjectStatus('已读取状态');
|
||
setCommandLog((current) => [...current, 'project.status']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectStatus(nextManifest, nextProjectPath),
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectIndex(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectIndexResult>(
|
||
'build_local_project_index',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已索引 ${result.fileCount} 个文件`);
|
||
setCommandLog((current) => [...current, 'project.index']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectIndex(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function queueOrExecuteProjectIndex() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
void executeProjectIndex(true);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (result.policy.confirmCommands.includes('project.index')) {
|
||
queuePendingCommand({ id: 'project.index' });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '准备刷新本地项目索引。' },
|
||
]);
|
||
return;
|
||
}
|
||
void executeProjectIndex(true);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectCheckpoint(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectCheckpointResult>(
|
||
'create_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已保存 checkpoint:${result.checkpointId}`);
|
||
setCommandLog((current) => [...current, 'project.checkpoint']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectCheckpoint(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectExportPackage(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectExportPackageResult>(
|
||
'export_local_project_package',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已导出本地试玩包:${result.packageRelativePath}`);
|
||
setCommandLog((current) => [...current, 'project.export_package']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectExportPackage(result),
|
||
draftCommand: '/open-project',
|
||
draftCommandLabel: '显示目录',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectExportPackages(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.export_list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的本地试玩包`,
|
||
'准备列出本地试玩包。',
|
||
() => void executeProjectExportPackages(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectExportPackagesResult>(
|
||
'list_local_project_export_packages',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setFileStatus(`已列出 ${result.packages.length} 个本地试玩包`);
|
||
setCommandLog((current) => [...current, 'project.export_list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectExportPackages(result),
|
||
draftCommand:
|
||
result.packages.length > 0 ? '/open-project' : '/export',
|
||
draftCommandLabel:
|
||
result.packages.length > 0 ? '显示目录' : '导出试玩包',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectCheckpoints(
|
||
announceToChat: boolean,
|
||
skipListPolicyConfirm = false,
|
||
skipReadPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipListPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的 checkpoint`,
|
||
'准备列出项目 checkpoint。',
|
||
() => void executeProjectCheckpoints(true, true, false),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const manifestFiles = sortCheckpointManifestFiles(result.files);
|
||
const visibleFiles = manifestFiles.slice(0, 5);
|
||
if (
|
||
announceToChat &&
|
||
visibleFiles.length > 0 &&
|
||
!skipReadPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 checkpoint manifest`,
|
||
'准备读取 checkpoint manifest。',
|
||
() => void executeProjectCheckpoints(true, true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const checkpoints: LocalProjectCheckpointSummary[] = [];
|
||
for (const file of visibleFiles) {
|
||
const manifest = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: file.path,
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
checkpoints.push(checkpointSummaryFromManifest(file, manifest.content));
|
||
}
|
||
setProjectFiles(result.files);
|
||
setProjectCheckpoints(checkpoints);
|
||
setFileStatus(`已列出 ${manifestFiles.length} 个 checkpoint`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'file.list',
|
||
...(visibleFiles.length > 0 ? ['file.read checkpoint manifests'] : []),
|
||
]);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectCheckpoints(
|
||
checkpoints,
|
||
manifestFiles.length - visibleFiles.length,
|
||
),
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectDiff(
|
||
checkpointId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'project.diff',
|
||
nextProjectPath,
|
||
`对比 ${nextProjectPath} 的 checkpoint:${checkpointId}`,
|
||
'准备对比项目 checkpoint。',
|
||
() => void executeProjectDiff(checkpointId, true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectDiffResult>(
|
||
'diff_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath, checkpointId },
|
||
);
|
||
setFileStatus(`已对比 checkpoint:${result.checkpointId}`);
|
||
setCommandLog((current) => [...current, 'project.diff']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectDiff(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectRestore(
|
||
checkpointId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectRestoreResult>(
|
||
'restore_local_project_checkpoint',
|
||
{ projectPath: nextProjectPath, checkpointId },
|
||
);
|
||
setFileStatus(
|
||
`已回滚 ${result.restoredCount} 个文件,删除 ${result.deletedCount} 个新增文件`,
|
||
);
|
||
setCommandLog((current) => [...current, 'project.restore']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已回滚 ${result.restoredCount} 个文件到 ${nextProjectPath},删除 ${result.deletedCount} 个新增文件:${result.checkpointId}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectPolicyRead(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectStatus('已读取权限策略');
|
||
setCommandLog((current) => [...current, 'project.policy_read']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectPolicy(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function queueProjectPolicyMutation(
|
||
commandId: string,
|
||
mode: 'deny' | 'allow' | 'confirm' | 'auto',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const alreadyDenied = result.policy.deniedCommands.includes(commandId);
|
||
const alreadyConfirmed =
|
||
result.policy.confirmCommands.includes(commandId);
|
||
if (mode === 'deny' && alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令已在拒绝列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'allow' && !alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令不在拒绝列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'confirm' && alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令已在确认列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'auto' && !alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `命令不在确认列表中:${commandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
const deniedCommands =
|
||
mode === 'deny'
|
||
? [...new Set([...result.policy.deniedCommands, commandId])]
|
||
: mode === 'allow'
|
||
? result.policy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: mode === 'confirm'
|
||
? result.policy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: result.policy.deniedCommands;
|
||
const confirmCommands =
|
||
mode === 'confirm'
|
||
? [...new Set([...result.policy.confirmCommands, commandId])]
|
||
: mode === 'deny'
|
||
? result.policy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: mode === 'auto'
|
||
? result.policy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: result.policy.confirmCommands;
|
||
queuePendingCommand({
|
||
id: 'project.policy_write',
|
||
policy: { ...result.policy, deniedCommands, confirmCommands },
|
||
});
|
||
const action =
|
||
mode === 'deny'
|
||
? '拒绝'
|
||
: mode === 'allow'
|
||
? '允许'
|
||
: mode === 'confirm'
|
||
? '确认'
|
||
: '自动执行';
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备${action}命令:${commandId}`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function queueAgentPolicyMutation(
|
||
agentId: string,
|
||
commandId: string,
|
||
mode: 'deny' | 'allow' | 'confirm' | 'auto',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const agentPolicies = { ...(result.policy.agentPolicies ?? {}) };
|
||
const agentPolicy = agentPolicies[agentId] ?? {
|
||
deniedCommands: [],
|
||
confirmCommands: [],
|
||
};
|
||
const alreadyDenied = agentPolicy.deniedCommands.includes(commandId);
|
||
const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId);
|
||
if (mode === 'deny' && alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'allow' && !alreadyDenied) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'confirm' && alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
if (mode === 'auto' && !alreadyConfirmed) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`,
|
||
},
|
||
]);
|
||
return;
|
||
}
|
||
const deniedCommands =
|
||
mode === 'deny'
|
||
? [...new Set([...agentPolicy.deniedCommands, commandId])]
|
||
: mode === 'allow'
|
||
? agentPolicy.deniedCommands.filter((value) => value !== commandId)
|
||
: mode === 'confirm'
|
||
? agentPolicy.deniedCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: agentPolicy.deniedCommands;
|
||
const confirmCommands =
|
||
mode === 'confirm'
|
||
? [...new Set([...agentPolicy.confirmCommands, commandId])]
|
||
: mode === 'deny'
|
||
? agentPolicy.confirmCommands.filter((value) => value !== commandId)
|
||
: mode === 'auto'
|
||
? agentPolicy.confirmCommands.filter(
|
||
(value) => value !== commandId,
|
||
)
|
||
: agentPolicy.confirmCommands;
|
||
agentPolicies[agentId] = {
|
||
deniedCommands,
|
||
confirmCommands,
|
||
};
|
||
queuePendingCommand({
|
||
id: 'project.policy_write',
|
||
policy: {
|
||
...result.policy,
|
||
agentPolicies,
|
||
},
|
||
});
|
||
const action =
|
||
mode === 'deny'
|
||
? '拒绝'
|
||
: mode === 'allow'
|
||
? '允许'
|
||
: mode === 'confirm'
|
||
? '确认'
|
||
: '自动执行';
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `准备${action} Agent ${agentId} 命令:${commandId}`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectPolicyWrite(
|
||
policy: ProjectPermissionPolicy,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ProjectPermissionPolicyView>(
|
||
'write_project_permission_policy',
|
||
{ projectPath: nextProjectPath, policy },
|
||
);
|
||
setProjectStatus('已写入权限策略');
|
||
setCommandLog((current) => [...current, 'project.policy_write']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectPolicy(result) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectFiles(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setFileStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目文件`,
|
||
'准备列出项目文件。',
|
||
() => void executeProjectFiles(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectFiles(result.files);
|
||
setFileStatus(`已列出 ${result.files.length} 项`);
|
||
setCommandLog((current) => [...current, 'file.list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectFiles(result.files) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectAssets(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAssetStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'asset.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目资产`,
|
||
'准备列出项目资产。',
|
||
() => void executeProjectAssets(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'asset.list' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setAssetStatus(`已列出 ${nextManifest.assets.length} 个资产`);
|
||
setCommandLog((current) => [...current, 'asset.list']);
|
||
if (announceToChat) {
|
||
const firstReadableAsset = firstReadableProjectAssetPath(nextManifest);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeProjectAssets(nextManifest),
|
||
draftCommand: firstReadableAsset
|
||
? `/read ${firstReadableAsset}`
|
||
: undefined,
|
||
draftCommandLabel: firstReadableAsset ? '读取首个资产' : undefined,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeProjectFileReadChat(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setFileStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const commandId = isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read';
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
commandId,
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
'准备读取项目文件。',
|
||
() => void executeProjectFileReadChat(relativePath, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath, commandId },
|
||
);
|
||
setFilePath(result.path);
|
||
setFileDraft(result.content);
|
||
setFileStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.read']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectFileContent(result) },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setFileStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeProjectTasks(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'task.list',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的任务拆分`,
|
||
'准备读取任务拆分。',
|
||
() => void executeProjectTasks(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'task.list' },
|
||
);
|
||
setManifest(nextManifest);
|
||
setProjectStatus('已读取任务拆分');
|
||
setCommandLog((current) => [...current, 'task.list']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeProjectTasks(nextManifest) },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setProjectStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeAgentTraceChat(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
'准备读取 Agent run trace。',
|
||
() => void executeAgentTraceChat(true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
const trace = parseAgentRunTrace(result.content);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(formatAgentRunStatus(trace));
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.trace_read',
|
||
'file.read .agent/run.latest.json',
|
||
]);
|
||
const traceSummary = summarizeAgentRunCompletionForChat(trace);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', ...traceSummary },
|
||
]);
|
||
} catch (error) {
|
||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||
const message = isMissingAgentRunTraceError(rawMessage)
|
||
? '暂无最近 Agent trace。先生成一次游戏草案后再查看。'
|
||
: rawMessage;
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeAgentRunControl(
|
||
action: string,
|
||
detail: string | undefined,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const commandId =
|
||
action === 'status'
|
||
? 'agent.run_status'
|
||
: (`agent.${action}` as GameCreationAppCommandDescriptor['id']);
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
commandId,
|
||
nextProjectPath,
|
||
action === 'status'
|
||
? `查看 ${nextProjectPath} 的最近 Agent run 状态`
|
||
: `执行 ${nextProjectPath} 的 ${commandId}`,
|
||
action === 'status'
|
||
? '准备查看 Agent run 状态。'
|
||
: '准备执行 Agent run 操作。',
|
||
() =>
|
||
void executeAgentRunControl(action, detail, announceToChat, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<AgentRunControlResult>('control_agent_run', {
|
||
projectPath: nextProjectPath,
|
||
action,
|
||
detail,
|
||
});
|
||
setAgentRunStatus(
|
||
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
commandId,
|
||
'file.write .agent/activity.jsonl',
|
||
'file.write .agent/output.jsonl',
|
||
'file.write .agent/context.bundle.json',
|
||
]);
|
||
if (announceToChat && action === 'status') {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'agent.run_status',
|
||
);
|
||
}
|
||
let traceReadQueued = false;
|
||
if (action === 'status') {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
() => void refreshAgentRunTrace(nextProjectPath),
|
||
);
|
||
traceReadQueued = true;
|
||
}
|
||
}
|
||
if (!traceReadQueued) {
|
||
await refreshAgentRunTrace(nextProjectPath);
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: [
|
||
result.message,
|
||
`run:${result.runId}`,
|
||
`状态:${result.status} / ${result.lifecycleStatus}`,
|
||
`下一步:${result.nextStep}`,
|
||
`事件:${result.activityPath}`,
|
||
`输出:${result.outputPath}`,
|
||
`上下文包:${result.contextBundlePath}`,
|
||
].join('\n'),
|
||
draftCommand: '/read .agent/output.jsonl',
|
||
draftCommandLabel: '读取 Run 输出',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||
const message = formatAgentRunControlError(action, rawMessage);
|
||
setAgentRunStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function queueAgentRunControlFromPanel(action: 'kill' | 'retry' | 'resume') {
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
queuePendingCommand({ id: `agent.${action}` });
|
||
setAgentRunStatus('等待确认');
|
||
}
|
||
|
||
async function executeAgentAuditChat(
|
||
confirmedPolicyCommands: GameCreationAppCommandDescriptor['id'][] = [],
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!confirmedPolicyCommands.includes('agent.audit') &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'agent.audit',
|
||
nextProjectPath,
|
||
`审计 ${nextProjectPath} 的 Agent 能力证据`,
|
||
'准备审计 Agent 能力证据。',
|
||
() => void executeAgentAuditChat(['agent.audit']),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const auditReadCommands: Array<GameCreationAppCommandDescriptor['id']> = [
|
||
'project.status',
|
||
'file.list',
|
||
'file.read',
|
||
'agent.trace_read',
|
||
];
|
||
const confirmCommandId = auditReadCommands.find(
|
||
(commandId) =>
|
||
policyView.policy.confirmCommands.includes(commandId) &&
|
||
!confirmedPolicyCommands.includes(commandId),
|
||
);
|
||
if (confirmCommandId) {
|
||
requestProjectPolicyConfirmation(
|
||
confirmCommandId,
|
||
nextProjectPath,
|
||
`审计 ${nextProjectPath} 需要读取 ${confirmCommandId}`,
|
||
() =>
|
||
void executeAgentAuditChat([
|
||
...confirmedPolicyCommands,
|
||
confirmCommandId,
|
||
]),
|
||
);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `准备确认审计读取:${confirmCommandId}` },
|
||
]);
|
||
return;
|
||
}
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath, commandId: 'agent.audit' },
|
||
);
|
||
const fileResult = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
let commandLogContent = '';
|
||
try {
|
||
const commandLogResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
commandLogContent = commandLogResult.content;
|
||
} catch {
|
||
commandLogContent = '';
|
||
}
|
||
let trace: GameCreationAgentRunTrace | null = null;
|
||
try {
|
||
const traceResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
trace = parseAgentRunTrace(traceResult.content);
|
||
} catch {
|
||
trace = null;
|
||
}
|
||
|
||
setManifest(nextManifest);
|
||
setProjectFiles(fileResult.files);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(
|
||
trace ? formatAgentRunStatus(trace) : '还没有最近一次 Agent run',
|
||
);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'agent.audit',
|
||
'project.status',
|
||
'file.list',
|
||
...(commandLogContent ? ['file.read .agent/logs/command.log'] : []),
|
||
...(trace ? ['agent.trace_read'] : []),
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: summarizeAgentAudit(
|
||
nextManifest,
|
||
nextProjectPath,
|
||
fileResult.files,
|
||
trace,
|
||
commandLogContent,
|
||
),
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStart() {
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
|
||
requestCommandConfirmation(
|
||
'preview.start',
|
||
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
|
||
() => void executePreviewStart(false),
|
||
);
|
||
}
|
||
|
||
async function executePreviewStart(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setPreviewStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setPreviewStatus('正在启动');
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.start',
|
||
nextProjectPath,
|
||
`启动 ${nextProjectPath}/game/ 并载入客户端运行视图`,
|
||
'准备启动本地预览。',
|
||
() => void executePreviewStart(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
updateClientPreview(result);
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
void refreshManifest(nextProjectPath);
|
||
setCommandLog((current) => [...current, 'preview.start']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `预览已在客户端运行视图启动:${result.url}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executePreviewOpen(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (announceToChat && !nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
nextProjectPath &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.open',
|
||
nextProjectPath,
|
||
`打开 ${nextProjectPath} 的本地预览`,
|
||
'准备打开当前本地预览。',
|
||
() => void executePreviewOpen(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewStatus>(
|
||
'activate_local_game_preview',
|
||
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
|
||
);
|
||
if (
|
||
result.status === 'running' &&
|
||
result.url &&
|
||
result.port &&
|
||
result.root
|
||
) {
|
||
updateClientPreview({
|
||
url: result.url,
|
||
port: result.port,
|
||
root: result.root,
|
||
});
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
}
|
||
setCommandLog((current) => [...current, 'preview.open']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: result.url
|
||
? `已切换到客户端运行视图:${result.url}`
|
||
: '已切换到客户端运行视图。',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStatus() {
|
||
void executePreviewStatus(false);
|
||
}
|
||
|
||
async function executePreviewStatus(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (announceToChat && !nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
nextProjectPath &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.status',
|
||
nextProjectPath,
|
||
`查看 ${nextProjectPath} 的预览状态`,
|
||
'准备查看预览状态。',
|
||
() => void executePreviewStatus(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalPreviewStatus>(
|
||
'get_local_game_preview_status',
|
||
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
|
||
);
|
||
if (
|
||
result.status === 'running' &&
|
||
result.url &&
|
||
result.port &&
|
||
result.root
|
||
) {
|
||
updateClientPreview({
|
||
url: result.url,
|
||
port: result.port,
|
||
root: result.root,
|
||
});
|
||
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
|
||
} else {
|
||
updateClientPreview(null);
|
||
setPreviewStatus('未启动');
|
||
}
|
||
setCommandLog((current) => [...current, 'preview.status']);
|
||
if (announceToChat && nextProjectPath) {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'preview.status',
|
||
);
|
||
}
|
||
if (announceToChat) {
|
||
const previewRunning = result.status === 'running' && result.url;
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: previewRunning ? `预览运行中:${result.url}` : '预览未启动。',
|
||
draftCommand: previewRunning ? '/open-preview' : '/preview',
|
||
draftCommandLabel: previewRunning
|
||
? '填入打开预览命令'
|
||
: '填入启动预览命令',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handlePreviewStop() {
|
||
void executePreviewStop(false);
|
||
}
|
||
|
||
async function executePreviewStop(
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setPreviewStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setPreviewStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'preview.stop',
|
||
nextProjectPath,
|
||
`停止 ${nextProjectPath} 的本地预览`,
|
||
'准备停止本地预览。',
|
||
() => void executePreviewStop(true, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
await invoke<LocalPreviewStatus>('stop_local_game_preview', {
|
||
projectPath: nextProjectPath,
|
||
});
|
||
updateClientPreview(null);
|
||
setPreviewStatus('已停止');
|
||
setCommandLog((current) => [...current, 'preview.stop']);
|
||
if (announceToChat) {
|
||
appendLocalPermissionLog(
|
||
nextProjectPath,
|
||
'command.auto',
|
||
'preview.stop',
|
||
);
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '预览已停止。' },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setPreviewStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleMemoryRead(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在读取');
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('memory.read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void handleMemoryRead(true),
|
||
);
|
||
setMemoryStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'read_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope: memoryScope },
|
||
);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.read']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function validateMemoryPanelProjectPath() {
|
||
const nextProjectPath = projectPath.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setMemoryStatus('请提供本地项目绝对路径。');
|
||
return null;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setMemoryStatus('本地项目路径不能包含控制字符。');
|
||
return null;
|
||
}
|
||
return nextProjectPath;
|
||
}
|
||
|
||
async function handleMemoryWrite() {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'memory.write',
|
||
`保存 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void executeMemoryWritePanel(nextProjectPath),
|
||
);
|
||
}
|
||
|
||
async function executeMemoryWritePanel(nextProjectPath: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'write_local_game_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
scope: memoryScope,
|
||
content: memoryDraft,
|
||
},
|
||
);
|
||
setMemoryStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.write']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleMemoryDelete() {
|
||
const nextProjectPath = validateMemoryPanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'memory.delete',
|
||
`删除 ${nextProjectPath}/${memoryScopePath(memoryScope)}`,
|
||
() => void executeMemoryDeletePanel(nextProjectPath),
|
||
);
|
||
}
|
||
|
||
async function executeMemoryDeletePanel(nextProjectPath: string) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setMemoryStatus('正在删除');
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'delete_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope: memoryScope },
|
||
);
|
||
setMemoryDraft('');
|
||
setMemoryStatus(`已删除:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.delete']);
|
||
} catch (error) {
|
||
setMemoryStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function executeMemoryReadChat(
|
||
scope: MemoryScope,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'memory.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${memoryScopePath(scope)}`,
|
||
'准备读取项目记忆。',
|
||
() => void executeMemoryReadChat(scope, true),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'read_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope },
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(
|
||
result.exists ? `已读取:${result.path}` : '记忆文件不存在',
|
||
);
|
||
setCommandLog((current) => [...current, 'memory.read']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: result.exists
|
||
? `${memoryScopeLabel(scope)}记忆:\n${result.content}`
|
||
: `${memoryScopeLabel(scope)}记忆为空。`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeMemoryWrite(
|
||
scope: MemoryScope,
|
||
content: string,
|
||
mode: MemoryWriteMode,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const currentMemory =
|
||
mode === 'append'
|
||
? await invoke<LocalGameMemoryResult>('read_local_game_memory', {
|
||
projectPath: nextProjectPath,
|
||
scope,
|
||
})
|
||
: null;
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'write_local_game_memory',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
scope,
|
||
content:
|
||
mode === 'append'
|
||
? appendMemoryContent(currentMemory?.content ?? '', content)
|
||
: content,
|
||
},
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft(result.content);
|
||
setMemoryStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
...(mode === 'append' ? ['memory.read'] : []),
|
||
'memory.write',
|
||
]);
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text:
|
||
mode === 'append'
|
||
? `已追加${memoryScopeLabel(scope)}记忆。`
|
||
: `已保存${memoryScopeLabel(scope)}记忆。`,
|
||
},
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function executeMemoryDeleteChat(scope: MemoryScope) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setMemoryStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalGameMemoryResult>(
|
||
'delete_local_game_memory',
|
||
{ projectPath: nextProjectPath, scope },
|
||
);
|
||
setMemoryScope(scope);
|
||
setMemoryDraft('');
|
||
setMemoryStatus(`已删除:${result.path}`);
|
||
setCommandLog((current) => [...current, 'memory.delete']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已删除${memoryScopeLabel(scope)}记忆。` },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setMemoryStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function handleLimitedCommandRun(
|
||
command: GameCreationAppLimitedRunCommandDescriptor,
|
||
) {
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath;
|
||
requestCommandConfirmation(
|
||
'command.run_limited',
|
||
`运行 ${command.title} 于 ${nextProjectPath}`,
|
||
() => void executeLimitedCommand(command.id, false),
|
||
);
|
||
}
|
||
|
||
async function refreshLimitedLocalCommands() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
setLimitedCommandStatus('正在读取内置命令');
|
||
try {
|
||
const commands = await invoke<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>('get_limited_local_commands');
|
||
setLimitedLocalCommands(
|
||
commands.length > 0
|
||
? commands
|
||
: [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS],
|
||
);
|
||
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
|
||
setCommandLog((current) => [...current, 'command.list_limited']);
|
||
} catch (error) {
|
||
setLimitedCommandStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
async function handleProjectLogRead(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setProjectLogStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setProjectLogStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
setProjectLogStatus(`正在读取 ${relativePath}`);
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'file.read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath}/${relativePath}`,
|
||
'准备读取项目日志。',
|
||
() => void handleProjectLogRead(relativePath, true),
|
||
))
|
||
) {
|
||
setProjectLogStatus('等待确认');
|
||
return;
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId: 'file.read',
|
||
},
|
||
);
|
||
setProjectLogContent(result.content);
|
||
setProjectLogStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, `file.read ${result.path}`]);
|
||
} catch (error) {
|
||
setProjectLogContent('');
|
||
setProjectLogStatus(
|
||
error instanceof Error ? error.message : String(error),
|
||
);
|
||
}
|
||
}
|
||
|
||
async function executeLimitedCommandList() {
|
||
const invoke = resolveTauriInvoke();
|
||
let commands: GameCreationAppLimitedRunCommandDescriptor[] = [
|
||
...GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||
];
|
||
if (invoke) {
|
||
try {
|
||
const nativeCommands = await invoke<
|
||
GameCreationAppLimitedRunCommandDescriptor[]
|
||
>('get_limited_local_commands');
|
||
if (nativeCommands.length > 0) {
|
||
commands = nativeCommands;
|
||
}
|
||
} catch {
|
||
commands = [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS];
|
||
}
|
||
}
|
||
setLimitedLocalCommands(commands);
|
||
setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`);
|
||
setCommandLog((current) => [...current, 'command.list_limited']);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summarizeLimitedLocalCommands(commands) },
|
||
]);
|
||
}
|
||
|
||
async function executeRunLocal(announceToChat: boolean) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '请先用 /project 设置本地项目。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setLimitedCommandStatus('正在运行');
|
||
try {
|
||
const smoke = await invoke<LimitedLocalCommandResult>(
|
||
'run_limited_local_command',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
commandId: 'game.static_smoke',
|
||
},
|
||
);
|
||
const previewResult = await invoke<LocalPreviewResult>(
|
||
'start_local_game_preview',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
updateClientPreview(previewResult);
|
||
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
|
||
setLimitedCommandStatus(smoke.output);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'game.run_local',
|
||
`command.run_limited ${smoke.commandId}`,
|
||
'preview.start',
|
||
]);
|
||
void refreshManifest(nextProjectPath);
|
||
void refreshAgentRunTrace(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `运行通过,已载入客户端运行视图:${previewResult.url}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setLimitedCommandStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executeLimitedCommand(
|
||
commandId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setLimitedCommandStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setLimitedCommandStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
setLimitedCommandStatus('正在运行');
|
||
try {
|
||
const result = await invoke<LimitedLocalCommandResult>(
|
||
'run_limited_local_command',
|
||
{ projectPath: nextProjectPath, commandId },
|
||
);
|
||
const summary = `${result.output}\n日志:${result.logPath}`;
|
||
setLimitedCommandStatus(result.output);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
`command.run_limited ${result.commandId}`,
|
||
]);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: summary },
|
||
]);
|
||
}
|
||
void refreshManifest(nextProjectPath);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setLimitedCommandStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleFileList(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在列出');
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('file.list')) {
|
||
requestProjectPolicyConfirmation(
|
||
'file.list',
|
||
nextProjectPath,
|
||
`列出 ${nextProjectPath} 的项目文件`,
|
||
() => void handleFileList(true),
|
||
);
|
||
setFileStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setProjectFiles(result.files);
|
||
setFileStatus(`已列出 ${result.files.length} 项`);
|
||
setCommandLog((current) => [...current, 'file.list']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function validateFilePanelProjectPath() {
|
||
const nextProjectPath = projectPath.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setFileStatus('请提供本地项目绝对路径。');
|
||
return null;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setFileStatus('本地项目路径不能包含控制字符。');
|
||
return null;
|
||
}
|
||
return nextProjectPath;
|
||
}
|
||
|
||
function validateFilePanelPath() {
|
||
if (!isSafeProjectRelativePath(filePath)) {
|
||
setFileStatus('文件路径必须是项目内相对路径。');
|
||
return null;
|
||
}
|
||
return filePath.trim();
|
||
}
|
||
|
||
async function handleFileRead(skipPolicyConfirm = false) {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在读取');
|
||
try {
|
||
const commandId = isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read';
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes(commandId)) {
|
||
requestProjectPolicyConfirmation(
|
||
commandId,
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
() => void handleFileRead(true),
|
||
);
|
||
setFileStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId,
|
||
},
|
||
);
|
||
setFilePath(result.path);
|
||
setFileDraft(result.content);
|
||
setFileStatus(`已读取:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.read']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleFileWrite() {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'file.write',
|
||
`保存 ${nextProjectPath}/${relativePath}`,
|
||
() => void executeFileWrite(nextProjectPath, relativePath),
|
||
);
|
||
}
|
||
|
||
async function executeFileWrite(
|
||
nextProjectPath: string,
|
||
relativePath: string,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在保存');
|
||
try {
|
||
const result = await invoke<LocalProjectFileMutationResult>(
|
||
'write_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath, content: fileDraft },
|
||
);
|
||
setFileStatus(`已保存:${result.path}`);
|
||
setCommandLog((current) => [...current, 'file.write']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleFileDelete() {
|
||
const nextProjectPath = validateFilePanelProjectPath();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
const relativePath = validateFilePanelPath();
|
||
if (!relativePath) {
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'file.delete',
|
||
`删除 ${nextProjectPath}/${relativePath}`,
|
||
() => void executeFileDelete(nextProjectPath, relativePath),
|
||
);
|
||
}
|
||
|
||
async function executeFileDelete(
|
||
nextProjectPath: string,
|
||
relativePath: string,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setFileStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
|
||
setFileStatus('正在删除');
|
||
try {
|
||
const result = await invoke<LocalProjectFileMutationResult>(
|
||
'delete_local_project_file',
|
||
{ projectPath: nextProjectPath, relativePath },
|
||
);
|
||
setFileDraft('');
|
||
setFileStatus(result.deleted ? `已删除:${result.path}` : '文件不存在');
|
||
setCommandLog((current) => [...current, 'file.delete']);
|
||
} catch (error) {
|
||
setFileStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function handleAssetRegister() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const localPath = assetLocalPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
const resourceId = assetResourceId.trim();
|
||
const nextAssetObjectId = assetObjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setAssetStatus('资产路径必须是项目内相对路径。');
|
||
return;
|
||
}
|
||
if (assetSourceKind === 'canvas') {
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!resourceId && !nextAssetObjectId) {
|
||
setAssetStatus('请提供资源 ID 或资产对象 ID。');
|
||
return;
|
||
}
|
||
}
|
||
requestCommandConfirmation(
|
||
'asset.register',
|
||
`登记 ${nextProjectPath}/${localPath}`,
|
||
() =>
|
||
void executeAssetRegister(
|
||
nextProjectPath,
|
||
localPath,
|
||
assetKind,
|
||
assetMediaType,
|
||
assetSourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
),
|
||
);
|
||
}
|
||
|
||
async function executeAssetRegister(
|
||
nextProjectPath: string,
|
||
localPath: string,
|
||
kind: string,
|
||
mediaType: string,
|
||
sourceKind: string,
|
||
canvasProjectId: string,
|
||
resourceId: string,
|
||
nextAssetObjectId: string,
|
||
announceToChat = false,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在登记');
|
||
try {
|
||
if (
|
||
announceToChat &&
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'asset.register',
|
||
nextProjectPath,
|
||
`登记 ${nextProjectPath}/${localPath}`,
|
||
'准备登记项目资产。',
|
||
() =>
|
||
void executeAssetRegister(
|
||
nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
sourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
true,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'register_local_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
sourceKind,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId: nextAssetObjectId,
|
||
taskId: '',
|
||
prompt: '',
|
||
model: '',
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
setAssetStatus(`已登记:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'asset.register']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已登记资产:${result.localPath}` },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleCanvasAssetImport() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const localPath = assetLocalPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
const resourceId = assetResourceId.trim();
|
||
const nextAssetObjectId = assetObjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!isSafeProjectRelativePath(localPath)) {
|
||
setAssetStatus('画板资产路径必须是项目内相对路径。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!resourceId && !nextAssetObjectId) {
|
||
setAssetStatus('请提供资源 ID 或资产对象 ID。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.asset_import',
|
||
`导入 ${nextProjectPath}/${localPath}`,
|
||
() =>
|
||
void executeCanvasAssetImport(
|
||
nextProjectPath,
|
||
localPath,
|
||
assetKind,
|
||
assetMediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
nextAssetObjectId,
|
||
false,
|
||
),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasAssetImport(
|
||
nextProjectPath: string | null,
|
||
localPath: string,
|
||
kind: string,
|
||
mediaType: string,
|
||
canvasProjectId: string,
|
||
resourceId: string,
|
||
assetObjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const targetProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: nextProjectPath;
|
||
if (!targetProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.asset_import',
|
||
targetProjectPath,
|
||
`导入 ${targetProjectPath}/${localPath} 的画板来源资产`,
|
||
'准备导入画板资产。',
|
||
() =>
|
||
void executeCanvasAssetImport(
|
||
nextProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认导入画板资产');
|
||
return;
|
||
}
|
||
setAssetStatus('正在导入画板资产');
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'import_canvas_asset',
|
||
{
|
||
projectPath: targetProjectPath,
|
||
localPath,
|
||
kind,
|
||
mediaType,
|
||
canvasProjectId,
|
||
resourceId,
|
||
assetObjectId,
|
||
taskId: '',
|
||
prompt: '',
|
||
model: '',
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
const canvasSource = formatCanvasAssetSource({
|
||
canvasProjectId,
|
||
canvasAssetId: resourceId,
|
||
canvasAssetObjectId: assetObjectId,
|
||
});
|
||
setAssetStatus(`已导入画板资产 ${canvasSource}:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'canvas.asset_import']);
|
||
void refreshManifest(targetProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已导入画板资产 ${canvasSource}:${result.localPath}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasAssetGenerate() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const prompt = assetGenerationPrompt.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!prompt) {
|
||
setAssetStatus('请提供美术生成提示词。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.asset_generate',
|
||
`生成 ${nextProjectPath} 的首版美术素材`,
|
||
() => void executeCanvasAssetGenerate(prompt, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasAssetGenerate(
|
||
prompt: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath.trim();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.asset_generate',
|
||
nextProjectPath,
|
||
`生成 ${nextProjectPath} 的首版美术素材`,
|
||
'准备生成首版美术素材。',
|
||
() => void executeCanvasAssetGenerate(prompt, announceToChat, true),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认生成美术素材');
|
||
return;
|
||
}
|
||
setAssetStatus('正在生成美术素材');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '正在生成首版美术素材。' },
|
||
]);
|
||
}
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'generate_platform_art_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
prompt,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const filtered = current.filter((asset) => asset.id !== result.id);
|
||
return [...filtered, result];
|
||
});
|
||
setAssetStatus(`已生成美术素材:${result.localPath}`);
|
||
setCommandLog((current) => [...current, 'canvas.asset_generate']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已生成美术素材:${result.localPath}` },
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
requestRuntimeConfigOpen();
|
||
}
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasProjectSync() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.project_sync',
|
||
`同步画板项目资源:${canvasProjectId}`,
|
||
() => void executeCanvasProjectSync(canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasProjectSync(
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.project_sync',
|
||
nextProjectPath,
|
||
`同步 ${nextProjectPath} 的画板项目资源:${canvasProjectId}`,
|
||
'准备同步画板项目资源。',
|
||
() =>
|
||
void executeCanvasProjectSync(
|
||
canvasProjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认同步画板项目');
|
||
return;
|
||
}
|
||
setAssetStatus('正在同步画板项目资源');
|
||
const result = await invoke<SyncCanvasProjectAssetsResult>(
|
||
'sync_canvas_project_assets',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
canvasProjectId,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const byId = new Map(current.map((asset) => [asset.id, asset]));
|
||
for (const asset of result.assets) {
|
||
byId.set(asset.id, asset);
|
||
}
|
||
return [...byId.values()];
|
||
});
|
||
setAssetStatus(
|
||
`已同步画板项目:${result.importedCount} 个资产,${result.importRoot}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'canvas.project_sync']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已同步 ${result.importedCount} 个画板资产自 ${result.canvasProjectId}:${result.importRoot}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCanvasExportImport() {
|
||
const nextProjectPath = projectPath.trim();
|
||
const exportPath = canvasExportPath.trim();
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) {
|
||
setAssetStatus('请提供本地项目绝对路径。');
|
||
return;
|
||
}
|
||
if (projectPathHasControlCharacter(nextProjectPath)) {
|
||
setAssetStatus('本地项目路径不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
if (
|
||
!isAbsoluteProjectPath(exportPath) ||
|
||
projectPathHasControlCharacter(exportPath)
|
||
) {
|
||
setAssetStatus('画板导出 ZIP 路径必须是绝对路径。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.export_import',
|
||
`导入画板导出包:${exportPath}`,
|
||
() => void executeCanvasExportImport(exportPath, canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasExportImport(
|
||
exportPath: string,
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
const nextProjectPath = announceToChat
|
||
? requireChatProjectForUserAction()
|
||
: projectPath;
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (
|
||
!skipPolicyConfirm &&
|
||
(await queueProjectPolicyConfirmationIfNeeded(
|
||
invoke,
|
||
'canvas.export_import',
|
||
nextProjectPath,
|
||
`导入 ${nextProjectPath} 的画板导出包:${exportPath}`,
|
||
'准备导入画板导出包。',
|
||
() =>
|
||
void executeCanvasExportImport(
|
||
exportPath,
|
||
canvasProjectId,
|
||
announceToChat,
|
||
true,
|
||
),
|
||
))
|
||
) {
|
||
setAssetStatus('等待确认导入画板导出包');
|
||
return;
|
||
}
|
||
setAssetStatus('正在导入画板导出包');
|
||
const result = await invoke<ImportCanvasExportResult>(
|
||
'import_canvas_export',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
exportPath,
|
||
canvasProjectId,
|
||
},
|
||
);
|
||
setUploadedAssets((current) => {
|
||
const byId = new Map(current.map((asset) => [asset.id, asset]));
|
||
for (const asset of result.assets) {
|
||
byId.set(asset.id, asset);
|
||
}
|
||
return [...byId.values()];
|
||
});
|
||
setAssetStatus(
|
||
`已导入画板导出包:${result.importedCount} 个资产,${result.importRoot}`,
|
||
);
|
||
setCommandLog((current) => [...current, 'canvas.export_import']);
|
||
void refreshManifest(nextProjectPath);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已导入 ${result.importedCount} 个画板资产自 ${canvasProjectId}:${result.importRoot}`,
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleCanvasProjectOpen() {
|
||
const canvasProjectId = assetCanvasProjectId.trim();
|
||
if (!canvasProjectId) {
|
||
setAssetStatus('请提供画板项目 ID。');
|
||
return;
|
||
}
|
||
if (!isSafeCanvasProjectId(canvasProjectId)) {
|
||
setAssetStatus('画板项目 ID 不能包含控制字符。');
|
||
return;
|
||
}
|
||
requestCommandConfirmation(
|
||
'canvas.project_open',
|
||
`打开本机画板项目 ${canvasProjectId}`,
|
||
() => void executeCanvasProjectOpen(canvasProjectId, false),
|
||
);
|
||
}
|
||
|
||
async function executeCanvasProjectOpen(
|
||
canvasProjectId: string,
|
||
announceToChat: boolean,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在打开画板');
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `正在打开画板:${canvasProjectId}` },
|
||
]);
|
||
}
|
||
try {
|
||
const result = await invoke<OpenCanvasProjectResult>(
|
||
'open_canvas_project',
|
||
{
|
||
canvasProjectId,
|
||
editorBaseUrl,
|
||
},
|
||
);
|
||
setAssetStatus(`已打开画板:${result.url}`);
|
||
setCommandLog((current) => [...current, 'canvas.project_open']);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: `已打开画板:${result.url}`,
|
||
draftCommand: `/sync-canvas-project ${canvasProjectId}`,
|
||
draftCommandLabel: '同步此画板',
|
||
},
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAssetStatus(message);
|
||
if (announceToChat) {
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||
const file = event.currentTarget.files?.[0];
|
||
event.currentTarget.value = '';
|
||
if (!file) {
|
||
return;
|
||
}
|
||
if (!requireChatProjectForUserAction()) {
|
||
return;
|
||
}
|
||
|
||
queuePendingCommand({ id: 'asset.upload', file });
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'user', text: `上传文件:${file.name}` },
|
||
{ role: 'assistant', text: `准备保存文件到本地项目:${file.name}` },
|
||
]);
|
||
}
|
||
|
||
async function executeAssetUpload(file: File) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAssetStatus('需要在 Tauri App 内运行');
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||
]);
|
||
return;
|
||
}
|
||
const nextProjectPath = requireChatProjectForUserAction();
|
||
if (!nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
setAssetStatus('正在上传');
|
||
try {
|
||
const bytes = Array.from(new Uint8Array(await file.arrayBuffer()));
|
||
const result = await invoke<UploadLocalAssetResult>(
|
||
'upload_local_asset',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
fileName: file.name,
|
||
mediaType: file.type || 'application/octet-stream',
|
||
bytes,
|
||
},
|
||
);
|
||
setLocalProject({
|
||
projectPath: nextProjectPath,
|
||
manifestPath: result.manifestPath,
|
||
manifest,
|
||
});
|
||
setUploadedAssets((current) => [...current, result]);
|
||
setAssetStatus(`已上传:${result.localPath}`);
|
||
setCommandLog((current) => [
|
||
...current,
|
||
'asset.upload',
|
||
'asset.register',
|
||
]);
|
||
void refreshManifest(nextProjectPath);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: `已保存文件:${result.localPath}` },
|
||
]);
|
||
} catch (error) {
|
||
setAssetStatus(error instanceof Error ? error.message : String(error));
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'assistant',
|
||
text: error instanceof Error ? error.message : String(error),
|
||
},
|
||
]);
|
||
}
|
||
}
|
||
|
||
async function refreshManifest(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const nextManifest = await invoke<GameCreationAppManifest>(
|
||
'get_local_game_manifest',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
setManifest(nextManifest);
|
||
} catch {
|
||
// Dev-only convenience; command errors are surfaced by the action that triggered them.
|
||
}
|
||
}
|
||
|
||
async function loadAgentRunTraceFile(
|
||
relativePath: string,
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return null;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath,
|
||
commandId: isAgentTraceFilePath(relativePath)
|
||
? 'agent.trace_read'
|
||
: 'file.read',
|
||
},
|
||
);
|
||
const trace = parseAgentRunTrace(result.content);
|
||
setAgentRunTrace(trace);
|
||
setAgentRunStatus(formatAgentRunStatus(trace));
|
||
setCommandLog((current) => [...current, `file.read ${relativePath}`]);
|
||
return summarizeAgentRunCompletionForChat(trace);
|
||
} catch (error) {
|
||
setAgentRunTrace(null);
|
||
setAgentRunStatus(
|
||
relativePath === '.agent/run.latest.json' &&
|
||
isMissingProjectFileError(error)
|
||
? '还没有最近一次 Agent run'
|
||
: error instanceof Error
|
||
? error.message
|
||
: String(error),
|
||
);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function handleAgentRunTraceFileOpen(
|
||
relativePath: string,
|
||
skipPolicyConfirm = false,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject) ?? '';
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的 ${relativePath}`,
|
||
() => void handleAgentRunTraceFileOpen(relativePath, true),
|
||
);
|
||
setAgentRunStatus('等待确认读取 Agent trace');
|
||
return;
|
||
}
|
||
}
|
||
|
||
await loadAgentRunTraceFile(relativePath, nextProjectPath);
|
||
}
|
||
|
||
async function readAgentRunHistoryItems(
|
||
invoke: TauriInvoke,
|
||
runFiles: LocalProjectFileEntry[],
|
||
nextProjectPath: string,
|
||
) {
|
||
const history = await Promise.all(
|
||
runFiles.map(async (file) => {
|
||
try {
|
||
const traceResult = await invoke<LocalProjectFileResult>(
|
||
'read_local_project_file',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
relativePath: file.path,
|
||
commandId: 'agent.trace_read',
|
||
},
|
||
);
|
||
return {
|
||
path: file.path,
|
||
size: file.size,
|
||
trace: parseAgentRunTrace(traceResult.content),
|
||
} satisfies AgentRunHistoryItem;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}),
|
||
);
|
||
return history
|
||
.filter((item): item is AgentRunHistoryItem => item !== null)
|
||
.sort((left, right) => right.trace.updatedAt - left.trace.updatedAt);
|
||
}
|
||
|
||
async function refreshAgentRunHistory(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRunHistoryOverflowCount(0);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<ListLocalProjectFilesResult>(
|
||
'list_local_project_files',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
const runFiles = result.files
|
||
.filter(
|
||
(file) =>
|
||
file.kind === 'file' &&
|
||
file.path.startsWith('.agent/runs/') &&
|
||
file.path.endsWith('.json'),
|
||
)
|
||
.sort(
|
||
(left, right) =>
|
||
(right.modifiedAt ?? 0) - (left.modifiedAt ?? 0) ||
|
||
right.path.localeCompare(left.path),
|
||
);
|
||
const runFilesToLoad = runFiles.slice(0, AGENT_RUN_HISTORY_MAX_COUNT);
|
||
const firstPage = await readAgentRunHistoryItems(
|
||
invoke,
|
||
runFilesToLoad.slice(0, AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT),
|
||
nextProjectPath,
|
||
);
|
||
setAgentRunHistory(firstPage);
|
||
setAgentRunHistoryFiles(runFilesToLoad);
|
||
setAgentRunHistoryOverflowCount(
|
||
Math.max(0, runFiles.length - runFilesToLoad.length),
|
||
);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
} catch {
|
||
setAgentRunHistory([]);
|
||
setAgentRunHistoryFiles([]);
|
||
setAgentRunHistoryOverflowCount(0);
|
||
setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT);
|
||
setAgentRunHistoryLoadingMore(false);
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
}
|
||
}
|
||
|
||
function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) {
|
||
if (!runtime) {
|
||
return;
|
||
}
|
||
setAgentRuntimeById((current) =>
|
||
mergeAgentRuntimeStateIntoMap(current, runtime, false),
|
||
);
|
||
}
|
||
|
||
async function refreshAgentRuntimes(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke || !nextProjectPath) {
|
||
setAgentRuntimeById({});
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
return;
|
||
}
|
||
try {
|
||
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
|
||
try {
|
||
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
|
||
'resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
for (const runtimeResult of resumedRuntimes) {
|
||
appendGameChatFinalReplyMessages(nextProjectPath, [runtimeResult]);
|
||
rememberAgentRuntimeState(
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const message =
|
||
error instanceof Error ? error.message : String(error);
|
||
if (isMissingAgentRuntimeResumeCommandError(error)) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
} else if (
|
||
message.includes('项目权限策略要求用户确认:agent.resume')
|
||
) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
requestProjectPolicyConfirmation(
|
||
'agent.resume',
|
||
nextProjectPath,
|
||
`恢复 ${nextProjectPath} 中未完成的 Agent Runtime 任务`,
|
||
() => {
|
||
void invoke<AgentRuntimeResult[]>(
|
||
'confirm_resume_game_creator_agent_runtime_tasks',
|
||
{ projectPath: nextProjectPath },
|
||
)
|
||
.then((resumedRuntimes) => {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
for (const runtimeResult of resumedRuntimes) {
|
||
appendGameChatFinalReplyMessages(nextProjectPath, [
|
||
runtimeResult,
|
||
]);
|
||
rememberAgentRuntimeState(
|
||
agentRuntimeStateFromResult(runtimeResult),
|
||
);
|
||
}
|
||
setAgentRunStatus('已确认恢复 Agent Runtime 任务');
|
||
})
|
||
.catch((resumeError) => {
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
setAgentRunStatus(
|
||
`Agent Runtime 恢复失败:${
|
||
resumeError instanceof Error
|
||
? resumeError.message
|
||
: String(resumeError)
|
||
}`,
|
||
);
|
||
});
|
||
},
|
||
);
|
||
setAgentRunStatus('等待确认恢复 Agent Runtime 任务');
|
||
} else if (message.includes('项目权限策略拒绝执行:agent.resume')) {
|
||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||
markProjectPolicyDenied('agent.resume', message);
|
||
} else {
|
||
agentRuntimeResumeProjectPathRef.current = null;
|
||
setAgentRunStatus(`Agent Runtime 恢复失败:${message}`);
|
||
}
|
||
}
|
||
}
|
||
const runtimes = await invoke<AgentRuntimeResult[]>(
|
||
'read_game_creator_agent_runtimes',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
const nextRuntimes: AgentRuntimeState[] = [];
|
||
for (const runtimeResult of runtimes) {
|
||
appendGameChatFinalReplyMessages(nextProjectPath, [runtimeResult]);
|
||
nextRuntimes.push(agentRuntimeStateFromResult(runtimeResult));
|
||
}
|
||
const supervisorRuntimeIndex = nextRuntimes.findIndex(
|
||
(runtime) => runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID,
|
||
);
|
||
const persistedSupervisorRuntime =
|
||
supervisorRuntimeIndex >= 0
|
||
? nextRuntimes[supervisorRuntimeIndex]!
|
||
: null;
|
||
if (projectSupervisorOnly && persistedSupervisorRuntime) {
|
||
const currentRuntime = projectSupervisorRuntimeRef.current;
|
||
if (
|
||
!currentRuntime ||
|
||
persistedSupervisorRuntime.updatedAt >= currentRuntime.updatedAt
|
||
) {
|
||
if (persistedSupervisorRuntime.sessionId) {
|
||
projectSupervisorSessionIdRef.current =
|
||
persistedSupervisorRuntime.sessionId;
|
||
setProjectSupervisorSessionId(persistedSupervisorRuntime.sessionId);
|
||
}
|
||
updateProjectSupervisorRuntime(persistedSupervisorRuntime);
|
||
updateProjectSupervisorResponseStream(
|
||
runtimes[supervisorRuntimeIndex]?.responseStream,
|
||
persistedSupervisorRuntime,
|
||
);
|
||
setProjectSupervisorRuntimeError('');
|
||
}
|
||
}
|
||
setAgentRuntimeById((current) =>
|
||
nextRuntimes.reduce(
|
||
(next, runtime) => mergeAgentRuntimeStateIntoMap(next, runtime, true),
|
||
current,
|
||
),
|
||
);
|
||
} catch {
|
||
if (localProjectPathRef.current === nextProjectPath) {
|
||
setAgentRuntimeById({});
|
||
}
|
||
}
|
||
}
|
||
|
||
async function refreshAgentRunTrace(
|
||
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
|
||
) {
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
await refreshAgentRunHistory(nextProjectPath);
|
||
return null;
|
||
}
|
||
const summary = await loadAgentRunTraceFile(
|
||
'.agent/run.latest.json',
|
||
nextProjectPath,
|
||
);
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
await refreshAgentRunHistory(nextProjectPath);
|
||
return summary;
|
||
}
|
||
|
||
async function handleAgentRunTraceRefresh(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (policyView.policy.confirmCommands.includes('agent.trace_read')) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.trace_read',
|
||
nextProjectPath,
|
||
`读取 ${nextProjectPath} 的最近 Agent run trace`,
|
||
() => void handleAgentRunTraceRefresh(true),
|
||
);
|
||
setAgentRunStatus('等待确认');
|
||
return;
|
||
}
|
||
}
|
||
await refreshAgentRunTrace(nextProjectPath);
|
||
} catch (error) {
|
||
setAgentRunStatus(error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
async function scheduleReadyAgentTasks(skipPolicyConfirm = false) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) {
|
||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||
return;
|
||
}
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
try {
|
||
if (!skipPolicyConfirm) {
|
||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||
'read_project_permission_policy',
|
||
{ projectPath: nextProjectPath },
|
||
);
|
||
if (
|
||
policyView.policy.confirmCommands.includes('agent.schedule_ready')
|
||
) {
|
||
requestProjectPolicyConfirmation(
|
||
'agent.schedule_ready',
|
||
nextProjectPath,
|
||
`调度 ${nextProjectPath} 中已 Ready 的 Agent 任务`,
|
||
() => void scheduleReadyAgentTasks(true),
|
||
);
|
||
setAgentRunStatus('等待确认调度 Ready 任务');
|
||
return;
|
||
}
|
||
}
|
||
setAgentRunStatus('正在调度 Ready 任务');
|
||
const scheduledRuntimes = await invoke<AgentRuntimeResult[]>(
|
||
'schedule_game_creator_agent_ready_tasks',
|
||
{ projectPath: nextProjectPath, limit: 16 },
|
||
);
|
||
for (const runtimeResult of scheduledRuntimes) {
|
||
rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult));
|
||
}
|
||
await refreshManifest(nextProjectPath);
|
||
await refreshAgentRuntimes(nextProjectPath);
|
||
const message =
|
||
scheduledRuntimes.length > 0
|
||
? `已调度 ${scheduledRuntimes.length} 个 Ready 任务。`
|
||
: '没有可调度的 Ready 任务。';
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
setAgentRunStatus(message);
|
||
setMessages((current) => [
|
||
...current,
|
||
{ role: 'assistant', text: message },
|
||
]);
|
||
}
|
||
}
|
||
|
||
const agentStatusCards = deriveAgentStatusCards(
|
||
manifest,
|
||
agentRunTrace ?? agentRunHistory[0]?.trace ?? null,
|
||
agentRuntimeById,
|
||
);
|
||
const professionalResultCandidates = taskRowsFromManifest(manifest).map(
|
||
(task) => ({
|
||
agentId: agentConversationId(task),
|
||
label: projectProfessionalAgentLabel(agentConversationId(task)),
|
||
runtimeUpdatedAt:
|
||
agentRuntimeById[agentConversationId(task)]?.updatedAt ?? 0,
|
||
}),
|
||
);
|
||
const professionalResultCandidateKey = professionalResultCandidates
|
||
.map((candidate) => `${candidate.agentId}:${candidate.runtimeUpdatedAt}`)
|
||
.join('|');
|
||
useEffect(() => {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = localProject?.projectPath ?? null;
|
||
if (
|
||
!projectSupervisorOnly ||
|
||
!invoke ||
|
||
!nextProjectPath ||
|
||
professionalResultCandidates.length === 0
|
||
) {
|
||
setProfessionalAgentResultsById({});
|
||
return;
|
||
}
|
||
let disposed = false;
|
||
void Promise.all(
|
||
professionalResultCandidates.map(async (candidate) => {
|
||
try {
|
||
const conversation = await invoke<LocalConversationResult>(
|
||
'read_local_conversation',
|
||
{
|
||
projectPath: nextProjectPath,
|
||
agentId: candidate.agentId,
|
||
},
|
||
);
|
||
const finalMessage = conversation.messages
|
||
.slice()
|
||
.reverse()
|
||
.find(
|
||
(message) =>
|
||
message.role === 'assistant' &&
|
||
message.content.trim() &&
|
||
isAgentFinalizationMessageId(message.messageId),
|
||
);
|
||
if (!finalMessage) {
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'missing' as const,
|
||
};
|
||
}
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'loaded' as const,
|
||
result: {
|
||
agentId: candidate.agentId,
|
||
runId: finalMessage.messageId!,
|
||
label: candidate.label,
|
||
title: `${candidate.label} 文本回执`,
|
||
content: finalMessage.content.trim(),
|
||
updatedAt: finalMessage.updatedAt,
|
||
} satisfies ProjectAgentResultSummary,
|
||
};
|
||
} catch {
|
||
return {
|
||
agentId: candidate.agentId,
|
||
status: 'failed' as const,
|
||
};
|
||
}
|
||
}),
|
||
).then((results) => {
|
||
if (disposed || localProjectPathRef.current !== nextProjectPath) {
|
||
return;
|
||
}
|
||
setProfessionalAgentResultsById((current) => {
|
||
const candidateAgentIds = new Set(
|
||
professionalResultCandidates.map((candidate) => candidate.agentId),
|
||
);
|
||
const next = Object.fromEntries(
|
||
Object.entries(current).filter(([agentId]) =>
|
||
candidateAgentIds.has(agentId),
|
||
),
|
||
);
|
||
for (const outcome of results) {
|
||
if (outcome.status === 'loaded') {
|
||
next[outcome.agentId] = outcome.result;
|
||
} else if (outcome.status === 'missing') {
|
||
delete next[outcome.agentId];
|
||
}
|
||
}
|
||
return next;
|
||
});
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
};
|
||
// The semantic candidate key replaces the freshly allocated candidates array.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
localProject?.projectPath,
|
||
professionalResultCandidateKey,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime?.runId,
|
||
]);
|
||
useEffect(() => {
|
||
if (!projectSupervisorOnly || !onAgentRuntimeSummariesChange) {
|
||
return;
|
||
}
|
||
onAgentRuntimeSummariesChange(
|
||
projectAgentRuntimeSummaries(
|
||
manifest,
|
||
projectSupervisorRuntime,
|
||
agentRuntimeById,
|
||
),
|
||
);
|
||
}, [
|
||
agentRuntimeById,
|
||
manifest,
|
||
onAgentRuntimeSummariesChange,
|
||
projectSupervisorOnly,
|
||
projectSupervisorRuntime,
|
||
]);
|
||
useEffect(() => {
|
||
if (!projectSupervisorOnly || !onAgentResultsChange) {
|
||
return;
|
||
}
|
||
onAgentResultsChange(Object.values(professionalAgentResultsById));
|
||
}, [
|
||
onAgentResultsChange,
|
||
professionalAgentResultsById,
|
||
projectSupervisorOnly,
|
||
]);
|
||
const mainProjectSummary = localProject
|
||
? summarizeMainProjectHeader(manifest, agentStatusCards)
|
||
: null;
|
||
const visibleAgentRunHistory = agentRunHistory.slice(
|
||
0,
|
||
agentRunHistoryVisibleCount,
|
||
);
|
||
const visibleMainProjectFiles = projectFiles
|
||
.filter((file) => file.kind === 'file' && !file.path.startsWith('.agent/'))
|
||
.slice(0, 8);
|
||
const visibleMainProjectAssets = manifest.assets
|
||
.filter(
|
||
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
|
||
)
|
||
.slice(0, 8);
|
||
const visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5);
|
||
const currentProjectTitle = localProject
|
||
? manifest.name.trim() || projectNameFromPath(localProject.projectPath)
|
||
: seedManifest.name;
|
||
const agentRunHistoryOmittedCount =
|
||
Math.max(0, agentRunHistoryFiles.length - agentRunHistoryVisibleCount) +
|
||
agentRunHistoryOverflowCount;
|
||
const canShowMoreAgentRunHistory =
|
||
agentRunHistoryVisibleCount < agentRunHistoryFiles.length;
|
||
|
||
async function showMoreAgentRunHistory() {
|
||
const invoke = resolveTauriInvoke();
|
||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||
if (!invoke || agentRunHistoryLoadingMoreRef.current) {
|
||
return;
|
||
}
|
||
if (!nextProjectPath) {
|
||
setAgentRunStatus('请先初始化本地项目');
|
||
return;
|
||
}
|
||
const nextVisibleCount = Math.min(
|
||
agentRunHistoryVisibleCount + AGENT_RUN_HISTORY_VISIBLE_STEP,
|
||
agentRunHistoryFiles.length,
|
||
);
|
||
const nextFiles = agentRunHistoryFiles.slice(
|
||
agentRunHistoryVisibleCount,
|
||
nextVisibleCount,
|
||
);
|
||
agentRunHistoryLoadingMoreRef.current = true;
|
||
setAgentRunHistoryLoadingMore(true);
|
||
try {
|
||
const nextItems = await readAgentRunHistoryItems(
|
||
invoke,
|
||
nextFiles,
|
||
nextProjectPath,
|
||
);
|
||
setAgentRunHistory((current) =>
|
||
[...current, ...nextItems].sort(
|
||
(left, right) => right.trace.updatedAt - left.trace.updatedAt,
|
||
),
|
||
);
|
||
setAgentRunHistoryVisibleCount(nextVisibleCount);
|
||
} finally {
|
||
agentRunHistoryLoadingMoreRef.current = false;
|
||
setAgentRunHistoryLoadingMore(false);
|
||
}
|
||
}
|
||
|
||
function handleAgentRunHistoryScroll(event: UIEvent<HTMLElement>) {
|
||
if (!canShowMoreAgentRunHistory) {
|
||
return;
|
||
}
|
||
const target = event.currentTarget;
|
||
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 24) {
|
||
void showMoreAgentRunHistory();
|
||
}
|
||
}
|
||
|
||
const visibleMessages = latestVisibleItems(
|
||
messages,
|
||
conversationVisibleCount,
|
||
);
|
||
const hiddenConversationCount = Math.max(
|
||
0,
|
||
messages.length - visibleMessages.length,
|
||
);
|
||
const projectSupervisorTransientReply =
|
||
projectSupervisorResponseStream?.accumulatedText.trim() ?? '';
|
||
const projectSupervisorNeedsUserInput = agentRuntimeNeedsUserInput(
|
||
projectSupervisorRuntime,
|
||
);
|
||
const projectSupervisorHasConversationControls = Boolean(
|
||
projectSupervisorRuntime?.pendingToolAction ||
|
||
projectSupervisorRuntime?.userInputRequest ||
|
||
projectSupervisorNeedsUserInput,
|
||
);
|
||
const visibleAgentConversationMessages = latestVisibleItems(
|
||
agentConversationMessages,
|
||
agentConversationVisibleCount,
|
||
);
|
||
const hiddenAgentConversationCount = Math.max(
|
||
0,
|
||
agentConversationMessages.length - visibleAgentConversationMessages.length,
|
||
);
|
||
const selectedAgentLlmStatus = selectedAgent
|
||
? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent)
|
||
: null;
|
||
const selectedAgentLlmWarning = selectedAgent
|
||
? formatAgentLlmConfigWarning(llmConfigStatus, selectedAgent)
|
||
: null;
|
||
const selectedAgentNeedsUserInput = agentRuntimeNeedsUserInput(
|
||
agentConversationRuntime,
|
||
);
|
||
const selectedAgentSteerRuntime = selectedAgent
|
||
? matchingAgentRuntimeForSteer(
|
||
[agentConversationRuntime],
|
||
selectedAgent.id,
|
||
agentConversationSessionId,
|
||
)
|
||
: null;
|
||
|
||
function showEarlierConversationMessages() {
|
||
setConversationVisibleCount((current) =>
|
||
Math.min(messages.length, current + CONVERSATION_VISIBLE_STEP),
|
||
);
|
||
}
|
||
|
||
function showEarlierAgentConversationMessages() {
|
||
setAgentConversationVisibleCount((current) =>
|
||
Math.min(
|
||
agentConversationMessages.length,
|
||
current + CONVERSATION_VISIBLE_STEP,
|
||
),
|
||
);
|
||
}
|
||
|
||
function handleConversationScroll(event: UIEvent<HTMLElement>) {
|
||
if (hiddenConversationCount === 0) {
|
||
return;
|
||
}
|
||
if (event.currentTarget.scrollTop <= 24) {
|
||
showEarlierConversationMessages();
|
||
}
|
||
}
|
||
|
||
function handleSupervisorChatScroll(event: UIEvent<HTMLDivElement>) {
|
||
handleConversationScroll(event);
|
||
const messageList = event.currentTarget;
|
||
const distanceFromBottom =
|
||
messageList.scrollHeight -
|
||
messageList.scrollTop -
|
||
messageList.clientHeight;
|
||
supervisorChatShouldFollowLatestRef.current =
|
||
distanceFromBottom <= AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD;
|
||
}
|
||
|
||
function handleAgentConversationScroll(event: UIEvent<HTMLElement>) {
|
||
if (hiddenAgentConversationCount === 0) {
|
||
return;
|
||
}
|
||
if (event.currentTarget.scrollTop <= 24) {
|
||
showEarlierAgentConversationMessages();
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!selectedAgent) {
|
||
return;
|
||
}
|
||
const refreshedAgent = agentStatusCards.find(
|
||
(agent) => agent.id === selectedAgent.id,
|
||
);
|
||
if (!refreshedAgent) {
|
||
closeAgentConversation();
|
||
return;
|
||
}
|
||
if (!sameAgentStatusCard(selectedAgent, refreshedAgent)) {
|
||
setSelectedAgent(refreshedAgent);
|
||
}
|
||
}, [agentStatusCards, selectedAgent]);
|
||
|
||
function handleProjectSupervisorOnlySubmit(
|
||
event: FormEvent<HTMLFormElement>,
|
||
) {
|
||
event.preventDefault();
|
||
const prompt = chatInput.trim();
|
||
if ((gameChatOnly || supervisorChatOnly) && prompt.startsWith('/')) {
|
||
void handleChatSubmit(event);
|
||
return;
|
||
}
|
||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||
return;
|
||
}
|
||
if (!prompt || chatAgentBusy) {
|
||
return;
|
||
}
|
||
if (supervisorChatOnly || gameChatOnly) {
|
||
supervisorChatShouldFollowLatestRef.current = true;
|
||
}
|
||
setChatInput('');
|
||
setMessages((current) => [
|
||
...current,
|
||
{
|
||
role: 'user',
|
||
text: prompt,
|
||
runtimeOwned: true,
|
||
updatedAt: Date.now(),
|
||
},
|
||
]);
|
||
void executeChatAgentReply(prompt);
|
||
}
|
||
|
||
const visibleProfessionalAgentCards = agentStatusCards.filter(
|
||
(agent) =>
|
||
agent.id !== PROJECT_SUPERVISOR_AGENT_ID &&
|
||
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
|
||
);
|
||
|
||
if (projectSupervisorOnly && gameChatOnly) {
|
||
const gameChatProjectPath = localProject?.projectPath ?? '';
|
||
return (
|
||
<SupervisorChatOnlyView
|
||
chatAgentBusy={chatAgentBusy}
|
||
chatInput={chatInput}
|
||
gameChatMode
|
||
messagesRef={supervisorChatMessagesRef}
|
||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||
onChatInputChange={setChatInput}
|
||
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
|
||
onConfirmConfirmation={confirmUiCommand}
|
||
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
|
||
onProjectPick={() => void handleGameChatProjectPick()}
|
||
onScroll={handleSupervisorChatScroll}
|
||
onShowEarlierMessages={showEarlierConversationMessages}
|
||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||
onToolAction={handleProjectSupervisorToolAction}
|
||
onUserInput={handleProjectSupervisorUserInput}
|
||
pendingConfirmation={pendingUiConfirmation}
|
||
pendingCommand={pendingCommand}
|
||
onCancelPendingCommand={handlePendingCommandCancel}
|
||
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
|
||
pendingNonEmptyProjectCreate={pendingNonEmptyProjectCreate}
|
||
onCancelNonEmptyProjectCreate={cancelProjectCreateInNonEmptyFolder}
|
||
onConfirmNonEmptyProjectCreate={confirmProjectCreateInNonEmptyFolder}
|
||
preview={preview}
|
||
previewRevision={gameChatPreviewRevision}
|
||
previewStatus={previewStatus}
|
||
projectPath={gameChatProjectPath}
|
||
projectReady={Boolean(localProject)}
|
||
projectSelectionBusy={gameChatProjectSelectionBusy}
|
||
runtime={projectSupervisorRuntime}
|
||
runtimeByAgentId={agentRuntimeById}
|
||
manifest={manifest}
|
||
runtimeConfigOpen={runtimeConfigOpen}
|
||
runtimeError={projectSupervisorRuntimeError}
|
||
transientReply={projectSupervisorTransientReply}
|
||
hasConversationControls={projectSupervisorHasConversationControls}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
needsUserInput={projectSupervisorNeedsUserInput}
|
||
visibleMessages={visibleMessages}
|
||
workspaceStatus={workspaceStatus}
|
||
expectedRunId={projectSupervisorExpectedRunId}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (projectSupervisorOnly && supervisorChatOnly) {
|
||
const supervisorProjectPath =
|
||
localProject?.projectPath || initialProjectPath || projectPath;
|
||
return (
|
||
<SupervisorChatOnlyView
|
||
chatAgentBusy={chatAgentBusy}
|
||
chatInput={chatInput}
|
||
messagesRef={supervisorChatMessagesRef}
|
||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||
onChatInputChange={setChatInput}
|
||
onCloseRuntimeConfig={() => setRuntimeConfigOpen(false)}
|
||
onConfirmConfirmation={confirmUiCommand}
|
||
onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)}
|
||
onScroll={handleSupervisorChatScroll}
|
||
onShowEarlierMessages={showEarlierConversationMessages}
|
||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||
onToolAction={handleProjectSupervisorToolAction}
|
||
onUserInput={handleProjectSupervisorUserInput}
|
||
pendingConfirmation={pendingUiConfirmation}
|
||
pendingCommand={pendingCommand}
|
||
onCancelPendingCommand={handlePendingCommandCancel}
|
||
onConfirmPendingCommand={() => void handlePendingCommandConfirm()}
|
||
projectPath={supervisorProjectPath}
|
||
runtime={projectSupervisorRuntime}
|
||
runtimeConfigOpen={runtimeConfigOpen}
|
||
runtimeError={projectSupervisorRuntimeError}
|
||
transientReply={projectSupervisorTransientReply}
|
||
hasConversationControls={projectSupervisorHasConversationControls}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
needsUserInput={projectSupervisorNeedsUserInput}
|
||
visibleMessages={visibleMessages}
|
||
workspaceStatus={workspaceStatus}
|
||
expectedRunId={projectSupervisorExpectedRunId}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (projectSupervisorOnly) {
|
||
return (
|
||
<ProjectSupervisorView
|
||
chatInput={chatInput}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
needsUserInput={projectSupervisorNeedsUserInput}
|
||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||
onChatInputChange={setChatInput}
|
||
onConfirmConfirmation={confirmUiCommand}
|
||
onScroll={handleConversationScroll}
|
||
onShowEarlierMessages={showEarlierConversationMessages}
|
||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||
pendingConfirmation={pendingUiConfirmation}
|
||
transientReply={projectSupervisorTransientReply}
|
||
visibleMessages={visibleMessages}
|
||
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
|
||
workspaceStatus={workspaceStatus}
|
||
runtime={projectSupervisorRuntime}
|
||
error={projectSupervisorRuntimeError}
|
||
runtimeByAgentId={agentRuntimeById}
|
||
controlBusy={chatAgentBusy}
|
||
professionalResultsByAgentId={professionalAgentResultsById}
|
||
onToolAction={handleProjectSupervisorToolAction}
|
||
onSupervisorRetry={handleProjectSupervisorRetry}
|
||
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
|
||
onProfessionalRetry={handleProjectProfessionalAgentRetry}
|
||
onUserInput={handleProjectSupervisorUserInput}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<main
|
||
className={`app-shell ${devMode ? 'app-shell--dev' : 'app-shell--user'}`}
|
||
>
|
||
<ProjectWorkspaceChatPane
|
||
agentRunStatus={agentRunStatus}
|
||
agentRuntimeById={agentRuntimeById}
|
||
agentStatusCards={agentStatusCards}
|
||
cancelProjectCreateInNonEmptyFolder={
|
||
cancelProjectCreateInNonEmptyFolder
|
||
}
|
||
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
||
chatAgentBusy={chatAgentBusy}
|
||
chatInput={chatInput}
|
||
chatInputRef={chatInputRef}
|
||
confirmProjectCreateInNonEmptyFolder={
|
||
confirmProjectCreateInNonEmptyFolder
|
||
}
|
||
confirmUiCommand={confirmUiCommand}
|
||
currentProjectTitle={currentProjectTitle}
|
||
devMode={devMode}
|
||
executeAgentAuditChat={executeAgentAuditChat}
|
||
executeAgentCapabilitiesChat={executeAgentCapabilitiesChat}
|
||
executeAgentRunControl={executeAgentRunControl}
|
||
executeAgentTraceChat={executeAgentTraceChat}
|
||
executeLimitedCommandList={executeLimitedCommandList}
|
||
executeLlmConfigStatus={executeLlmConfigStatus}
|
||
executeMemoryReadChat={executeMemoryReadChat}
|
||
executePreviewStatus={executePreviewStatus}
|
||
executePreviewStop={executePreviewStop}
|
||
executeProjectAssets={executeProjectAssets}
|
||
executeProjectCheckpoints={executeProjectCheckpoints}
|
||
executeProjectDiff={executeProjectDiff}
|
||
executeProjectFileReadChat={executeProjectFileReadChat}
|
||
executeProjectFiles={executeProjectFiles}
|
||
executeProjectPolicyRead={executeProjectPolicyRead}
|
||
executeProjectStatus={executeProjectStatus}
|
||
executeProjectTasks={executeProjectTasks}
|
||
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
|
||
handleAssetUpload={handleAssetUpload}
|
||
handleChatSubmit={handleChatSubmit}
|
||
handleConversationScroll={handleConversationScroll}
|
||
handleOpenLauncherWindow={handleOpenLauncherWindow}
|
||
handlePendingCommandCancel={handlePendingCommandCancel}
|
||
handlePendingCommandConfirm={handlePendingCommandConfirm}
|
||
handlePickCanvasExportFileDraft={handlePickCanvasExportFileDraft}
|
||
handleProjectProfessionalAgentRetry={
|
||
handleProjectProfessionalAgentRetry
|
||
}
|
||
handleProjectProfessionalAgentToolAction={
|
||
handleProjectProfessionalAgentToolAction
|
||
}
|
||
handleProjectSupervisorRetry={handleProjectSupervisorRetry}
|
||
handleProjectSupervisorToolAction={handleProjectSupervisorToolAction}
|
||
handleProjectSupervisorUserInput={handleProjectSupervisorUserInput}
|
||
handleRevealCurrentProjectDirectory={
|
||
handleRevealCurrentProjectDirectory
|
||
}
|
||
handleRuntimeConfigOpen={handleRuntimeConfigOpen}
|
||
hiddenConversationCount={hiddenConversationCount}
|
||
llmConfigStatus={llmConfigStatus}
|
||
loadProjectConversation={loadProjectConversation}
|
||
localProject={localProject}
|
||
mainProjectSummary={mainProjectSummary}
|
||
openAgentConversation={openAgentConversation}
|
||
pendingCommand={pendingCommand}
|
||
pendingNonEmptyProjectCreate={pendingNonEmptyProjectCreate}
|
||
pendingUiConfirmation={pendingUiConfirmation}
|
||
prepareChatCommandDraft={prepareChatCommandDraft}
|
||
prepareProjectAssetRegisterDraft={prepareProjectAssetRegisterDraft}
|
||
previewStatus={previewStatus}
|
||
professionalAgentResultsById={professionalAgentResultsById}
|
||
projectPath={projectPath}
|
||
projectSupervisorNeedsUserInput={projectSupervisorNeedsUserInput}
|
||
projectSupervisorRuntime={projectSupervisorRuntime}
|
||
projectSupervisorRuntimeError={projectSupervisorRuntimeError}
|
||
projectSupervisorTransientReply={projectSupervisorTransientReply}
|
||
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
|
||
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
|
||
queuePendingCommand={queuePendingCommand}
|
||
queuePreviewOpenShortcut={queuePreviewOpenShortcut}
|
||
queuePreviewStartShortcut={queuePreviewStartShortcut}
|
||
queueProjectCheckpointShortcut={queueProjectCheckpointShortcut}
|
||
queueProjectExportPackageShortcut={queueProjectExportPackageShortcut}
|
||
queueRunLocalShortcut={queueRunLocalShortcut}
|
||
queueStaticSmokeShortcut={queueStaticSmokeShortcut}
|
||
scheduleReadyAgentTasks={scheduleReadyAgentTasks}
|
||
setChatInput={setChatInput}
|
||
showChatHelp={showChatHelp}
|
||
showEarlierConversationMessages={showEarlierConversationMessages}
|
||
visibleMainProjectAssets={visibleMainProjectAssets}
|
||
visibleMainProjectCheckpoints={visibleMainProjectCheckpoints}
|
||
visibleMainProjectFiles={visibleMainProjectFiles}
|
||
visibleMessages={visibleMessages}
|
||
workspaceStatus={workspaceStatus}
|
||
/>
|
||
|
||
{runtimeConfigOpen ? (
|
||
<RuntimeConfigDialog
|
||
projectPath={localProject?.projectPath}
|
||
onClose={() => setRuntimeConfigOpen(false)}
|
||
onLog={(entry) => setCommandLog((current) => [...current, entry])}
|
||
/>
|
||
) : null}
|
||
|
||
{selectedAgent ? (
|
||
<AgentConversationOverlay
|
||
agentConversationBackgroundBusy={agentConversationBackgroundBusy}
|
||
agentConversationInput={agentConversationInput}
|
||
agentConversationMessages={agentConversationMessages}
|
||
agentConversationRunSubmitMode={agentConversationRunSubmitMode}
|
||
agentConversationRuntime={agentConversationRuntime}
|
||
agentConversationRuntimeError={agentConversationRuntimeError}
|
||
agentConversationSaving={agentConversationSaving}
|
||
agentConversationStatus={agentConversationStatus}
|
||
agentMemoryContent={agentMemoryContent}
|
||
agentMemoryStatus={agentMemoryStatus}
|
||
answerSelectedAgentRuntimeUserInput={
|
||
answerSelectedAgentRuntimeUserInput
|
||
}
|
||
cancelSelectedAgentRuntimeTask={cancelSelectedAgentRuntimeTask}
|
||
closeAgentConversation={closeAgentConversation}
|
||
confirmSelectedAgentRuntimeTask={confirmSelectedAgentRuntimeTask}
|
||
executeLlmConfigStatus={executeLlmConfigStatus}
|
||
handleAgentBackgroundTaskSubmit={handleAgentBackgroundTaskSubmit}
|
||
handleAgentConversationScroll={handleAgentConversationScroll}
|
||
handleAgentConversationSubmit={handleAgentConversationSubmit}
|
||
handleAgentPrivateMemorySubmit={handleAgentPrivateMemorySubmit}
|
||
hiddenAgentConversationCount={hiddenAgentConversationCount}
|
||
openAgentConversation={openAgentConversation}
|
||
prepareAgentEvidenceReadDraft={prepareAgentEvidenceReadDraft}
|
||
prepareSuggestedToolCommandDraft={prepareSuggestedToolCommandDraft}
|
||
rejectSelectedAgentRuntimeTask={rejectSelectedAgentRuntimeTask}
|
||
retrySelectedAgentRuntimeTask={retrySelectedAgentRuntimeTask}
|
||
selectedAgent={selectedAgent}
|
||
selectedAgentLlmStatus={selectedAgentLlmStatus}
|
||
selectedAgentLlmWarning={selectedAgentLlmWarning}
|
||
selectedAgentNeedsUserInput={selectedAgentNeedsUserInput}
|
||
selectedAgentSteerRuntime={selectedAgentSteerRuntime}
|
||
setAgentConversationInput={setAgentConversationInput}
|
||
setAgentConversationRunSubmitMode={setAgentConversationRunSubmitMode}
|
||
setRuntimeConfigOpen={setRuntimeConfigOpen}
|
||
showEarlierAgentConversationMessages={
|
||
showEarlierAgentConversationMessages
|
||
}
|
||
visibleAgentConversationMessages={visibleAgentConversationMessages}
|
||
/>
|
||
) : null}
|
||
|
||
{devMode ? (
|
||
<section className="developer-pane" aria-label="开发环境">
|
||
<DeveloperRuntimePanels
|
||
agentRunHistory={agentRunHistory}
|
||
agentRunHistoryLoadingMore={agentRunHistoryLoadingMore}
|
||
agentRunHistoryOmittedCount={agentRunHistoryOmittedCount}
|
||
agentRunStatus={agentRunStatus}
|
||
agentRunTrace={agentRunTrace}
|
||
canShowMoreAgentRunHistory={canShowMoreAgentRunHistory}
|
||
handleAgentRunHistoryScroll={handleAgentRunHistoryScroll}
|
||
handleAgentRunTraceFileOpen={handleAgentRunTraceFileOpen}
|
||
handleAgentRunTraceRefresh={handleAgentRunTraceRefresh}
|
||
manifest={manifest}
|
||
prepareChatCommandDraft={prepareChatCommandDraft}
|
||
refreshManifest={refreshManifest}
|
||
showMoreAgentRunHistory={showMoreAgentRunHistory}
|
||
visibleAgentRunHistory={visibleAgentRunHistory}
|
||
/>
|
||
<DeveloperProjectPanels
|
||
assetCanvasProjectId={assetCanvasProjectId}
|
||
assetGenerationPrompt={assetGenerationPrompt}
|
||
assetKind={assetKind}
|
||
assetLocalPath={assetLocalPath}
|
||
assetMediaType={assetMediaType}
|
||
assetObjectId={assetObjectId}
|
||
assetResourceId={assetResourceId}
|
||
assetSourceKind={assetSourceKind}
|
||
assetStatus={assetStatus}
|
||
canvasExportPath={canvasExportPath}
|
||
commandLog={commandLog}
|
||
editorBaseUrl={editorBaseUrl}
|
||
fileDraft={fileDraft}
|
||
filePath={filePath}
|
||
fileStatus={fileStatus}
|
||
handleAssetRegister={handleAssetRegister}
|
||
handleCanvasAssetGenerate={handleCanvasAssetGenerate}
|
||
handleCanvasAssetImport={handleCanvasAssetImport}
|
||
handleCanvasExportImport={handleCanvasExportImport}
|
||
handleCanvasProjectOpen={handleCanvasProjectOpen}
|
||
handleCanvasProjectSync={handleCanvasProjectSync}
|
||
handleFileDelete={handleFileDelete}
|
||
handleFileList={handleFileList}
|
||
handleFileRead={handleFileRead}
|
||
handleFileWrite={handleFileWrite}
|
||
handleLimitedCommandRun={handleLimitedCommandRun}
|
||
handleMemoryDelete={handleMemoryDelete}
|
||
handleMemoryRead={handleMemoryRead}
|
||
handleMemoryWrite={handleMemoryWrite}
|
||
handlePreviewStart={handlePreviewStart}
|
||
handlePreviewStatus={handlePreviewStatus}
|
||
handlePreviewStop={handlePreviewStop}
|
||
handleProjectInit={handleProjectInit}
|
||
handleProjectLogRead={handleProjectLogRead}
|
||
limitedCommandStatus={limitedCommandStatus}
|
||
limitedLocalCommands={limitedLocalCommands}
|
||
localProject={localProject}
|
||
memoryDraft={memoryDraft}
|
||
memoryScope={memoryScope}
|
||
memoryStatus={memoryStatus}
|
||
preview={preview}
|
||
previewStatus={previewStatus}
|
||
projectFiles={projectFiles}
|
||
projectLogContent={projectLogContent}
|
||
projectLogStatus={projectLogStatus}
|
||
projectPath={projectPath}
|
||
projectStatus={projectStatus}
|
||
refreshLimitedLocalCommands={refreshLimitedLocalCommands}
|
||
setAssetCanvasProjectId={setAssetCanvasProjectId}
|
||
setAssetGenerationPrompt={setAssetGenerationPrompt}
|
||
setAssetKind={setAssetKind}
|
||
setAssetLocalPath={setAssetLocalPath}
|
||
setAssetMediaType={setAssetMediaType}
|
||
setAssetObjectId={setAssetObjectId}
|
||
setAssetResourceId={setAssetResourceId}
|
||
setAssetSourceKind={setAssetSourceKind}
|
||
setCanvasExportPath={setCanvasExportPath}
|
||
setEditorBaseUrl={setEditorBaseUrl}
|
||
setFileDraft={setFileDraft}
|
||
setFilePath={setFilePath}
|
||
setMemoryDraft={setMemoryDraft}
|
||
setMemoryScope={setMemoryScope}
|
||
setProjectPath={setProjectPath}
|
||
uploadedAssets={uploadedAssets}
|
||
/>
|
||
</section>
|
||
) : null}
|
||
</main>
|
||
);
|
||
}
|