Add auto_compact agent settings (#58883)

Adds two new agent settings nested under `auto_compact`, `enabled` and
`threshold`, which control automatic compaction of the agent's context
window as part of the handoff feature. Both surface in the settings UI
under Agent Configuration, gated behind the `handoff` feature flag so
they only appear for users on that flag.

The `threshold` accepts three forms: a percentage string ending in `%`
(e.g. `"80%"` or `"95.5%"`) measured against the model's context window,
a positive integer to compact after that many tokens have been used, or
a negative integer to compact once that many tokens remain in the
window. `0` and bare non-integer numbers are rejected. It deserializes
from either a JSON string or integer and resolves to a typed enum,
falling back to the `"80%"` default (with a logged warning) when the
configured value is invalid. In the settings UI the threshold is a
free-form text field so all three forms can be entered.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
Richard Feldman 2026-06-09 06:52:58 -04:00 committed by GitHub
parent e5052961af
commit 9baefe701e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 446 additions and 33 deletions

View file

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

View file

@ -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<EnvVar> =
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<ControlFlow<()>> {
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<usize> {
fn compaction_message_target_ix(&self, cx: &App) -> Option<usize> {
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);
});
});

View file

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

View file

@ -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<AutoCompactThreshold> {
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<LanguageModelParameters>,
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();

View file

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

View file

@ -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<String> for AutoCompactThreshold {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<AutoCompactThreshold> for String {
fn from(value: AutoCompactThreshold) -> Self {
value.0
}
}
impl AsRef<str> for AutoCompactThreshold {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Serialize for AutoCompactThreshold {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if self.0.ends_with('%') {
serializer.serialize_str(&self.0)
} else if let Ok(tokens) = self.0.parse::<i64>() {
serializer.serialize_i64(tokens)
} else {
serializer.serialize_str(&self.0)
}
}
}
impl<'de> Deserialize<'de> for AutoCompactThreshold {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(AutoCompactThreshold(value.to_owned()))
}
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(AutoCompactThreshold(value.to_string()))
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
Ok(AutoCompactThreshold(value.to_string()))
}
fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
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<bool>,
/// 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<AutoCompactThreshold>,
}
#[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<LanguageModelParameters>,
/// 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<AutoCompactSettingsContent>,
/// Whether to show thumb buttons for feedback in the agent panel.
///
/// Default: true

View file

@ -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::<feature_flags::HandoffFeatureFlag>() {
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()
}

View file

@ -565,6 +565,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::EditPredictionPromptFormatContent>(render_dropdown)
.add_basic_renderer::<settings::EditPredictionDataCollectionChoice>(render_dropdown)
.add_basic_renderer::<f32>(render_editable_number_field)
.add_basic_renderer::<settings::AutoCompactThreshold>(render_text_field)
.add_basic_renderer::<u32>(render_editable_number_field)
.add_basic_renderer::<u64>(render_editable_number_field)
.add_basic_renderer::<usize>(render_editable_number_field)