agent_ui.rs

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