Persist fast mode across new threads (#53356)

Nathan Sobo and Ben Brandt created

When toggling fast mode, the setting is now written to `settings.json`
under `agent.default_model.speed`, so new threads start with the same
speed. This follows the same pattern as `enable_thinking` and `effort`.

The `speed` field uses the existing `Speed` enum (`"fast"` /
`"standard"`) rather than a boolean, to leave room for future speed
tiers.

Example settings:
```json
{
  "agent": {
    "default_model": {
      "provider": "zed.dev",
      "model": "claude-sonnet-4",
      "speed": "fast"
    }
  }
}
```

cc @benbrandt

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

crates/agent/src/agent.rs                                        |  2 
crates/agent/src/native_agent_server.rs                          |  1 
crates/agent/src/thread.rs                                       |  6 
crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs |  1 
crates/agent_ui/src/agent_panel.rs                               |  1 
crates/agent_ui/src/conversation_view/thread_view.rs             | 21 +
crates/agent_ui/src/favorite_models.rs                           |  1 
crates/language_model_core/src/request.rs                        |  4 
crates/settings_content/src/agent.rs                             |  2 
crates/settings_content/src/merge_from.rs                        |  1 
10 files changed, 31 insertions(+), 9 deletions(-)

Detailed changes

crates/agent/src/agent.rs 🔗

@@ -1355,6 +1355,7 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
                 let provider = model.provider_id().0.to_string();
                 let model = model.id().0.to_string();
                 let enable_thinking = thread.read(cx).thinking_enabled();
+                let speed = thread.read(cx).speed();
                 settings
                     .agent
                     .get_or_insert_default()
@@ -1363,6 +1364,7 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
                         model,
                         enable_thinking,
                         effort,
+                        speed,
                     });
             },
         );

crates/agent/src/native_agent_server.rs 🔗

@@ -97,6 +97,7 @@ fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
         model: model.to_owned(),
         enable_thinking: false,
         effort: None,
+        speed: None,
     }
 }
 

crates/agent/src/thread.rs 🔗

@@ -1041,6 +1041,10 @@ impl Thread {
             .default_model
             .as_ref()
             .and_then(|model| model.effort.clone());
+        let speed = settings
+            .default_model
+            .as_ref()
+            .and_then(|model| model.speed);
         let (prompt_capabilities_tx, prompt_capabilities_rx) =
             watch::channel(Self::prompt_capabilities(model.as_deref()));
         Self {
@@ -1072,7 +1076,7 @@ impl Thread {
             model,
             summarization_model: None,
             thinking_enabled: enable_thinking,
-            speed: None,
+            speed,
             thinking_effort,
             prompt_capabilities_tx,
             prompt_capabilities_rx,

crates/agent_ui/src/conversation_view/thread_view.rs 🔗

@@ -8467,13 +8467,20 @@ impl ThreadView {
             return;
         };
         thread.update(cx, |thread, cx| {
-            thread.set_speed(
-                thread
-                    .speed()
-                    .map(|speed| speed.toggle())
-                    .unwrap_or(Speed::Fast),
-                cx,
-            );
+            let new_speed = thread
+                .speed()
+                .map(|speed| speed.toggle())
+                .unwrap_or(Speed::Fast);
+            thread.set_speed(new_speed, cx);
+
+            let fs = thread.project().read(cx).fs().clone();
+            update_settings_file(fs, cx, move |settings, _| {
+                if let Some(agent) = settings.agent.as_mut()
+                    && let Some(default_model) = agent.default_model.as_mut()
+                {
+                    default_model.speed = Some(new_speed);
+                }
+            });
         });
     }
 

crates/agent_ui/src/favorite_models.rs 🔗

@@ -11,6 +11,7 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
         model: model.id().0.to_string(),
         enable_thinking: false,
         effort: None,
+        speed: None,
     }
 }
 

crates/language_model_core/src/request.rs 🔗

@@ -333,7 +333,9 @@ pub struct LanguageModelRequest {
     pub speed: Option<Speed>,
 }
 
-#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
+#[derive(
+    Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum Speed {
     #[default]

crates/settings_content/src/agent.rs 🔗

@@ -256,6 +256,7 @@ impl AgentSettingsContent {
             model,
             enable_thinking: false,
             effort: None,
+            speed: None,
         });
     }
 
@@ -397,6 +398,7 @@ pub struct LanguageModelSelection {
     #[serde(default)]
     pub enable_thinking: bool,
     pub effort: Option<String>,
+    pub speed: Option<language_model_core::Speed>,
 }
 
 #[with_fallible_options]

crates/settings_content/src/merge_from.rs 🔗

@@ -56,6 +56,7 @@ merge_from_overwrites!(
     std::sync::Arc<str>,
     std::path::PathBuf,
     std::sync::Arc<std::path::Path>,
+    language_model_core::Speed,
 );
 
 impl<T: Clone + MergeFrom> MergeFrom for Option<T> {