diff --git a/crates/op-host-desktop/Cargo.toml b/crates/op-host-desktop/Cargo.toml
index 3d9435e79..475d4043f 100644
--- a/crates/op-host-desktop/Cargo.toml
+++ b/crates/op-host-desktop/Cargo.toml
@@ -98,6 +98,11 @@ op-mcp = { path = "../op-mcp" }
# S3a design orchestrator — driven by `chat_orchestrator.rs` when a chat
# message classifies as a design request.
op-orchestrator = { path = "../op-orchestrator" }
+# Debug-mode logging: a tracing subscriber over the orchestrator's tracing
+# events (LLM calls + parse failures with raw output). Silent unless RUST_LOG
+# is set — see `init_tracing` in main.rs.
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
# Phase 7 strangler reorg: the transport-free AI chat data shapes
# (`ChatProvider` trait + model catalog + agent-settings state) were
# extracted into op-ai; the `src/chat_*.rs` real transports + the
diff --git a/crates/op-host-desktop/src/chat_session.rs b/crates/op-host-desktop/src/chat_session.rs
index 9444ffe54..6b3de7224 100644
--- a/crates/op-host-desktop/src/chat_session.rs
+++ b/crates/op-host-desktop/src/chat_session.rs
@@ -19,7 +19,7 @@ use op_host_services::chat_canvas_tools::execute_chat_tool;
#[path = "chat_session_launch.rs"]
mod launch;
#[cfg(test)]
-pub(crate) use launch::{builtin_provider_with_tools, clear_fresh_starter_frame_for_design};
+pub(crate) use launch::builtin_provider_with_tools;
pub use launch::{drain_new_chat_request, drain_stop_request, launch_if_pending};
pub(crate) use launch::{provider_for_selected_model, selected_cli_model_id};
diff --git a/crates/op-host-desktop/src/design_session.rs b/crates/op-host-desktop/src/design_session.rs
index 8d319aedb..304bd59c3 100644
--- a/crates/op-host-desktop/src/design_session.rs
+++ b/crates/op-host-desktop/src/design_session.rs
@@ -136,6 +136,15 @@ fn render_progress(progress: &[Progress]) -> String {
fn progress_label(p: &Progress) -> String {
match p {
Progress::Planning => "• Planning…".into(),
+ Progress::Planned { subtasks } => {
+ // Full task checklist upfront (TS parity) — one row per planned
+ // section so the user sees the whole plan immediately.
+ let rows: String = subtasks
+ .iter()
+ .map(|(_, label)| format!("\n ☐ {label}"))
+ .collect();
+ format!("• Plan — {} sections:{}", subtasks.len(), rows)
+ }
Progress::ScaffoldDone => "• Scaffold ready".into(),
Progress::SubtaskStarted { id, label } => format!("• Subtask `{id}` — {label}"),
Progress::SubtaskDone { id, node_count } => {
diff --git a/crates/op-host-desktop/src/main.rs b/crates/op-host-desktop/src/main.rs
index 52e9fb82a..16e4c4cce 100644
--- a/crates/op-host-desktop/src/main.rs
+++ b/crates/op-host-desktop/src/main.rs
@@ -925,6 +925,21 @@ fn prompt_update_available(locale: op_editor_core::Locale, version: &str) {
}
}
+/// Install a stderr tracing subscriber for debug-mode logging — orchestrator
+/// LLM calls + parse failures (with the model's raw output). Writes to stderr
+/// so it never pollutes the `--mcp` stdout JSON-RPC stream. The default `warn`
+/// filter surfaces parse failures with no env var; `RUST_LOG=op_orchestrator=debug`
+/// (and/or `op_host_desktop=debug` for per-subtask design progress) opens the
+/// full firehose.
+fn init_tracing() {
+ use tracing_subscriber::{fmt, EnvFilter};
+ let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
+ let _ = fmt()
+ .with_env_filter(filter)
+ .with_writer(std::io::stderr)
+ .try_init();
+}
+
fn main() {
// Register the brand-logo catalog (omitted from the wasm bundle, embedded in
// this binary) BEFORE any path that can render natively — the GUI app, the
@@ -932,6 +947,7 @@ fn main() {
// simple-icons instead of the unknown-glyph fallback dot. Set-once /
// idempotent.
op_editor_ui::set_brand_catalog(op_host_services::web_static::ICONIFY_BRANDS_JSON);
+ init_tracing();
// `--mcp` / `--mcp-http` swap the GUI for an MCP server mode;
// when one of those ran, exit instead of opening a window.
if mcp_serve::run_cli_if_requested() {
diff --git a/crates/op-host-desktop/src/persistence.rs b/crates/op-host-desktop/src/persistence.rs
index 44bd6c000..fb7daa78f 100644
--- a/crates/op-host-desktop/src/persistence.rs
+++ b/crates/op-host-desktop/src/persistence.rs
@@ -84,6 +84,10 @@ pub fn handle_save_as(
match save_as_dialog(host.editor_state()) {
Ok(Some(path)) => {
crate::settings_io::touch_recent(host, &path);
+ // Mirror handle_save: refresh the in-chrome file name too, not
+ // just the OS window title — without this, first Save As writes
+ // the file but the TopBar keeps showing "Untitled".
+ set_display_name(host, Some(&path));
*current_path = Some(path);
refresh_title(current_path, window);
true
diff --git a/crates/op-host-services/src/chat_builtin_http.rs b/crates/op-host-services/src/chat_builtin_http.rs
index 5b48ba001..44bfa97db 100644
--- a/crates/op-host-services/src/chat_builtin_http.rs
+++ b/crates/op-host-services/src/chat_builtin_http.rs
@@ -225,6 +225,15 @@ fn is_minimax_model(model: &str) -> bool {
m.starts_with("minimax") || m.starts_with("abab")
}
+/// GLM-4.5+/GLM-5.x(智谱 / 方舟 coding plan)同为推理模型:reasoning 走
+/// `reasoning_content`,不关思考会烧光输出预算把 `content` 留空(实测 glm-5.2
+/// 一个设计子任务 thinking_len≈3 万、text_len=0,整段 parse 失败)。它接受和
+/// MiniMax 同样的 `thinking:{type:"disabled"}`(curl 对 ark glm-5.2 验证:关思考后
+/// reasoning_tokens=0、content 为干净 JSON)。按名判定,只对 GLM 下发。
+fn is_glm_model(model: &str) -> bool {
+ model.to_ascii_lowercase().contains("glm")
+}
+
async fn run_openai_chat(
provider: ConfiguredBuiltinProvider,
system_prompt: String,
@@ -257,13 +266,13 @@ async fn run_openai_chat(
"max_tokens": max_output_tokens,
"messages": messages,
});
- // MiniMax M 系是推理模型,默认把 `…` 注进 `content`,既烧光
- // 输出预算(JSON 被截断)又逼解析层去剥 think。当调用方明确要求关思考时
- // (`disable_thinking`,如编排器的设计子任务),在线级关掉(实测确认 MiniMax
- // 接受 `thinking:{type:"disabled"}` 返回干净 content)。普通对话用 `Adaptive`、
- // 不会进这里,保留 M3 推理。仅对 MiniMax 加此字段,不碰别的 openai-compat
- // provider(DeepSeek / 方舟 / Qwen)。
- if disable_thinking && is_minimax_model(&provider.model) {
+ // MiniMax M 系和 GLM-5.x 都是推理模型:不关思考会把 reasoning 烧到占满输出
+ // 预算,JSON content 被截断甚至留空(glm-5.2 实测一个设计子任务 thinking≈3 万
+ // 字符、content 0,整段 parse 失败、重试也撞同一堵墙)。当调用方明确要求关思考
+ // (`disable_thinking`,如编排器的设计子任务),在线级下发 `thinking:{type:"disabled"}`
+ // (实测 MiniMax 与 ark glm-5.2 都接受、返回干净 content)。普通对话走 `Adaptive`
+ // 不进这里,保留推理。只对这两类加此字段,不碰别的 openai-compat(DeepSeek / Qwen)。
+ if disable_thinking && (is_minimax_model(&provider.model) || is_glm_model(&provider.model)) {
if let Some(obj) = body.as_object_mut() {
obj.insert("thinking".into(), json!({ "type": "disabled" }));
}
diff --git a/crates/op-host-services/src/web_chat_standard.rs b/crates/op-host-services/src/web_chat_standard.rs
index 986f98ffb..f3e69dbe2 100644
--- a/crates/op-host-services/src/web_chat_standard.rs
+++ b/crates/op-host-services/src/web_chat_standard.rs
@@ -549,6 +549,9 @@ fn write_error_event(out: &mut W, message: &str) -> std::io::Result<()
fn progress_label(p: &Progress) -> String {
match p {
Progress::Planning => "• Planning…".into(),
+ Progress::Planned { subtasks } => {
+ format!("• Plan — {} section(s)", subtasks.len())
+ }
Progress::ScaffoldDone => "• Scaffold ready".into(),
Progress::SubtaskStarted { id, label } => format!("• Subtask `{id}` — {label}"),
Progress::SubtaskDone { id, node_count } => {