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}