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