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