diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 92cc89bd6..2cbad6c5b 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -2971,9 +2971,6 @@ function ProjectSupervisorRuntimePanel({ projectSupervisorCollaboratingAgentCount(runtime, runtimeByAgentId), ) : ''; - const pendingToolAction = runtime?.pendingToolAction ?? null; - const userInputRequest = runtime?.userInputRequest ?? null; - const needsUserInput = agentRuntimeNeedsUserInput(runtime); return (
{compactProgress} ) : null} + +
+ ); +} + +function ProjectSupervisorRuntimeControls({ + runtime, + controlBusy, + onToolAction, + onUserInput, +}: { + runtime: AgentRuntimeState | null; + controlBusy: boolean; + onToolAction: (decision: 'confirm' | 'reject') => void | Promise; + onUserInput: ( + request: AgentRuntimeUserInputRequest, + responseId: string, + answers: Record, + ) => void | Promise; +}) { + const pendingToolAction = runtime?.pendingToolAction ?? null; + const userInputRequest = runtime?.userInputRequest ?? null; + const needsUserInput = agentRuntimeNeedsUserInput(runtime); + if (!pendingToolAction && !userInputRequest && !needsUserInput) { + return null; + } + + return ( + <> {userInputRequest ? ( ) : null} - + ); } @@ -3060,6 +3091,46 @@ function readInitialProjectPath() { return params.get('projectPath') ?? ''; } +const SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX = + 'genarrative.supervisor-chat.draft'; + +function supervisorChatDraftStorageKey(projectPath: string) { + return `${SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX}:${projectPath}`; +} + +function readSupervisorChatDraft(projectPath: string) { + const normalizedProjectPath = projectPath.trim(); + if (!normalizedProjectPath) { + return ''; + } + try { + return ( + window.sessionStorage.getItem( + supervisorChatDraftStorageKey(normalizedProjectPath), + ) ?? '' + ); + } catch { + return ''; + } +} + +function persistSupervisorChatDraft(projectPath: string, draft: string) { + const normalizedProjectPath = projectPath.trim(); + if (!normalizedProjectPath) { + return; + } + try { + const storageKey = supervisorChatDraftStorageKey(normalizedProjectPath); + if (draft) { + window.sessionStorage.setItem(storageKey, draft); + } else { + window.sessionStorage.removeItem(storageKey); + } + } catch { + // A disabled session store must not block the development chat surface. + } +} + const defaultRuntimeConfigDraft: GameCreatorAppConfig = { llm: { apiKey: '', @@ -17061,7 +17132,11 @@ export function App({ ); const [preview, setPreview] = useState(null); const [previewStatus, setPreviewStatus] = useState('未启动'); - const [chatInput, setChatInput] = useState(''); + const [chatInput, setChatInput] = useState(() => + supervisorChatOnly && initialProjectPath + ? readSupervisorChatDraft(initialProjectPath) + : '', + ); const [chatAgentBusy, setChatAgentBusy] = useState(false); const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState< string | null @@ -17612,6 +17687,13 @@ export function App({ supervisorChatOnly, ]); + useEffect(() => { + if (!supervisorChatOnly) { + return; + } + persistSupervisorChatDraft(initialProjectPath, chatInput); + }, [chatInput, initialProjectPath, supervisorChatOnly]); + useEffect(() => { latestMessagesRef.current = messages; const invoke = resolveTauriInvoke(); @@ -18367,6 +18449,12 @@ export function App({ return ''; } const message = error instanceof Error ? error.message : String(error); + if ( + message.includes('项目权限策略要求用户确认:agent.resume') || + message.includes('项目权限策略拒绝执行:agent.resume') + ) { + return ''; + } if (isRuntimeConfigMissingError(message)) { requestRuntimeConfigOpen(); } @@ -26619,6 +26707,11 @@ export function App({ const projectSupervisorNeedsUserInput = agentRuntimeNeedsUserInput( projectSupervisorRuntime, ); + const projectSupervisorHasConversationControls = Boolean( + projectSupervisorRuntime?.pendingToolAction || + projectSupervisorRuntime?.userInputRequest || + projectSupervisorNeedsUserInput, + ); const visibleAgentConversationMessages = latestVisibleItems( agentConversationMessages, agentConversationVisibleCount, @@ -26816,6 +26909,43 @@ export function App({ ) : null} + {projectSupervisorHasConversationControls ? ( +
+ +
+ ) : null} + {pendingUiConfirmation ? ( +
+
+ + {pendingUiConfirmation.commandId} + {pendingUiConfirmation.detail} + + + +
+
+ ) : null}
{ cleanup(); window.history.pushState({}, '', '/'); window.localStorage.clear(); + window.sessionStorage.clear(); delete window.__TAURI__; vi.restoreAllMocks(); }); @@ -6179,6 +6180,261 @@ describe('AI 游戏创作 App 界面边界', () => { ); }); + it('keeps an unsent standalone Project Supervisor draft across window navigation reloads', async () => { + const projectPath = '/tmp/supervisor-chat-only-draft'; + const harness = createProjectSupervisorRuntimeHarness({ projectPath }); + window.__TAURI__ = { + core: { invoke: harness.invoke }, + event: { listen: harness.listen }, + }; + const renderSupervisorChat = () => + render( + React.createElement(App, { + initialProjectPath: projectPath, + projectSupervisorOnly: true, + supervisorChatOnly: true, + }), + ); + + renderSupervisorChat(); + const firstSurface = await screen.findByLabelText( + '项目总控 Agent 纯聊天', + ); + await within(firstSurface).findByLabelText('项目总控消息'); + fireEvent.change( + within(firstSurface).getByLabelText('项目总控对话内容'), + { target: { value: '这条草稿还没有发送' } }, + ); + + cleanup(); + renderSupervisorChat(); + + const restoredInput = await screen.findByLabelText('项目总控对话内容'); + expect(restoredInput).toHaveProperty('value', '这条草稿还没有发送'); + }); + + it('shows and handles runtime recovery confirmation in the standalone Project Supervisor chat', async () => { + const projectPath = '/tmp/supervisor-chat-only-resume'; + const harness = createProjectSupervisorRuntimeHarness({ projectPath }); + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'resume_game_creator_agent_runtime_tasks') { + throw new Error('项目权限策略要求用户确认:agent.resume'); + } + if (command === 'confirm_resume_game_creator_agent_runtime_tasks') { + return []; + } + return harness.invoke(command, args); + }, + ); + window.__TAURI__ = { + core: { invoke }, + event: { listen: harness.listen }, + }; + render( + React.createElement(App, { + initialProjectPath: projectPath, + projectSupervisorOnly: true, + supervisorChatOnly: true, + }), + ); + + const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); + const detail = await within(surface).findByText( + `恢复 ${projectPath} 中未完成的 Agent Runtime 任务`, + ); + expect(within(surface).queryByText(/项目总控 Agent 恢复失败/)).toBeNull(); + const confirmation = detail.closest('.pending-command'); + expect(confirmation).not.toBeNull(); + + fireEvent.click( + within(confirmation as HTMLElement).getByRole('button', { + name: '确认', + }), + ); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith( + 'confirm_resume_game_creator_agent_runtime_tasks', + { projectPath }, + ); + expect( + within(surface).queryByText( + `恢复 ${projectPath} 中未完成的 Agent Runtime 任务`, + ), + ).toBeNull(); + }); + }); + + it('confirms and rejects pending actions in the standalone Project Supervisor chat', async () => { + const projectPath = '/tmp/supervisor-chat-only-confirmation'; + const runId = 'supervisor-chat-only-confirmation-run'; + const harness = createProjectSupervisorRuntimeHarness({ + projectPath, + initialRuntime: { + runId, + status: 'waiting-for-confirmation', + phase: 'waiting-for-confirmation', + pendingToolAction: { + actionId: 'standalone-action-confirm', + actionFingerprint: 'standalone-fingerprint-confirm', + tool: 'file.write', + inputSummary: 'game/index.html', + reason: null, + requestedAt: 3000, + }, + }, + }); + harness.setConfirmRuntime( + harness.runtimeState({ + runId, + status: 'waiting-for-confirmation', + phase: 'waiting-for-confirmation', + pendingToolAction: { + actionId: 'standalone-action-reject', + actionFingerprint: 'standalone-fingerprint-reject', + tool: 'command.exec', + inputSummary: 'npm test', + reason: null, + requestedAt: 4000, + }, + }), + ); + harness.setRejectRuntime( + harness.runtimeState({ + runId, + status: 'running', + phase: 'planning', + pendingToolAction: null, + }), + ); + window.__TAURI__ = { + core: { invoke: harness.invoke }, + event: { listen: harness.listen }, + }; + render( + React.createElement(App, { + initialProjectPath: projectPath, + projectSupervisorOnly: true, + supervisorChatOnly: true, + }), + ); + + const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); + let pendingAction = await within(surface).findByLabelText( + '项目总控 Agent 待确认动作', + ); + expect(within(pendingAction).getByText('file.write')).not.toBeNull(); + expect(within(pendingAction).getByText('game/index.html')).not.toBeNull(); + fireEvent.click( + within(pendingAction).getByRole('button', { name: '确认' }), + ); + await waitFor(() => { + expect(harness.invoke).toHaveBeenCalledWith( + 'confirm_game_creator_agent_runtime_task', + { + projectPath, + agentId: 'project-supervisor', + runId, + actionId: 'standalone-action-confirm', + note: '用户已确认待执行工具动作', + }, + ); + }); + + pendingAction = await within(surface).findByLabelText( + '项目总控 Agent 待确认动作', + ); + expect(within(pendingAction).getByText('command.exec')).not.toBeNull(); + expect(within(pendingAction).getByText('npm test')).not.toBeNull(); + fireEvent.click( + within(pendingAction).getByRole('button', { name: '拒绝' }), + ); + await waitFor(() => { + expect(harness.invoke).toHaveBeenCalledWith( + 'reject_game_creator_agent_runtime_task', + { + projectPath, + agentId: 'project-supervisor', + runId, + actionId: 'standalone-action-reject', + note: '用户已拒绝待执行工具动作', + }, + ); + }); + }); + + it('answers structured questions in the standalone Project Supervisor chat', async () => { + const projectPath = '/tmp/supervisor-chat-only-user-input'; + const sessionId = 'supervisor-chat-only-user-input-session'; + const runId = 'supervisor-chat-only-user-input-run'; + const request = agentRuntimeUserInputRequest({ + agentId: 'project-supervisor', + sessionId, + runId, + }); + const harness = createProjectSupervisorRuntimeHarness({ + projectPath, + sessionId, + initialRuntime: { + runId, + status: 'waiting-for-user-input', + phase: 'waiting-for-user-input', + currentTask: '准备首版角色规范图', + currentAction: '等待用户补充关键信息', + waitingOn: '你的澄清回答', + nextStep: '提交全部回答后继续同一 Run', + userInputRequest: request, + updatedAt: 6000, + }, + }); + window.__TAURI__ = { + core: { invoke: harness.invoke }, + event: { listen: harness.listen }, + }; + render( + React.createElement(App, { + initialProjectPath: projectPath, + projectSupervisorOnly: true, + supervisorChatOnly: true, + }), + ); + + const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); + const card = await within(surface).findByLabelText('Needs input'); + expect(within(card).getByText('1. 美术方向')).not.toBeNull(); + expect( + within(card).getByText('首版角色规范图采用哪种美术方向?'), + ).not.toBeNull(); + expect( + within(card).getByText('优先验证轮廓与动作可读性。'), + ).not.toBeNull(); + expect( + (within(surface).getByLabelText('项目总控对话内容') as HTMLTextAreaElement) + .disabled, + ).toBe(true); + + fireEvent.click(within(card).getByRole('button', { name: /像素风/ })); + fireEvent.click(within(card).getByRole('button', { name: '提交回答' })); + await waitFor(() => { + expect(harness.invoke).toHaveBeenCalledWith( + 'answer_game_creator_agent_runtime_user_input', + { + projectPath, + agentId: 'project-supervisor', + runId, + actionId: request.actionId, + requestId: request.requestId, + responseId: expect.stringMatching(/^app-user-input-/), + answers: { visual_direction: '像素风' }, + }, + ); + }); + await waitFor(() => { + expect(within(surface).queryByLabelText('Needs input')).toBeNull(); + }); + }); + it('opens an existing project into the active Supervisor Session, restores history, then starts and steers the same run', async () => { const projectPath = '/tmp/launcher-supervisor-game'; const manifest = createGameCreationAppManifest(