agent_ui.rs

  1mod agent_configuration;
  2pub(crate) mod agent_connection_store;
  3mod agent_diff;
  4mod agent_model_selector;
  5mod agent_panel;
  6mod agent_registry_ui;
  7mod buffer_codegen;
  8mod completion_provider;
  9mod config_options;
 10mod context;
 11mod context_server_configuration;
 12pub(crate) mod conversation_view;
 13mod diagnostics;
 14mod entry_view_state;
 15mod external_source_prompt;
 16mod favorite_models;
 17mod inline_assistant;
 18mod inline_prompt_editor;
 19mod language_model_selector;
 20mod mention_set;
 21mod message_editor;
 22mod mode_selector;
 23mod model_selector;
 24mod model_selector_popover;
 25mod profile_selector;
 26mod terminal_codegen;
 27mod terminal_inline_assistant;
 28#[cfg(any(test, feature = "test-support"))]
 29pub mod test_support;
 30mod thread_history;
 31mod thread_history_view;
 32mod thread_import;
 33pub mod thread_metadata_store;
 34pub mod thread_worktree_archive;
 35mod thread_worktree_picker;
 36pub mod threads_archive_view;
 37mod ui;
 38mod worktree_names;
 39
 40use std::path::PathBuf;
 41use std::rc::Rc;
 42use std::sync::Arc;
 43
 44use ::ui::IconName;
 45use agent_client_protocol as acp;
 46use agent_settings::{AgentProfileId, AgentSettings};
 47use command_palette_hooks::CommandPaletteFilter;
 48use feature_flags::FeatureFlagAppExt as _;
 49use fs::Fs;
 50use gpui::{Action, App, Context, Entity, SharedString, UpdateGlobal as _, Window, actions};
 51use language::{
 52    LanguageRegistry,
 53    language_settings::{AllLanguageSettings, EditPredictionProvider},
 54};
 55use language_model::{
 56    ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
 57};
 58use project::{AgentId, DisableAiSettings};
 59use prompt_store::PromptBuilder;
 60use release_channel::ReleaseChannel;
 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, MaxIdleRetainedThreads, WorktreeCreationStatus,
 70};
 71use crate::agent_registry_ui::AgentRegistryPage;
 72pub use crate::inline_assistant::InlineAssistant;
 73pub use crate::thread_metadata_store::ThreadId;
 74pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
 75pub use conversation_view::ConversationView;
 76pub use external_source_prompt::ExternalSourcePrompt;
 77pub(crate) use mode_selector::ModeSelector;
 78pub(crate) use model_selector::ModelSelector;
 79pub(crate) use model_selector_popover::ModelSelectorPopover;
 80pub(crate) use thread_history::ThreadHistory;
 81pub(crate) use thread_history_view::*;
 82pub use thread_import::{
 83    AcpThreadImportOnboarding, CrossChannelImportOnboarding, ThreadImportModal,
 84    channels_with_threads, import_threads_from_other_channels,
 85};
 86use zed_actions;
 87
 88pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
 89const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
 90actions!(
 91    agent,
 92    [
 93        /// Toggles the menu to create new agent threads.
 94        ToggleNewThreadMenu,
 95        /// Toggles the worktree selector popover for choosing which worktree to use.
 96        ToggleWorktreeSelector,
 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        /// Scroll the output by one page up.
186        ScrollOutputPageUp,
187        /// Scroll the output by one page down.
188        ScrollOutputPageDown,
189        /// Scroll the output up by three lines.
190        ScrollOutputLineUp,
191        /// Scroll the output down by three lines.
192        ScrollOutputLineDown,
193        /// Scroll the output to the top.
194        ScrollOutputToTop,
195        /// Scroll the output to the bottom.
196        ScrollOutputToBottom,
197        /// Scroll the output to the previous user message.
198        ScrollOutputToPreviousMessage,
199        /// Scroll the output to the next user message.
200        ScrollOutputToNextMessage,
201        /// Import agent threads from other Zed release channels (e.g. Preview, Nightly).
202        ImportThreadsFromOtherChannels,
203    ]
204);
205
206/// Action to authorize a tool call with a specific permission option.
207/// This is used by the permission granularity dropdown to authorize tool calls.
208#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
209#[action(namespace = agent)]
210#[serde(deny_unknown_fields)]
211pub struct AuthorizeToolCall {
212    /// The tool call ID to authorize.
213    pub tool_call_id: String,
214    /// The permission option ID to use.
215    pub option_id: String,
216    /// The kind of permission option (serialized as string).
217    pub option_kind: String,
218}
219
220/// Action to select a permission granularity option from the dropdown.
221/// This updates the selected granularity without triggering authorization.
222#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
223#[action(namespace = agent)]
224#[serde(deny_unknown_fields)]
225pub struct SelectPermissionGranularity {
226    /// The tool call ID for which to select the granularity.
227    pub tool_call_id: String,
228    /// The index of the selected granularity option.
229    pub index: usize,
230}
231
232/// Action to toggle a command pattern checkbox in the permission dropdown.
233#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
234#[action(namespace = agent)]
235#[serde(deny_unknown_fields)]
236pub struct ToggleCommandPattern {
237    /// The tool call ID for which to toggle the pattern.
238    pub tool_call_id: String,
239    /// The index of the command pattern to toggle.
240    pub pattern_index: usize,
241}
242
243/// Creates a new conversation thread, optionally based on an existing thread.
244#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
245#[action(namespace = agent)]
246#[serde(deny_unknown_fields)]
247pub struct NewThread;
248
249/// Creates a new external agent conversation thread.
250#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
251#[action(namespace = agent)]
252#[serde(deny_unknown_fields)]
253pub struct NewExternalAgentThread {
254    /// Which agent to use for the conversation.
255    agent: Option<Agent>,
256}
257
258#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
259#[action(namespace = agent)]
260#[serde(deny_unknown_fields)]
261pub struct NewNativeAgentThreadFromSummary {
262    from_session_id: agent_client_protocol::SessionId,
263}
264
265#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
266#[serde(rename_all = "snake_case")]
267#[non_exhaustive]
268pub enum Agent {
269    #[default]
270    #[serde(alias = "NativeAgent", alias = "TextThread")]
271    NativeAgent,
272    #[serde(alias = "Custom")]
273    Custom {
274        #[serde(rename = "name")]
275        id: AgentId,
276    },
277    #[cfg(any(test, feature = "test-support"))]
278    Stub,
279}
280
281impl From<AgentId> for Agent {
282    fn from(id: AgentId) -> Self {
283        if id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
284            Self::NativeAgent
285        } else {
286            Self::Custom { id }
287        }
288    }
289}
290
291impl Agent {
292    pub fn id(&self) -> AgentId {
293        match self {
294            Self::NativeAgent => agent::ZED_AGENT_ID.clone(),
295            Self::Custom { id } => id.clone(),
296            #[cfg(any(test, feature = "test-support"))]
297            Self::Stub => "stub".into(),
298        }
299    }
300
301    pub fn is_native(&self) -> bool {
302        matches!(self, Self::NativeAgent)
303    }
304
305    pub fn label(&self) -> SharedString {
306        match self {
307            Self::NativeAgent => "Zed Agent".into(),
308            Self::Custom { id, .. } => id.0.clone(),
309            #[cfg(any(test, feature = "test-support"))]
310            Self::Stub => "Stub Agent".into(),
311        }
312    }
313
314    pub fn icon(&self) -> Option<IconName> {
315        match self {
316            Self::NativeAgent => None,
317            Self::Custom { .. } => Some(IconName::Sparkle),
318            #[cfg(any(test, feature = "test-support"))]
319            Self::Stub => None,
320        }
321    }
322
323    pub fn server(
324        &self,
325        fs: Arc<dyn fs::Fs>,
326        thread_store: Entity<agent::ThreadStore>,
327    ) -> Rc<dyn agent_servers::AgentServer> {
328        match self {
329            Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
330            Self::Custom { id: name } => {
331                Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
332            }
333            #[cfg(any(test, feature = "test-support"))]
334            Self::Stub => Rc::new(crate::test_support::StubAgentServer::default_response()),
335        }
336    }
337}
338
339/// Describes which branch to use when creating a new git worktree.
340#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
341#[serde(rename_all = "snake_case", tag = "kind")]
342pub enum NewWorktreeBranchTarget {
343    /// Create a new randomly named branch from the current HEAD.
344    /// Will match worktree name if the newly created worktree was also randomly named.
345    #[default]
346    CurrentBranch,
347    /// Check out an existing branch, or create a new branch from it if it's
348    /// already occupied by another worktree.
349    ExistingBranch { name: String },
350    /// Create a new branch with an explicit name, optionally from a specific ref.
351    CreateBranch {
352        name: String,
353        #[serde(default)]
354        from_ref: Option<String>,
355    },
356}
357
358/// Creates a new git worktree and switches the workspace to it.
359/// Dispatched by the unified worktree picker when the user selects a "Create new worktree" entry.
360#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
361#[action(namespace = agent)]
362#[serde(deny_unknown_fields)]
363pub struct CreateWorktree {
364    /// When this is None, Zed will randomly generate a worktree name.
365    pub worktree_name: Option<String>,
366    pub branch_target: NewWorktreeBranchTarget,
367}
368
369/// Switches the workspace to an existing linked worktree.
370/// Dispatched by the unified worktree picker when the user selects an existing worktree.
371#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
372#[action(namespace = agent)]
373#[serde(deny_unknown_fields)]
374pub struct SwitchWorktree {
375    pub path: PathBuf,
376    pub display_name: String,
377}
378
379/// Content to initialize new external agent with.
380pub enum AgentInitialContent {
381    ThreadSummary {
382        session_id: acp::SessionId,
383        title: Option<SharedString>,
384    },
385    ContentBlock {
386        blocks: Vec<agent_client_protocol::ContentBlock>,
387        auto_submit: bool,
388    },
389    FromExternalSource(ExternalSourcePrompt),
390}
391
392impl From<ExternalSourcePrompt> for AgentInitialContent {
393    fn from(prompt: ExternalSourcePrompt) -> Self {
394        Self::FromExternalSource(prompt)
395    }
396}
397
398/// Opens the profile management interface for configuring agent tools and settings.
399#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
400#[action(namespace = agent)]
401#[serde(deny_unknown_fields)]
402pub struct ManageProfiles {
403    #[serde(default)]
404    pub customize_tools: Option<AgentProfileId>,
405}
406
407impl ManageProfiles {
408    pub fn customize_tools(profile_id: AgentProfileId) -> Self {
409        Self {
410            customize_tools: Some(profile_id),
411        }
412    }
413}
414
415#[derive(Clone)]
416pub(crate) enum ModelUsageContext {
417    InlineAssistant,
418}
419
420impl ModelUsageContext {
421    pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
422        match self {
423            Self::InlineAssistant => {
424                LanguageModelRegistry::read_global(cx).inline_assistant_model()
425            }
426        }
427    }
428}
429
430pub(crate) fn humanize_token_count(count: u64) -> String {
431    match count {
432        0..=999 => count.to_string(),
433        1000..=9999 => {
434            let thousands = count / 1000;
435            let hundreds = (count % 1000 + 50) / 100;
436            if hundreds == 0 {
437                format!("{}k", thousands)
438            } else if hundreds == 10 {
439                format!("{}k", thousands + 1)
440            } else {
441                format!("{}.{}k", thousands, hundreds)
442            }
443        }
444        10_000..=999_999 => format!("{}k", (count + 500) / 1000),
445        1_000_000..=9_999_999 => {
446            let millions = count / 1_000_000;
447            let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
448            if hundred_thousands == 0 {
449                format!("{}M", millions)
450            } else if hundred_thousands == 10 {
451                format!("{}M", millions + 1)
452            } else {
453                format!("{}.{}M", millions, hundred_thousands)
454            }
455        }
456        10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
457    }
458}
459
460/// Initializes the `agent` crate.
461pub fn init(
462    fs: Arc<dyn Fs>,
463    prompt_builder: Arc<PromptBuilder>,
464    language_registry: Arc<LanguageRegistry>,
465    is_new_install: bool,
466    is_eval: bool,
467    cx: &mut App,
468) {
469    agent::ThreadStore::init_global(cx);
470    rules_library::init(cx);
471    if !is_eval {
472        // Initializing the language model from the user settings messes with the eval, so we only initialize them when
473        // we're not running inside of the eval.
474        init_language_model_settings(cx);
475    }
476    agent_panel::init(cx);
477    context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
478    thread_metadata_store::init(cx);
479
480    inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
481    terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
482    cx.observe_new(move |workspace, window, cx| {
483        ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
484    })
485    .detach();
486    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
487        workspace.register_action(
488            move |workspace: &mut Workspace,
489                  _: &zed_actions::AcpRegistry,
490                  window: &mut Window,
491                  cx: &mut Context<Workspace>| {
492                let existing = workspace
493                    .active_pane()
494                    .read(cx)
495                    .items()
496                    .find_map(|item| item.downcast::<AgentRegistryPage>());
497
498                if let Some(existing) = existing {
499                    existing.update(cx, |_, cx| {
500                        project::AgentRegistryStore::global(cx)
501                            .update(cx, |store, cx| store.refresh(cx));
502                    });
503                    workspace.activate_item(&existing, true, true, window, cx);
504                } else {
505                    let registry_page = AgentRegistryPage::new(workspace, window, cx);
506                    workspace.add_item_to_active_pane(
507                        Box::new(registry_page),
508                        None,
509                        true,
510                        window,
511                        cx,
512                    );
513                }
514            },
515        );
516    })
517    .detach();
518    cx.observe_new(ManageProfilesModal::register).detach();
519    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
520        workspace.register_action(
521            |workspace: &mut Workspace,
522             _: &ImportThreadsFromOtherChannels,
523             _window: &mut Window,
524             cx: &mut Context<Workspace>| {
525                import_threads_from_other_channels(workspace, cx);
526            },
527        );
528    })
529    .detach();
530
531    // Update command palette filter based on AI settings
532    update_command_palette_filter(cx);
533
534    // Watch for settings changes
535    cx.observe_global::<SettingsStore>(|app_cx| {
536        // When settings change, update the command palette filter
537        update_command_palette_filter(app_cx);
538    })
539    .detach();
540
541    cx.on_flags_ready(|_, cx| {
542        update_command_palette_filter(cx);
543    })
544    .detach();
545
546    let agent_v2_enabled = agent_v2_enabled(cx);
547    if agent_v2_enabled {
548        maybe_backfill_editor_layout(fs, is_new_install, cx);
549    }
550
551    SettingsStore::update_global(cx, |store, cx| {
552        store.update_default_settings(cx, |defaults| {
553            if agent_v2_enabled {
554                defaults.agent.get_or_insert_default().dock = Some(DockPosition::Left);
555                defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Right);
556                defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Right);
557                defaults.collaboration_panel.get_or_insert_default().dock =
558                    Some(DockPosition::Right);
559                defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right);
560            } else {
561                defaults.agent.get_or_insert_default().dock = Some(DockPosition::Right);
562                defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Left);
563                defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Left);
564                defaults.collaboration_panel.get_or_insert_default().dock =
565                    Some(DockPosition::Left);
566                defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Left);
567            }
568        });
569    });
570}
571
572fn agent_v2_enabled(cx: &App) -> bool {
573    !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable))
574}
575
576fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut App) {
577    let kvp = db::kvp::KeyValueStore::global(cx);
578    let already_backfilled =
579        util::ResultExt::log_err(kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY))
580            .flatten()
581            .is_some();
582
583    if !already_backfilled {
584        if !is_new_install {
585            AgentSettings::backfill_editor_layout(fs, cx);
586        }
587
588        db::write_and_log(cx, move || async move {
589            kvp.write_kvp(
590                PARALLEL_AGENT_LAYOUT_BACKFILL_KEY.to_string(),
591                "1".to_string(),
592            )
593            .await
594        });
595    }
596}
597
598fn update_command_palette_filter(cx: &mut App) {
599    let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
600    let agent_enabled = AgentSettings::get_global(cx).enabled;
601    let agent_v2_enabled = agent_v2_enabled(cx);
602
603    let edit_prediction_provider = AllLanguageSettings::get_global(cx)
604        .edit_predictions
605        .provider;
606
607    CommandPaletteFilter::update_global(cx, |filter, _| {
608        use editor::actions::{
609            AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
610            NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
611        };
612        let edit_prediction_actions = [
613            TypeId::of::<AcceptEditPrediction>(),
614            TypeId::of::<AcceptNextWordEditPrediction>(),
615            TypeId::of::<AcceptNextLineEditPrediction>(),
616            TypeId::of::<AcceptEditPrediction>(),
617            TypeId::of::<ShowEditPrediction>(),
618            TypeId::of::<NextEditPrediction>(),
619            TypeId::of::<PreviousEditPrediction>(),
620            TypeId::of::<ToggleEditPrediction>(),
621        ];
622
623        if disable_ai {
624            filter.hide_namespace("agent");
625            filter.hide_namespace("agents");
626            filter.hide_namespace("assistant");
627            filter.hide_namespace("copilot");
628            filter.hide_namespace("zed_predict_onboarding");
629            filter.hide_namespace("edit_prediction");
630
631            filter.hide_action_types(&edit_prediction_actions);
632            filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
633        } else {
634            if agent_enabled {
635                filter.show_namespace("agent");
636                filter.show_namespace("agents");
637                filter.show_namespace("assistant");
638            } else {
639                filter.hide_namespace("agent");
640                filter.hide_namespace("agents");
641                filter.hide_namespace("assistant");
642            }
643
644            match edit_prediction_provider {
645                EditPredictionProvider::None => {
646                    filter.hide_namespace("edit_prediction");
647                    filter.hide_namespace("copilot");
648                    filter.hide_action_types(&edit_prediction_actions);
649                }
650                EditPredictionProvider::Copilot => {
651                    filter.show_namespace("edit_prediction");
652                    filter.show_namespace("copilot");
653                    filter.show_action_types(edit_prediction_actions.iter());
654                }
655                EditPredictionProvider::Zed
656                | EditPredictionProvider::Codestral
657                | EditPredictionProvider::Ollama
658                | EditPredictionProvider::OpenAiCompatibleApi
659                | EditPredictionProvider::Mercury
660                | EditPredictionProvider::Experimental(_) => {
661                    filter.show_namespace("edit_prediction");
662                    filter.hide_namespace("copilot");
663                    filter.show_action_types(edit_prediction_actions.iter());
664                }
665            }
666
667            filter.show_namespace("zed_predict_onboarding");
668            filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
669        }
670
671        if agent_v2_enabled {
672            filter.show_namespace("multi_workspace");
673        } else {
674            filter.hide_namespace("multi_workspace");
675        }
676    });
677}
678
679fn init_language_model_settings(cx: &mut App) {
680    update_active_language_model_from_settings(cx);
681
682    cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
683        .detach();
684    cx.subscribe(
685        &LanguageModelRegistry::global(cx),
686        |_, event: &language_model::Event, cx| match event {
687            language_model::Event::ProviderStateChanged(_)
688            | language_model::Event::AddedProvider(_)
689            | language_model::Event::RemovedProvider(_)
690            | language_model::Event::ProvidersChanged => {
691                update_active_language_model_from_settings(cx);
692            }
693            _ => {}
694        },
695    )
696    .detach();
697}
698
699fn update_active_language_model_from_settings(cx: &mut App) {
700    let settings = AgentSettings::get_global(cx);
701
702    fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
703        language_model::SelectedModel {
704            provider: LanguageModelProviderId::from(selection.provider.0.clone()),
705            model: LanguageModelId::from(selection.model.clone()),
706        }
707    }
708
709    let default = settings.default_model.as_ref().map(to_selected_model);
710    let inline_assistant = settings
711        .inline_assistant_model
712        .as_ref()
713        .map(to_selected_model);
714    let commit_message = settings
715        .commit_message_model
716        .as_ref()
717        .map(to_selected_model);
718    let thread_summary = settings
719        .thread_summary_model
720        .as_ref()
721        .map(to_selected_model);
722    let inline_alternatives = settings
723        .inline_alternatives
724        .iter()
725        .map(to_selected_model)
726        .collect::<Vec<_>>();
727
728    LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
729        registry.select_default_model(default.as_ref(), cx);
730        registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
731        registry.select_commit_message_model(commit_message.as_ref(), cx);
732        registry.select_thread_summary_model(thread_summary.as_ref(), cx);
733        registry.select_inline_alternative_models(inline_alternatives, cx);
734    });
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use agent_settings::{AgentProfileId, AgentSettings};
741    use command_palette_hooks::CommandPaletteFilter;
742    use db::kvp::KeyValueStore;
743    use editor::actions::AcceptEditPrediction;
744    use gpui::{BorrowAppContext, TestAppContext, px};
745    use project::DisableAiSettings;
746    use settings::{
747        DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings, SettingsStore,
748    };
749
750    #[gpui::test]
751    fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
752        // Init settings
753        cx.update(|cx| {
754            let store = SettingsStore::test(cx);
755            cx.set_global(store);
756            command_palette_hooks::init(cx);
757            AgentSettings::register(cx);
758            DisableAiSettings::register(cx);
759            AllLanguageSettings::register(cx);
760        });
761
762        let agent_settings = AgentSettings {
763            enabled: true,
764            button: true,
765            dock: DockPosition::Right,
766            flexible: true,
767            default_width: px(300.),
768            default_height: px(600.),
769            max_content_width: px(850.),
770            default_model: None,
771            inline_assistant_model: None,
772            inline_assistant_use_streaming_tools: false,
773            commit_message_model: None,
774            thread_summary_model: None,
775            inline_alternatives: vec![],
776            favorite_models: vec![],
777            default_profile: AgentProfileId::default(),
778            profiles: Default::default(),
779            notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
780            play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
781            single_file_review: false,
782            model_parameters: vec![],
783            enable_feedback: false,
784            expand_edit_card: true,
785            expand_terminal_card: true,
786            cancel_generation_on_terminal_stop: true,
787            use_modifier_to_send: true,
788            message_editor_min_lines: 1,
789            tool_permissions: Default::default(),
790            show_turn_stats: false,
791            show_merge_conflict_indicator: true,
792            new_thread_location: Default::default(),
793            sidebar_side: Default::default(),
794            thinking_display: Default::default(),
795        };
796
797        cx.update(|cx| {
798            AgentSettings::override_global(agent_settings.clone(), cx);
799            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
800
801            // Initial update
802            update_command_palette_filter(cx);
803        });
804
805        // Assert visible
806        cx.update(|cx| {
807            let filter = CommandPaletteFilter::try_global(cx).unwrap();
808            assert!(
809                !filter.is_hidden(&NewThread),
810                "NewThread should be visible by default"
811            );
812        });
813
814        // Disable agent
815        cx.update(|cx| {
816            let mut new_settings = agent_settings.clone();
817            new_settings.enabled = false;
818            AgentSettings::override_global(new_settings, cx);
819
820            // Trigger update
821            update_command_palette_filter(cx);
822        });
823
824        // Assert hidden
825        cx.update(|cx| {
826            let filter = CommandPaletteFilter::try_global(cx).unwrap();
827            assert!(
828                filter.is_hidden(&NewThread),
829                "NewThread should be hidden when agent is disabled"
830            );
831        });
832
833        // Test EditPredictionProvider
834        // Enable EditPredictionProvider::Copilot
835        cx.update(|cx| {
836            cx.update_global::<SettingsStore, _>(|store, cx| {
837                store.update_user_settings(cx, |s| {
838                    s.project
839                        .all_languages
840                        .edit_predictions
841                        .get_or_insert(Default::default())
842                        .provider = Some(EditPredictionProvider::Copilot);
843                });
844            });
845            update_command_palette_filter(cx);
846        });
847
848        cx.update(|cx| {
849            let filter = CommandPaletteFilter::try_global(cx).unwrap();
850            assert!(
851                !filter.is_hidden(&AcceptEditPrediction),
852                "EditPrediction should be visible when provider is Copilot"
853            );
854        });
855
856        // Disable EditPredictionProvider (None)
857        cx.update(|cx| {
858            cx.update_global::<SettingsStore, _>(|store, cx| {
859                store.update_user_settings(cx, |s| {
860                    s.project
861                        .all_languages
862                        .edit_predictions
863                        .get_or_insert(Default::default())
864                        .provider = Some(EditPredictionProvider::None);
865                });
866            });
867            update_command_palette_filter(cx);
868        });
869
870        cx.update(|cx| {
871            let filter = CommandPaletteFilter::try_global(cx).unwrap();
872            assert!(
873                filter.is_hidden(&AcceptEditPrediction),
874                "EditPrediction should be hidden when provider is None"
875            );
876        });
877    }
878
879    async fn setup_backfill_test(cx: &mut TestAppContext) -> Arc<dyn Fs> {
880        let fs = fs::FakeFs::new(cx.background_executor.clone());
881        fs.save(
882            paths::settings_file().as_path(),
883            &"{}".into(),
884            Default::default(),
885        )
886        .await
887        .unwrap();
888
889        cx.update(|cx| {
890            cx.set_global(db::AppDatabase::test_new());
891            let store = SettingsStore::test(cx);
892            cx.set_global(store);
893            AgentSettings::register(cx);
894            DisableAiSettings::register(cx);
895            cx.set_staff(true);
896        });
897
898        fs
899    }
900
901    #[gpui::test]
902    async fn test_backfill_sets_kvp_flag(cx: &mut TestAppContext) {
903        let fs = setup_backfill_test(cx).await;
904
905        cx.update(|cx| {
906            let kvp = KeyValueStore::global(cx);
907            assert!(
908                kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
909                    .unwrap()
910                    .is_none()
911            );
912
913            maybe_backfill_editor_layout(fs.clone(), false, cx);
914        });
915
916        cx.run_until_parked();
917
918        let kvp = cx.update(|cx| KeyValueStore::global(cx));
919        assert!(
920            kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
921                .unwrap()
922                .is_some(),
923            "flag should be set after backfill"
924        );
925    }
926
927    #[gpui::test]
928    async fn test_backfill_new_install_sets_flag_without_writing_settings(cx: &mut TestAppContext) {
929        let fs = setup_backfill_test(cx).await;
930
931        cx.update(|cx| {
932            maybe_backfill_editor_layout(fs.clone(), true, cx);
933        });
934
935        cx.run_until_parked();
936
937        let kvp = cx.update(|cx| KeyValueStore::global(cx));
938        assert!(
939            kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
940                .unwrap()
941                .is_some(),
942            "flag should be set even for new installs"
943        );
944
945        let written = fs.load(paths::settings_file().as_path()).await.unwrap();
946        assert_eq!(written.trim(), "{}", "settings file should be unchanged");
947    }
948
949    #[gpui::test]
950    async fn test_backfill_is_idempotent(cx: &mut TestAppContext) {
951        let fs = setup_backfill_test(cx).await;
952
953        cx.update(|cx| {
954            maybe_backfill_editor_layout(fs.clone(), false, cx);
955        });
956
957        cx.run_until_parked();
958
959        let after_first = fs.load(paths::settings_file().as_path()).await.unwrap();
960
961        cx.update(|cx| {
962            maybe_backfill_editor_layout(fs.clone(), false, cx);
963        });
964
965        cx.run_until_parked();
966
967        let after_second = fs.load(paths::settings_file().as_path()).await.unwrap();
968        assert_eq!(
969            after_first, after_second,
970            "second call should not change settings"
971        );
972    }
973
974    #[test]
975    fn test_deserialize_external_agent_variants() {
976        assert_eq!(
977            serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
978            Agent::NativeAgent,
979        );
980        assert_eq!(
981            serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
982            Agent::Custom {
983                id: "my-agent".into(),
984            },
985        );
986    }
987}