diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index 053c13cfb..67dd50dcd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -36,6 +36,7 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [ DIRECT_CODEX_BACKGROUND_ASSET_PATH, DIRECT_CODEX_SPRITESHEET_ASSET_PATH, ]; +const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art"; const DIRECT_CODEX_GAME_OUTPUTS: [(&str, &str, &str); 3] = [ ("game/index.html", "game-entry", "text/html"), ("game/style.css", "game-style", "text/css"), @@ -249,7 +250,7 @@ fn direct_taonier_art_generation_runtime_context( _ => return Err("直连美术生成请求包含未声明的输出路径".to_string()), }; Ok(PlatformArtGenerationRuntimeContext { - agent_id: "direct-codex-art".to_string(), + agent_id: DIRECT_CODEX_ART_AGENT_ID.to_string(), task_id: format!("direct-codex-art-{stage}"), session_id: project_id, run_id: stage.to_string(), @@ -676,6 +677,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at( let _ = std::fs::remove_file(&output); return Err(error); } + emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID); emit_direct_game_creator_progress( root, "art.spritesheet.recovered", @@ -796,6 +798,7 @@ async fn generate_direct_taonier_art_asset_at( )); } }; + emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID); if generated.asset.local_path != output_path { return Err(format!( "陶泥儿美术包生成返回后未形成可用的已登记平台素材合同 {output_path},已终止代码生成" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index 4ddb4fe01..19d0853e6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -261,10 +261,10 @@ pub(crate) use entrypoints::{ chat_with_game_creator_role_agent_stream_at, chat_with_game_creator_role_agent_stream_for_session_at, configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress, - emit_game_creator_agent_runtime_update, game_creator_agent_runtime_update_event, - generate_local_game_draft_at, read_game_creator_agent_runtime_at, - read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at, - set_game_creator_agent_runtime_update_app_handle, + emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated, + game_creator_agent_runtime_update_event, generate_local_game_draft_at, + read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at, + read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle, start_game_creator_manifest_invalidation_event_sink, }; #[cfg(test)] diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs index 09659a149..895ff838d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs @@ -155,6 +155,18 @@ fn relay_game_creator_manifest_invalidation(root: &Path, agent_id: &str) -> Resu .map_err(|error| format!("发送 manifest 失效事件失败:{error}")) } +pub(crate) fn emit_game_creator_manifest_invalidated(root: &Path, agent_id: &str) { + let event = GameCreatorManifestInvalidatedEvent { + project_path: root.to_string_lossy().into_owned(), + agent_id: agent_id.to_string(), + }; + if let Some(app) = GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.get() { + let _ = app.emit("game-creator-manifest-invalidated", event); + return; + } + let _ = relay_game_creator_manifest_invalidation(root, agent_id); +} + pub(crate) fn game_creator_agent_runtime_update_event( root: &Path, runtime: AgentRuntimeResult, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index e936da8ad..03666a111 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -105,6 +105,33 @@ fn manifest_invalidation_sink_isolation_relays_non_supervisor_runtime_update() { fs::remove_dir_all(root).ok(); } +#[test] +fn direct_codex_art_commit_relays_standalone_manifest_invalidation() { + let sink_guard = acquire_game_creator_manifest_invalidation_event_sink_test_guard(); + let root = unique_project_path(); + let relay_listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) + .expect("bind direct Codex manifest invalidation relay fixture"); + let relay_port = relay_listener + .local_addr() + .expect("read direct Codex manifest invalidation relay fixture address") + .port(); + let relay_token = "c".repeat(64); + sink_guard + .configure(relay_port, &relay_token) + .expect("configure direct Codex manifest invalidation relay fixture"); + + emit_game_creator_manifest_invalidated(&root, "direct-codex-art"); + + let relay_payload = read_manifest_invalidation_relay_payload_with_deadline(&relay_listener) + .expect("receive direct Codex manifest invalidation relay within deadline"); + let relay: GameCreatorManifestInvalidationRelayEnvelope = + serde_json::from_slice(&relay_payload) + .expect("parse direct Codex manifest invalidation relay"); + assert_eq!(relay.token, relay_token); + assert_eq!(relay.event.project_path, root.to_string_lossy()); + assert_eq!(relay.event.agent_id, "direct-codex-art"); +} + #[test] fn manifest_invalidation_sink_isolation_bounds_timeouts_and_cleans_up_with_raii() { let cleanup_listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 7bbcc3fe5..d3c32a94e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -5907,16 +5907,10 @@ export function App({ return sessionId; } - async function refreshDirectProjectSurface( + async function refreshDirectProjectPreview( invoke: TauriInvoke, nextProjectPath: string, ) { - try { - await refreshManifest(nextProjectPath); - } catch { - // The direct turn result is still authoritative for the chat. A missing - // or legacy manifest must not hide the Codex reply. - } try { const current = await invoke( 'get_local_game_preview_status', @@ -6020,6 +6014,7 @@ export function App({ setChatAgentBusy(true); setDirectCodexProgress('已提交需求,正在准备智能创作'); setProjectSupervisorRuntimeError(''); + let directTurnSucceeded = false; try { const reply = await directInvoke( 'chat_with_game_creator_direct_codex', @@ -6028,6 +6023,7 @@ export function App({ prompt, }, ); + directTurnSucceeded = true; if (localProjectPathRef.current === directProjectPath) { setMessages((current) => [ ...appendDirectUserMessageIfMissing(current), @@ -6042,7 +6038,6 @@ export function App({ }, ]); setDirectCodexProgress('项目已更新,正在刷新资源和运行预览'); - await refreshDirectProjectSurface(directInvoke, directProjectPath); } } catch (error) { const message = @@ -6068,8 +6063,20 @@ export function App({ ]); } } finally { - setChatAgentBusy(false); - setDirectCodexProgress(''); + try { + if (localProjectPathRef.current === directProjectPath) { + await refreshManifest(directProjectPath); + if (directTurnSucceeded) { + await refreshDirectProjectPreview( + directInvoke, + directProjectPath, + ); + } + } + } finally { + setChatAgentBusy(false); + setDirectCodexProgress(''); + } } return; } diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 025259260..28fa6e4c1 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -4858,6 +4858,10 @@ iframe.preview-frame { min-height: 0; } +.game-workbench-layout.is-ui-editor { + grid-template-columns: minmax(0, 1fr); +} + .game-workbench-stage, .game-workbench-chat { min-width: 0; @@ -4879,10 +4883,17 @@ iframe.preview-frame { box-shadow: var(--platform-nav-active-shadow); } -.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'] { +.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'], +.game-workbench-stage[data-resource-view-state='resources.ui-editor'] { grid-template-rows: auto minmax(0, 1fr); } +.game-workbench-stage[data-resource-view-state='resources.ui-editor'] + > div:last-child { + grid-row: 2; + min-height: 0; +} + .game-workbench-stage[data-resource-view-state^='resources.asset-canvas'] > .asset-canvas-surface { grid-row: 2; diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 982df9bac..6fd9d773e 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -2514,7 +2514,11 @@ export default function ProjectDevelopmentView({ const openResourceEditor = useCallback( (resource: ProjectResource) => { - if (resource.subtype === 'UI' && resource.manifestAssetId !== null) { + if (resource.subtype === 'UI') { + if (resource.manifestAssetId === null) { + setAssetCanvasNotice('该 UI 资源缺少有效的正式资产身份'); + return; + } canvasOpenEpochRef.current += 1; advanceFocusGeneration(); activeFocusFlowIdRef.current = null; @@ -3218,7 +3222,7 @@ export default function ProjectDevelopmentView({ } function showRunView() { - if (!runAvailable) { + if (!runAvailable || uiEditorRoute) { return; } canvasOpenEpochRef.current += 1; @@ -3245,7 +3249,9 @@ export default function ProjectDevelopmentView({ className="launcher-page launcher-project-development game-project-workbench" aria-label="项目开发工作台" > -
+
运行 @@ -3295,7 +3302,7 @@ export default function ProjectDevelopmentView({ {mode === 'resources' && !assetCanvasRoute && - !resourceEditorRoute ? ( + !resourceEditorRoute && + !uiEditorRoute ? ( <> {pendingResourceEdits.length > 0 || pendingResourceEditsLoadState === 'failed' ? ( @@ -3325,6 +3333,14 @@ export default function ProjectDevelopmentView({ : `管理未完成编辑 (${pendingResourceEdits.length})`} ) : null} +
- + {!uiEditorRoute ? ( + + ) : null}
- {professionalDagVisible ? ( + {professionalDagVisible && !uiEditorRoute ? (