Skip to repository content1299 lines · 46.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:51:01.051Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
agent_ui.rs
1mod agent_configuration;
2pub 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;
14pub mod draft_prompt_store;
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;
26pub mod omega_executor_disclosure;
27pub mod omega_exo_connection;
28pub mod omega_host_bridge;
29pub mod omega_router;
30pub mod omega_send_queue;
31mod profile_selector;
32mod terminal_codegen;
33mod terminal_inline_assistant;
34pub mod terminal_thread_metadata_store;
35#[cfg(any(test, feature = "test-support"))]
36pub mod test_support;
37mod thread_import;
38pub mod thread_metadata_store;
39pub mod thread_worktree_archive;
40
41pub mod threads_archive_view;
42mod ui;
43mod unicode_confusables;
44
45use std::rc::Rc;
46use std::sync::Arc;
47
48use ::ui::IconName;
49use agent_client_protocol::schema::v1 as acp;
50use agent_settings::{AgentProfileId, AgentSettings};
51use command_palette_hooks::CommandPaletteFilter;
52use editor::{Editor, SelectionEffects, scroll::Autoscroll};
53use feature_flags::FeatureFlagAppExt as _;
54use fs::Fs;
55use gpui::{
56 Action, App, Context, Entity, ImageSource, ReadGlobal as _, Resource, SharedString, SharedUri,
57 TaskExt, Window, actions,
58};
59use language::{
60 LanguageRegistry,
61 language_settings::{AllLanguageSettings, EditPredictionProvider},
62};
63use language_model::{
64 ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
65};
66use project::{AgentId, DisableAiSettings};
67use prompt_store::{self, PromptBuilder, rules_to_skills_migration};
68use rope::Point;
69use schemars::JsonSchema;
70use serde::{Deserialize, Serialize};
71use settings::{LanguageModelSelection, Settings as _, SettingsStore, SidebarSide};
72use std::any::TypeId;
73use std::path::{Path, PathBuf};
74use workspace::{OpenOptions, Workspace};
75
76use crate::agent_configuration::ManageProfilesModal;
77pub use crate::agent_connection_store::{ActiveAcpConnection, AgentConnectionStore};
78pub use crate::agent_panel::{
79 AgentPanel, AgentPanelEvent, AgentPanelTerminalInfo, MaxIdleRetainedThreads, TerminalId,
80 ThreadTitleRegenerationResult,
81};
82use crate::agent_registry_ui::AgentRegistryPage;
83pub use crate::inline_assistant::InlineAssistant;
84pub use crate::message_editor::MessageEditorEvent;
85pub use crate::omega_host_bridge::omega_effectd_host_handler;
86pub use crate::thread_metadata_store::ThreadId;
87pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
88pub use conversation_view::open_markdown_in_workspace;
89pub use conversation_view::{ConversationView, StateChange};
90pub use external_source_prompt::ExternalSourcePrompt;
91pub(crate) use mode_selector::ModeSelector;
92pub(crate) use model_selector::ModelSelector;
93pub(crate) use model_selector_popover::ModelSelectorPopover;
94pub use thread_import::{
95 AcpThreadImportOnboarding, CrossChannelImportOnboarding, ThreadImportModal,
96 channels_with_threads, import_threads_from_other_channels,
97};
98use zed_actions;
99pub use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, SwitchWorktree};
100
101pub(crate) fn resolve_agent_image(
102 dest_url: &str,
103 worktree_roots: &[PathBuf],
104) -> Option<ImageSource> {
105 if dest_url.starts_with("http://") || dest_url.starts_with("https://") {
106 return Some(ImageSource::Resource(Resource::Uri(SharedUri::from(
107 dest_url.to_string(),
108 ))));
109 }
110
111 let path = Path::new(dest_url);
112 if path.is_absolute() && path.exists() {
113 return Some(ImageSource::Resource(Resource::Path(Arc::from(path))));
114 }
115
116 for root in worktree_roots {
117 let absolute_path = root.join(dest_url);
118 if absolute_path.exists() {
119 return Some(ImageSource::Resource(Resource::Path(Arc::from(
120 absolute_path.as_path(),
121 ))));
122 }
123 }
124
125 None
126}
127
128/// Opens `abs_path` in the workspace, moving the cursor to `point` when one
129/// is given. Paths outside every worktree are only opened when a file exists
130/// there, so broken agent links don't create empty buffers.
131pub(crate) fn open_abs_path_at_point(
132 workspace: &mut Workspace,
133 abs_path: PathBuf,
134 point: Option<Point>,
135 window: &mut Window,
136 cx: &mut Context<Workspace>,
137) {
138 let project_path = workspace
139 .project()
140 .update(cx, |project, cx| project.find_project_path(&abs_path, cx));
141 let fs = workspace.project().read(cx).fs().clone();
142 let workspace = cx.weak_entity();
143 window
144 .spawn(cx, async move |cx| {
145 let item = if let Some(project_path) = project_path {
146 workspace
147 .update_in(cx, |workspace, window, cx| {
148 workspace.open_path(project_path, None, true, window, cx)
149 })?
150 .await?
151 } else {
152 let metadata = fs.metadata(&abs_path).await?;
153 anyhow::ensure!(
154 metadata.is_some_and(|metadata| !metadata.is_dir),
155 "no file found at path {abs_path:?}"
156 );
157 workspace
158 .update_in(cx, |workspace, window, cx| {
159 workspace.open_abs_path(
160 abs_path,
161 OpenOptions {
162 focus: Some(true),
163 ..Default::default()
164 },
165 window,
166 cx,
167 )
168 })?
169 .await?
170 };
171 let Some(point) = point else {
172 return Ok(());
173 };
174 let Some(editor) = item.downcast::<Editor>() else {
175 return Ok(());
176 };
177 editor
178 .update_in(cx, |editor, window, cx| {
179 editor.change_selections(
180 SelectionEffects::scroll(Autoscroll::center()),
181 window,
182 cx,
183 |selections| selections.select_ranges([point..point]),
184 );
185 })
186 .ok();
187 anyhow::Ok(())
188 })
189 .detach_and_log_err(cx);
190}
191
192pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
193const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
194
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum AgentThreadSource {
197 AgentPanel,
198 GitPanel,
199 Sidebar,
200}
201
202impl AgentThreadSource {
203 pub fn as_str(self) -> &'static str {
204 match self {
205 Self::AgentPanel => "agent_panel",
206 Self::GitPanel => "git_panel",
207 Self::Sidebar => "sidebar",
208 }
209 }
210}
211
212pub(crate) fn agent_sidebar_side(cx: &App) -> &'static str {
213 match AgentSettings::get_global(cx).sidebar_side() {
214 SidebarSide::Left => "left",
215 SidebarSide::Right => "right",
216 }
217}
218
219actions!(
220 agent,
221 [
222 /// Toggles the menu to create new agent threads.
223 ToggleNewThreadMenu,
224 /// Toggles the options menu for agent settings and preferences.
225 ToggleOptionsMenu,
226 /// Toggles the profile or mode selector for switching between agent profiles.
227 ToggleProfileSelector,
228 /// Cycles through available session modes.
229 CycleModeSelector,
230 /// Cycles through favorited models in the ACP model selector.
231 CycleFavoriteModels,
232 /// Expands the message editor to full size.
233 ExpandMessageEditor,
234 /// Archives the currently selected thread.
235 ArchiveSelectedThread,
236 /// Removes the currently selected thread.
237 RemoveSelectedThread,
238 /// Renames the currently selected thread.
239 RenameSelectedThread,
240 /// Starts a chat conversation with follow-up enabled.
241 ChatWithFollow,
242 /// Cycles to the next inline assist suggestion.
243 CycleNextInlineAssist,
244 /// Cycles to the previous inline assist suggestion.
245 CyclePreviousInlineAssist,
246 /// Moves focus up in the interface.
247 FocusUp,
248 /// Moves focus down in the interface.
249 FocusDown,
250 /// Moves focus left in the interface.
251 FocusLeft,
252 /// Moves focus right in the interface.
253 FocusRight,
254 /// Opens the active thread as a markdown file.
255 OpenActiveThreadAsMarkdown,
256 /// Opens the agent diff view to review changes.
257 OpenAgentDiff,
258 /// Copies the current thread to the clipboard as JSON for debugging.
259 CopyThreadToClipboard,
260 /// Loads a thread from the clipboard JSON for debugging.
261 LoadThreadFromClipboard,
262 /// Reruns the rules-to-skills migration.
263 RerunRulesToSkillsMigration,
264 /// Keeps the current suggestion or change.
265 Keep,
266 /// Rejects the current suggestion or change.
267 Reject,
268 /// Rejects all suggestions or changes.
269 RejectAll,
270 /// Undoes the most recent reject operation, restoring the rejected changes.
271 UndoLastReject,
272 /// Keeps all suggestions or changes.
273 KeepAll,
274 /// Allow this operation only this time.
275 AllowOnce,
276 /// Allow this operation and remember the choice.
277 AllowAlways,
278 /// Reject this operation only this time.
279 RejectOnce,
280 /// Follows the agent's suggestions.
281 Follow,
282 /// Resets the trial upsell notification.
283 ResetTrialUpsell,
284 /// Resets the trial end upsell notification.
285 ResetTrialEndUpsell,
286 /// Re-enables the fast mode warning for every provider and model.
287 ResetFastModeWarnings,
288 /// Opens the "Add Context" menu in the message editor.
289 OpenAddContextMenu,
290 /// Interrupts the current generation and sends the message immediately.
291 SendImmediately,
292 /// Sends the next queued message immediately.
293 SendNextQueuedMessage,
294 /// Removes the first message from the queue (the next one to be sent).
295 RemoveFirstQueuedMessage,
296 /// Edits the first message in the queue (the next one to be sent).
297 EditFirstQueuedMessage,
298 /// Toggles steering for the first queued message: when on, it interrupts
299 /// the agent at its next step instead of waiting for it to finish.
300 ToggleSteerFirstQueuedMessage,
301 /// Clears all messages from the queue.
302 ClearMessageQueue,
303 /// Opens the permission granularity dropdown for the current tool call.
304 OpenPermissionDropdown,
305 /// Toggles thinking mode for models that support extended thinking.
306 ToggleThinkingMode,
307 /// Cycles through available thinking effort levels for the current model.
308 CycleThinkingEffort,
309 /// Toggles the thinking effort selector menu open or closed.
310 ToggleThinkingEffortMenu,
311 /// Toggles fast mode for models that support it.
312 ToggleFastMode,
313 /// Scroll the output by one page up.
314 ScrollOutputPageUp,
315 /// Scroll the output by one page down.
316 ScrollOutputPageDown,
317 /// Scroll the output up by three lines.
318 ScrollOutputLineUp,
319 /// Scroll the output down by three lines.
320 ScrollOutputLineDown,
321 /// Scroll the output to the top.
322 ScrollOutputToTop,
323 /// Scroll the output to the bottom.
324 ScrollOutputToBottom,
325 /// Scroll the output to the previous user message.
326 ScrollOutputToPreviousMessage,
327 /// Scroll the output to the next user message.
328 ScrollOutputToNextMessage,
329 /// Toggles in-thread search over the current agent thread's contents.
330 ToggleSearch,
331 /// Import agent threads from other Omega release channels (e.g. Preview, Nightly).
332 ImportThreadsFromOtherChannels,
333 /// Starts a new terminal thread.
334 NewTerminalThread,
335 ]
336);
337
338actions!(
339 dev,
340 [
341 /// Shows metadata for the currently active thread.
342 ShowThreadMetadata,
343 /// Shows metadata for all threads in the sidebar.
344 ShowAllSidebarThreadMetadata,
345 ]
346);
347
348/// Action to authorize a tool call with a specific permission option.
349/// This is used by the permission granularity dropdown to authorize tool calls.
350#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
351#[action(namespace = agent)]
352#[serde(deny_unknown_fields)]
353pub struct AuthorizeToolCall {
354 /// The tool call ID to authorize.
355 pub tool_call_id: String,
356 /// The permission option ID to use.
357 pub option_id: String,
358 /// The kind of permission option (serialized as string).
359 pub option_kind: String,
360}
361
362/// Action to select a permission granularity option from the dropdown.
363/// This updates the selected granularity without triggering authorization.
364#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
365#[action(namespace = agent)]
366#[serde(deny_unknown_fields)]
367pub struct SelectPermissionGranularity {
368 /// The tool call ID for which to select the granularity.
369 pub tool_call_id: String,
370 /// The index of the selected granularity option.
371 pub index: usize,
372}
373
374/// Action to toggle a command pattern checkbox in the permission dropdown.
375#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
376#[action(namespace = agent)]
377#[serde(deny_unknown_fields)]
378pub struct ToggleCommandPattern {
379 /// The tool call ID for which to toggle the pattern.
380 pub tool_call_id: String,
381 /// The index of the command pattern to toggle.
382 pub pattern_index: usize,
383}
384
385/// Creates a new conversation thread, optionally based on an existing thread.
386#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
387#[action(namespace = agent)]
388#[serde(deny_unknown_fields)]
389pub struct NewThread;
390
391/// Creates a new external agent conversation thread.
392#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
393#[action(namespace = agent)]
394#[serde(deny_unknown_fields)]
395pub struct NewExternalAgentThread {
396 /// The agent id to use for the conversation.
397 #[serde(deserialize_with = "deserialize_external_agent_id")]
398 agent: AgentId,
399}
400
401fn deserialize_external_agent_id<'de, D>(deserializer: D) -> Result<AgentId, D::Error>
402where
403 D: serde::Deserializer<'de>,
404{
405 #[derive(Deserialize)]
406 #[serde(untagged)]
407 enum AgentIdOrLegacyAgent {
408 LegacyAgent(Agent),
409 AgentId(AgentId),
410 }
411
412 match AgentIdOrLegacyAgent::deserialize(deserializer)? {
413 AgentIdOrLegacyAgent::AgentId(agent_id) => Ok(agent_id),
414 AgentIdOrLegacyAgent::LegacyAgent(Agent::Custom { id }) => Ok(id),
415 AgentIdOrLegacyAgent::LegacyAgent(Agent::NativeAgent) => Ok(Agent::NativeAgent.id()),
416 #[cfg(any(test, feature = "test-support"))]
417 AgentIdOrLegacyAgent::LegacyAgent(Agent::Stub) => Ok(Agent::Stub.id()),
418 }
419}
420
421#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
422#[action(namespace = agent)]
423#[serde(deny_unknown_fields)]
424pub struct NewNativeAgentThreadFromSummary {
425 from_session_id: acp::SessionId,
426}
427
428#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
429#[serde(rename_all = "snake_case")]
430#[non_exhaustive]
431pub enum Agent {
432 #[default]
433 #[serde(alias = "NativeAgent", alias = "TextThread")]
434 NativeAgent,
435 #[serde(alias = "Custom")]
436 Custom {
437 #[serde(rename = "name")]
438 id: AgentId,
439 },
440 #[cfg(any(test, feature = "test-support"))]
441 Stub,
442}
443
444impl From<AgentId> for Agent {
445 fn from(id: AgentId) -> Self {
446 if id.as_ref() == agent::OMEGA_AGENT_ID.as_ref() {
447 return Self::NativeAgent;
448 }
449 #[cfg(any(test, feature = "test-support"))]
450 if id.as_ref() == "stub" {
451 return Self::Stub;
452 }
453 Self::Custom { id }
454 }
455}
456
457impl Agent {
458 pub fn id(&self) -> AgentId {
459 match self {
460 Self::NativeAgent => agent::OMEGA_AGENT_ID.clone(),
461 Self::Custom { id } => id.clone(),
462 #[cfg(any(test, feature = "test-support"))]
463 Self::Stub => "stub".into(),
464 }
465 }
466
467 pub fn is_native(&self) -> bool {
468 matches!(self, Self::NativeAgent)
469 }
470
471 pub fn label(&self) -> SharedString {
472 match self {
473 Self::NativeAgent => "Omega Agent".into(),
474 Self::Custom { id, .. } => id.0.clone(),
475 #[cfg(any(test, feature = "test-support"))]
476 Self::Stub => "Stub Agent".into(),
477 }
478 }
479
480 pub fn icon(&self) -> Option<IconName> {
481 match self {
482 Self::NativeAgent => Some(IconName::OmegaAgent),
483 Self::Custom { .. } => Some(IconName::Sparkle),
484 #[cfg(any(test, feature = "test-support"))]
485 Self::Stub => None,
486 }
487 }
488
489 pub fn server(
490 &self,
491 fs: Arc<dyn fs::Fs>,
492 thread_store: Entity<agent::ThreadStore>,
493 ) -> Rc<dyn agent_servers::AgentServer> {
494 match self {
495 // OMEGA-DELTA-0035. The native agent, behind Omega Agent's router.
496 // Every new first-party session is routed on purpose and the
497 // decision is written to the route journal before the turn exists.
498 // The thread still carries the executor's connection, so omega#77's
499 // disclosure classifies the executor and not the router.
500 Self::NativeAgent => Rc::new(crate::omega_router::OmegaRouterServer::new(
501 agent::NativeAgentServer::new(fs, thread_store),
502 // `ZED_STATELESS` means "do not persist this run", and a
503 // rendering harness or a debug session sets it. Writing route
504 // decisions into the owner's real Omega data directory from a
505 // harness would put sessions nobody started into a record an
506 // operator reads. The choice is made here rather than inside
507 // the router, because the router is not allowed to read the
508 // environment — see `RouteJournal::data_dir_path`.
509 if std::env::var("ZED_STATELESS").is_ok() {
510 std::env::temp_dir()
511 .join(format!("omega-route-journal-{}.json", std::process::id()))
512 } else {
513 crate::omega_router::RouteJournal::data_dir_path()
514 },
515 // OMEGA-DELTA-0042, omega#87. The Exo harness lane's
516 // configuration. A stateless run gets a path inside the
517 // temporary directory, which does not exist, so a rendering
518 // harness never spawns somebody's Exo — the same reasoning as
519 // the journal above, and the same reason the choice is made
520 // here rather than inside the router.
521 if std::env::var("ZED_STATELESS").is_ok() {
522 std::env::temp_dir()
523 .join(format!("omega-exo-lane-{}.json", std::process::id()))
524 } else {
525 crate::omega_exo_connection::ExoLaneConfig::data_dir_path()
526 },
527 )),
528 Self::Custom { id: name } => {
529 Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
530 }
531 #[cfg(any(test, feature = "test-support"))]
532 Self::Stub => Rc::new(crate::test_support::StubAgentServer::default_response()),
533 }
534 }
535}
536
537/// Content to initialize new external agent with.
538pub enum AgentInitialContent {
539 ThreadSummary {
540 session_id: acp::SessionId,
541 title: Option<SharedString>,
542 },
543 ContentBlock {
544 blocks: Vec<acp::ContentBlock>,
545 auto_submit: bool,
546 },
547 FromExternalSource(ExternalSourcePrompt),
548}
549
550impl From<ExternalSourcePrompt> for AgentInitialContent {
551 fn from(prompt: ExternalSourcePrompt) -> Self {
552 Self::FromExternalSource(prompt)
553 }
554}
555
556/// Opens the profile management interface for configuring agent tools and settings.
557#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
558#[action(namespace = agent)]
559#[serde(deny_unknown_fields)]
560pub struct ManageProfiles {
561 #[serde(default)]
562 pub customize_tools: Option<AgentProfileId>,
563}
564
565impl ManageProfiles {
566 pub fn customize_tools(profile_id: AgentProfileId) -> Self {
567 Self {
568 customize_tools: Some(profile_id),
569 }
570 }
571}
572
573#[derive(Clone)]
574pub(crate) enum ModelUsageContext {
575 InlineAssistant,
576}
577
578impl ModelUsageContext {
579 pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
580 match self {
581 Self::InlineAssistant => {
582 LanguageModelRegistry::read_global(cx).inline_assistant_model()
583 }
584 }
585 }
586}
587
588pub(crate) fn humanize_token_count(count: u64) -> String {
589 match count {
590 0..=999 => count.to_string(),
591 1000..=9999 => {
592 let thousands = count / 1000;
593 let hundreds = (count % 1000 + 50) / 100;
594 if hundreds == 0 {
595 format!("{}k", thousands)
596 } else if hundreds == 10 {
597 format!("{}k", thousands + 1)
598 } else {
599 format!("{}.{}k", thousands, hundreds)
600 }
601 }
602 10_000..=999_999 => format!("{}k", (count + 500) / 1000),
603 1_000_000..=9_999_999 => {
604 let millions = count / 1_000_000;
605 let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
606 if hundred_thousands == 0 {
607 format!("{}M", millions)
608 } else if hundred_thousands == 10 {
609 format!("{}M", millions + 1)
610 } else {
611 format!("{}.{}M", millions, hundred_thousands)
612 }
613 }
614 10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
615 }
616}
617
618/// Initializes the `agent` crate.
619pub fn init(
620 fs: Arc<dyn Fs>,
621 prompt_builder: Arc<PromptBuilder>,
622 language_registry: Arc<LanguageRegistry>,
623 is_new_install: bool,
624 is_eval: bool,
625 cx: &mut App,
626) {
627 agent::ThreadStore::init_global(cx);
628 prompt_store::init(cx);
629
630 cx.set_global(agent_skills::SkillsUpdatedHook(std::rc::Rc::new(|cx| {
631 let workspaces: Vec<_> = workspace::AppState::global(cx)
632 .workspace_store
633 .read(cx)
634 .workspaces()
635 .cloned()
636 .collect();
637
638 for workspace in workspaces {
639 workspace
640 .update(cx, |workspace, cx| {
641 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
642 panel.update(cx, |panel, cx| panel.refresh_skills(cx));
643 }
644 })
645 .ok();
646 }
647 })));
648
649 if !is_eval {
650 // Initializing the language model from the user settings messes with the eval, so we only initialize them when
651 // we're not running inside of the eval.
652 init_language_model_settings(cx);
653 }
654 agent_panel::init(cx);
655 context_server_configuration::init(language_registry, fs.clone(), cx);
656 thread_metadata_store::init(cx);
657 terminal_thread_metadata_store::init(cx);
658
659 inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
660 terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
661 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
662 workspace.register_action(
663 move |workspace: &mut Workspace,
664 _: &zed_actions::AcpRegistry,
665 window: &mut Window,
666 cx: &mut Context<Workspace>| {
667 let existing = workspace
668 .active_pane()
669 .read(cx)
670 .items()
671 .find_map(|item| item.downcast::<AgentRegistryPage>());
672
673 if let Some(existing) = existing {
674 existing.update(cx, |_, cx| {
675 project::AgentRegistryStore::global(cx)
676 .update(cx, |store, cx| store.refresh(cx));
677 });
678 workspace.activate_item(&existing, true, true, window, cx);
679 } else {
680 let registry_page = AgentRegistryPage::new(workspace, window, cx);
681 workspace.add_item_to_active_pane(
682 Box::new(registry_page),
683 None,
684 true,
685 window,
686 cx,
687 );
688 }
689 },
690 );
691 })
692 .detach();
693 cx.observe_new(ManageProfilesModal::register).detach();
694 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
695 workspace.register_action(
696 |workspace: &mut Workspace,
697 _: &ImportThreadsFromOtherChannels,
698 _window: &mut Window,
699 cx: &mut Context<Workspace>| {
700 import_threads_from_other_channels(workspace, cx);
701 },
702 );
703 })
704 .detach();
705
706 {
707 let fs = fs.clone();
708 cx.observe_new(move |workspace: &mut Workspace, _window, _cx| {
709 let fs = fs.clone();
710 workspace.register_action(
711 move |workspace: &mut Workspace,
712 _: &RerunRulesToSkillsMigration,
713 window: &mut Window,
714 cx: &mut Context<Workspace>| {
715 rerun_rules_to_skills_migration(workspace, fs.clone(), window, cx);
716 },
717 );
718 })
719 .detach();
720 }
721
722 // Update command palette filter based on AI settings
723 update_command_palette_filter(cx);
724
725 // Watch for settings changes
726 cx.observe_global::<SettingsStore>(|app_cx| {
727 // When settings change, update the command palette filter
728 update_command_palette_filter(app_cx);
729 })
730 .detach();
731
732 cx.on_flags_ready(|_, cx| {
733 update_command_palette_filter(cx);
734 })
735 .detach();
736
737 // Kick off the one-time migration of non-Default Rules to global
738 // Skills. Test builds keep the old feature-flag deferral because
739 // server flags are never received in `gpui::test` contexts, avoiding
740 // sqlite worker activity that can race with the deterministic scheduler.
741 #[cfg(any(test, feature = "test-support"))]
742 {
743 let fs = fs.clone();
744 cx.on_flags_ready(move |_, cx| {
745 rules_to_skills_migration::migrate_rules_to_skills_if_needed(fs.clone(), cx);
746 })
747 .detach();
748 }
749 #[cfg(not(any(test, feature = "test-support")))]
750 {
751 rules_to_skills_migration::migrate_rules_to_skills_if_needed(fs.clone(), cx);
752 }
753
754 maybe_backfill_editor_layout(fs, is_new_install, cx);
755}
756
757fn rerun_rules_to_skills_migration(
758 _workspace: &mut Workspace,
759 fs: Arc<dyn Fs>,
760 window: &mut Window,
761 cx: &mut Context<Workspace>,
762) {
763 let workspace = cx.weak_entity();
764 window
765 .spawn(cx, async move |cx| {
766 let migration_task = cx.update(|_window, cx| {
767 rules_to_skills_migration::rerun_rules_to_skills_migration(fs, cx)
768 })?;
769 let result = migration_task.await?;
770 log::info!("Forced rules-to-skills migration result: {result:?}");
771
772 cx.update(|_window, cx| {
773 show_rules_to_skills_migration_toast(
774 &workspace,
775 "Rules-to-skills migration rerun. Please double-check AGENTS.md and Skills for missing or duplicated prompts.",
776 cx,
777 );
778 })?;
779 anyhow::Ok(())
780 })
781 .detach_and_log_err(cx);
782}
783
784fn show_rules_to_skills_migration_toast(
785 workspace: &gpui::WeakEntity<Workspace>,
786 message: &'static str,
787 cx: &mut App,
788) {
789 if let Some(workspace) = workspace.upgrade() {
790 workspace.update(cx, |workspace, cx| {
791 struct RulesToSkillsMigrationRerunToast;
792 workspace.show_toast(
793 workspace::Toast::new(
794 workspace::notifications::NotificationId::unique::<
795 RulesToSkillsMigrationRerunToast,
796 >(),
797 message,
798 )
799 .autohide(),
800 cx,
801 );
802 });
803 }
804}
805
806fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut App) {
807 let kvp = db::kvp::KeyValueStore::global(cx);
808 let already_backfilled =
809 util::ResultExt::log_err(kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY))
810 .flatten()
811 .is_some();
812
813 if !already_backfilled {
814 if !is_new_install {
815 AgentSettings::backfill_editor_layout(fs, cx);
816 }
817
818 db::write_and_log(cx, move || async move {
819 kvp.write_kvp(
820 PARALLEL_AGENT_LAYOUT_BACKFILL_KEY.to_string(),
821 "1".to_string(),
822 )
823 .await
824 });
825 }
826}
827
828fn update_command_palette_filter(cx: &mut App) {
829 let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
830 let agent_enabled = AgentSettings::get_global(cx).enabled;
831
832 let edit_prediction_provider = AllLanguageSettings::get_global(cx)
833 .edit_predictions
834 .provider;
835
836 CommandPaletteFilter::update_global(cx, |filter, _| {
837 use editor::actions::{
838 AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
839 NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
840 };
841 let edit_prediction_actions = [
842 TypeId::of::<AcceptEditPrediction>(),
843 TypeId::of::<AcceptNextWordEditPrediction>(),
844 TypeId::of::<AcceptNextLineEditPrediction>(),
845 TypeId::of::<ShowEditPrediction>(),
846 TypeId::of::<NextEditPrediction>(),
847 TypeId::of::<PreviousEditPrediction>(),
848 TypeId::of::<ToggleEditPrediction>(),
849 ];
850
851 let manage_skills_action = [TypeId::of::<zed_actions::assistant::ManageSkills>()];
852 let skill_creator_actions = [
853 TypeId::of::<zed_actions::assistant::OpenSkillCreator>(),
854 TypeId::of::<zed_actions::assistant::CreateSkillFromUrl>(),
855 ];
856
857 if disable_ai {
858 filter.hide_namespace("agent");
859 filter.hide_namespace("agents");
860 filter.hide_namespace("assistant");
861 filter.hide_namespace("copilot");
862 filter.hide_namespace("omega_predict_onboarding");
863 filter.hide_namespace("edit_prediction");
864
865 filter.hide_action_types(&edit_prediction_actions);
866 filter.hide_action_types(&[TypeId::of::<zed_actions::OpenOmegaPredictOnboarding>()]);
867 } else {
868 if agent_enabled {
869 filter.show_namespace("agent");
870 filter.show_namespace("agents");
871 filter.show_namespace("assistant");
872 } else {
873 filter.hide_namespace("agent");
874 filter.hide_namespace("agents");
875 filter.hide_namespace("assistant");
876 }
877
878 match edit_prediction_provider {
879 EditPredictionProvider::None => {
880 filter.hide_namespace("edit_prediction");
881 filter.hide_namespace("copilot");
882 filter.hide_action_types(&edit_prediction_actions);
883 }
884 EditPredictionProvider::Copilot => {
885 filter.show_namespace("edit_prediction");
886 filter.show_namespace("copilot");
887 filter.show_action_types(edit_prediction_actions.iter());
888 }
889 EditPredictionProvider::Zed
890 | EditPredictionProvider::Codestral
891 | EditPredictionProvider::Ollama
892 | EditPredictionProvider::OpenAiCompatibleApi
893 | EditPredictionProvider::Mercury => {
894 filter.show_namespace("edit_prediction");
895 filter.hide_namespace("copilot");
896 filter.show_action_types(edit_prediction_actions.iter());
897 }
898 }
899
900 filter.show_namespace("omega_predict_onboarding");
901 filter.show_action_types(&[TypeId::of::<zed_actions::OpenOmegaPredictOnboarding>()]);
902
903 filter.show_namespace("multi_workspace");
904 }
905
906 // Hide `agent: manage skills` — skills are surfaced through the
907 // settings UI now. Applied after the disable-ai / agent-enabled
908 // branches so it overrides the `show_namespace("assistant")` call
909 // above without affecting the rest of that namespace's actions.
910 if !disable_ai {
911 filter.hide_action_types(&manage_skills_action);
912 filter.show_action_types(skill_creator_actions.iter());
913 } else {
914 filter.show_action_types(manage_skills_action.iter());
915 filter.hide_action_types(&skill_creator_actions);
916 }
917 });
918}
919
920fn init_language_model_settings(cx: &mut App) {
921 update_active_language_model_from_settings(cx);
922
923 cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
924 .detach();
925 cx.subscribe(
926 &LanguageModelRegistry::global(cx),
927 |registry, event: &language_model::Event, cx| match event {
928 language_model::Event::ProviderStateChanged(_)
929 | language_model::Event::AddedProvider(_)
930 | language_model::Event::RemovedProvider(_)
931 | language_model::Event::ProvidersChanged => {
932 registry.update(cx, |registry, cx| registry.refresh_fallback_model(cx));
933 update_active_language_model_from_settings(cx);
934 }
935 _ => {}
936 },
937 )
938 .detach();
939}
940
941fn update_active_language_model_from_settings(cx: &mut App) {
942 let settings = AgentSettings::get_global(cx);
943
944 fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
945 language_model::SelectedModel {
946 provider: LanguageModelProviderId::from(selection.provider.0.clone()),
947 model: LanguageModelId::from(selection.model.clone()),
948 }
949 }
950
951 let should_use_fallback = SettingsStore::global(cx)
952 .raw_user_settings()
953 .and_then(|user| user.content.agent.as_ref())
954 .and_then(|agent| agent.default_model.as_ref())
955 .is_none();
956
957 let default = settings.default_model.as_ref().map(to_selected_model);
958 let inline_assistant = settings
959 .inline_assistant_model
960 .as_ref()
961 .map(to_selected_model);
962 let commit_message = settings
963 .commit_message_model
964 .as_ref()
965 .map(to_selected_model);
966 let thread_summary = settings
967 .thread_summary_model
968 .as_ref()
969 .map(to_selected_model);
970 let compaction = settings.compaction_model.as_ref().map(to_selected_model);
971 let inline_alternatives = settings
972 .inline_alternatives
973 .iter()
974 .map(to_selected_model)
975 .collect::<Vec<_>>();
976
977 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
978 registry.select_default_model(default.as_ref(), cx);
979 registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
980 registry.select_commit_message_model(commit_message.as_ref(), cx);
981 registry.select_thread_summary_model(thread_summary.as_ref(), cx);
982 registry.select_compaction_model(compaction.as_ref(), cx);
983 registry.select_inline_alternative_models(inline_alternatives, cx);
984 registry.set_should_use_fallback(should_use_fallback);
985 });
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991 use agent_settings::{AgentProfileId, AgentSettings};
992 use command_palette_hooks::CommandPaletteFilter;
993 use db::kvp::KeyValueStore;
994 use editor::actions::AcceptEditPrediction;
995 use gpui::{BorrowAppContext, TestAppContext, px};
996 use project::DisableAiSettings;
997 use settings::{
998 DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings, SettingsStore,
999 };
1000
1001 #[gpui::test]
1002 fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
1003 // Init settings
1004 cx.update(|cx| {
1005 let store = SettingsStore::test(cx);
1006 cx.set_global(store);
1007 command_palette_hooks::init(cx);
1008 AgentSettings::register(cx);
1009 DisableAiSettings::register(cx);
1010 AllLanguageSettings::register(cx);
1011 });
1012
1013 let agent_settings = AgentSettings {
1014 enabled: true,
1015 button: true,
1016 dock: DockPosition::Right,
1017 flexible: true,
1018 default_width: px(300.),
1019 default_height: px(600.),
1020 max_content_width: Some(px(850.)),
1021 default_model: None,
1022 subagent_model: None,
1023 inline_assistant_model: None,
1024 inline_assistant_use_streaming_tools: false,
1025 commit_message_model: None,
1026 commit_message_include_project_rules: true,
1027 commit_message_instructions: None,
1028 thread_summary_model: None,
1029 compaction_model: None,
1030 inline_alternatives: vec![],
1031 favorite_models: vec![],
1032 default_profile: AgentProfileId::default(),
1033 profiles: Default::default(),
1034 notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
1035 play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
1036 single_file_review: false,
1037 model_parameters: vec![],
1038 auto_compact: agent_settings::AutoCompactSettings {
1039 enabled: false,
1040 threshold: agent_settings::AutoCompactThreshold::DEFAULT,
1041 },
1042 enable_feedback: false,
1043 expand_edit_card: true,
1044 expand_terminal_card: true,
1045 terminal_init_command: None,
1046 cancel_generation_on_terminal_stop: true,
1047 use_modifier_to_send: true,
1048 message_editor_min_lines: 1,
1049 tool_permissions: Default::default(),
1050 sandbox_permissions: Default::default(),
1051 show_turn_stats: false,
1052 show_merge_conflict_indicator: true,
1053 sidebar_side: Default::default(),
1054 thinking_display: Default::default(),
1055 };
1056
1057 cx.update(|cx| {
1058 AgentSettings::override_global(agent_settings.clone(), cx);
1059 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
1060
1061 // Initial update
1062 update_command_palette_filter(cx);
1063 });
1064
1065 // Assert visible
1066 cx.update(|cx| {
1067 let filter = CommandPaletteFilter::try_global(cx).unwrap();
1068 assert!(
1069 !filter.is_hidden(&NewThread),
1070 "NewThread should be visible by default"
1071 );
1072 assert!(
1073 !filter.is_hidden(&NewTerminalThread),
1074 "NewTerminalThread should be visible by default"
1075 );
1076 assert!(
1077 !filter.is_hidden(&zed_actions::assistant::OpenSkillCreator),
1078 "OpenSkillCreator should be visible by default"
1079 );
1080 assert!(
1081 !filter.is_hidden(&zed_actions::assistant::CreateSkillFromUrl),
1082 "CreateSkillFromUrl should be visible by default"
1083 );
1084 assert!(
1085 !filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
1086 "OpenGlobalAgentsMdRules should be visible by default"
1087 );
1088 assert!(
1089 !filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
1090 "OpenProjectAgentsMdRules should be visible by default"
1091 );
1092 });
1093
1094 // Disable agent
1095 cx.update(|cx| {
1096 let mut new_settings = agent_settings.clone();
1097 new_settings.enabled = false;
1098 AgentSettings::override_global(new_settings, cx);
1099
1100 // Trigger update
1101 update_command_palette_filter(cx);
1102 });
1103
1104 // Assert hidden
1105 cx.update(|cx| {
1106 let filter = CommandPaletteFilter::try_global(cx).unwrap();
1107 assert!(
1108 filter.is_hidden(&NewThread),
1109 "NewThread should be hidden when agent is disabled"
1110 );
1111 assert!(
1112 filter.is_hidden(&NewTerminalThread),
1113 "NewTerminalThread should be hidden when agent is disabled"
1114 );
1115 assert!(
1116 filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
1117 "OpenGlobalAgentsMdRules should be hidden when agent is disabled"
1118 );
1119 assert!(
1120 filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
1121 "OpenProjectAgentsMdRules should be hidden when agent is disabled"
1122 );
1123 });
1124
1125 // Test EditPredictionProvider
1126 // Enable EditPredictionProvider::Copilot
1127 cx.update(|cx| {
1128 cx.update_global::<SettingsStore, _>(|store, cx| {
1129 store.update_user_settings(cx, |s| {
1130 s.project
1131 .all_languages
1132 .edit_predictions
1133 .get_or_insert(Default::default())
1134 .provider = Some(EditPredictionProvider::Copilot);
1135 });
1136 });
1137 update_command_palette_filter(cx);
1138 });
1139
1140 cx.update(|cx| {
1141 let filter = CommandPaletteFilter::try_global(cx).unwrap();
1142 assert!(
1143 !filter.is_hidden(&AcceptEditPrediction),
1144 "EditPrediction should be visible when provider is Copilot"
1145 );
1146 });
1147
1148 // Disable EditPredictionProvider (None)
1149 cx.update(|cx| {
1150 cx.update_global::<SettingsStore, _>(|store, cx| {
1151 store.update_user_settings(cx, |s| {
1152 s.project
1153 .all_languages
1154 .edit_predictions
1155 .get_or_insert(Default::default())
1156 .provider = Some(EditPredictionProvider::None);
1157 });
1158 });
1159 update_command_palette_filter(cx);
1160 });
1161
1162 cx.update(|cx| {
1163 let filter = CommandPaletteFilter::try_global(cx).unwrap();
1164 assert!(
1165 filter.is_hidden(&AcceptEditPrediction),
1166 "EditPrediction should be hidden when provider is None"
1167 );
1168 });
1169 }
1170
1171 async fn setup_backfill_test(cx: &mut TestAppContext) -> Arc<dyn Fs> {
1172 let fs = fs::FakeFs::new(cx.background_executor.clone());
1173 fs.save(
1174 paths::settings_file().as_path(),
1175 &"{}".into(),
1176 Default::default(),
1177 )
1178 .await
1179 .unwrap();
1180
1181 cx.update(|cx| {
1182 cx.set_global(db::AppDatabase::test_new());
1183 let store = SettingsStore::test(cx);
1184 cx.set_global(store);
1185 AgentSettings::register(cx);
1186 DisableAiSettings::register(cx);
1187 cx.set_staff(true);
1188 });
1189
1190 fs
1191 }
1192
1193 #[gpui::test]
1194 async fn test_backfill_sets_kvp_flag(cx: &mut TestAppContext) {
1195 let fs = setup_backfill_test(cx).await;
1196
1197 cx.update(|cx| {
1198 let kvp = KeyValueStore::global(cx);
1199 assert!(
1200 kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
1201 .unwrap()
1202 .is_none()
1203 );
1204
1205 maybe_backfill_editor_layout(fs.clone(), false, cx);
1206 });
1207
1208 cx.run_until_parked();
1209
1210 let kvp = cx.update(|cx| KeyValueStore::global(cx));
1211 assert!(
1212 kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
1213 .unwrap()
1214 .is_some(),
1215 "flag should be set after backfill"
1216 );
1217 }
1218
1219 #[gpui::test]
1220 async fn test_backfill_new_install_sets_flag_without_writing_settings(cx: &mut TestAppContext) {
1221 let fs = setup_backfill_test(cx).await;
1222
1223 cx.update(|cx| {
1224 maybe_backfill_editor_layout(fs.clone(), true, cx);
1225 });
1226
1227 cx.run_until_parked();
1228
1229 let kvp = cx.update(|cx| KeyValueStore::global(cx));
1230 assert!(
1231 kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
1232 .unwrap()
1233 .is_some(),
1234 "flag should be set even for new installs"
1235 );
1236
1237 let written = fs.load(paths::settings_file().as_path()).await.unwrap();
1238 assert_eq!(written.trim(), "{}", "settings file should be unchanged");
1239 }
1240
1241 #[gpui::test]
1242 async fn test_backfill_is_idempotent(cx: &mut TestAppContext) {
1243 let fs = setup_backfill_test(cx).await;
1244
1245 cx.update(|cx| {
1246 maybe_backfill_editor_layout(fs.clone(), false, cx);
1247 });
1248
1249 cx.run_until_parked();
1250
1251 let after_first = fs.load(paths::settings_file().as_path()).await.unwrap();
1252
1253 cx.update(|cx| {
1254 maybe_backfill_editor_layout(fs.clone(), false, cx);
1255 });
1256
1257 cx.run_until_parked();
1258
1259 let after_second = fs.load(paths::settings_file().as_path()).await.unwrap();
1260 assert_eq!(
1261 after_first, after_second,
1262 "second call should not change settings"
1263 );
1264 }
1265
1266 #[test]
1267 fn test_deserialize_external_agent_variants() {
1268 assert_eq!(
1269 serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
1270 Agent::NativeAgent,
1271 );
1272 assert_eq!(
1273 serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
1274 Agent::Custom {
1275 id: "my-agent".into(),
1276 },
1277 );
1278 }
1279
1280 #[test]
1281 fn test_deserialize_new_external_agent_thread() {
1282 let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"gemini"}"#)
1283 .expect("should deserialize agent id");
1284 assert_eq!(action.agent, AgentId::from("gemini"));
1285
1286 let action = serde_json::from_str::<NewExternalAgentThread>(
1287 r#"{"agent":{"custom":{"name":"gemini"}}}"#,
1288 )
1289 .expect("should deserialize legacy custom agent payload");
1290 assert_eq!(action.agent, AgentId::from("gemini"));
1291
1292 let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"NativeAgent"}"#)
1293 .expect("should deserialize legacy native agent payload");
1294 assert_eq!(action.agent, Agent::NativeAgent.id());
1295
1296 assert!(serde_json::from_str::<NewExternalAgentThread>(r#"{}"#).is_err());
1297 }
1298}
1299