From a59d068c1d641c95ab5bc3f11bbea0f9c092acf1 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:47:36 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E7=BB=99=20Direct=20=E5=9B=9E=E5=90=88?= =?UTF-8?q?=E7=9A=84=E9=9D=99=E9=BB=98=E6=97=A9=E9=80=80=E8=A1=A5=E4=B8=8A?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TurnReservation 兜底收口时写应用日志,并用 thread::panicking 区分 panic 与 future 被丢弃/取消 新增 Direct 回合 panic hook:task-local 携带回合身份,panic 时记录位置与负载 两处均 cfg(not(test)),不污染用例与本机 AppData --- .../src/agent/thread_manager/dispatch.rs | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 5eabb23b6..4bfcadd2e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -84,13 +84,80 @@ impl Drop for TurnReservation { fn drop(&mut self) { // 兜底:任务 panic、future 被丢弃、或今后在终态之前新增的 `?` 早退。 // 这类失败说不出原因,只给分类;能说清原因的错误必须由调用方在更早的地方显式收口。 - let _ = self.finish_if_unfinished(DirectTurnTerminal::host_dropped()); + let finalized_by_guard = self.finish_if_unfinished(DirectTurnTerminal::host_dropped()); + // 兜底一旦真的收口,就说明这一轮**从未写下终态**:深层既没成功也没失败地退出了。 + // 这条必须留应用日志,否则离线只剩一个 `phase=working` 的账本,无从判断是哪一层 + // 提前退出(真实案例:2026-10-02 连续两轮只留下 working 账本,errors/ 与 + // application.log 全空)。测试态不写,避免用例污染本机 AppData。 + #[cfg(not(test))] + if finalized_by_guard { + let _ = crate::append_application_log( + "error".to_string(), + "direct-turn".to_string(), + format!( + "agent.direct_turn.host_dropped threadId={} turnToken={} userItemId={} panicking={}", + self.thread_id, + self.token, + self.user_item_id.as_deref().unwrap_or("-"), + // true = 这一轮是被 panic 带走的;false = future 被丢弃或取消。 + std::thread::panicking() + ), + ); + } // 占用的释放就是"这个项目腾出了跑回合的位置":踢一脚,队里排着的消息不必等下一次用户动作。 // panic 也走这里,所以队列不会因为一个任务炸掉而永久停住。 kick_queue_dispatch(Path::new(&self.thread_id)); } } +// 运行段经 task-local 携带回合身份,panic hook 据此把 panic 位置与负载写进应用日志。 +// task-local 而非 thread-local:多线程 runtime 下 future 会跨 worker 迁移。 +tokio::task_local! { + static DIRECT_TURN_PANIC_CONTEXT: Option<(String, String)>; +} + +/// 兜底诊断:Direct 回合任务 panic 时,把位置与负载写进应用日志。 +/// +/// 没有它时,panic 只会让 `TurnReservation::drop` 把回合收成 `HostDropped`——那是"说不出原因" +/// 的分类,离线只留一个 `phase=working` 的账本,`.agent/runtime/errors/` 与 `application.log` +/// 全空,无法判断是哪一层退出的(真实案例:2026-10-02 连续两轮如此)。 +fn ensure_direct_turn_panic_hook() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + if let Ok(Some((thread_id, token))) = DIRECT_TURN_PANIC_CONTEXT.try_with(Clone::clone) { + let location = info + .location() + .map(|location| { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + }) + .unwrap_or_else(|| "未知位置".to_string()); + #[cfg(not(test))] + let _ = crate::append_application_log( + "error".to_string(), + "direct-turn".to_string(), + format!( + "agent.direct_turn.panic threadId={} turnToken={} location={} payload={}", + thread_id, + token, + location, + info.payload_as_str().unwrap_or("未知 panic 负载") + ), + ); + #[cfg(test)] + let _ = (thread_id, token, location); + } + previous(info); + })); + }); +} + /// 踢一脚:让队首那条待发消息有机会变成一轮真的回合。 /// /// 幂等,三个调用点:**入队之后**(队列空且没有回合在跑时,放行就是这一脚,不必再等一个调度周期)、 @@ -109,10 +176,12 @@ pub(crate) fn kick_queue_dispatch(root: &Path) { crate::platform_session::current_platform_session_write_state().identity_generation; let reservation = TurnReservation::resume(&thread_id, &dispatched); let root = root.to_path_buf(); - tauri::async_runtime::spawn(async move { + ensure_direct_turn_panic_hook(); + let panic_context = Some((thread_id.clone(), dispatched.token.clone())); + tauri::async_runtime::spawn(DIRECT_TURN_PANIC_CONTEXT.scope(panic_context, async move { run_dispatched_direct_turn(root, dispatched, reservation, release_identity_generation) .await; - }); + })); } /// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。 From a6428228d1a13c8cdf1b98d43eef718340445f15 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:51:52 +0800 Subject: [PATCH 2/6] =?UTF-8?q?Direct=20=E5=9B=9E=E5=90=88=20panic=20?= =?UTF-8?q?=E8=BD=AC=E6=88=90=E5=8F=AF=E8=AF=8A=E6=96=AD=E7=9A=84=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatch 层用 AssertUnwindSafe/catch_unwind 接住回合体 panic,不再落到 HostDropped 兜底 panic 负载转成直接可读文本并写进脱敏诊断与应用日志,终态改为分类失败 补齐 futures::FutureExt 导入 --- .../src/agent/thread_manager/dispatch.rs | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 4bfcadd2e..9e8a43357 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -13,13 +13,16 @@ use std::path::{Path, PathBuf}; +use futures::FutureExt; + #[cfg(test)] use crate::agent::PendingTurn; use crate::agent::{ append_direct_project_user_message_at, claim_pending_turn, complete_turn_if_reserved, - direct_codex_user_item_to_prompt, direct_now_ms, redact_agent_runtime_error, - run_direct_game_creator_turn_at_with_creation_type_and_emitter, thread_id_for_project, - DirectGameCreatorTurnUpdateEmitter, DirectTurnError, DirectTurnTerminal, DispatchedTurn, + direct_codex_user_item_to_prompt, direct_now_ms, record_direct_codex_failure, + redact_agent_runtime_error, run_direct_game_creator_turn_at_with_creation_type_and_emitter, + thread_id_for_project, DirectCodexFailureStage, DirectGameCreatorTurnUpdateEmitter, + DirectTurnError, DirectTurnTerminal, DispatchedTurn, }; /// 一次放行的占用。持有它就代表这一轮还没收口。 @@ -184,6 +187,17 @@ pub(crate) fn kick_queue_dispatch(root: &Path) { })); } +/// 把 panic 负载转成可读文本:`panic!("…")` 的负载是 `&str`,`panic!("{x}")` 是 `String`。 +fn direct_turn_panic_detail(payload: &(dyn std::any::Any + Send)) -> String { + if let Some(text) = payload.downcast_ref::<&str>() { + (*text).to_string() + } else if let Some(text) = payload.downcast_ref::() { + text.clone() + } else { + "未知 panic 负载".to_string() + } +} + /// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。 /// /// 这一段的每个失败都是**回合失败**:占用与开始事件已经写好,只走占用对象的终态出口,不再回到任何 @@ -218,16 +232,38 @@ async fn run_dispatched_direct_turn( crate::agent::codex_app_server::emit_direct_thread_user_item(&root, &canonical_user_item); let capture = crate::analytics::gui::capture_writer_context(); let emitter = DirectGameCreatorTurnUpdateEmitter::new(&root, turn_id); - let outcome = run_direct_game_creator_turn_at_with_creation_type_and_emitter( - &root, - &prompt, - dispatched.pending.creation_type.as_deref(), - Some(&emitter), - Some(canonical_user_item.clone()), - capture, - release_identity_generation, + // 回合体 panic 不能落到 `TurnReservation` 的 Drop 兜底:那会把这一轮收成"说不出原因"的 + // `HostDropped`,症状正是用户看到的「本轮执行已中断,请重试」,而项目侧诊断与应用日志 + // **一行都不会有**(真实案例:2026-10-02 连续两轮)。这里把 panic 转成分类失败, + // 走与正常失败同一条收口:先写脱敏诊断,再写终态。 + let outcome = match std::panic::AssertUnwindSafe( + run_direct_game_creator_turn_at_with_creation_type_and_emitter( + &root, + &prompt, + dispatched.pending.creation_type.as_deref(), + Some(&emitter), + Some(canonical_user_item.clone()), + capture, + release_identity_generation, + ), ) - .await; + .catch_unwind() + .await + { + Ok(result) => result, + Err(payload) => { + let failure = DirectTurnError::turn_failed( + DirectCodexFailureStage::CodeGeneration, + format!( + "DirectProject 宿主任务 panic:{}", + direct_turn_panic_detail(payload.as_ref()) + ), + ); + let stage = failure.turn_failure_stage(); + let detail = record_direct_codex_failure(&root, &failure, Some(turn_id.as_str())); + Err(DirectTurnError::TurnFailed { stage, detail }) + } + }; match outcome { Ok(_) => { // 深层的终态出口已经在 `run_turn` 里写出 `turn.completed`;这里只补最后一条回合更新。 From b8a659c12f4f67b7c1e04583028d014d144a228d Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:54:50 +0800 Subject: [PATCH 3/6] =?UTF-8?q?Direct=20=E5=9B=9E=E5=90=88=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E6=94=B6=E5=8F=A3=E5=90=8C=E6=97=B6=E5=86=99=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=BE=A7=E8=84=B1=E6=95=8F=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TurnReservation 兜底收口时除应用日志外,再写一份 .agent/runtime/errors 脱敏诊断 诊断正文带 threadId 与 turnToken,便于与账本、运行日志对账 --- .../src-tauri/src/agent/thread_manager/dispatch.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 9e8a43357..be5bc35cb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -94,6 +94,16 @@ impl Drop for TurnReservation { // application.log 全空)。测试态不写,避免用例污染本机 AppData。 #[cfg(not(test))] if finalized_by_guard { + // 项目侧也留一份脱敏诊断:用户在项目目录里就能看到这一轮的收场, + // 不必只依赖 AppData 的应用日志。 + let failure = DirectTurnError::TurnFailed { + stage: DirectCodexFailureStage::CodeGeneration, + detail: format!( + "DirectProject 宿主任务提前结束(panic、future 被丢弃或被取消),本轮未写下终态;threadId={} turnToken={}", + self.thread_id, self.token + ), + }; + let _ = record_direct_codex_failure(Path::new(&self.thread_id), &failure, None); let _ = crate::append_application_log( "error".to_string(), "direct-turn".to_string(), From 35af46dd94fb69cfe55cda6067c8d6cefb9a0efb Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:56:52 +0800 Subject: [PATCH 4/6] =?UTF-8?q?Direct=20=E5=9B=9E=E5=90=88=E5=9C=A8=20begi?= =?UTF-8?q?n=20=E4=B9=8B=E5=90=8E=E8=A1=A5=E5=88=86=E6=AE=B5=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 direct_turn_trace:按 stage 写应用日志,定位回合在 begin 与执行器之间停在哪一步 覆盖 session-ready / run-analytics-accepted / goal-analytics-accepted / system-prompt-ready / executor-dispatch 仅在 cfg(not(test)) 生效,不污染用例与本机 AppData --- .../src-tauri/src/agent/direct_runtime/mod.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index f5059ed82..35ef1db4d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -4294,6 +4294,24 @@ pub(crate) fn check_direct_turn_preconditions( .map_err(|detail| DirectTurnError::InputRejected { detail })?; Ok(()) } +/// Direct 回合在 `direct_execution::begin` 之后的分段 trace。 +/// +/// 起因:2026-10-02 连续四轮回合都停在 `begin` 之后——账本 `phase=working`、 +/// `nextSequence=0`、`active={}`、没有任何错误 sidecar、stderr 无 panic、连 +/// `agent.direct_codex.*` 都没有。窗口就夹在 `begin` 与执行器调用之间,需要知道 +/// 具体停在哪一步。测试态不写,避免污染本机 AppData。 +#[cfg(not(test))] +fn direct_turn_trace(stage: &str) { + let _ = crate::append_application_log( + "info".to_string(), + "direct-turn".to_string(), + format!("agent.direct_turn.trace stage={stage}"), + ); +} + +#[cfg(test)] +fn direct_turn_trace(_stage: &str) {} + pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter( root: &Path, prompt: &str, @@ -4401,6 +4419,7 @@ async fn run_direct_game_creator_turn_inner( })?; let execution_session = execution_guard.session(); execution_session.set_analytics_capture(capture.clone()); + direct_turn_trace("session-ready"); let started = execution_session .newly_accepted .then(std::time::Instant::now); @@ -4411,6 +4430,7 @@ async fn run_direct_game_creator_turn_inner( } } } + direct_turn_trace("run-analytics-accepted"); // 在 guard 仍存活时冻结整体结果,避免 Drop 的中断收尾覆盖真实失败原因。 let result: Result = async { if let Some(report) = super::direct_delivery::terminal_report(&execution_session) { @@ -4444,6 +4464,7 @@ async fn run_direct_game_creator_turn_inner( ); } } + direct_turn_trace("goal-analytics-accepted"); emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息"); if let Some(emitter) = turn_emitter { emitter.emit("running", Some("preparing")); @@ -4460,6 +4481,7 @@ async fn run_direct_game_creator_turn_inner( DirectTurnError::turn_failed(DirectCodexFailureStage::CodeGeneration, error) }, )?; + direct_turn_trace("system-prompt-ready"); // 三维请求:把"自选三维技术栈、解除 Phaser 固定约束"的合同放在系统提示最前, // 避免被长度上限截断,也不阻断任何工具。 let engine_contract = direct_engine_three_dimensional_contract(root, prompt); @@ -4471,6 +4493,7 @@ async fn run_direct_game_creator_turn_inner( None => base_system_prompt, }; let reply = if let Some(emitter) = turn_emitter { + direct_turn_trace("executor-dispatch"); let client_turn_id = emitter.turn_id().to_string(); let emitter = emitter.clone(); let mut observer = |observation: DirectCodexTurnObservation| { From 50925d96eb34ea3a0639fe19b081df0147c60ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 13:46:33 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E7=BB=9F=E4=B8=80=20AGC=20launcher=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E9=AB=98=E5=BA=A6=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B8=AE=E5=8A=A9=E9=A1=B5=E5=BA=95=E9=83=A8=E8=A2=AB=E8=A3=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - styles.css:新增页面高度契约,.launcher-main 改纵向 flex 列、页面根节点 flex: 1 1 auto,帮助页自带滚动 - src/view/home/index.tsx:首页根节点去掉 h-screen 与 h-[calc(100vh-32px)],改用外壳分配高度的钩子类 - src/view/template-library/index.tsx:模板库去掉 JS 量父级高度与内联 height,改用外壳分配高度的钩子类 - decision-log.md、pitfalls.md:记录页面高度契约与自绘标题栏里自己算 100vh 的排障口径 Co-authored-by: Junie --- apps/ai-game-creator-shell/src/styles.css | 27 ++++++++++++++ .../src/view/home/index.tsx | 6 +-- .../src/view/template-library/index.tsx | 37 ++----------------- .../shared-memory/decision-log.md | 8 ++++ docs/project-memory/shared-memory/pitfalls.md | 7 ++++ 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 86fb836d4..74d8e5e41 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -1332,6 +1332,19 @@ textarea { overflow: hidden; } +/* + * 页面高度契约:`.launcher-main` 是纵向 flex 列,页面根节点自己 `flex: 1 1 auto` 吃掉 + * 舞台剩余高度(有横幅时自动少掉横幅那一份),页面内部的 `height: 100%`、内层滚动容器 + * 才有确定高度可用。 + * + * 页面根节点上不要再写 `100vh` / `100dvh` / `calc(100vh - 32px)`:整窗高度含自绘标题栏, + * 比真实舞台高一个标题栏(窗口 100vh=800、舞台 750),底部会被裁掉,横幅也会被重复扣一次。 + * 窗口外壳用 `height: 100%` 覆盖这里的 `100dvh`,让页面贴合 `.window-chrome__content` + * 的真实舞台;没有窗口外壳时 `100dvh` 才是正确值。 + */ +.launcher-main:has(.launcher-home-page), +.launcher-main:has(.launcher-help-page), +.launcher-main:has(.launcher-template-library), .launcher-main:has(.launcher-projects-page) { display: flex; flex-direction: column; @@ -1340,6 +1353,20 @@ textarea { padding-bottom: 0; } +.launcher-main > .launcher-home-page, +.launcher-main > .launcher-help-page, +.launcher-main > .launcher-template-library { + flex: 1 1 auto; + height: auto; + min-height: 0; +} + +/* 帮助页没有内层滚动容器:占满剩余高度后自己滚动,底部留出与外壳一致的呼吸位。 */ +.launcher-main > .launcher-help-page { + overflow-y: auto; + padding-bottom: 42px; +} + .launcher-page header { display: flex; align-items: flex-end; diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index 79fbbc52a..64307288d 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -209,11 +209,7 @@ export default function HomeView({ } return ( -
+
(null); const viewportRef = useRef(null); - const sectionRef = useRef(null); - const [sectionHeight, setSectionHeight] = useState(null); const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 }); const showEmptyLibrary = status === 'ready' && templates.length === 0; @@ -162,35 +160,10 @@ export default function TemplateLibraryView({ const showGrid = status === 'ready' && visibleTemplates.length > 0; /** - * 页面高度按**父级实测高度**定,不用百分比也不用 100vh。 - * - * 外壳样式 `.launcher-main > .platform-theme { height: 100% }` 特异性高于 Tailwind - * 工具类,而它的百分比在 `.launcher-shell { min-height: 100vh }` 这条链上是不定高, - * 页面会退化成内容高度(虚拟网格视口高度 0、卡片区空白);`100vh` 又比真实舞台高 - * 一个标题栏(窗口 100vh=800、舞台 750),底部会被裁掉。这里直接量父级。 + * 页面高度不再自己量:`.launcher-main:has(.launcher-template-library)` 把外壳改成纵向 + * flex 列,`flex: 1 1 auto` 让这一页拿到确定高度(有横幅时自动少掉横幅那一份), + * 下面的网格视口才能用 `flex-1` 量出真实尺寸。 */ - useEffect(() => { - const section = sectionRef.current; - const parent = section?.parentElement; - if (!section || !parent) { - return; - } - const apply = () => { - const height = Math.round(parent.getBoundingClientRect().height); - if (height > 0) { - setSectionHeight((current) => (current === height ? current : height)); - } - }; - apply(); - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', apply); - return () => window.removeEventListener('resize', apply); - } - const observer = new ResizeObserver(apply); - observer.observe(parent); - return () => observer.disconnect(); - }, []); - useEffect(() => { const element = viewportRef.current; if (!element) { @@ -279,9 +252,7 @@ export default function TemplateLibraryView({ return (
diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e1c5c9503..8b51f9a89 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-10-02 launcher 页面高度契约:外壳分高度,页面不再自己算窗口高度 + +- 背景:PR #228(`6d2c275d3`)只给项目页补了「外壳纵向 flex + 页面 `flex: 1 1 auto`」的高度修复;其余页面仍各自算高度——帮助页没写高度也没有内层滚动容器,内容一长就被外壳 `overflow: hidden` 裁掉且无法滚动;首页用 `h-screen` / `h-[calc(100vh-32px)]`,模板库用 JS 量父级高度写内联 `height`。 +- 决策:把 #228 的修复提升为 `apps/ai-game-creator-shell/src/styles.css` 里的通用契约——`.launcher-main:has(<页面钩子>)` 改纵向 flex 列(`height: 100dvh`,窗口外壳 `height: 100%` 命中时贴合真实舞台;`min-height: 0`、`padding-bottom: 0`),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`;帮助页额外 `overflow-y: auto; padding-bottom: 42px`(它没有内层滚动容器)。契约钩子:`launcher-home-page`、`launcher-help-page`、`launcher-template-library`、`launcher-projects-page`。 +- 边界:页面根节点不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;横幅(`.launcher-promo`)是外壳里的真实行,有横幅就靠 flex 自动少一份,不手算偏移。工作台页(`.game-project-workbench`)在窗口外壳里已有等价契约(`.window-chrome__content ...` 那几条),移动端媒体查询里遗留的 `100dvh` / `calc(100dvh - 32px)` 由外层 `flex: 1 1 auto` 收缩兜住,本次不动。 +- 影响范围:`apps/ai-game-creator-shell/src/styles.css`、`src/view/home/index.tsx`、`src/view/template-library/index.tsx`。 +- 验证:Chromium 真机量测(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅)——首页 / 模板库 / 项目页改动前后高度一致,帮助页从「被裁 112~150px 且无可滚动祖先」变为正好等于 `.window-chrome__content` 高度并可滚动到底;`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`eslint` 通过。 + ## 2026-10-01 游戏广场评分展示边界 - 用户确认广场卡片增加一位小数的 10 分制平均分与评分人数,无有效评价显示“暂无评分”;保留现有排序、筛选、卡片打开详情及返回上下文。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 833613fbe..5387fd6f0 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2,6 +2,13 @@ 这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。 +## 2026-10-02 AGC 页面在自绘标题栏外壳里自己算 `100vh`:底部被裁而且没得滚 + +- **现象**:帮助页(使用指南 / 联系客服 / 更新日志)在矮窗口里底部卡片看不到,把窗口拉高才出现;外壳 `.launcher-main { overflow: hidden }` 之下没有任何可滚动祖先,页面既滚不动也裁得干净。首页在通知横幅出现时用 `h-[calc(100vh-32px)]`,同样把窗口高度当成了舞台高度。 +- **原因**:AGC 桌面外壳是自绘标题栏(`--window-chrome-height`;窗口 100vh=800 时舞台只有 750),页面根节点写 `100vh` / `100dvh` / `calc(100vh - Npx)` 就比真实舞台高一整个标题栏,差额被外壳裁掉;横幅是 `.launcher-main` 里的真实行,再写 `-32px` 等于重复扣一次。帮助页还没有内层滚动容器,连「内容超高就在内部滚动」这条兜底也不存在。 +- **处理(现行口径)**:页面高度只由外壳分配——`apps/ai-game-creator-shell/src/styles.css` 里 `.launcher-main:has(<页面钩子>)` 是纵向 flex 列(`height: 100dvh`,窗口外壳命中 `height: 100%` 时贴合真实舞台),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`,帮助页这类没有内层滚动容器的再加 `overflow-y: auto`。页面根节点一律不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;有横幅就靠 flex 自动少一份,不要手算偏移。 +- **验证**:真机判据是 Vite + Chromium 量页面根节点是否正好等于 `.window-chrome__content` 的高度(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅),帮助页应可滚动到底。 + ## 2026-10-02 固定试玩误判祖先的指针穿透样式 - `pointer-events:none` 不会强制禁用整棵子树;后代显式 `auto` 可以恢复命中。控件探针只检查目标的计算样式,继承未覆盖的 `none` 仍拒绝;可见性、遮挡、disabled 与 inert 保留各自检查。 From 863148e11c1e338c4bd18a5545ef81a3d484febe Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:01:19 +0800 Subject: [PATCH 6/6] =?UTF-8?q?Claude=20Code=20Direct=20=E5=9B=9E=E5=90=88?= =?UTF-8?q?=E8=A1=A5=E5=88=86=E6=AE=B5=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - claude_code_cli:cc Direct 回合在执行器入口、MCP loopback 就绪、sidecar 启动、sidecar 返回、解析 stream 前各写一条 agent.direct_turn.trace - direct_runtime:direct_turn_trace 提升为 pub(crate),让 Claude Code 执行器复用同一份分段留痕 --- .../src-tauri/src/agent/claude_code_cli.rs | 5 +++++ .../src-tauri/src/agent/direct_runtime/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs index 827b06682..9f5b91221 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs @@ -732,7 +732,9 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at( client_turn_id: Option<&str>, observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>, ) -> Result { + direct_turn_trace("claude-executor-enter"); let (mcp_url, mcp_token) = start_external_mcp_loopback(root, llm.web_search_enabled).await?; + direct_turn_trace("claude-mcp-loopback-ready"); let _mcp_guard = ExternalMcpStopGuard; let home = crate::project::resolve_local_project_path(root, ".agent/runtime/claude-code/home")?; std::fs::create_dir_all(&home) @@ -742,6 +744,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at( .lock() .ok() .and_then(|sessions| sessions.get(&session_key).cloned()); + direct_turn_trace("claude-sidecar-start"); let payload = serde_json::json!({ "type": "turn", "id": client_turn_id.unwrap_or("claude-direct-turn"), @@ -769,6 +772,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at( client_turn_id, ) .await?; + direct_turn_trace("claude-sidecar-done"); if let Some(session_id) = sidecar .result .get("session_id") @@ -785,6 +789,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at( .filter_map(|event| serde_json::to_string(&event).ok()) .collect::>(); stream.push(String::new()); + direct_turn_trace("claude-parse-start"); parse_direct_stream_result(stream.join("\n").as_bytes(), observer) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 35ef1db4d..92d673ebb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -4301,7 +4301,7 @@ pub(crate) fn check_direct_turn_preconditions( /// `agent.direct_codex.*` 都没有。窗口就夹在 `begin` 与执行器调用之间,需要知道 /// 具体停在哪一步。测试态不写,避免污染本机 AppData。 #[cfg(not(test))] -fn direct_turn_trace(stage: &str) { +pub(crate) fn direct_turn_trace(stage: &str) { let _ = crate::append_application_log( "info".to_string(), "direct-turn".to_string(), @@ -4310,7 +4310,7 @@ fn direct_turn_trace(stage: &str) { } #[cfg(test)] -fn direct_turn_trace(_stage: &str) {} +pub(crate) fn direct_turn_trace(_stage: &str) {} pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter( root: &Path,