diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index c02711d5dd9328..148e60b2bf87b0 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -14,7 +14,6 @@ mod tools; use context_server::ContextServerId; pub use db::*; -use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag}; use itertools::Itertools; pub use native_agent_server::NativeAgentServer; pub use pattern_extraction::*; @@ -1494,24 +1493,21 @@ impl NativeAgent { let Some(state) = project_state else { return Vec::new(); }; - let compact_command = cx.has_flag::().then(|| { - acp::AvailableCommand::new( - COMPACT_COMMAND_NAME, - "Summarize the conversation so far to free up context", - ) - .meta(acp_thread::meta_with_command_category( - acp_thread::CommandCategory::Native, - )) - }); + let compact_command = acp::AvailableCommand::new( + COMPACT_COMMAND_NAME, + "Summarize the conversation so far to free up context", + ) + .meta(acp_thread::meta_with_command_category( + acp_thread::CommandCategory::Native, + )); let registry = state.context_server_registry.read(cx); - // Reserve the built-in command name (when active) so a same-named MCP - // prompt is force-prefixed (`/.compact`) and stays reachable: - // an unqualified `/compact` always routes to the native command. - let reserved = compact_command.as_ref().map(|_| COMPACT_COMMAND_NAME); + // Reserve the built-in command name so a same-named MCP prompt is + // force-prefixed (`/.compact`) and stays reachable: an + // unqualified `/compact` always routes to the native command. let ambiguous_prompt_names = ambiguous_mcp_prompt_names( - reserved, + [COMPACT_COMMAND_NAME], registry.prompts().map(|p| p.prompt.name.as_str()), ); @@ -1550,7 +1546,9 @@ impl NativeAgent { Some(command) }); - compact_command.into_iter().chain(mcp_commands).collect() + std::iter::once(compact_command) + .chain(mcp_commands) + .collect() } pub fn load_thread( @@ -2583,9 +2581,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }; if let Some(parsed_command) = Command::parse(¶ms.prompt) { - if cx.has_flag::() - && parsed_command.is_unqualified(COMPACT_COMMAND_NAME) - { + if parsed_command.is_unqualified(COMPACT_COMMAND_NAME) { return self.0.update(cx, |agent, cx| { agent.send_compact_command(id, session_id, cx) }); @@ -3594,7 +3590,7 @@ mod internal_tests { use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; use agent_settings::COMPACTION_PROMPT; use fs::FakeFs; - use gpui::{TestAppContext, UpdateGlobal}; + use gpui::TestAppContext; use indoc::formatdoc; use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; use language_model::{ @@ -3666,27 +3662,9 @@ mod internal_tests { .collect() } - fn set_handoff_flag_override(value: &str, cx: &mut TestAppContext) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, _| { - store.register_setting::(); - }); - cx.update_flags(false, vec![]); - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |content| { - content - .feature_flags - .get_or_insert_default() - .insert("handoff".to_string(), value.to_string()); - }); - }); - }); - } - #[gpui::test] - async fn test_compact_command_requires_handoff_feature_flag(cx: &mut TestAppContext) { + async fn test_compact_command_is_available(cx: &mut TestAppContext) { init_test(cx); - set_handoff_flag_override("off", cx); let fs = FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [], cx).await; let thread_store = cx.new(|cx| ThreadStore::new(cx)); @@ -3706,30 +3684,11 @@ mod internal_tests { .unwrap(); cx.run_until_parked(); - cx.update(|cx| { - let commands = acp_thread.read(cx).available_commands(); - assert!(commands.is_empty()); - }); - - set_handoff_flag_override("on", cx); - - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_session( - project.clone(), - PathList::new(&[Path::new("/")]), - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - cx.update(|cx| { let commands = acp_thread.read(cx).available_commands(); let compact = commands.iter().find(|command| command.name == "compact"); - let compact = compact.expect("compact command should be available behind the flag"); + let compact = compact.expect("compact command should be available"); assert_eq!( acp_thread::command_category_from_meta(&compact.meta), Some(acp_thread::CommandCategory::Native), @@ -3738,43 +3697,8 @@ mod internal_tests { } #[gpui::test] - async fn test_compact_prompt_is_regular_prompt_without_handoff(cx: &mut TestAppContext) { - init_test(cx); - set_handoff_flag_override("off", cx); - - let (connection, agent, _project, acp_thread) = setup_native_agent_session(cx).await; - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx)); - let model = Arc::new(FakeLanguageModel::default()); - cx.update(|cx| thread.update(cx, |thread, cx| thread.set_model(model.clone(), cx))); - - let message_id = UserMessageId::new(); - let prompt_task = cx.update(|cx| { - connection.prompt( - message_id.clone(), - acp::PromptRequest::new(session_id.clone(), vec!["/compact".into()]), - cx, - ) - }); - cx.run_until_parked(); - - let request = model.pending_completions().pop().unwrap(); - assert_eq!(request.intent, Some(CompletionIntent::UserPrompt)); - assert_eq!( - request_texts_after_system(&request.messages), - vec!["/compact".to_string()] - ); - - model.send_completion_stream_text_chunk(&request, "regular response"); - model.end_completion_stream(&request); - cx.run_until_parked(); - prompt_task.await.unwrap(); - } - - #[gpui::test] - async fn test_compact_prompt_routes_to_manual_compaction_with_handoff(cx: &mut TestAppContext) { + async fn test_compact_prompt_routes_to_manual_compaction(cx: &mut TestAppContext) { init_test(cx); - cx.update(|cx| cx.update_flags(true, vec!["handoff".to_string()])); let (connection, agent, project, acp_thread) = setup_native_agent_session(cx).await; let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx)); @@ -3833,7 +3757,7 @@ mod internal_tests { assert!(ambiguous.contains("compact")); assert!(!ambiguous.contains("deploy")); - // Without the reservation (handoff off), a unique MCP prompt is left bare. + // Without the reservation, a unique MCP prompt is left bare. let ambiguous = ambiguous_mcp_prompt_names([], ["compact", "deploy"]); assert!(ambiguous.is_empty()); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 0cf5fc84d84a5a..c152082748473a 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -10,7 +10,6 @@ use crate::{ use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent_settings::UserAgentsMd; -use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag}; use crate::sandboxing::{SandboxRequest, ThreadSandboxGrants, sandboxing_enabled}; use agent_client_protocol::schema as acp; @@ -2428,78 +2427,76 @@ impl Thread { // Set when a refusal fallback occurs so subsequent iterations use the fallback model. let mut refusal_fallback_model: Option> = None; loop { - if cx.update(|cx| cx.has_flag::()) { - match Self::perform_compaction_if_needed( - this, - event_stream, - cancellation_rx.clone(), - cx, - ) - .await - { - // On success the telemetry event is deferred until the - // completion below reports usage, so we can record an - // accurate post-compaction context size (see - // `handle_completion_event`). - Ok(ControlFlow::Continue(())) => {} - Ok(ControlFlow::Break(())) => { - this.update(cx, |this, _| { - this.emit_compaction_telemetry_outcome("canceled", None) - })?; - return Ok(()); - } - Err(error) => { - log::error!("Compaction failed: {}", error); - let error_message = error.to_string(); - match error.downcast::() { - Ok(error) => { - attempt += 1; - match Self::retry_completion_error( - this, - event_stream, - &mut cancellation_rx, - error, - attempt, - cx, - ) - .await - { - Ok(ControlFlow::Break(())) => { - this.update(cx, |this, _| { - this.emit_compaction_telemetry_outcome("canceled", None) - })?; - return Ok(()); - } - Ok(ControlFlow::Continue(())) => { - this.update(cx, |this, _| { - if let Some(telemetry) = - this.pending_compaction_telemetry.as_mut() - { - telemetry.retries += 1; - } - })?; - continue; - } - Err(retry_error) => { - this.update(cx, |this, _| { - this.emit_compaction_telemetry_outcome( - "failed", - Some(error_message), - ) - })?; - return Err(retry_error); - } + match Self::perform_compaction_if_needed( + this, + event_stream, + cancellation_rx.clone(), + cx, + ) + .await + { + // On success the telemetry event is deferred until the + // completion below reports usage, so we can record an + // accurate post-compaction context size (see + // `handle_completion_event`). + Ok(ControlFlow::Continue(())) => {} + Ok(ControlFlow::Break(())) => { + this.update(cx, |this, _| { + this.emit_compaction_telemetry_outcome("canceled", None) + })?; + return Ok(()); + } + Err(error) => { + log::error!("Compaction failed: {}", error); + let error_message = error.to_string(); + match error.downcast::() { + Ok(error) => { + attempt += 1; + match Self::retry_completion_error( + this, + event_stream, + &mut cancellation_rx, + error, + attempt, + cx, + ) + .await + { + Ok(ControlFlow::Break(())) => { + this.update(cx, |this, _| { + this.emit_compaction_telemetry_outcome("canceled", None) + })?; + return Ok(()); + } + Ok(ControlFlow::Continue(())) => { + this.update(cx, |this, _| { + if let Some(telemetry) = + this.pending_compaction_telemetry.as_mut() + { + telemetry.retries += 1; + } + })?; + continue; + } + Err(retry_error) => { + this.update(cx, |this, _| { + this.emit_compaction_telemetry_outcome( + "failed", + Some(error_message), + ) + })?; + return Err(retry_error); } } - Err(error) => { - this.update(cx, |this, _| { - this.emit_compaction_telemetry_outcome( - "failed", - Some(error_message), - ) - })?; - return Err(error); - } + } + Err(error) => { + this.update(cx, |this, _| { + this.emit_compaction_telemetry_outcome( + "failed", + Some(error_message), + ) + })?; + return Err(error); } } } @@ -6127,7 +6124,6 @@ mod tests { let new_user_message_id = UserMessageId::new(); cx.update(|cx| { - cx.update_flags(true, vec!["handoff".to_string()]); thread.update(cx, |thread, cx| { thread.set_model(model.clone(), cx); thread @@ -6443,7 +6439,6 @@ mod tests { }; cx.update(|cx| { - cx.update_flags(true, vec!["handoff".to_string()]); thread.update(cx, |thread, cx| { thread.set_model(model.clone(), cx); thread diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 925b813cd0a091..f59420b1f2b097 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -24,7 +24,7 @@ use editor::scroll::Autoscroll; use editor::{ Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior, }; -use feature_flags::{AgentSharingFeatureFlag, FeatureFlagAppExt as _, HandoffFeatureFlag}; +use feature_flags::{AgentSharingFeatureFlag, FeatureFlagAppExt as _}; use file_icons::FileIcons; use fs::Fs; use futures::FutureExt as _; diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 7f85a297ff72ac..b5722c86e75b4e 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -10150,14 +10150,12 @@ impl ThreadView { let token_usage = self.thread.read(cx).token_usage()?; - // When auto-compaction is available (the handoff feature flag is enabled - // and the model's context window is large enough), the thread is - // compacted automatically before it reaches the limit, so there's no - // need to warn the user. Models with a context window that's too small - // can't be auto-compacted, so we fall back to the normal warning. - if cx.has_flag::() - && token_usage.max_tokens >= agent::MIN_COMPACTION_CONTEXT_WINDOW - { + // When auto-compaction is available (the model's context window is large + // enough), the thread is compacted automatically before it reaches the + // limit, so there's no need to warn the user. Models with a context + // window that's too small can't be auto-compacted, so we fall back to + // the normal warning. + if token_usage.max_tokens >= agent::MIN_COMPACTION_CONTEXT_WINDOW { return None; } diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index d2131a0a65e43c..e84b08d5772529 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -35,14 +35,6 @@ impl FeatureFlag for AgentSharingFeatureFlag { } register_feature_flag!(AgentSharingFeatureFlag); -pub struct HandoffFeatureFlag; - -impl FeatureFlag for HandoffFeatureFlag { - const NAME: &'static str = "handoff"; - type Value = PresenceFlag; -} -register_feature_flag!(HandoffFeatureFlag); - pub struct DiffReviewFeatureFlag; impl FeatureFlag for DiffReviewFeatureFlag { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index f54c841e0b9b74..4480d60855373b 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -75,7 +75,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { terminal_page(), version_control_page(), collaboration_page(), - ai_page(cx), + ai_page(), network_page(), developer_page(cx), ] @@ -7806,7 +7806,7 @@ fn collaboration_page() -> SettingsPage { } } -fn ai_page(cx: &App) -> SettingsPage { +fn ai_page() -> SettingsPage { fn general_section() -> [SettingsPageItem; 3] { [ SettingsPageItem::SectionHeader("General"), @@ -7841,9 +7841,7 @@ fn ai_page(cx: &App) -> SettingsPage { ] } - fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> { - use feature_flags::FeatureFlagAppExt as _; - + fn agent_configuration_section() -> Box<[SettingsPageItem]> { let mut items = vec![ SettingsPageItem::SectionHeader("Agent Configuration"), SettingsPageItem::SubPageLink(SubPageLink { @@ -8129,67 +8127,65 @@ 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, + 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; + }, }), - 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, + 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() } @@ -8250,7 +8246,7 @@ fn ai_page(cx: &App) -> SettingsPage { title: "AI", items: concat_sections![ general_section(), - agent_configuration_section(cx), + agent_configuration_section(), context_servers_section(), edit_prediction_language_settings_section(), edit_prediction_display_sub_section()