agent_ui.rs

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