Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:51:01.051Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

agent_ui.rs

1299 lines · 46.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.