agent_ui.rs

  1mod agent_configuration;
  2pub(crate) mod agent_connection_store;
  3mod agent_diff;
  4mod agent_model_selector;
  5mod agent_panel;
  6mod agent_registry_ui;
  7mod buffer_codegen;
  8mod completion_provider;
  9mod config_options;
 10mod context;
 11mod context_server_configuration;
 12pub(crate) mod conversation_view;
 13mod diagnostics;
 14mod entry_view_state;
 15mod external_source_prompt;
 16mod favorite_models;
 17mod inline_assistant;
 18mod inline_prompt_editor;
 19mod language_model_selector;
 20mod mention_set;
 21mod message_editor;
 22mod mode_selector;
 23mod model_selector;
 24mod model_selector_popover;
 25mod profile_selector;
 26mod terminal_codegen;
 27mod terminal_inline_assistant;
 28#[cfg(any(test, feature = "test-support"))]
 29pub mod test_support;
 30mod thread_import;
 31pub mod thread_metadata_store;
 32pub mod thread_worktree_archive;
 33
 34pub mod threads_archive_view;
 35mod ui;
 36
 37use std::rc::Rc;
 38use std::sync::Arc;
 39
 40use ::ui::IconName;
 41use agent_client_protocol as acp;
 42use agent_settings::{AgentProfileId, AgentSettings};
 43use command_palette_hooks::CommandPaletteFilter;
 44use feature_flags::FeatureFlagAppExt as _;
 45use fs::Fs;
 46use gpui::{Action, App, Context, Entity, SharedString, Window, actions};
 47use language::{
 48    LanguageRegistry,
 49    language_settings::{AllLanguageSettings, EditPredictionProvider},
 50};
 51use language_model::{
 52    ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
 53};
 54use project::{AgentId, DisableAiSettings};
 55use prompt_store::PromptBuilder;
 56use schemars::JsonSchema;
 57use serde::{Deserialize, Serialize};
 58use settings::{LanguageModelSelection, Settings as _, SettingsStore};
 59use std::any::TypeId;
 60use workspace::Workspace;
 61
 62use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
 63pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, MaxIdleRetainedThreads};
 64use crate::agent_registry_ui::AgentRegistryPage;
 65pub use crate::inline_assistant::InlineAssistant;
 66pub use crate::thread_metadata_store::ThreadId;
 67pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
 68pub use conversation_view::ConversationView;
 69pub use external_source_prompt::ExternalSourcePrompt;
 70pub(crate) use mode_selector::ModeSelector;
 71pub(crate) use model_selector::ModelSelector;
 72pub(crate) use model_selector_popover::ModelSelectorPopover;
 73pub use thread_import::{
 74    AcpThreadImportOnboarding, CrossChannelImportOnboarding, ThreadImportModal,
 75    channels_with_threads, import_threads_from_other_channels,
 76};
 77use zed_actions;
 78pub use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, SwitchWorktree};
 79
 80pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
 81const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
 82actions!(
 83    agent,
 84    [
 85        /// Toggles the menu to create new agent threads.
 86        ToggleNewThreadMenu,
 87        /// Toggles the options menu for agent settings and preferences.
 88        ToggleOptionsMenu,
 89        /// Toggles the profile or mode selector for switching between agent profiles.
 90        ToggleProfileSelector,
 91        /// Cycles through available session modes.
 92        CycleModeSelector,
 93        /// Cycles through favorited models in the ACP model selector.
 94        CycleFavoriteModels,
 95        /// Expands the message editor to full size.
 96        ExpandMessageEditor,
 97        /// Adds a context server to the configuration.
 98        AddContextServer,
 99        /// Archives the currently selected thread.
100        ArchiveSelectedThread,
101        /// Removes the currently selected thread.
102        RemoveSelectedThread,
103        /// Starts a chat conversation with follow-up enabled.
104        ChatWithFollow,
105        /// Cycles to the next inline assist suggestion.
106        CycleNextInlineAssist,
107        /// Cycles to the previous inline assist suggestion.
108        CyclePreviousInlineAssist,
109        /// Moves focus up in the interface.
110        FocusUp,
111        /// Moves focus down in the interface.
112        FocusDown,
113        /// Moves focus left in the interface.
114        FocusLeft,
115        /// Moves focus right in the interface.
116        FocusRight,
117        /// Opens the active thread as a markdown file.
118        OpenActiveThreadAsMarkdown,
119        /// Opens the agent diff view to review changes.
120        OpenAgentDiff,
121        /// Copies the current thread to the clipboard as JSON for debugging.
122        CopyThreadToClipboard,
123        /// Loads a thread from the clipboard JSON for debugging.
124        LoadThreadFromClipboard,
125        /// Keeps the current suggestion or change.
126        Keep,
127        /// Rejects the current suggestion or change.
128        Reject,
129        /// Rejects all suggestions or changes.
130        RejectAll,
131        /// Undoes the most recent reject operation, restoring the rejected changes.
132        UndoLastReject,
133        /// Keeps all suggestions or changes.
134        KeepAll,
135        /// Allow this operation only this time.
136        AllowOnce,
137        /// Allow this operation and remember the choice.
138        AllowAlways,
139        /// Reject this operation only this time.
140        RejectOnce,
141        /// Follows the agent's suggestions.
142        Follow,
143        /// Resets the trial upsell notification.
144        ResetTrialUpsell,
145        /// Resets the trial end upsell notification.
146        ResetTrialEndUpsell,
147        /// Opens the "Add Context" menu in the message editor.
148        OpenAddContextMenu,
149        /// Continues the current thread.
150        ContinueThread,
151        /// Interrupts the current generation and sends the message immediately.
152        SendImmediately,
153        /// Sends the next queued message immediately.
154        SendNextQueuedMessage,
155        /// Removes the first message from the queue (the next one to be sent).
156        RemoveFirstQueuedMessage,
157        /// Edits the first message in the queue (the next one to be sent).
158        EditFirstQueuedMessage,
159        /// Clears all messages from the queue.
160        ClearMessageQueue,
161        /// Opens the permission granularity dropdown for the current tool call.
162        OpenPermissionDropdown,
163        /// Toggles thinking mode for models that support extended thinking.
164        ToggleThinkingMode,
165        /// Cycles through available thinking effort levels for the current model.
166        CycleThinkingEffort,
167        /// Toggles the thinking effort selector menu open or closed.
168        ToggleThinkingEffortMenu,
169        /// Toggles fast mode for models that support it.
170        ToggleFastMode,
171        /// Scroll the output by one page up.
172        ScrollOutputPageUp,
173        /// Scroll the output by one page down.
174        ScrollOutputPageDown,
175        /// Scroll the output up by three lines.
176        ScrollOutputLineUp,
177        /// Scroll the output down by three lines.
178        ScrollOutputLineDown,
179        /// Scroll the output to the top.
180        ScrollOutputToTop,
181        /// Scroll the output to the bottom.
182        ScrollOutputToBottom,
183        /// Scroll the output to the previous user message.
184        ScrollOutputToPreviousMessage,
185        /// Scroll the output to the next user message.
186        ScrollOutputToNextMessage,
187        /// Import agent threads from other Zed release channels (e.g. Preview, Nightly).
188        ImportThreadsFromOtherChannels,
189    ]
190);
191
192actions!(
193    dev,
194    [
195        /// Shows metadata for the currently active thread.
196        ShowThreadMetadata,
197        /// Shows metadata for all threads in the sidebar.
198        ShowAllSidebarThreadMetadata,
199    ]
200);
201
202/// Action to authorize a tool call with a specific permission option.
203/// This is used by the permission granularity dropdown to authorize tool calls.
204#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
205#[action(namespace = agent)]
206#[serde(deny_unknown_fields)]
207pub struct AuthorizeToolCall {
208    /// The tool call ID to authorize.
209    pub tool_call_id: String,
210    /// The permission option ID to use.
211    pub option_id: String,
212    /// The kind of permission option (serialized as string).
213    pub option_kind: String,
214}
215
216/// Action to select a permission granularity option from the dropdown.
217/// This updates the selected granularity without triggering authorization.
218#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
219#[action(namespace = agent)]
220#[serde(deny_unknown_fields)]
221pub struct SelectPermissionGranularity {
222    /// The tool call ID for which to select the granularity.
223    pub tool_call_id: String,
224    /// The index of the selected granularity option.
225    pub index: usize,
226}
227
228/// Action to toggle a command pattern checkbox in the permission dropdown.
229#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
230#[action(namespace = agent)]
231#[serde(deny_unknown_fields)]
232pub struct ToggleCommandPattern {
233    /// The tool call ID for which to toggle the pattern.
234    pub tool_call_id: String,
235    /// The index of the command pattern to toggle.
236    pub pattern_index: usize,
237}
238
239/// Creates a new conversation thread, optionally based on an existing thread.
240#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
241#[action(namespace = agent)]
242#[serde(deny_unknown_fields)]
243pub struct NewThread;
244
245/// Creates a new external agent conversation thread.
246#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
247#[action(namespace = agent)]
248#[serde(deny_unknown_fields)]
249pub struct NewExternalAgentThread {
250    /// Which agent to use for the conversation.
251    agent: Option<Agent>,
252}
253
254#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
255#[action(namespace = agent)]
256#[serde(deny_unknown_fields)]
257pub struct NewNativeAgentThreadFromSummary {
258    from_session_id: agent_client_protocol::SessionId,
259}
260
261#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
262#[serde(rename_all = "snake_case")]
263#[non_exhaustive]
264pub enum Agent {
265    #[default]
266    #[serde(alias = "NativeAgent", alias = "TextThread")]
267    NativeAgent,
268    #[serde(alias = "Custom")]
269    Custom {
270        #[serde(rename = "name")]
271        id: AgentId,
272    },
273    #[cfg(any(test, feature = "test-support"))]
274    Stub,
275}
276
277impl From<AgentId> for Agent {
278    fn from(id: AgentId) -> Self {
279        if id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
280            Self::NativeAgent
281        } else {
282            Self::Custom { id }
283        }
284    }
285}
286
287impl Agent {
288    pub fn id(&self) -> AgentId {
289        match self {
290            Self::NativeAgent => agent::ZED_AGENT_ID.clone(),
291            Self::Custom { id } => id.clone(),
292            #[cfg(any(test, feature = "test-support"))]
293            Self::Stub => "stub".into(),
294        }
295    }
296
297    pub fn is_native(&self) -> bool {
298        matches!(self, Self::NativeAgent)
299    }
300
301    pub fn label(&self) -> SharedString {
302        match self {
303            Self::NativeAgent => "Zed Agent".into(),
304            Self::Custom { id, .. } => id.0.clone(),
305            #[cfg(any(test, feature = "test-support"))]
306            Self::Stub => "Stub Agent".into(),
307        }
308    }
309
310    pub fn icon(&self) -> Option<IconName> {
311        match self {
312            Self::NativeAgent => None,
313            Self::Custom { .. } => Some(IconName::Sparkle),
314            #[cfg(any(test, feature = "test-support"))]
315            Self::Stub => None,
316        }
317    }
318
319    pub fn server(
320        &self,
321        fs: Arc<dyn fs::Fs>,
322        thread_store: Entity<agent::ThreadStore>,
323    ) -> Rc<dyn agent_servers::AgentServer> {
324        match self {
325            Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
326            Self::Custom { id: name } => {
327                Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
328            }
329            #[cfg(any(test, feature = "test-support"))]
330            Self::Stub => Rc::new(crate::test_support::StubAgentServer::default_response()),
331        }
332    }
333}
334
335/// Content to initialize new external agent with.
336pub enum AgentInitialContent {
337    ThreadSummary {
338        session_id: acp::SessionId,
339        title: Option<SharedString>,
340    },
341    ContentBlock {
342        blocks: Vec<agent_client_protocol::ContentBlock>,
343        auto_submit: bool,
344    },
345    FromExternalSource(ExternalSourcePrompt),
346}
347
348impl From<ExternalSourcePrompt> for AgentInitialContent {
349    fn from(prompt: ExternalSourcePrompt) -> Self {
350        Self::FromExternalSource(prompt)
351    }
352}
353
354/// Opens the profile management interface for configuring agent tools and settings.
355#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
356#[action(namespace = agent)]
357#[serde(deny_unknown_fields)]
358pub struct ManageProfiles {
359    #[serde(default)]
360    pub customize_tools: Option<AgentProfileId>,
361}
362
363impl ManageProfiles {
364    pub fn customize_tools(profile_id: AgentProfileId) -> Self {
365        Self {
366            customize_tools: Some(profile_id),
367        }
368    }
369}
370
371#[derive(Clone)]
372pub(crate) enum ModelUsageContext {
373    InlineAssistant,
374}
375
376impl ModelUsageContext {
377    pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
378        match self {
379            Self::InlineAssistant => {
380                LanguageModelRegistry::read_global(cx).inline_assistant_model()
381            }
382        }
383    }
384}
385
386pub(crate) fn humanize_token_count(count: u64) -> String {
387    match count {
388        0..=999 => count.to_string(),
389        1000..=9999 => {
390            let thousands = count / 1000;
391            let hundreds = (count % 1000 + 50) / 100;
392            if hundreds == 0 {
393                format!("{}k", thousands)
394            } else if hundreds == 10 {
395                format!("{}k", thousands + 1)
396            } else {
397                format!("{}.{}k", thousands, hundreds)
398            }
399        }
400        10_000..=999_999 => format!("{}k", (count + 500) / 1000),
401        1_000_000..=9_999_999 => {
402            let millions = count / 1_000_000;
403            let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
404            if hundred_thousands == 0 {
405                format!("{}M", millions)
406            } else if hundred_thousands == 10 {
407                format!("{}M", millions + 1)
408            } else {
409                format!("{}.{}M", millions, hundred_thousands)
410            }
411        }
412        10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
413    }
414}
415
416/// Initializes the `agent` crate.
417pub fn init(
418    fs: Arc<dyn Fs>,
419    prompt_builder: Arc<PromptBuilder>,
420    language_registry: Arc<LanguageRegistry>,
421    is_new_install: bool,
422    is_eval: bool,
423    cx: &mut App,
424) {
425    agent::ThreadStore::init_global(cx);
426    rules_library::init(cx);
427    if !is_eval {
428        // Initializing the language model from the user settings messes with the eval, so we only initialize them when
429        // we're not running inside of the eval.
430        init_language_model_settings(cx);
431    }
432    agent_panel::init(cx);
433    context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
434    thread_metadata_store::init(cx);
435
436    inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
437    terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
438    cx.observe_new(move |workspace, window, cx| {
439        ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
440    })
441    .detach();
442    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
443        workspace.register_action(
444            move |workspace: &mut Workspace,
445                  _: &zed_actions::AcpRegistry,
446                  window: &mut Window,
447                  cx: &mut Context<Workspace>| {
448                let existing = workspace
449                    .active_pane()
450                    .read(cx)
451                    .items()
452                    .find_map(|item| item.downcast::<AgentRegistryPage>());
453
454                if let Some(existing) = existing {
455                    existing.update(cx, |_, cx| {
456                        project::AgentRegistryStore::global(cx)
457                            .update(cx, |store, cx| store.refresh(cx));
458                    });
459                    workspace.activate_item(&existing, true, true, window, cx);
460                } else {
461                    let registry_page = AgentRegistryPage::new(workspace, window, cx);
462                    workspace.add_item_to_active_pane(
463                        Box::new(registry_page),
464                        None,
465                        true,
466                        window,
467                        cx,
468                    );
469                }
470            },
471        );
472    })
473    .detach();
474    cx.observe_new(ManageProfilesModal::register).detach();
475    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
476        workspace.register_action(
477            |workspace: &mut Workspace,
478             _: &ImportThreadsFromOtherChannels,
479             _window: &mut Window,
480             cx: &mut Context<Workspace>| {
481                import_threads_from_other_channels(workspace, cx);
482            },
483        );
484    })
485    .detach();
486
487    // Update command palette filter based on AI settings
488    update_command_palette_filter(cx);
489
490    // Watch for settings changes
491    cx.observe_global::<SettingsStore>(|app_cx| {
492        // When settings change, update the command palette filter
493        update_command_palette_filter(app_cx);
494    })
495    .detach();
496
497    cx.on_flags_ready(|_, cx| {
498        update_command_palette_filter(cx);
499    })
500    .detach();
501
502    maybe_backfill_editor_layout(fs, is_new_install, cx);
503}
504
505fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut App) {
506    let kvp = db::kvp::KeyValueStore::global(cx);
507    let already_backfilled =
508        util::ResultExt::log_err(kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY))
509            .flatten()
510            .is_some();
511
512    if !already_backfilled {
513        if !is_new_install {
514            AgentSettings::backfill_editor_layout(fs, cx);
515        }
516
517        db::write_and_log(cx, move || async move {
518            kvp.write_kvp(
519                PARALLEL_AGENT_LAYOUT_BACKFILL_KEY.to_string(),
520                "1".to_string(),
521            )
522            .await
523        });
524    }
525}
526
527fn update_command_palette_filter(cx: &mut App) {
528    let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
529    let agent_enabled = AgentSettings::get_global(cx).enabled;
530
531    let edit_prediction_provider = AllLanguageSettings::get_global(cx)
532        .edit_predictions
533        .provider;
534
535    CommandPaletteFilter::update_global(cx, |filter, _| {
536        use editor::actions::{
537            AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
538            NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
539        };
540        let edit_prediction_actions = [
541            TypeId::of::<AcceptEditPrediction>(),
542            TypeId::of::<AcceptNextWordEditPrediction>(),
543            TypeId::of::<AcceptNextLineEditPrediction>(),
544            TypeId::of::<AcceptEditPrediction>(),
545            TypeId::of::<ShowEditPrediction>(),
546            TypeId::of::<NextEditPrediction>(),
547            TypeId::of::<PreviousEditPrediction>(),
548            TypeId::of::<ToggleEditPrediction>(),
549        ];
550
551        if disable_ai {
552            filter.hide_namespace("agent");
553            filter.hide_namespace("agents");
554            filter.hide_namespace("assistant");
555            filter.hide_namespace("copilot");
556            filter.hide_namespace("zed_predict_onboarding");
557            filter.hide_namespace("edit_prediction");
558
559            filter.hide_action_types(&edit_prediction_actions);
560            filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
561        } else {
562            if agent_enabled {
563                filter.show_namespace("agent");
564                filter.show_namespace("agents");
565                filter.show_namespace("assistant");
566            } else {
567                filter.hide_namespace("agent");
568                filter.hide_namespace("agents");
569                filter.hide_namespace("assistant");
570            }
571
572            match edit_prediction_provider {
573                EditPredictionProvider::None => {
574                    filter.hide_namespace("edit_prediction");
575                    filter.hide_namespace("copilot");
576                    filter.hide_action_types(&edit_prediction_actions);
577                }
578                EditPredictionProvider::Copilot => {
579                    filter.show_namespace("edit_prediction");
580                    filter.show_namespace("copilot");
581                    filter.show_action_types(edit_prediction_actions.iter());
582                }
583                EditPredictionProvider::Zed
584                | EditPredictionProvider::Codestral
585                | EditPredictionProvider::Ollama
586                | EditPredictionProvider::OpenAiCompatibleApi
587                | EditPredictionProvider::Mercury
588                | EditPredictionProvider::Experimental(_) => {
589                    filter.show_namespace("edit_prediction");
590                    filter.hide_namespace("copilot");
591                    filter.show_action_types(edit_prediction_actions.iter());
592                }
593            }
594
595            filter.show_namespace("zed_predict_onboarding");
596            filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
597
598            filter.show_namespace("multi_workspace");
599        }
600    });
601}
602
603fn init_language_model_settings(cx: &mut App) {
604    update_active_language_model_from_settings(cx);
605
606    cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
607        .detach();
608    cx.subscribe(
609        &LanguageModelRegistry::global(cx),
610        |_, event: &language_model::Event, cx| match event {
611            language_model::Event::ProviderStateChanged(_)
612            | language_model::Event::AddedProvider(_)
613            | language_model::Event::RemovedProvider(_)
614            | language_model::Event::ProvidersChanged => {
615                update_active_language_model_from_settings(cx);
616            }
617            _ => {}
618        },
619    )
620    .detach();
621}
622
623fn update_active_language_model_from_settings(cx: &mut App) {
624    let settings = AgentSettings::get_global(cx);
625
626    fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
627        language_model::SelectedModel {
628            provider: LanguageModelProviderId::from(selection.provider.0.clone()),
629            model: LanguageModelId::from(selection.model.clone()),
630        }
631    }
632
633    let default = settings.default_model.as_ref().map(to_selected_model);
634    let inline_assistant = settings
635        .inline_assistant_model
636        .as_ref()
637        .map(to_selected_model);
638    let commit_message = settings
639        .commit_message_model
640        .as_ref()
641        .map(to_selected_model);
642    let thread_summary = settings
643        .thread_summary_model
644        .as_ref()
645        .map(to_selected_model);
646    let inline_alternatives = settings
647        .inline_alternatives
648        .iter()
649        .map(to_selected_model)
650        .collect::<Vec<_>>();
651
652    LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
653        registry.select_default_model(default.as_ref(), cx);
654        registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
655        registry.select_commit_message_model(commit_message.as_ref(), cx);
656        registry.select_thread_summary_model(thread_summary.as_ref(), cx);
657        registry.select_inline_alternative_models(inline_alternatives, cx);
658    });
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use agent_settings::{AgentProfileId, AgentSettings};
665    use command_palette_hooks::CommandPaletteFilter;
666    use db::kvp::KeyValueStore;
667    use editor::actions::AcceptEditPrediction;
668    use gpui::{BorrowAppContext, TestAppContext, px};
669    use project::DisableAiSettings;
670    use settings::{
671        DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings, SettingsStore,
672    };
673
674    #[gpui::test]
675    fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
676        // Init settings
677        cx.update(|cx| {
678            let store = SettingsStore::test(cx);
679            cx.set_global(store);
680            command_palette_hooks::init(cx);
681            AgentSettings::register(cx);
682            DisableAiSettings::register(cx);
683            AllLanguageSettings::register(cx);
684        });
685
686        let agent_settings = AgentSettings {
687            enabled: true,
688            button: true,
689            dock: DockPosition::Right,
690            flexible: true,
691            default_width: px(300.),
692            default_height: px(600.),
693            max_content_width: Some(px(850.)),
694            default_model: None,
695            inline_assistant_model: None,
696            inline_assistant_use_streaming_tools: false,
697            commit_message_model: None,
698            thread_summary_model: None,
699            inline_alternatives: vec![],
700            favorite_models: vec![],
701            default_profile: AgentProfileId::default(),
702            profiles: Default::default(),
703            notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
704            play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
705            single_file_review: false,
706            model_parameters: vec![],
707            enable_feedback: false,
708            expand_edit_card: true,
709            expand_terminal_card: true,
710            cancel_generation_on_terminal_stop: true,
711            use_modifier_to_send: true,
712            message_editor_min_lines: 1,
713            tool_permissions: Default::default(),
714            show_turn_stats: false,
715            show_merge_conflict_indicator: true,
716            new_thread_location: Default::default(),
717            sidebar_side: Default::default(),
718            thinking_display: Default::default(),
719        };
720
721        cx.update(|cx| {
722            AgentSettings::override_global(agent_settings.clone(), cx);
723            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
724
725            // Initial update
726            update_command_palette_filter(cx);
727        });
728
729        // Assert visible
730        cx.update(|cx| {
731            let filter = CommandPaletteFilter::try_global(cx).unwrap();
732            assert!(
733                !filter.is_hidden(&NewThread),
734                "NewThread should be visible by default"
735            );
736        });
737
738        // Disable agent
739        cx.update(|cx| {
740            let mut new_settings = agent_settings.clone();
741            new_settings.enabled = false;
742            AgentSettings::override_global(new_settings, cx);
743
744            // Trigger update
745            update_command_palette_filter(cx);
746        });
747
748        // Assert hidden
749        cx.update(|cx| {
750            let filter = CommandPaletteFilter::try_global(cx).unwrap();
751            assert!(
752                filter.is_hidden(&NewThread),
753                "NewThread should be hidden when agent is disabled"
754            );
755        });
756
757        // Test EditPredictionProvider
758        // Enable EditPredictionProvider::Copilot
759        cx.update(|cx| {
760            cx.update_global::<SettingsStore, _>(|store, cx| {
761                store.update_user_settings(cx, |s| {
762                    s.project
763                        .all_languages
764                        .edit_predictions
765                        .get_or_insert(Default::default())
766                        .provider = Some(EditPredictionProvider::Copilot);
767                });
768            });
769            update_command_palette_filter(cx);
770        });
771
772        cx.update(|cx| {
773            let filter = CommandPaletteFilter::try_global(cx).unwrap();
774            assert!(
775                !filter.is_hidden(&AcceptEditPrediction),
776                "EditPrediction should be visible when provider is Copilot"
777            );
778        });
779
780        // Disable EditPredictionProvider (None)
781        cx.update(|cx| {
782            cx.update_global::<SettingsStore, _>(|store, cx| {
783                store.update_user_settings(cx, |s| {
784                    s.project
785                        .all_languages
786                        .edit_predictions
787                        .get_or_insert(Default::default())
788                        .provider = Some(EditPredictionProvider::None);
789                });
790            });
791            update_command_palette_filter(cx);
792        });
793
794        cx.update(|cx| {
795            let filter = CommandPaletteFilter::try_global(cx).unwrap();
796            assert!(
797                filter.is_hidden(&AcceptEditPrediction),
798                "EditPrediction should be hidden when provider is None"
799            );
800        });
801    }
802
803    async fn setup_backfill_test(cx: &mut TestAppContext) -> Arc<dyn Fs> {
804        let fs = fs::FakeFs::new(cx.background_executor.clone());
805        fs.save(
806            paths::settings_file().as_path(),
807            &"{}".into(),
808            Default::default(),
809        )
810        .await
811        .unwrap();
812
813        cx.update(|cx| {
814            cx.set_global(db::AppDatabase::test_new());
815            let store = SettingsStore::test(cx);
816            cx.set_global(store);
817            AgentSettings::register(cx);
818            DisableAiSettings::register(cx);
819            cx.set_staff(true);
820        });
821
822        fs
823    }
824
825    #[gpui::test]
826    async fn test_backfill_sets_kvp_flag(cx: &mut TestAppContext) {
827        let fs = setup_backfill_test(cx).await;
828
829        cx.update(|cx| {
830            let kvp = KeyValueStore::global(cx);
831            assert!(
832                kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
833                    .unwrap()
834                    .is_none()
835            );
836
837            maybe_backfill_editor_layout(fs.clone(), false, cx);
838        });
839
840        cx.run_until_parked();
841
842        let kvp = cx.update(|cx| KeyValueStore::global(cx));
843        assert!(
844            kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
845                .unwrap()
846                .is_some(),
847            "flag should be set after backfill"
848        );
849    }
850
851    #[gpui::test]
852    async fn test_backfill_new_install_sets_flag_without_writing_settings(cx: &mut TestAppContext) {
853        let fs = setup_backfill_test(cx).await;
854
855        cx.update(|cx| {
856            maybe_backfill_editor_layout(fs.clone(), true, cx);
857        });
858
859        cx.run_until_parked();
860
861        let kvp = cx.update(|cx| KeyValueStore::global(cx));
862        assert!(
863            kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
864                .unwrap()
865                .is_some(),
866            "flag should be set even for new installs"
867        );
868
869        let written = fs.load(paths::settings_file().as_path()).await.unwrap();
870        assert_eq!(written.trim(), "{}", "settings file should be unchanged");
871    }
872
873    #[gpui::test]
874    async fn test_backfill_is_idempotent(cx: &mut TestAppContext) {
875        let fs = setup_backfill_test(cx).await;
876
877        cx.update(|cx| {
878            maybe_backfill_editor_layout(fs.clone(), false, cx);
879        });
880
881        cx.run_until_parked();
882
883        let after_first = fs.load(paths::settings_file().as_path()).await.unwrap();
884
885        cx.update(|cx| {
886            maybe_backfill_editor_layout(fs.clone(), false, cx);
887        });
888
889        cx.run_until_parked();
890
891        let after_second = fs.load(paths::settings_file().as_path()).await.unwrap();
892        assert_eq!(
893            after_first, after_second,
894            "second call should not change settings"
895        );
896    }
897
898    #[test]
899    fn test_deserialize_external_agent_variants() {
900        assert_eq!(
901            serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
902            Agent::NativeAgent,
903        );
904        assert_eq!(
905            serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
906            Agent::Custom {
907                id: "my-agent".into(),
908            },
909        );
910    }
911}