diff --git a/assets/settings/default.json b/assets/settings/default.json index 326182ff519..1896bddbdde 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1098,6 +1098,22 @@ }, // When enabled, agent edits will be displayed in single-file editors for review "single_file_review": false, + // Settings for automatic agent context compaction, which summarizes earlier + // messages to free up room in the model's context window once it grows too + // large. + "auto_compact": { + // Whether to automatically compact the agent's context near the limit. + "enabled": true, + // The threshold at which auto-compaction runs. One of: + // - A percentage string ending in "%" (e.g. "90%"), measured against + // the model's context window. Decimals are allowed (e.g. "95.5%"). + // - A positive integer: compact after that many tokens have been used + // (e.g. 100000 compacts after 100,000 tokens are used). + // - A negative integer: compact once that many tokens remain in the + // context window (e.g. -20000 compacts once fewer than 20,000 remain). + // 0 is not a valid threshold. + "threshold": "90%", + }, // When enabled, show voting thumbs for feedback on agent edits. "enable_feedback": true, "default_profile": "write", diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 3fd029e8d74..22c96859083 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -11,13 +11,12 @@ use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent_settings::UserAgentsMd; use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag}; -use zed_env_vars::{EnvVar, env_var}; use crate::sandboxing::{SandboxRequest, ThreadSandboxGrants, sandboxing_enabled}; use agent_client_protocol::schema as acp; use agent_settings::{ - AgentProfileId, AgentSettings, COMPACTION_PROMPT, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, + AgentProfileId, AgentSettings, AutoCompactThreshold, COMPACTION_PROMPT, + SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Local, Utc}; @@ -70,17 +69,12 @@ const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; pub const MAX_TOOL_NAME_LENGTH: usize = 64; pub const MAX_SUBAGENT_DEPTH: u8 = 1; -const AGENT_COMPACTION_REMAINING_TOKEN_BUDGET: u64 = 40_000; - /// Auto-compaction is only available for models whose context window is at least /// this large. For smaller models there isn't enough headroom for a compaction /// pass to be worthwhile, so we leave the thread uncompacted and let the UI warn /// the user instead. pub const MIN_COMPACTION_CONTEXT_WINDOW: u64 = 80_000; -static AGENT_COMPACTION_REMAINING_TOKEN_BUDGET_ENV_VAR: std::sync::LazyLock = - env_var!("AGENT_COMPACTION_REMAINING_TOKEN_BUDGET"); - // Using the heuristic that 1 token is about 4 bytes, keep the last 80K bytes of user-message content (~20k tokens). const COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET: usize = 80_000; @@ -2658,7 +2652,7 @@ impl Thread { cx: &mut AsyncApp, ) -> Result> { let Some((model, request, insertion_ix)) = this.update(cx, |this, cx| { - let insertion_ix = this.compaction_message_target_ix()?; + let insertion_ix = this.compaction_message_target_ix(cx)?; let model = this.model.clone()?; let request = this.build_compaction_request(insertion_ix, &model, cx); this.current_request_token_usage = TokenUsage::default(); @@ -3765,11 +3759,17 @@ impl Thread { .rposition(|message| matches!(&**message, Message::Compaction(_))) } - fn compaction_message_target_ix(&self) -> Option { + fn compaction_message_target_ix(&self, cx: &App) -> Option { + let auto_compact = AgentSettings::get_global(cx).auto_compact; + if !auto_compact.enabled { + return None; + } + let model = self.model.as_ref()?; + let max_token_count = model.max_token_count(); // Models with a small context window don't leave enough headroom for a // compaction pass; the UI warns the user about the token limit instead. - if model.max_token_count() < MIN_COMPACTION_CONTEXT_WINDOW { + if max_token_count < MIN_COMPACTION_CONTEXT_WINDOW { return None; } let (usage_ix, usage) = { @@ -3796,17 +3796,8 @@ impl Thread { } let active_tokens = total_input_tokens(usage).saturating_add(usage.output_tokens); - - let remaining_budget = AGENT_COMPACTION_REMAINING_TOKEN_BUDGET_ENV_VAR - .value - .as_ref() - .and_then(|v| v.parse().ok()) - .unwrap_or(AGENT_COMPACTION_REMAINING_TOKEN_BUDGET); - - let compaction_threshold = model - .max_token_count() - .saturating_sub(model.max_output_tokens().unwrap_or_default()) - .saturating_sub(remaining_budget); + let compaction_threshold = + auto_compact_threshold_token_count(auto_compact.threshold, max_token_count); if active_tokens < compaction_threshold { return None; } @@ -4024,6 +4015,21 @@ fn total_input_tokens(usage: language_model::TokenUsage) -> u64 { .saturating_add(usage.cache_read_input_tokens) } +fn auto_compact_threshold_token_count( + threshold: AutoCompactThreshold, + max_token_count: u64, +) -> u64 { + match threshold { + AutoCompactThreshold::Percentage(percent) => { + ((max_token_count as f64) * percent).ceil() as u64 + } + AutoCompactThreshold::TokensUsed(tokens) => tokens, + AutoCompactThreshold::TokensRemaining(tokens) => { + max_token_count.saturating_sub(tokens).saturating_add(1) + } + } +} + fn user_message_byte_len(message: &LanguageModelRequestMessage) -> usize { message .content @@ -5599,6 +5605,12 @@ mod tests { }) } + fn set_auto_compact_settings(cx: &mut App, auto_compact: agent_settings::AutoCompactSettings) { + let mut settings = AgentSettings::get_global(cx).clone(); + settings.auto_compact = auto_compact; + AgentSettings::override_global(settings, cx); + } + #[test] fn test_summary_compaction_renders_for_request_and_markdown() { let message = Message::Compaction(CompactionInfo::Summary("Older context".into())); @@ -5654,12 +5666,54 @@ mod tests { } #[gpui::test] - async fn test_compaction_threshold_uses_latest_reported_usage(cx: &mut TestAppContext) { + async fn test_compaction_threshold_uses_percentage_setting(cx: &mut TestAppContext) { let (thread, _event_stream) = setup_thread_for_test(cx).await; let model = Arc::new(FakeLanguageModel::default()); let user_message_id = UserMessageId::new(); cx.update(|cx| { + thread.update(cx, |thread, cx| { + thread.set_model(model, cx); + thread + .messages + .push(user_text_message(user_message_id.clone(), "below limit")); + thread.request_token_usage.insert( + user_message_id.clone(), + language_model::TokenUsage { + input_tokens: 899_999, + ..Default::default() + }, + ); + + assert_eq!(thread.compaction_message_target_ix(cx), None); + + thread.request_token_usage.insert( + user_message_id.clone(), + language_model::TokenUsage { + input_tokens: 900_000, + ..Default::default() + }, + ); + + assert_eq!(thread.compaction_message_target_ix(cx), Some(1)); + }); + }); + } + + #[gpui::test] + async fn test_compaction_threshold_respects_enabled_setting(cx: &mut TestAppContext) { + let (thread, _event_stream) = setup_thread_for_test(cx).await; + let model = Arc::new(FakeLanguageModel::default()); + let user_message_id = UserMessageId::new(); + + cx.update(|cx| { + set_auto_compact_settings( + cx, + agent_settings::AutoCompactSettings { + enabled: false, + threshold: AutoCompactThreshold::Percentage(0.9), + }, + ); thread.update(cx, |thread, cx| { thread.set_model(model, cx); thread @@ -5673,35 +5727,77 @@ mod tests { }, ); - assert_eq!(thread.compaction_message_target_ix(), Some(1)); + assert_eq!(thread.compaction_message_target_ix(cx), None); }); }); } #[gpui::test] - async fn test_compaction_threshold_reserves_max_output_tokens(cx: &mut TestAppContext) { + async fn test_compaction_threshold_respects_token_settings(cx: &mut TestAppContext) { let (thread, _event_stream) = setup_thread_for_test(cx).await; let model = Arc::new(FakeLanguageModel::default()); - model.set_max_token_count(272_000); - model.set_max_output_tokens(Some(128_000)); let user_message_id = UserMessageId::new(); cx.update(|cx| { + set_auto_compact_settings( + cx, + agent_settings::AutoCompactSettings { + enabled: true, + threshold: AutoCompactThreshold::TokensUsed(100_000), + }, + ); thread.update(cx, |thread, cx| { thread.set_model(model, cx); thread.messages.push(user_text_message( user_message_id.clone(), - "near output-reserved limit", + "fixed token limit", )); thread.request_token_usage.insert( user_message_id.clone(), language_model::TokenUsage { - input_tokens: 105_000, + input_tokens: 99_999, ..Default::default() }, ); - assert_eq!(thread.compaction_message_target_ix(), Some(1)); + assert_eq!(thread.compaction_message_target_ix(cx), None); + + thread.request_token_usage.insert( + user_message_id.clone(), + language_model::TokenUsage { + input_tokens: 100_000, + ..Default::default() + }, + ); + + assert_eq!(thread.compaction_message_target_ix(cx), Some(1)); + + set_auto_compact_settings( + cx, + agent_settings::AutoCompactSettings { + enabled: true, + threshold: AutoCompactThreshold::TokensRemaining(20_000), + }, + ); + thread.request_token_usage.insert( + user_message_id.clone(), + language_model::TokenUsage { + input_tokens: 980_000, + ..Default::default() + }, + ); + + assert_eq!(thread.compaction_message_target_ix(cx), None); + + thread.request_token_usage.insert( + user_message_id.clone(), + language_model::TokenUsage { + input_tokens: 980_001, + ..Default::default() + }, + ); + + assert_eq!(thread.compaction_message_target_ix(cx), Some(1)); }); }); } @@ -5728,7 +5824,7 @@ mod tests { }, ); - assert_eq!(thread.compaction_message_target_ix(), None); + assert_eq!(thread.compaction_message_target_ix(cx), None); }); }); } @@ -5830,7 +5926,7 @@ mod tests { .push(user_text_message(user_message_id.clone(), "old user")); thread.messages.push(agent_text_message("old assistant")); // Auto-compaction would be a no-op here. - assert_eq!(thread.compaction_message_target_ix(), None); + assert_eq!(thread.compaction_message_target_ix(cx), None); }); }); diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 20dc68f3bd3..59d52f563d8 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -590,6 +590,10 @@ mod tests { play_sound_when_agent_done: PlaySoundWhenAgentDone::default(), single_file_review: false, model_parameters: vec![], + auto_compact: agent_settings::AutoCompactSettings { + enabled: false, + threshold: agent_settings::AutoCompactThreshold::DEFAULT, + }, enable_feedback: false, expand_edit_card: true, expand_terminal_card: true, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 8884ec27ad2..7ba386236b1 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -1,9 +1,11 @@ mod agent_profile; mod user_agents_md; +use std::cmp::Ordering::{Equal, Greater, Less}; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, LazyLock}; +use anyhow::Context as _; use collections::{HashSet, IndexMap}; use fs::Fs; use futures::channel::oneshot; @@ -18,6 +20,7 @@ use settings::{ SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode, update_settings_file, update_settings_file_with_completion, }; +use util::ResultExt as _; pub use crate::agent_profile::*; pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; @@ -135,6 +138,58 @@ impl WindowLayout { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AutoCompactThreshold { + /// Compact once the context window is at least this full, as a fraction in + /// the range `(0.0, 1.0]`. + Percentage(f64), + /// Compact once at least this many tokens have been used. + TokensUsed(u64), + /// Compact once fewer than this many tokens remain in the context window. + TokensRemaining(u64), +} + +impl AutoCompactThreshold { + /// The threshold used when none is configured, or when the configured value + /// is invalid (90% of the context window). + pub const DEFAULT: Self = Self::Percentage(0.9); +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AutoCompactSettings { + pub enabled: bool, + pub threshold: AutoCompactThreshold, +} + +fn parse_auto_compact_threshold(raw: &str) -> anyhow::Result { + let trimmed = raw.trim(); + if let Some(percent) = trimmed.strip_suffix('%') { + let value: f64 = percent + .trim_end() + .parse() + .with_context(|| format!("invalid auto_compact threshold percentage {raw:?}"))?; + anyhow::ensure!( + value > 0.0 && value <= 100.0, + "auto_compact threshold percentage must be between 0% and 100%, got {raw:?}" + ); + Ok(AutoCompactThreshold::Percentage(value / 100.0)) + } else { + let tokens: i64 = trimmed.parse().with_context(|| { + format!( + "invalid auto_compact threshold {raw:?}; \ + expected a percentage like \"90%\" or an integer number of tokens" + ) + })?; + match tokens.cmp(&0) { + Greater => Ok(AutoCompactThreshold::TokensUsed(tokens as u64)), + Less => Ok(AutoCompactThreshold::TokensRemaining(tokens.unsigned_abs())), + Equal => { + anyhow::bail!("auto_compact threshold of 0 is not valid") + } + } + } +} + #[derive(Clone, Debug, RegisterSetting)] pub struct AgentSettings { pub enabled: bool, @@ -161,6 +216,7 @@ pub struct AgentSettings { pub play_sound_when_agent_done: PlaySoundWhenAgentDone, pub single_file_review: bool, pub model_parameters: Vec, + pub auto_compact: AutoCompactSettings, pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, @@ -682,6 +738,16 @@ impl Settings for AgentSettings { play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap_or_default(), single_file_review: agent.single_file_review.unwrap(), model_parameters: agent.model_parameters, + auto_compact: { + let auto_compact = agent.auto_compact.unwrap(); + let threshold = parse_auto_compact_threshold(&auto_compact.threshold.unwrap().0) + .log_err() + .unwrap_or(AutoCompactThreshold::DEFAULT); + AutoCompactSettings { + enabled: auto_compact.enabled.unwrap(), + threshold, + } + }, enable_feedback: agent.enable_feedback.unwrap(), expand_edit_card: agent.expand_edit_card.unwrap(), expand_terminal_card: agent.expand_terminal_card.unwrap(), @@ -823,6 +889,47 @@ mod tests { use settings::ToolPermissionMode; use settings::ToolPermissionsContent; + #[test] + fn test_parse_auto_compact_threshold() { + use AutoCompactThreshold::*; + + assert_eq!( + parse_auto_compact_threshold("90%").unwrap(), + Percentage(0.9) + ); + assert_eq!(AutoCompactThreshold::DEFAULT, Percentage(0.9)); + assert_eq!( + parse_auto_compact_threshold(" 92.5% ").unwrap(), + Percentage(0.925) + ); + assert_eq!( + parse_auto_compact_threshold("95.5%").unwrap(), + Percentage(0.955) + ); + assert_eq!( + parse_auto_compact_threshold("100%").unwrap(), + Percentage(1.0) + ); + // Token counts must be integers; a non-integer token value is invalid. + assert!(parse_auto_compact_threshold("100.5").is_err()); + assert_eq!( + parse_auto_compact_threshold("100000").unwrap(), + TokensUsed(100_000) + ); + assert_eq!( + parse_auto_compact_threshold("-20000").unwrap(), + TokensRemaining(20_000) + ); + + // 0 is invalid in every form. + assert!(parse_auto_compact_threshold("0").is_err()); + assert!(parse_auto_compact_threshold("0%").is_err()); + // Out-of-range percentages and bare decimals are invalid. + assert!(parse_auto_compact_threshold("150%").is_err()); + assert!(parse_auto_compact_threshold("0.8").is_err()); + assert!(parse_auto_compact_threshold("eighty percent").is_err()); + } + #[test] fn test_compiled_regex_case_insensitive() { let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap(); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 46fefb9e00d..71b52a42432 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -957,6 +957,10 @@ mod tests { play_sound_when_agent_done: PlaySoundWhenAgentDone::Never, single_file_review: false, model_parameters: vec![], + auto_compact: agent_settings::AutoCompactSettings { + enabled: false, + threshold: agent_settings::AutoCompactThreshold::DEFAULT, + }, enable_feedback: false, expand_edit_card: true, expand_terminal_card: true, diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 91c702fa833..e8c3faefe81 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -71,6 +71,123 @@ pub enum ThinkingBlockDisplay { AlwaysCollapsed, } +/// Threshold at which agent auto-compaction runs. See +/// [`AutoCompactSettingsContent::threshold`] for the accepted formats. +/// +/// The canonical textual form is stored verbatim so it can round-trip through +/// the settings UI; it is serialized back as a JSON string for percentages and +/// as a JSON integer for token counts. +#[derive(Clone, Debug, PartialEq, Eq, MergeFrom)] +pub struct AutoCompactThreshold(pub String); + +impl From for AutoCompactThreshold { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From for String { + fn from(value: AutoCompactThreshold) -> Self { + value.0 + } +} + +impl AsRef for AutoCompactThreshold { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Serialize for AutoCompactThreshold { + fn serialize(&self, serializer: S) -> Result { + if self.0.ends_with('%') { + serializer.serialize_str(&self.0) + } else if let Ok(tokens) = self.0.parse::() { + serializer.serialize_i64(tokens) + } else { + serializer.serialize_str(&self.0) + } + } +} + +impl<'de> Deserialize<'de> for AutoCompactThreshold { + fn deserialize>(deserializer: D) -> Result { + struct ThresholdVisitor; + + impl serde::de::Visitor<'_> for ThresholdVisitor { + type Value = AutoCompactThreshold; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter + .write_str("a percentage string like \"90%\" or an integer number of tokens") + } + + fn visit_str(self, value: &str) -> Result { + Ok(AutoCompactThreshold(value.to_owned())) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(AutoCompactThreshold(value.to_string())) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(AutoCompactThreshold(value.to_string())) + } + + fn visit_f64(self, value: f64) -> Result { + Ok(AutoCompactThreshold(value.to_string())) + } + } + + deserializer.deserialize_any(ThresholdVisitor) + } +} + +impl JsonSchema for AutoCompactThreshold { + fn schema_name() -> Cow<'static, str> { + "AutoCompactThreshold".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+(\\.\\d+)?%$" + }, + { + "type": "integer" + } + ] + }) + } +} + +#[with_fallible_options] +#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] +pub struct AutoCompactSettingsContent { + /// Whether to automatically compact the agent's context when it grows too + /// large, summarizing earlier messages to free up room in the model's + /// context window. + /// + /// Default: true + pub enabled: Option, + /// The threshold at which auto-compaction runs. This is one of: + /// + /// - A percentage string ending in `%`, e.g. `"90%"`, measured against the + /// model's context window. `"90%"` compacts once the context is 90% full. + /// - A positive integer: compaction runs once that many tokens have been + /// used. For example, `100000` compacts after 100,000 tokens are used. + /// - A negative integer: compaction runs once that many tokens remain in + /// the context window. For example, `-20000` compacts once there are + /// fewer than 20,000 tokens of headroom left in the context window. + /// + /// `0` is not a valid threshold. + /// + /// Default: "90%" + pub threshold: Option, +} + #[with_fallible_options] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] pub struct AgentSettingsContent { @@ -165,6 +282,10 @@ pub struct AgentSettingsContent { /// Default: [] #[serde(default)] pub model_parameters: Vec, + /// Settings for automatic agent context compaction, which summarizes + /// earlier messages to free up room in the model's context window once the + /// context grows too large. + pub auto_compact: Option, /// Whether to show thumb buttons for feedback in the agent panel. /// /// Default: true diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index e8fc1f7248f..d71255cd9d1 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -7819,7 +7819,9 @@ fn ai_page(cx: &App) -> SettingsPage { ] } - fn agent_configuration_section(_cx: &App) -> Box<[SettingsPageItem]> { + fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> { + use feature_flags::FeatureFlagAppExt as _; + let mut items = vec![ SettingsPageItem::SectionHeader("Agent Configuration"), SettingsPageItem::SubPageLink(SubPageLink { @@ -8105,6 +8107,68 @@ fn ai_page(cx: &App) -> SettingsPage { }), ]); + if cx.has_flag::() { + items.extend([ + SettingsPageItem::SettingItem(SettingItem { + title: "Auto Compact", + description: "Automatically compact the agent's context when it grows too large, summarizing earlier messages to free up room in the model's context window.", + field: Box::new(SettingField { + organization_override: None, + json_path: Some("agent.auto_compact.enabled"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .auto_compact + .as_ref()? + .enabled + .as_ref() + }, + write: |settings_content, value, _| { + settings_content + .agent + .get_or_insert_default() + .auto_compact + .get_or_insert_default() + .enabled = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Auto Compact Threshold", + description: "When auto compaction runs. A percentage string like \"90%\" is measured against the context window. A positive integer is the number of used tokens to compact after. A negative integer is the number of tokens remaining in the context window before compacting.", + field: Box::new(SettingField { + organization_override: None, + json_path: Some("agent.auto_compact.threshold"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .auto_compact + .as_ref()? + .threshold + .as_ref() + }, + write: |settings_content, value, _| { + settings_content + .agent + .get_or_insert_default() + .auto_compact + .get_or_insert_default() + .threshold = value; + }, + }), + metadata: Some(Box::new(SettingsFieldMetadata { + placeholder: Some("90%"), + ..Default::default() + })), + files: USER, + }), + ]); + } + items.into_boxed_slice() } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index b6cb1944d44..ffff6f4e870 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -565,6 +565,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_editable_number_field) + .add_basic_renderer::(render_text_field) .add_basic_renderer::(render_editable_number_field) .add_basic_renderer::(render_editable_number_field) .add_basic_renderer::(render_editable_number_field)