From 63069cf4c9265e155756f69d5cd29b924c0c5dbf Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Fri, 12 Jun 2026 17:32:38 +0800 Subject: [PATCH] fix(acp): replay config observables on cold-start so selectors show current values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session/new drains the bootstrap broadcast (cold_start_emit's Permission/Model/ Mode/ThinkingChanged) before the IDE subscribes, so snapshot replay is the only reliable cold-start source — but build_replay_events only replayed bgTask/crons/tasks/mcp, never the config observables. AgentsMesh's permission selector (and model/mode/thinking console mirrors) showed "—"/empty until the user manually switched. Replay permission_mode/model/mode/thinking from state.agent.observable. thinking reads thinking_config but emits under `thinking` to match the live ThinkingChanged notification's field name. v0.6.2's panel.rs arm fixed only the run_event_loop path (live switching); this covers the session/new cold-start path. --- crates/loopal-acp/src/adapter/snapshot.rs | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/crates/loopal-acp/src/adapter/snapshot.rs b/crates/loopal-acp/src/adapter/snapshot.rs index 30fa7d1e..fde48424 100644 --- a/crates/loopal-acp/src/adapter/snapshot.rs +++ b/crates/loopal-acp/src/adapter/snapshot.rs @@ -99,9 +99,34 @@ fn build_replay_events(session_id: &str, state: &Value) -> Vec<(String, Value)> json!({ "servers": state["mcp_status"] }), )); } + push_config_observables(session_id, state, &mut events); events } +/// Replay the agent's config observables (permission_mode / model / mode / +/// thinking) from `state.agent.observable`. The bootstrap broadcast emits these +/// on agent start, but `session/new` drains that channel before the IDE +/// subscribes — the snapshot is the only cold-start source. `thinking` reads the +/// snapshot's `thinking_config` but emits under `thinking` to match the live +/// `ThinkingChanged` notification's field name. +fn push_config_observables(session_id: &str, state: &Value, events: &mut Vec<(String, Value)>) { + let obs = &state["agent"]["observable"]; + let mut push = |ext_type: &str, raw: &Value| { + if let Some(s) = raw.as_str().filter(|s| !s.is_empty()) { + let data = Value::Object( + [(ext_type.to_string(), Value::String(s.to_string()))] + .into_iter() + .collect(), + ); + events.push(ext_notification(session_id, ext_type, data)); + } + }; + push("permission_mode", &obs["permission_mode"]); + push("model", &obs["model"]); + push("mode", &obs["mode"]); + push("thinking", &obs["thinking_config"]); +} + #[cfg(test)] mod tests { use super::build_replay_events; @@ -151,4 +176,26 @@ mod tests { assert!(!m.contains(&"_loopal/mcp")); assert!(m.contains(&"_loopal/crons")); } + + #[test] + fn replays_config_observables_skipping_empty() { + let state = json!({ + "agent": {"observable": { + "permission_mode": "bypass", "model": "opus", "mode": "", "thinking_config": "auto" + }}, + "bg_tasks": {}, "crons": [], "tasks": [] + }); + let ev = build_replay_events("s", &state); + let m = methods(&ev); + assert!(m.contains(&"_loopal/permission_mode")); + assert!(m.contains(&"_loopal/model")); + assert!(!m.contains(&"_loopal/mode")); // empty → skipped + assert!(m.contains(&"_loopal/thinking")); + let by = |k: &str| ev.iter().find(|(meth, _)| meth == k).unwrap().1.clone(); + assert_eq!( + by("_loopal/permission_mode")["data"]["permission_mode"], + "bypass" + ); + assert_eq!(by("_loopal/thinking")["data"]["thinking"], "auto"); // thinking_config → thinking + } }