diff --git a/crates/ide-agent/src/lib.rs b/crates/ide-agent/src/lib.rs index 73e311b20..1c8a35344 100644 --- a/crates/ide-agent/src/lib.rs +++ b/crates/ide-agent/src/lib.rs @@ -169,8 +169,10 @@ impl Shared { let commands = config::commands_of(&json!({ "availableCommands": commands }), &spec.id); let options = config::options_of(&json!({ "configOptions": options })); let _ = tokio::task::spawn_blocking(move || { + // Notifies subscribers itself when the discovery + // changed the cached config; unchanged results + // stay silent. shared.remember_discovered(&spec.id, &options, &commands); - shared.emit(AgentEvent::Providers); }) .await; } @@ -253,6 +255,13 @@ impl Shared { { tracing::warn!("saving agent settings: {e}"); } + // The cached provider config actually changed (a session report, discovery, + // or a turn update): tell subscribers to re-list, so a changed model list + // reaches the composer without waiting for the next task (#1490). Unchanged + // reports stay silent, so routine session traffic emits nothing. The cache + // lock is released first; the sink never runs under it. + drop(cache); + self.emit(AgentEvent::Providers); } } diff --git a/crates/ide-agent/src/tests.rs b/crates/ide-agent/src/tests.rs index e38114b23..69aecb82a 100644 --- a/crates/ide-agent/src/tests.rs +++ b/crates/ide-agent/src/tests.rs @@ -2146,6 +2146,49 @@ async fn reload_providers_updates_registry_install_clears_stale_probes_and_notif assert!(probe.error.is_none()); } +/// A session report that changes the cached provider config notifies subscribers, +/// so the composer re-lists models without waiting for the next task (#1490). +/// Reports that change nothing stay silent. +#[tokio::test(flavor = "multi_thread")] +async fn remembered_config_changes_notify_subscribers_once() { + let tmp = tempfile::tempdir().unwrap(); + let mgr = manager(tmp.path(), Arc::new(FakeAgent::default())); + let events = Arc::new(Mutex::new(Vec::new())); + let sink = events.clone(); + mgr.subscribe(Arc::new(move |e| sink.lock().push(e))); + events.lock().clear(); + + let reported = config::options_of(&json!({ "configOptions": [ + { "id": "model", "category": "model", "type": "select", "currentValue": "default", + "options": [{ "value": "default" }, { "value": "custom-opus" }] }, + ] })); + mgr.shared.remember("claude", Some(&reported), None, Some("default")); + assert!( + events.lock().iter().any(|ev| matches!(ev, AgentEvent::Providers)), + "a first report changing the cache notifies subscribers" + ); + events.lock().clear(); + + // The same report again: the cache is unchanged, so subscribers hear nothing. + mgr.shared.remember("claude", Some(&reported), None, Some("default")); + assert!(events.lock().is_empty(), "an unchanged report must stay silent"); + + // A changed model list notifies again, and providers() serves the new list. + let updated = config::options_of(&json!({ "configOptions": [ + { "id": "model", "category": "model", "type": "select", "currentValue": "default", + "options": [{ "value": "default" }, { "value": "custom-opus" }, { "value": "custom-sonnet" }] }, + ] })); + mgr.shared.remember("claude", Some(&updated), None, Some("default")); + assert!( + events.lock().iter().any(|ev| matches!(ev, AgentEvent::Providers)), + "a changed model list notifies subscribers" + ); + let claude = mgr.providers().into_iter().find(|p| p.id == "claude").unwrap(); + let model = claude.config.iter().find(|o| o.category.as_deref() == Some("model")).unwrap(); + let values: Vec<_> = model.values.iter().map(|v| v.value.as_str()).collect(); + assert_eq!(values, ["default", "custom-opus", "custom-sonnet"]); +} + #[tokio::test(flavor = "multi_thread")] async fn idle_agents_stop_and_resume_their_session_on_the_next_message() { let new = || NewAgentTask {