fix(editor): desktop chat session + builtin http handling

This commit is contained in:
Fini 2026-06-21 23:18:20 +08:00
parent df57392a1a
commit ea3f94093c
7 changed files with 54 additions and 8 deletions

View file

@ -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

View file

@ -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};

View file

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

View file

@ -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() {

View file

@ -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

View file

@ -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 系是推理模型,默认把 `<think>…</think>` 注进 `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" }));
}

View file

@ -549,6 +549,9 @@ fn write_error_event<W: Write>(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 } => {