diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json
index f52858571..6aa7c6f9f 100644
--- a/apps/ai-game-creator-shell/package.json
+++ b/apps/ai-game-creator-shell/package.json
@@ -30,6 +30,8 @@
"lucide-react": "^0.546.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-markdown": "^10.1.0",
+ "remark-gfm": "^4.0.1",
"vite": "^6.2.0",
"zustand": "^5.0.14"
},
diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 7af269b5b..02fe199d5 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -26,10 +26,12 @@ const viteConfigSource = fs.readFileSync(
new URL('../vite.config.ts', import.meta.url),
'utf8',
);
-const appInvokeSource = fs.readFileSync(
+const appInvokeSource = [
new URL('../src/App.tsx', import.meta.url),
- 'utf8',
-);
+ new URL('../src/view/project-development/index.tsx', import.meta.url),
+]
+ .map((path) => fs.readFileSync(path, 'utf8'))
+ .join('\n');
const appEntrypointSource = fs.readFileSync(
new URL('../src/main.tsx', import.meta.url),
'utf8',
@@ -152,6 +154,14 @@ function assertNoNativeBrowserConfirm(paths) {
}
}
+function assertNoBlockingNativeFilePicker(source) {
+ if (/\.blocking_pick_(?:file|files|folder|folders)\s*\(/.test(source)) {
+ throw new Error(
+ 'AI game creator shell native file pickers must not block the Tauri event loop',
+ );
+ }
+}
+
function extractConstArrayBlock(source, name) {
const start = source.indexOf(`const ${name}`);
if (start === -1) {
@@ -167,9 +177,7 @@ function extractConstArrayBlock(source, name) {
function parseTsCommands(source) {
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
return Array.from(
- block.matchAll(
- /\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g,
- ),
+ block.matchAll(/\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g),
([, id, permission]) => ({ id, permission }),
);
}
@@ -313,6 +321,7 @@ assertNoEnvironmentConfigFallbacks([
]);
assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]);
+assertNoBlockingNativeFilePicker(tauriRustSource);
assertContractRecordsMatch(
'AI game creator shell command contract',
@@ -512,12 +521,14 @@ if (
const clientWindow = windows[0];
if (
- clientWindow.width !== 820 ||
- clientWindow.height !== 640 ||
- clientWindow.minWidth !== 720 ||
- clientWindow.minHeight !== 520
+ clientWindow.width !== 1280 ||
+ clientWindow.height !== 800 ||
+ clientWindow.minWidth !== 1280 ||
+ clientWindow.minHeight !== 800
) {
- throw new Error('AI game creator shell client window must stay compact');
+ throw new Error(
+ 'AI game creator shell client window must keep the landscape workbench size',
+ );
}
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
@@ -687,10 +698,9 @@ for (const snippet of [
'LLM API Key',
'画板 API Key',
'runtime_config.save',
- "'/run:运行自检,启动本地 HTTP 预览并交给外部浏览器'",
- 'async function openPreviewInExternalBrowser',
- "'open_local_game_preview'",
- '已交给外部浏览器打开。',
+ "'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
+ "'activate_local_game_preview'",
+ '已切换到客户端运行视图',
'async function executeRunLocal',
'function needsInitializedChatProject',
'function resolvePendingCommandProjectPath',
diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
index b5f866ba7..1985d8cd0 100644
--- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock
+++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
@@ -509,6 +509,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "chromiumoxide"
version = "0.9.1"
@@ -676,6 +687,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1500,8 +1520,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -1523,8 +1545,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
]
[[package]]
@@ -1851,6 +1876,22 @@ dependencies = [
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http 1.4.2",
+ "hyper 1.10.1",
+ "hyper-util",
+ "rustls",
+ "rustls-native-certs",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -2351,6 +2392,12 @@ dependencies = [
"weezl",
]
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
[[package]]
name = "markup5ever"
version = "0.38.0"
@@ -3223,6 +3270,62 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases 0.2.1",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.5.10",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand 0.10.2",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases 0.2.1",
+ "libc",
+ "once_cell",
+ "socket2 0.5.10",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "quote"
version = "1.0.46"
@@ -3251,7 +3354,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
- "rand_core",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
]
[[package]]
@@ -3261,7 +3375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.5",
]
[[package]]
@@ -3273,6 +3387,21 @@ dependencies = [
"getrandom 0.3.4",
]
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "rangemap"
version = "1.7.1"
@@ -3411,6 +3540,7 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
+ "hyper-rustls",
"hyper-tls 0.6.0",
"hyper-util",
"js-sys",
@@ -3418,6 +3548,9 @@ dependencies = [
"native-tls",
"percent-encoding",
"pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-native-certs",
"rustls-pki-types",
"serde",
"serde_json",
@@ -3425,6 +3558,7 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-native-tls",
+ "tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -3498,6 +3632,20 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rmcp"
version = "2.2.0"
@@ -3549,6 +3697,32 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "rustls"
+version = "0.23.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@@ -3564,9 +3738,21 @@ version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
+ "web-time",
"zeroize",
]
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -3907,7 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -3918,7 +4104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -4109,6 +4295,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -4707,6 +4899,16 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-stream"
version = "0.1.18"
@@ -4966,7 +5168,7 @@ dependencies = [
"http 1.4.2",
"httparse",
"log",
- "rand",
+ "rand 0.9.5",
"sha1",
"thiserror 2.0.18",
"utf-8",
@@ -5078,6 +5280,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -5290,6 +5498,16 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "web_atoms"
version = "0.2.5"
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs
index 45bb3762c..a0f2094d7 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs
@@ -623,6 +623,7 @@ fn read_game_creator_agent_runtime_with_session_filter_at(
visible_game_creator_agent_runtime_response_stream_at(root, &state).unwrap_or(None);
Ok(AgentRuntimeResult {
state,
+ accepted_run_id: None,
session_path: session_path.to_string_lossy().into_owned(),
event_path: event_path.to_string_lossy().into_owned(),
task_path: task_path.to_string_lossy().into_owned(),
@@ -4205,6 +4206,57 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
"Agent Runtime 任务仍在运行,不能重试:{target_run_id}"
));
}
+ if let (Some(parent_agent_id), Some(parent_run_id)) = (
+ task.parent_agent_id.as_deref(),
+ task.parent_run_id.as_deref(),
+ ) {
+ let normalized_parent_agent_id = normalize_game_creator_runtime_agent_id(parent_agent_id)?;
+ let normalized_parent_run_id =
+ normalize_game_creator_agent_runtime_run_id(&normalized_parent_agent_id, parent_run_id);
+ if let Some(parent_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
+ root,
+ &normalized_parent_agent_id,
+ &normalized_parent_run_id,
+ )? {
+ if game_creator_agent_runtime_terminal_status(&parent_task).is_some() {
+ return Err(
+ "父级 Agent 任务已经结束,请先重试项目总控,再由新总控重新安排专业任务"
+ .to_string(),
+ );
+ }
+ }
+ }
+ let retry_lock_id = format!(
+ "{:x}",
+ Sha256::digest(format!("{agent_id}:{target_run_id}").as_bytes())
+ );
+ let _retry_lock = try_acquire_game_creator_agent_delegation_lock_with_wait(
+ root,
+ &retry_lock_id,
+ "runtime-retry",
+ )?
+ .ok_or_else(|| format!("Agent Runtime 重试正在受理:{target_run_id}"))?;
+ if let Some((retry_run_id, retry_session_id)) =
+ read_non_terminal_game_creator_agent_runtime_retry_successor(
+ root,
+ &agent_id,
+ &target_run_id,
+ )?
+ {
+ let mut result = read_game_creator_agent_runtime_for_session_at(
+ root,
+ &agent_id,
+ Some(&retry_session_id),
+ )?;
+ result.accepted_run_id = Some(retry_run_id.clone());
+ notify_external_agent_runner_after_background_task_enqueue(
+ root,
+ &agent_id,
+ &retry_session_id,
+ &retry_run_id,
+ )?;
+ return Ok(result);
+ }
remove_game_creator_agent_runtime_cancel_request(root, &agent_id, &target_run_id);
let retry_run_id = if next_run_id.trim().is_empty() {
format!("{target_run_id}-retry-{}", unix_timestamp())
@@ -4233,18 +4285,26 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
}),
_ => None,
};
- let (result, actual_retry_run_id) = start_game_creator_agent_background_task_with_link_at(
+ let retry_source = if retry_link.is_some() {
+ "agent-delegate-retry"
+ } else {
+ "agent-background-task"
+ };
+ let (mut result, actual_retry_run_id) = with_agent_conversation_session_lane_at(
root,
&agent_id,
- Some(&task.session_id),
- &task.task,
- &retry_run_id,
- if retry_link.is_some() {
- "agent-delegate-retry"
- } else {
- "agent-background-task"
+ "Agent Runtime 重试入队",
+ || {
+ start_game_creator_agent_background_task_with_link_in_session_lane_at(
+ root,
+ &agent_id,
+ Some(&task.session_id),
+ &task.task,
+ &retry_run_id,
+ retry_source,
+ retry_link.as_ref(),
+ )
},
- retry_link.as_ref(),
)?;
let retry_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
let retry_task_chars = task.task.chars().count();
@@ -4272,9 +4332,56 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
"delegated": retry_delegated,
}),
)?;
+ result.accepted_run_id = Some(actual_retry_run_id.clone());
+ notify_external_agent_runner_after_background_task_enqueue(
+ root,
+ &agent_id,
+ &task.session_id,
+ &actual_retry_run_id,
+ )?;
Ok(result)
}
+fn read_non_terminal_game_creator_agent_runtime_retry_successor(
+ root: &Path,
+ agent_id: &str,
+ source_run_id: &str,
+) -> Result