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 } => {