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