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