agent_ui.rs

  1pub mod acp;
  2mod agent_configuration;
  3mod agent_diff;
  4mod agent_model_selector;
  5mod agent_panel;
  6mod buffer_codegen;
  7mod completion_provider;
  8mod context;
  9mod context_server_configuration;
 10mod favorite_models;
 11mod inline_assistant;
 12mod inline_prompt_editor;
 13mod language_model_selector;
 14mod mention_set;
 15mod profile_selector;
 16mod slash_command;
 17mod slash_command_picker;
 18mod terminal_codegen;
 19mod terminal_inline_assistant;
 20mod text_thread_editor;
 21mod text_thread_history;
 22mod ui;
 23
 24use std::rc::Rc;
 25use std::sync::Arc;
 26
 27use agent_settings::{AgentProfileId, AgentSettings};
 28use assistant_slash_command::SlashCommandRegistry;
 29use client::Client;
 30use command_palette_hooks::CommandPaletteFilter;
 31use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
 32use fs::Fs;
 33use gpui::{Action, App, Entity, SharedString, actions};
 34use language::{
 35    LanguageRegistry,
 36    language_settings::{AllLanguageSettings, EditPredictionProvider},
 37};
 38use language_model::{
 39    ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
 40};
 41use project::DisableAiSettings;
 42use prompt_store::PromptBuilder;
 43use schemars::JsonSchema;
 44use serde::{Deserialize, Serialize};
 45use settings::{LanguageModelSelection, Settings as _, SettingsStore};
 46use std::any::TypeId;
 47
 48use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
 49pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
 50pub use crate::inline_assistant::InlineAssistant;
 51pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
 52pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
 53use zed_actions;
 54
 55actions!(
 56    agent,
 57    [
 58        /// Creates a new text-based conversation thread.
 59        NewTextThread,
 60        /// Toggles the menu to create new agent threads.
 61        ToggleNewThreadMenu,
 62        /// Toggles the navigation menu for switching between threads and views.
 63        ToggleNavigationMenu,
 64        /// Toggles the options menu for agent settings and preferences.
 65        ToggleOptionsMenu,
 66        /// Toggles the profile or mode selector for switching between agent profiles.
 67        ToggleProfileSelector,
 68        /// Cycles through available session modes.
 69        CycleModeSelector,
 70        /// Cycles through favorited models in the ACP model selector.
 71        CycleFavoriteModels,
 72        /// Expands the message editor to full size.
 73        ExpandMessageEditor,
 74        /// Removes all thread history.
 75        RemoveHistory,
 76        /// Opens the conversation history view.
 77        OpenHistory,
 78        /// Adds a context server to the configuration.
 79        AddContextServer,
 80        /// Removes the currently selected thread.
 81        RemoveSelectedThread,
 82        /// Starts a chat conversation with follow-up enabled.
 83        ChatWithFollow,
 84        /// Cycles to the next inline assist suggestion.
 85        CycleNextInlineAssist,
 86        /// Cycles to the previous inline assist suggestion.
 87        CyclePreviousInlineAssist,
 88        /// Moves focus up in the interface.
 89        FocusUp,
 90        /// Moves focus down in the interface.
 91        FocusDown,
 92        /// Moves focus left in the interface.
 93        FocusLeft,
 94        /// Moves focus right in the interface.
 95        FocusRight,
 96        /// Opens the active thread as a markdown file.
 97        OpenActiveThreadAsMarkdown,
 98        /// Opens the agent diff view to review changes.
 99        OpenAgentDiff,
100        /// Keeps the current suggestion or change.
101        Keep,
102        /// Rejects the current suggestion or change.
103        Reject,
104        /// Rejects all suggestions or changes.
105        RejectAll,
106        /// Keeps all suggestions or changes.
107        KeepAll,
108        /// Allow this operation only this time.
109        AllowOnce,
110        /// Allow this operation and remember the choice.
111        AllowAlways,
112        /// Reject this operation only this time.
113        RejectOnce,
114        /// Follows the agent's suggestions.
115        Follow,
116        /// Resets the trial upsell notification.
117        ResetTrialUpsell,
118        /// Resets the trial end upsell notification.
119        ResetTrialEndUpsell,
120        /// Continues the current thread.
121        ContinueThread,
122        /// Interrupts the current generation and sends the message immediately.
123        SendImmediately,
124        /// Sends the next queued message immediately.
125        SendNextQueuedMessage,
126        /// Removes the first message from the queue (the next one to be sent).
127        RemoveFirstQueuedMessage,
128        /// Clears all messages from the queue.
129        ClearMessageQueue,
130        /// Opens the permission granularity dropdown for the current tool call.
131        OpenPermissionDropdown,
132    ]
133);
134
135/// Action to authorize a tool call with a specific permission option.
136/// This is used by the permission granularity dropdown to authorize tool calls.
137#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
138#[action(namespace = agent)]
139#[serde(deny_unknown_fields)]
140pub struct AuthorizeToolCall {
141    /// The tool call ID to authorize.
142    pub tool_call_id: String,
143    /// The permission option ID to use.
144    pub option_id: String,
145    /// The kind of permission option (serialized as string).
146    pub option_kind: String,
147}
148
149/// Action to select a permission granularity option from the dropdown.
150/// This updates the selected granularity without triggering authorization.
151#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
152#[action(namespace = agent)]
153#[serde(deny_unknown_fields)]
154pub struct SelectPermissionGranularity {
155    /// The tool call ID for which to select the granularity.
156    pub tool_call_id: String,
157    /// The index of the selected granularity option.
158    pub index: usize,
159}
160
161/// Creates a new conversation thread, optionally based on an existing thread.
162#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
163#[action(namespace = agent)]
164#[serde(deny_unknown_fields)]
165pub struct NewThread;
166
167/// Creates a new external agent conversation thread.
168#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
169#[action(namespace = agent)]
170#[serde(deny_unknown_fields)]
171pub struct NewExternalAgentThread {
172    /// Which agent to use for the conversation.
173    agent: Option<ExternalAgent>,
174}
175
176#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
177#[action(namespace = agent)]
178#[serde(deny_unknown_fields)]
179pub struct NewNativeAgentThreadFromSummary {
180    from_session_id: agent_client_protocol::SessionId,
181}
182
183// TODO unify this with AgentType
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
185#[serde(rename_all = "snake_case")]
186pub enum ExternalAgent {
187    Gemini,
188    ClaudeCode,
189    Codex,
190    NativeAgent,
191    Custom { name: SharedString },
192}
193
194impl ExternalAgent {
195    pub fn server(
196        &self,
197        fs: Arc<dyn fs::Fs>,
198        thread_store: Entity<agent::ThreadStore>,
199    ) -> Rc<dyn agent_servers::AgentServer> {
200        match self {
201            Self::Gemini => Rc::new(agent_servers::Gemini),
202            Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
203            Self::Codex => Rc::new(agent_servers::Codex),
204            Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
205            Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
206        }
207    }
208}
209
210/// Opens the profile management interface for configuring agent tools and settings.
211#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
212#[action(namespace = agent)]
213#[serde(deny_unknown_fields)]
214pub struct ManageProfiles {
215    #[serde(default)]
216    pub customize_tools: Option<AgentProfileId>,
217}
218
219impl ManageProfiles {
220    pub fn customize_tools(profile_id: AgentProfileId) -> Self {
221        Self {
222            customize_tools: Some(profile_id),
223        }
224    }
225}
226
227#[derive(Clone)]
228pub(crate) enum ModelUsageContext {
229    InlineAssistant,
230}
231
232impl ModelUsageContext {
233    pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
234        match self {
235            Self::InlineAssistant => {
236                LanguageModelRegistry::read_global(cx).inline_assistant_model()
237            }
238        }
239    }
240}
241
242/// Initializes the `agent` crate.
243pub fn init(
244    fs: Arc<dyn Fs>,
245    client: Arc<Client>,
246    prompt_builder: Arc<PromptBuilder>,
247    language_registry: Arc<LanguageRegistry>,
248    is_eval: bool,
249    cx: &mut App,
250) {
251    assistant_text_thread::init(client, cx);
252    rules_library::init(cx);
253    if !is_eval {
254        // Initializing the language model from the user settings messes with the eval, so we only initialize them when
255        // we're not running inside of the eval.
256        init_language_model_settings(cx);
257    }
258    assistant_slash_command::init(cx);
259    agent_panel::init(cx);
260    context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
261    TextThreadEditor::init(cx);
262
263    register_slash_commands(cx);
264    inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
265    terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
266    cx.observe_new(move |workspace, window, cx| {
267        ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
268    })
269    .detach();
270    cx.observe_new(ManageProfilesModal::register).detach();
271
272    // Update command palette filter based on AI settings
273    update_command_palette_filter(cx);
274
275    // Watch for settings changes
276    cx.observe_global::<SettingsStore>(|app_cx| {
277        // When settings change, update the command palette filter
278        update_command_palette_filter(app_cx);
279    })
280    .detach();
281
282    cx.on_flags_ready(|_, cx| {
283        update_command_palette_filter(cx);
284    })
285    .detach();
286}
287
288fn update_command_palette_filter(cx: &mut App) {
289    let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
290    let agent_enabled = AgentSettings::get_global(cx).enabled;
291    let agent_v2_enabled = cx.has_flag::<AgentV2FeatureFlag>();
292    let edit_prediction_provider = AllLanguageSettings::get_global(cx)
293        .edit_predictions
294        .provider;
295
296    CommandPaletteFilter::update_global(cx, |filter, _| {
297        use editor::actions::{
298            AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
299            NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
300        };
301        let edit_prediction_actions = [
302            TypeId::of::<AcceptEditPrediction>(),
303            TypeId::of::<AcceptNextWordEditPrediction>(),
304            TypeId::of::<AcceptNextLineEditPrediction>(),
305            TypeId::of::<AcceptEditPrediction>(),
306            TypeId::of::<ShowEditPrediction>(),
307            TypeId::of::<NextEditPrediction>(),
308            TypeId::of::<PreviousEditPrediction>(),
309            TypeId::of::<ToggleEditPrediction>(),
310        ];
311
312        if disable_ai {
313            filter.hide_namespace("agent");
314            filter.hide_namespace("agents");
315            filter.hide_namespace("assistant");
316            filter.hide_namespace("copilot");
317            filter.hide_namespace("supermaven");
318            filter.hide_namespace("zed_predict_onboarding");
319            filter.hide_namespace("edit_prediction");
320
321            filter.hide_action_types(&edit_prediction_actions);
322            filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
323        } else {
324            if agent_enabled {
325                filter.show_namespace("agent");
326                filter.show_namespace("agents");
327            } else {
328                filter.hide_namespace("agent");
329                filter.hide_namespace("agents");
330            }
331
332            filter.show_namespace("assistant");
333
334            match edit_prediction_provider {
335                EditPredictionProvider::None => {
336                    filter.hide_namespace("edit_prediction");
337                    filter.hide_namespace("copilot");
338                    filter.hide_namespace("supermaven");
339                    filter.hide_action_types(&edit_prediction_actions);
340                }
341                EditPredictionProvider::Copilot => {
342                    filter.show_namespace("edit_prediction");
343                    filter.show_namespace("copilot");
344                    filter.hide_namespace("supermaven");
345                    filter.show_action_types(edit_prediction_actions.iter());
346                }
347                EditPredictionProvider::Supermaven => {
348                    filter.show_namespace("edit_prediction");
349                    filter.hide_namespace("copilot");
350                    filter.show_namespace("supermaven");
351                    filter.show_action_types(edit_prediction_actions.iter());
352                }
353                EditPredictionProvider::Zed
354                | EditPredictionProvider::Codestral
355                | EditPredictionProvider::Experimental(_) => {
356                    filter.show_namespace("edit_prediction");
357                    filter.hide_namespace("copilot");
358                    filter.hide_namespace("supermaven");
359                    filter.show_action_types(edit_prediction_actions.iter());
360                }
361            }
362
363            filter.show_namespace("zed_predict_onboarding");
364            filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
365            if !agent_v2_enabled {
366                filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
367            }
368        }
369    });
370}
371
372fn init_language_model_settings(cx: &mut App) {
373    update_active_language_model_from_settings(cx);
374
375    cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
376        .detach();
377    cx.subscribe(
378        &LanguageModelRegistry::global(cx),
379        |_, event: &language_model::Event, cx| match event {
380            language_model::Event::ProviderStateChanged(_)
381            | language_model::Event::AddedProvider(_)
382            | language_model::Event::RemovedProvider(_)
383            | language_model::Event::ProvidersChanged => {
384                update_active_language_model_from_settings(cx);
385            }
386            _ => {}
387        },
388    )
389    .detach();
390}
391
392fn update_active_language_model_from_settings(cx: &mut App) {
393    let settings = AgentSettings::get_global(cx);
394
395    fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
396        language_model::SelectedModel {
397            provider: LanguageModelProviderId::from(selection.provider.0.clone()),
398            model: LanguageModelId::from(selection.model.clone()),
399        }
400    }
401
402    let default = settings.default_model.as_ref().map(to_selected_model);
403    let inline_assistant = settings
404        .inline_assistant_model
405        .as_ref()
406        .map(to_selected_model);
407    let commit_message = settings
408        .commit_message_model
409        .as_ref()
410        .map(to_selected_model);
411    let thread_summary = settings
412        .thread_summary_model
413        .as_ref()
414        .map(to_selected_model);
415    let inline_alternatives = settings
416        .inline_alternatives
417        .iter()
418        .map(to_selected_model)
419        .collect::<Vec<_>>();
420
421    LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
422        registry.select_default_model(default.as_ref(), cx);
423        registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
424        registry.select_commit_message_model(commit_message.as_ref(), cx);
425        registry.select_thread_summary_model(thread_summary.as_ref(), cx);
426        registry.select_inline_alternative_models(inline_alternatives, cx);
427    });
428}
429
430fn register_slash_commands(cx: &mut App) {
431    let slash_command_registry = SlashCommandRegistry::global(cx);
432
433    slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
434    slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
435    slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
436    slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
437    slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
438    slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
439    slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
440    slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
441    slash_command_registry
442        .register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
443    slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
444
445    cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
446        move |is_enabled, _cx| {
447            if is_enabled {
448                slash_command_registry.register_command(
449                    assistant_slash_commands::StreamingExampleSlashCommand,
450                    false,
451                );
452            }
453        }
454    })
455    .detach();
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use agent_settings::{AgentProfileId, AgentSettings};
462    use command_palette_hooks::CommandPaletteFilter;
463    use editor::actions::AcceptEditPrediction;
464    use gpui::{BorrowAppContext, TestAppContext, px};
465    use project::DisableAiSettings;
466    use settings::{
467        DefaultAgentView, DockPosition, DockSide, NotifyWhenAgentWaiting, Settings, SettingsStore,
468    };
469
470    #[gpui::test]
471    fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
472        // Init settings
473        cx.update(|cx| {
474            let store = SettingsStore::test(cx);
475            cx.set_global(store);
476            command_palette_hooks::init(cx);
477            AgentSettings::register(cx);
478            DisableAiSettings::register(cx);
479            AllLanguageSettings::register(cx);
480        });
481
482        let agent_settings = AgentSettings {
483            enabled: true,
484            button: true,
485            dock: DockPosition::Right,
486            agents_panel_dock: DockSide::Left,
487            default_width: px(300.),
488            default_height: px(600.),
489            default_model: None,
490            inline_assistant_model: None,
491            inline_assistant_use_streaming_tools: false,
492            commit_message_model: None,
493            thread_summary_model: None,
494            inline_alternatives: vec![],
495            favorite_models: vec![],
496            default_profile: AgentProfileId::default(),
497            default_view: DefaultAgentView::Thread,
498            profiles: Default::default(),
499            always_allow_tool_actions: false,
500            notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
501            play_sound_when_agent_done: false,
502            single_file_review: false,
503            model_parameters: vec![],
504            enable_feedback: false,
505            expand_edit_card: true,
506            expand_terminal_card: true,
507            use_modifier_to_send: true,
508            message_editor_min_lines: 1,
509            tool_permissions: Default::default(),
510            show_turn_stats: false,
511        };
512
513        cx.update(|cx| {
514            AgentSettings::override_global(agent_settings.clone(), cx);
515            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
516
517            // Initial update
518            update_command_palette_filter(cx);
519        });
520
521        // Assert visible
522        cx.update(|cx| {
523            let filter = CommandPaletteFilter::try_global(cx).unwrap();
524            assert!(
525                !filter.is_hidden(&NewThread),
526                "NewThread should be visible by default"
527            );
528        });
529
530        // Disable agent
531        cx.update(|cx| {
532            let mut new_settings = agent_settings.clone();
533            new_settings.enabled = false;
534            AgentSettings::override_global(new_settings, cx);
535
536            // Trigger update
537            update_command_palette_filter(cx);
538        });
539
540        // Assert hidden
541        cx.update(|cx| {
542            let filter = CommandPaletteFilter::try_global(cx).unwrap();
543            assert!(
544                filter.is_hidden(&NewThread),
545                "NewThread should be hidden when agent is disabled"
546            );
547        });
548
549        // Test EditPredictionProvider
550        // Enable EditPredictionProvider::Copilot
551        cx.update(|cx| {
552            cx.update_global::<SettingsStore, _>(|store, cx| {
553                store.update_user_settings(cx, |s| {
554                    s.project
555                        .all_languages
556                        .features
557                        .get_or_insert(Default::default())
558                        .edit_prediction_provider = Some(EditPredictionProvider::Copilot);
559                });
560            });
561            update_command_palette_filter(cx);
562        });
563
564        cx.update(|cx| {
565            let filter = CommandPaletteFilter::try_global(cx).unwrap();
566            assert!(
567                !filter.is_hidden(&AcceptEditPrediction),
568                "EditPrediction should be visible when provider is Copilot"
569            );
570        });
571
572        // Disable EditPredictionProvider (None)
573        cx.update(|cx| {
574            cx.update_global::<SettingsStore, _>(|store, cx| {
575                store.update_user_settings(cx, |s| {
576                    s.project
577                        .all_languages
578                        .features
579                        .get_or_insert(Default::default())
580                        .edit_prediction_provider = Some(EditPredictionProvider::None);
581                });
582            });
583            update_command_palette_filter(cx);
584        });
585
586        cx.update(|cx| {
587            let filter = CommandPaletteFilter::try_global(cx).unwrap();
588            assert!(
589                filter.is_hidden(&AcceptEditPrediction),
590                "EditPrediction should be hidden when provider is None"
591            );
592        });
593    }
594}