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