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