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