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