From c229a353be69650386a7820eef9f8c5281d5db87 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 15 Sep 2026 18:12:10 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E5=88=92?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=BE=93=E5=85=A5=E6=8F=90=E7=A4=BA=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 策划模式保留输入框但隐藏占位文案 补充策划模式输入框显示回归测试 --- .../project-workspace/ProjectSupervisorView.tsx | 4 +++- .../tests/appSurface/plan-gdd.suite.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index 655d5cef5..fb292db68 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -491,7 +491,9 @@ export function ProjectSupervisorView({ placeholder={ directCodex ? '告诉陶泥儿接下来要做什么,或输入 @ 选择资源' - : '告诉项目总控接下来要做什么,或输入 @ 选择资源' + : planningSurfaceActive + ? '' + : '告诉项目总控接下来要做什么,或输入 @ 选择资源' } onChange={onChatInputChange} /> diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index 510d73029..310faa120 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts @@ -577,6 +577,21 @@ export function registerPlanGddApprovalTests() { expect(screen.queryByText(/计划 \d+\/\d+/)).toBeNull(); }); + it('keeps the chat composer but hides its placeholder in the planning lane', async () => { + const harness = createProjectSupervisorRuntimeHarness({ + planningV2Result: planningV2WorkingResult(), + }); + await mountPlanningSurface(harness); + + expect(screen.getByRole('textbox', { name: '项目需求' })).not.toBeNull(); + expect( + screen.queryByText('告诉策划 Agent 接下来要做什么,或输入 @ 选择资源'), + ).toBeNull(); + expect( + screen.queryByText('告诉项目总控接下来要做什么,或输入 @ 选择资源'), + ).toBeNull(); + }); + it('still surfaces the clarification card on the planning lane', async () => { // 澄清卡是 V2 策划链路唯一需要用户动手的交互面之一。 const harness = createProjectSupervisorRuntimeHarness({ From e90f1473c6b85ef1c24c6c2b8b72d58ecffc10f2 Mon Sep 17 00:00:00 2001 From: Git Hooks Test Date: Wed, 16 Sep 2026 02:30:10 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8DDirectProject=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=9B=BE=E7=89=87=E6=B3=A8=E5=85=A5=E8=B6=85=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 压缩MCP大图回传并限制恢复图片预算 保留canonical历史并更新恢复文档 --- .../direct_project_history_wire.rs | 100 +++++++++++++++++- .../src-tauri/src/agent/direct_tool_bridge.rs | 75 +++++++++++-- ...ectProject Codex原始历史与异常恢复-2026-09-04.md | 2 +- 3 files changed, 167 insertions(+), 10 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs index 627002154..d903e008b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs @@ -7,17 +7,89 @@ use super::direct_project_history_injection_oversize_error; use serde_json::Value; use std::path::Path; +const DIRECT_PROJECT_HISTORY_IMAGE_TOTAL_MAX_BYTES: usize = 8 * 1024 * 1024; +const DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT: &str = + "[历史图片预览已省略:本次恢复图片预算已用尽]"; + +fn omit_image_block(object: &mut serde_json::Map, text_type: &str) { + object.clear(); + object.insert("type".to_string(), Value::String(text_type.to_string())); + object.insert( + "text".to_string(), + Value::String(DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT.to_string()), + ); +} + +fn compact_history_images(value: &mut Value, remaining_bytes: &mut usize) { + match value { + Value::Array(values) => values + .iter_mut() + .for_each(|value| compact_history_images(value, remaining_bytes)), + Value::Object(object) => { + let is_image_block = object.get("type").and_then(Value::as_str) == Some("image"); + if is_image_block { + if let Some(data) = object.get("data").and_then(Value::as_str) { + if let Some((preview, mime_type)) = crate::agent::compact_mcp_image_data(data) { + if preview.len() > *remaining_bytes { + omit_image_block(object, "text"); + } else { + *remaining_bytes -= preview.len(); + object.insert("data".to_string(), Value::String(preview)); + object.insert( + "mimeType".to_string(), + Value::String(mime_type.to_string()), + ); + } + } + } + } + if object.get("type").and_then(Value::as_str) == Some("input_image") { + if let Some(url) = object + .get("image_url") + .and_then(Value::as_str) + .map(str::to_string) + { + if let Some((header, data)) = url.split_once(",") { + if header.ends_with(";base64") { + if let Some((preview, mime_type)) = + crate::agent::compact_mcp_image_data(data) + { + if preview.len() > *remaining_bytes { + omit_image_block(object, "input_text"); + } else { + *remaining_bytes -= preview.len(); + object.insert( + "image_url".to_string(), + Value::String(format!("data:{mime_type};base64,{preview}")), + ); + } + } + } + } + } + } + object + .values_mut() + .for_each(|value| compact_history_images(value, remaining_bytes)); + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } +} + pub(super) fn build_direct_project_history_injection_params( history_root: &Path, thread_id: &str, ) -> Result { let canonical_items = read_direct_project_history_items_at(history_root) .map_err(platform_llm::LlmError::InvalidRequest)?; + let mut remaining_image_bytes = DIRECT_PROJECT_HISTORY_IMAGE_TOTAL_MAX_BYTES; let items = canonical_items .iter() .map(|item| { - direct_codex_user_item_to_response_item(history_root, item) - .map_err(platform_llm::LlmError::InvalidRequest) + let mut projected = direct_codex_user_item_to_response_item(history_root, item) + .map_err(platform_llm::LlmError::InvalidRequest)?; + compact_history_images(&mut projected, &mut remaining_image_bytes); + Ok(projected) }) .collect::, _>>()?; let params = serde_json::json!({"threadId": thread_id, "items": items}); @@ -30,3 +102,27 @@ pub(super) fn build_direct_project_history_injection_params( } Ok(params) } + +#[cfg(test)] +mod tests { + use super::{compact_history_images, DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT}; + use serde_json::json; + + #[test] + fn history_image_budget_omits_only_wire_preview_when_exhausted() { + let mut item = json!({ + "type": "function_call_output", + "output": {"content": [{ + "type": "image", + "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + "mimeType": "image/png" + }]} + }); + let mut remaining = 1; + compact_history_images(&mut item, &mut remaining); + let block = &item["output"]["content"][0]; + assert_eq!(block["type"], "text"); + assert_eq!(block["text"], DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT); + assert_eq!(remaining, 1); + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 059fd8481..2e3889f46 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -19,6 +19,8 @@ const DIRECT_TOOL_BRIDGE_MAX_WRITE_CONTENT_BYTES: usize = 1_500_000; const DIRECT_TOOL_BRIDGE_MAX_ART_BRIEF_CHARS: usize = 4_000; const DIRECT_TOOL_BRIDGE_MAX_IMAGE_PROMPT_CHARS: usize = 32_000; const DIRECT_TOOL_BRIDGE_MAX_IMAGE_BYTES: u64 = 6 * 1024 * 1024; +const DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_BYTES: usize = 256 * 1024; +const DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_DIMENSION: u32 = 1024; const DIRECT_TOOL_BRIDGE_MAX_SEARCH_QUERY_CHARS: usize = 400; const DIRECT_TOOL_BRIDGE_MAX_SEARCH_RESULTS: usize = 5; const DIRECT_TOOL_BRIDGE_SEARCH_URL: &str = "https://www.bing.com/search?format=rss"; @@ -694,14 +696,44 @@ fn direct_tool_bridge_state_with_search( }) } +/// 将 MCP 图片 block 限制为可安全回显和持久化的预览。 +/// +/// 工具结果会被 Codex 原样写入 DirectProject 历史;这里保留小图的原始 +/// PNG,大图则缩放并转成 JPEG。项目文件中的原图不受影响,历史恢复仍有 +/// 可见证据,但不会把多张几 MiB 的截图永久复制进上下文。 +pub(crate) fn compact_mcp_image_data(data: &str) -> Option<(String, &'static str)> { + let bytes = BASE64_STANDARD.decode(data).ok()?; + if bytes.is_empty() { + return None; + } + if bytes.len() <= DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_BYTES { + return Some((data.to_string(), "image/png")); + } + + let image = image::load_from_memory(&bytes).ok()?; + let mut preview = image.thumbnail( + DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_DIMENSION, + DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_DIMENSION, + ); + for (dimension, quality) in [(1024, 78), (768, 70), (512, 60), (384, 50)] { + if preview.width() > dimension || preview.height() > dimension { + preview = image.thumbnail(dimension, dimension); + } + let mut encoded = Vec::new(); + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded, quality); + preview.write_with_encoder(encoder).ok()?; + if encoded.len() <= DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_BYTES { + return Some((BASE64_STANDARD.encode(encoded), "image/jpeg")); + } + } + None +} + fn bridge_tool_result(text: String, images: Vec, is_error: bool) -> Value { let mut content = vec![json!({ "type": "text", "text": text })]; - content.extend(images.into_iter().map(|data| { - json!({ - "type": "image", - "data": data, - "mimeType": "image/png" - }) + content.extend(images.into_iter().filter_map(|data| { + let (data, mime_type) = compact_mcp_image_data(&data).unwrap_or((data, "image/png")); + Some(json!({ "type": "image", "data": data, "mimeType": mime_type })) })); json!({ "content": content, "isError": is_error }) } @@ -2672,7 +2704,7 @@ pub(crate) async fn start_direct_tool_bridge( #[cfg(test)] mod tests { use super::*; - use std::io::{Read, Write}; + use std::io::{Cursor, Read, Write}; #[tokio::test] async fn controlled_search_client_omits_agc_marker() { @@ -2766,6 +2798,35 @@ mod tests { assert!(bridge_search_max_results(&json!({ "maxResults": 6 })).is_err()); } + #[test] + fn large_mcp_images_are_reduced_to_bounded_jpeg_previews() { + let image = image::RgbaImage::from_fn(1600, 1200, |x, y| { + image::Rgba([ + (x % 251) as u8, + (y % 251) as u8, + ((x.wrapping_mul(31) + y.wrapping_mul(17)) % 251) as u8, + u8::MAX, + ]) + }); + let mut png = Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(image) + .write_to(&mut png, image::ImageFormat::Png) + .expect("encode image fixture"); + assert!(png.get_ref().len() > DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_BYTES); + + let (preview, mime_type) = + compact_mcp_image_data(&BASE64_STANDARD.encode(png.into_inner())) + .expect("large valid image should produce preview"); + assert_eq!(mime_type, "image/jpeg"); + assert!( + BASE64_STANDARD + .decode(preview) + .expect("preview base64") + .len() + <= DIRECT_TOOL_BRIDGE_IMAGE_PREVIEW_MAX_BYTES + ); + } + #[test] fn search_parser_accepts_only_bounded_public_https_results() { let body = r#"Tauri & Rusthttps://tauri.app/<b>Cross-platform apps</b>Privatehttp://127.0.0.1:8082/privateprivateCredentialshttps://user:pass@example.test/pathprivateLoopback hosthttps://localhost/privateprivateLocal hosthttps://service.internal/privateprivate"#; diff --git a/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md b/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md index 45accf8b4..a2b266ecb 100644 --- a/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md +++ b/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md @@ -49,7 +49,7 @@ Codex 启动时注入的 `host_skills.instructions`、`permissions.instructions` ## 恢复 -创建新的 ephemeral thread 后,读取 `project.jsonl` 中所有 `response_item.payload`,按文件行顺序一次调用 `thread/inject_items`,再执行新的 `turn/start`。Codex 负责上下文窗口管理;注入失败直接失败,AGC 不截断、摘要或改写历史。新 thread 已进入连接池但历史读取或注入失败时,必须先从池中淘汰并取消订阅该 thread,重试只能创建新 thread 并重新注入。 +创建新的 ephemeral thread 后,读取 `project.jsonl` 中所有 `response_item.payload`,按文件行顺序一次调用 `thread/inject_items`,再执行新的 `turn/start`。Codex 负责上下文窗口管理。磁盘上的 canonical 历史永不改写;恢复 wire 载荷会把 MCP `image` block(以及 `input_image` data URL)转换为每张最多 `256 KiB` 的 JPEG 预览,整次恢复图片预算为 `8 MiB`,保留图片证据并避免旧项目把完整 PNG Base64 重复注入。预算耗尽的图片只在 wire 载荷中替换为省略标记。工具新回传图片也在进入 Codex 前执行同一预览上限。除图片二进制预览外,不截断、摘要或改写历史;若其它内容仍超过单行上限,继续失败关闭并指出 `itemId`。新 thread 已进入连接池但历史读取或注入失败时,必须先从池中淘汰并取消订阅该 thread,重试只能创建新 thread 并重新注入。 `clientUserMessageId` 仅作为 Codex 用户消息的稳定标识随 `turn/start` 发送,不等价于 turn 级 exactly-once 幂等。断线后的重试仍须由项目侧持久化 turn ledger 或服务端去重合同决定,不能仅凭该字段再次执行。 From d8a167e0f3bea4e58626f2c01564c832dcfe1da1 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 15 Sep 2026 18:33:37 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E5=88=92?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=A6=96=E6=9D=A1=E5=AF=B9=E8=AF=9D=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E9=94=81=E7=AB=9E=E4=BA=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目对话保存改为使用有界等待项目写锁 --- apps/ai-game-creator-shell/src-tauri/src/commands.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 6b00b2ff1..9de008de2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -5170,7 +5170,12 @@ pub(crate) fn create_game_creator_agent_session( let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.read")?; enforce_project_permission_policy(root, "conversation.write")?; - let _lock = acquire_project_write_lock(root, "conversation.write")?; + // 首轮策划消息可能紧跟项目初始化写入到达;对话保存应等待这段短暂的 + // 项目锁竞争,避免把可恢复的初始化竞态直接显示成保存失败。 + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + root, + "conversation.write", + )?; create_game_creator_agent_session_at(root, agent_id.trim(), title.trim()) } From d650bf9b7496f3c2bd2fc27baaaada777da31703 Mon Sep 17 00:00:00 2001 From: Git Hooks Test Date: Wed, 16 Sep 2026 01:54:29 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=85=A5=E5=8F=A3=E4=B8=8E=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E5=85=91=E6=8D=A2=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 DirectProject 提交时的项目快照空值类型错误 将运行中项目列表移入窗口标题栏并支持展开查看 增加项目内打开项目目录按钮 在账户资产条中直接显示兑换码入口并保留兑换弹窗 补充相关测试、类型检查与产品文档 --- apps/ai-game-creator-shell/src/App.tsx | 11 +- .../src/components/WindowChrome.tsx | 46 +++- .../src/components/windowChromeContext.ts | 15 ++ .../src/features/app-shell/AccountWallet.tsx | 33 +++ .../app-shell/ActiveProjectRunsPanel.tsx | 127 +++++++++- .../features/app-shell/WorkspaceLauncher.tsx | 42 +++- .../features/app-shell/useAccountWallet.ts | 61 +++++ .../src/services/clientApi.ts | 13 + apps/ai-game-creator-shell/src/styles.css | 233 +++++++++++++++++- .../src/view/project-development/index.tsx | 14 ++ .../tests/WindowChrome.test.tsx | 49 ++++ .../tests/directActiveTurns.test.tsx | 38 +++ ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 4 + ...合跨页面生命周期与运行中项目可见性-2026-09-15.md | 2 +- ...合跨页面生命周期与运行中项目可见性-2026-09-15.md | 4 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- .../index.test.tsx | 19 ++ .../PlatformMudPointWalletEntry/index.tsx | 33 +++ 18 files changed, 712 insertions(+), 34 deletions(-) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 7d0e734d6..5c353d32e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -6528,8 +6528,15 @@ export function App({ // The legacy Supervisor/harness path remains below for rollback and tests. if (directCodexProductRuntime) { const directInvoke = resolveTauriInvoke(); - const directProjectPath = resolveChatProjectPath(localProject); - if (directProjectPath && directInvoke) { + // Capture the project snapshot before any asynchronous policy/session work. + // `resolveChatProjectPath` only returns a path and TypeScript cannot infer + // that the source project is still non-null after an await; keeping the + // immutable snapshot also prevents a project switch from changing the + // projectId used by this turn halfway through submission. + const directProject = localProject; + const directProjectPath = resolveChatProjectPath(directProject); + const directProjectId = directProject?.manifest.projectId; + if (directProjectPath && directProjectId && directInvoke) { const clientTurnId = directConversationTurnId ?? createDirectCodexConversationTurnId(); const effectiveUserItem = diff --git a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx index a9c5e2f29..948cf54a0 100644 --- a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx +++ b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx @@ -3,12 +3,14 @@ import { Copy, Minus, Square, X } from 'lucide-react'; import { type ReactNode, useCallback, useEffect, useState } from 'react'; import brandIcon from '../../../../packages/shared/src/icons/taonier-product-ip.png'; +import { ActiveProjectRunsPanel } from '../features/app-shell/ActiveProjectRunsPanel'; import { subscribeTauriEvent } from '../services/tauriEventSubscription'; import { AppUpdateNotice } from './AppUpdateNotice'; import { WINDOW_CHROME_DEFAULT_TITLE, WindowChromeContext, type WindowChromeContextValue, + type WindowChromeActiveProjectRuns, } from './windowChromeContext'; type WindowChromeProps = { @@ -39,6 +41,8 @@ function getNativeWindow() { export function WindowChrome({ children }: WindowChromeProps) { const [title, setTitleState] = useState(WINDOW_CHROME_DEFAULT_TITLE); const [walletSlot, setWalletSlot] = useState(null); + const [activeProjectRuns, setActiveProjectRuns] = + useState(null); const setTitle = useCallback((nextTitle: string | null | undefined) => { const normalizedTitle = nextTitle?.trim(); @@ -50,6 +54,8 @@ export function WindowChrome({ children }: WindowChromeProps) { title, setTitle, walletSlot, + activeProjectRuns, + setActiveProjectRuns, }; const [isMaximized, setIsMaximized] = useState(false); @@ -142,19 +148,33 @@ export function WindowChrome({ children }: WindowChromeProps) {
- - +
+ {activeProjectRuns && + (activeProjectRuns.activeTurns.length > 0 || + activeProjectRuns.readFailed) ? ( + + ) : ( + <> +
diff --git a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts index e84a79830..5e9141404 100644 --- a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts +++ b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts @@ -1,5 +1,7 @@ import { createContext, useContext } from 'react'; +import type { GameCreatorDirectActiveTurn } from '../app/types'; + export const WINDOW_CHROME_DEFAULT_TITLE = '创作工作台'; export type WindowChromeContextValue = { @@ -7,6 +9,17 @@ export type WindowChromeContextValue = { title: string; setTitle: (title: string | null | undefined) => void; walletSlot: HTMLElement | null; + activeProjectRuns: WindowChromeActiveProjectRuns | null; + setActiveProjectRuns: ( + activeProjectRuns: WindowChromeActiveProjectRuns | null, + ) => void; +}; + +export type WindowChromeActiveProjectRuns = { + activeTurns: GameCreatorDirectActiveTurn[]; + currentProjectPath?: string | null; + readFailed?: boolean; + onOpenProject?: (projectPath: string) => void; }; export const WindowChromeContext = createContext({ @@ -14,6 +27,8 @@ export const WindowChromeContext = createContext({ title: WINDOW_CHROME_DEFAULT_TITLE, setTitle: () => undefined, walletSlot: null, + activeProjectRuns: null, + setActiveProjectRuns: () => undefined, }); export function useWindowChrome() { diff --git a/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx b/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx index 3a9933491..33096e243 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx @@ -1,6 +1,7 @@ import { PlatformMudPointWalletEntry } from '../../../../../packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformProfileRechargeModal } from '../../../../../packages/shared/src/components/PlatformProfileRechargeModal'; import { PlatformProfileWalletLedgerModal } from '../../../../../packages/shared/src/components/PlatformProfileWalletLedgerModal'; +import { ThemedModal } from '../../components/modal/ThemedModal'; import type { AccountWalletController } from './useAccountWallet'; export function AccountWalletBar({ @@ -21,6 +22,7 @@ export function AccountWalletBar({ onRequestDetails={() => void controller.onWalletBalanceMayHaveChanged()} onRecharge={controller.openRecharge} onOpenLedger={controller.openWalletLedger} + onRedeemCode={controller.openRedeemCode} />
); @@ -63,6 +65,37 @@ export function AccountWalletDialogs({ onRetry={() => void controller.loadWalletLedger()} /> ) : null} + +
+ 兑换码 + +
+
{ + event.preventDefault(); + void controller.redeemCode(); + }} + > + controller.setRedeemCodeInput(event.target.value)} + placeholder="输入兑换码" + aria-label="兑换码" + autoFocus + /> + {controller.redeemCodeError ?

{controller.redeemCodeError}

: null} + {controller.redeemCodeSuccess ?

{controller.redeemCodeSuccess}

: null} + +
+
); } diff --git a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx index 4147abb9b..c51a6fa49 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx @@ -1,9 +1,12 @@ +import { ChevronDown } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + import type { GameCreatorDirectActiveTurn } from '../../app/types'; import { projectNameFromPath } from '../agent-runtime'; import { projectPathsMatchForInvalidation } from '../project-summary/projectPath'; /** - * 左上角的"正在运行的项目"面板。 + * 窗口标题栏的"正在运行的项目"入口,也保留面板布局供独立组件测试和复用。 * * 数据来自 Rust 的活动回合注册表(同一个只读快照也用于重新进入项目时的进度重连), * 面板只负责呈现:项目名、阶段、已运行时长,以及点击进入该项目。没有在跑回合时 @@ -14,6 +17,7 @@ export type ActiveProjectRunsPanelProps = { currentProjectPath?: string | null; readFailed?: boolean; onOpenProject?: (projectPath: string) => void; + placement?: 'panel' | 'titlebar'; }; const ACTIVE_TURN_STATUS_LABELS: Record = { @@ -56,11 +60,47 @@ export function ActiveProjectRunsPanel({ currentProjectPath = null, readFailed = false, onOpenProject, + placement = 'panel', }: ActiveProjectRunsPanelProps) { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + if (!open || placement !== 'titlebar') { + return; + } + const handlePointerDown = (event: PointerEvent) => { + if (!menuRef.current?.contains(event.target as Node)) { + setOpen(false); + } + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpen(false); + } + }; + document.addEventListener('pointerdown', handlePointerDown); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('pointerdown', handlePointerDown); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [open, placement]); + if (activeTurns.length === 0) { if (!readFailed) { return null; } + if (placement === 'titlebar') { + return ( + + 正在运行的项目读取失败 + + ); + } // 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。 return (