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