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