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