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