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