mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Rules library has been replaced by skills, so we can safely remove it Release Notes: - N/A
1126 lines
39 KiB
Rust
1126 lines
39 KiB
Rust
mod agent_configuration;
|
|
pub mod agent_connection_store;
|
|
mod agent_diff;
|
|
mod agent_model_selector;
|
|
mod agent_panel;
|
|
mod agent_registry_ui;
|
|
mod buffer_codegen;
|
|
mod completion_provider;
|
|
mod config_options;
|
|
mod context;
|
|
mod context_server_configuration;
|
|
pub(crate) mod conversation_view;
|
|
mod diagnostics;
|
|
pub mod draft_prompt_store;
|
|
mod entry_view_state;
|
|
mod external_source_prompt;
|
|
mod favorite_models;
|
|
mod inline_assistant;
|
|
mod inline_prompt_editor;
|
|
mod language_model_selector;
|
|
mod mention_set;
|
|
mod message_editor;
|
|
mod mode_selector;
|
|
mod model_selector;
|
|
mod model_selector_popover;
|
|
mod profile_selector;
|
|
mod terminal_codegen;
|
|
mod terminal_inline_assistant;
|
|
pub mod terminal_thread_metadata_store;
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub mod test_support;
|
|
mod thread_import;
|
|
pub mod thread_metadata_store;
|
|
pub mod thread_worktree_archive;
|
|
|
|
pub mod threads_archive_view;
|
|
mod ui;
|
|
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
|
|
use ::ui::IconName;
|
|
use agent_client_protocol::schema as acp;
|
|
use agent_settings::{AgentProfileId, AgentSettings};
|
|
use command_palette_hooks::CommandPaletteFilter;
|
|
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
|
use feature_flags::FeatureFlagAppExt as _;
|
|
use fs::Fs;
|
|
use gpui::{
|
|
Action, App, Context, Entity, ImageSource, Resource, SharedString, SharedUri, TaskExt, Window,
|
|
actions,
|
|
};
|
|
use language::{
|
|
LanguageRegistry,
|
|
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
|
};
|
|
use language_model::{
|
|
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
|
};
|
|
use project::{AgentId, DisableAiSettings};
|
|
use prompt_store::{self, PromptBuilder, rules_to_skills_migration};
|
|
use rope::Point;
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use settings::{LanguageModelSelection, Settings as _, SettingsStore, SidebarSide};
|
|
use std::any::TypeId;
|
|
use std::path::{Path, PathBuf};
|
|
use workspace::Workspace;
|
|
|
|
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
|
pub use crate::agent_connection_store::{ActiveAcpConnection, AgentConnectionStore};
|
|
pub use crate::agent_panel::{
|
|
AgentPanel, AgentPanelEvent, AgentPanelTerminalInfo, MaxIdleRetainedThreads, TerminalId,
|
|
};
|
|
use crate::agent_registry_ui::AgentRegistryPage;
|
|
pub use crate::inline_assistant::InlineAssistant;
|
|
pub use crate::thread_metadata_store::ThreadId;
|
|
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
|
pub use conversation_view::ConversationView;
|
|
pub use external_source_prompt::ExternalSourcePrompt;
|
|
pub(crate) use mode_selector::ModeSelector;
|
|
pub(crate) use model_selector::ModelSelector;
|
|
pub(crate) use model_selector_popover::ModelSelectorPopover;
|
|
pub use thread_import::{
|
|
AcpThreadImportOnboarding, CrossChannelImportOnboarding, ThreadImportModal,
|
|
channels_with_threads, import_threads_from_other_channels,
|
|
};
|
|
use zed_actions;
|
|
pub use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, SwitchWorktree};
|
|
|
|
pub(crate) fn resolve_agent_image(
|
|
dest_url: &str,
|
|
worktree_roots: &[PathBuf],
|
|
) -> Option<ImageSource> {
|
|
if dest_url.starts_with("http://") || dest_url.starts_with("https://") {
|
|
return Some(ImageSource::Resource(Resource::Uri(SharedUri::from(
|
|
dest_url.to_string(),
|
|
))));
|
|
}
|
|
|
|
let path = Path::new(dest_url);
|
|
if path.is_absolute() && path.exists() {
|
|
return Some(ImageSource::Resource(Resource::Path(Arc::from(path))));
|
|
}
|
|
|
|
for root in worktree_roots {
|
|
let absolute_path = root.join(dest_url);
|
|
if absolute_path.exists() {
|
|
return Some(ImageSource::Resource(Resource::Path(Arc::from(
|
|
absolute_path.as_path(),
|
|
))));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub(crate) fn open_abs_path_at_point(
|
|
workspace: &mut Workspace,
|
|
abs_path: PathBuf,
|
|
point: Point,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) -> bool {
|
|
let project = workspace.project();
|
|
let Some(path) = project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
|
|
else {
|
|
return false;
|
|
};
|
|
|
|
let item = workspace.open_path(path, None, true, window, cx);
|
|
window
|
|
.spawn(cx, async move |cx| {
|
|
let Some(editor) = item.await?.downcast::<Editor>() else {
|
|
return Ok(());
|
|
};
|
|
let range = point..point;
|
|
editor
|
|
.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::center()),
|
|
window,
|
|
cx,
|
|
|selections| selections.select_ranges([range]),
|
|
);
|
|
})
|
|
.ok();
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
true
|
|
}
|
|
|
|
pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
|
|
const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum AgentThreadSource {
|
|
AgentPanel,
|
|
GitPanel,
|
|
Sidebar,
|
|
}
|
|
|
|
impl AgentThreadSource {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::AgentPanel => "agent_panel",
|
|
Self::GitPanel => "git_panel",
|
|
Self::Sidebar => "sidebar",
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn agent_sidebar_side(cx: &App) -> &'static str {
|
|
match AgentSettings::get_global(cx).sidebar_side() {
|
|
SidebarSide::Left => "left",
|
|
SidebarSide::Right => "right",
|
|
}
|
|
}
|
|
|
|
actions!(
|
|
agent,
|
|
[
|
|
/// Toggles the menu to create new agent threads.
|
|
ToggleNewThreadMenu,
|
|
/// Toggles the options menu for agent settings and preferences.
|
|
ToggleOptionsMenu,
|
|
/// Toggles the profile or mode selector for switching between agent profiles.
|
|
ToggleProfileSelector,
|
|
/// Cycles through available session modes.
|
|
CycleModeSelector,
|
|
/// Cycles through favorited models in the ACP model selector.
|
|
CycleFavoriteModels,
|
|
/// Expands the message editor to full size.
|
|
ExpandMessageEditor,
|
|
/// Adds a context server to the configuration.
|
|
AddContextServer,
|
|
/// Archives the currently selected thread.
|
|
ArchiveSelectedThread,
|
|
/// Removes the currently selected thread.
|
|
RemoveSelectedThread,
|
|
/// Renames the currently selected thread.
|
|
RenameSelectedThread,
|
|
/// Starts a chat conversation with follow-up enabled.
|
|
ChatWithFollow,
|
|
/// Cycles to the next inline assist suggestion.
|
|
CycleNextInlineAssist,
|
|
/// Cycles to the previous inline assist suggestion.
|
|
CyclePreviousInlineAssist,
|
|
/// Moves focus up in the interface.
|
|
FocusUp,
|
|
/// Moves focus down in the interface.
|
|
FocusDown,
|
|
/// Moves focus left in the interface.
|
|
FocusLeft,
|
|
/// Moves focus right in the interface.
|
|
FocusRight,
|
|
/// Opens the active thread as a markdown file.
|
|
OpenActiveThreadAsMarkdown,
|
|
/// Opens the agent diff view to review changes.
|
|
OpenAgentDiff,
|
|
/// Copies the current thread to the clipboard as JSON for debugging.
|
|
CopyThreadToClipboard,
|
|
/// Loads a thread from the clipboard JSON for debugging.
|
|
LoadThreadFromClipboard,
|
|
/// Keeps the current suggestion or change.
|
|
Keep,
|
|
/// Rejects the current suggestion or change.
|
|
Reject,
|
|
/// Rejects all suggestions or changes.
|
|
RejectAll,
|
|
/// Undoes the most recent reject operation, restoring the rejected changes.
|
|
UndoLastReject,
|
|
/// Keeps all suggestions or changes.
|
|
KeepAll,
|
|
/// Allow this operation only this time.
|
|
AllowOnce,
|
|
/// Allow this operation and remember the choice.
|
|
AllowAlways,
|
|
/// Reject this operation only this time.
|
|
RejectOnce,
|
|
/// Follows the agent's suggestions.
|
|
Follow,
|
|
/// Resets the trial upsell notification.
|
|
ResetTrialUpsell,
|
|
/// Resets the trial end upsell notification.
|
|
ResetTrialEndUpsell,
|
|
/// Re-enables the fast mode warning for every provider and model.
|
|
ResetFastModeWarnings,
|
|
/// Opens the "Add Context" menu in the message editor.
|
|
OpenAddContextMenu,
|
|
/// Interrupts the current generation and sends the message immediately.
|
|
SendImmediately,
|
|
/// Sends the next queued message immediately.
|
|
SendNextQueuedMessage,
|
|
/// Removes the first message from the queue (the next one to be sent).
|
|
RemoveFirstQueuedMessage,
|
|
/// Edits the first message in the queue (the next one to be sent).
|
|
EditFirstQueuedMessage,
|
|
/// Clears all messages from the queue.
|
|
ClearMessageQueue,
|
|
/// Opens the permission granularity dropdown for the current tool call.
|
|
OpenPermissionDropdown,
|
|
/// Toggles thinking mode for models that support extended thinking.
|
|
ToggleThinkingMode,
|
|
/// Cycles through available thinking effort levels for the current model.
|
|
CycleThinkingEffort,
|
|
/// Toggles the thinking effort selector menu open or closed.
|
|
ToggleThinkingEffortMenu,
|
|
/// Toggles fast mode for models that support it.
|
|
ToggleFastMode,
|
|
/// Scroll the output by one page up.
|
|
ScrollOutputPageUp,
|
|
/// Scroll the output by one page down.
|
|
ScrollOutputPageDown,
|
|
/// Scroll the output up by three lines.
|
|
ScrollOutputLineUp,
|
|
/// Scroll the output down by three lines.
|
|
ScrollOutputLineDown,
|
|
/// Scroll the output to the top.
|
|
ScrollOutputToTop,
|
|
/// Scroll the output to the bottom.
|
|
ScrollOutputToBottom,
|
|
/// Scroll the output to the previous user message.
|
|
ScrollOutputToPreviousMessage,
|
|
/// Scroll the output to the next user message.
|
|
ScrollOutputToNextMessage,
|
|
/// Import agent threads from other Zed release channels (e.g. Preview, Nightly).
|
|
ImportThreadsFromOtherChannels,
|
|
/// Starts a new terminal thread.
|
|
NewTerminalThread,
|
|
]
|
|
);
|
|
|
|
actions!(
|
|
dev,
|
|
[
|
|
/// Shows metadata for the currently active thread.
|
|
ShowThreadMetadata,
|
|
/// Shows metadata for all threads in the sidebar.
|
|
ShowAllSidebarThreadMetadata,
|
|
]
|
|
);
|
|
|
|
/// Action to authorize a tool call with a specific permission option.
|
|
/// This is used by the permission granularity dropdown to authorize tool calls.
|
|
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct AuthorizeToolCall {
|
|
/// The tool call ID to authorize.
|
|
pub tool_call_id: String,
|
|
/// The permission option ID to use.
|
|
pub option_id: String,
|
|
/// The kind of permission option (serialized as string).
|
|
pub option_kind: String,
|
|
}
|
|
|
|
/// Action to select a permission granularity option from the dropdown.
|
|
/// This updates the selected granularity without triggering authorization.
|
|
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectPermissionGranularity {
|
|
/// The tool call ID for which to select the granularity.
|
|
pub tool_call_id: String,
|
|
/// The index of the selected granularity option.
|
|
pub index: usize,
|
|
}
|
|
|
|
/// Action to toggle a command pattern checkbox in the permission dropdown.
|
|
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ToggleCommandPattern {
|
|
/// The tool call ID for which to toggle the pattern.
|
|
pub tool_call_id: String,
|
|
/// The index of the command pattern to toggle.
|
|
pub pattern_index: usize,
|
|
}
|
|
|
|
/// Creates a new conversation thread, optionally based on an existing thread.
|
|
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct NewThread;
|
|
|
|
/// Creates a new external agent conversation thread.
|
|
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct NewExternalAgentThread {
|
|
/// The agent id to use for the conversation.
|
|
#[serde(deserialize_with = "deserialize_external_agent_id")]
|
|
agent: AgentId,
|
|
}
|
|
|
|
fn deserialize_external_agent_id<'de, D>(deserializer: D) -> Result<AgentId, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum AgentIdOrLegacyAgent {
|
|
LegacyAgent(Agent),
|
|
AgentId(AgentId),
|
|
}
|
|
|
|
match AgentIdOrLegacyAgent::deserialize(deserializer)? {
|
|
AgentIdOrLegacyAgent::AgentId(agent_id) => Ok(agent_id),
|
|
AgentIdOrLegacyAgent::LegacyAgent(Agent::Custom { id }) => Ok(id),
|
|
AgentIdOrLegacyAgent::LegacyAgent(Agent::NativeAgent) => Ok(Agent::NativeAgent.id()),
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
AgentIdOrLegacyAgent::LegacyAgent(Agent::Stub) => Ok(Agent::Stub.id()),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct NewNativeAgentThreadFromSummary {
|
|
from_session_id: acp::SessionId,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "snake_case")]
|
|
#[non_exhaustive]
|
|
pub enum Agent {
|
|
#[default]
|
|
#[serde(alias = "NativeAgent", alias = "TextThread")]
|
|
NativeAgent,
|
|
#[serde(alias = "Custom")]
|
|
Custom {
|
|
#[serde(rename = "name")]
|
|
id: AgentId,
|
|
},
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Stub,
|
|
}
|
|
|
|
impl From<AgentId> for Agent {
|
|
fn from(id: AgentId) -> Self {
|
|
if id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
|
|
return Self::NativeAgent;
|
|
}
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
if id.as_ref() == "stub" {
|
|
return Self::Stub;
|
|
}
|
|
Self::Custom { id }
|
|
}
|
|
}
|
|
|
|
impl Agent {
|
|
pub fn id(&self) -> AgentId {
|
|
match self {
|
|
Self::NativeAgent => agent::ZED_AGENT_ID.clone(),
|
|
Self::Custom { id } => id.clone(),
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Self::Stub => "stub".into(),
|
|
}
|
|
}
|
|
|
|
pub fn is_native(&self) -> bool {
|
|
matches!(self, Self::NativeAgent)
|
|
}
|
|
|
|
pub fn label(&self) -> SharedString {
|
|
match self {
|
|
Self::NativeAgent => "Zed Agent".into(),
|
|
Self::Custom { id, .. } => id.0.clone(),
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Self::Stub => "Stub Agent".into(),
|
|
}
|
|
}
|
|
|
|
pub fn icon(&self) -> Option<IconName> {
|
|
match self {
|
|
Self::NativeAgent => None,
|
|
Self::Custom { .. } => Some(IconName::Sparkle),
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Self::Stub => None,
|
|
}
|
|
}
|
|
|
|
pub fn server(
|
|
&self,
|
|
fs: Arc<dyn fs::Fs>,
|
|
thread_store: Entity<agent::ThreadStore>,
|
|
) -> Rc<dyn agent_servers::AgentServer> {
|
|
match self {
|
|
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
|
|
Self::Custom { id: name } => {
|
|
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
|
}
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Self::Stub => Rc::new(crate::test_support::StubAgentServer::default_response()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Content to initialize new external agent with.
|
|
pub enum AgentInitialContent {
|
|
ThreadSummary {
|
|
session_id: acp::SessionId,
|
|
title: Option<SharedString>,
|
|
},
|
|
ContentBlock {
|
|
blocks: Vec<acp::ContentBlock>,
|
|
auto_submit: bool,
|
|
},
|
|
FromExternalSource(ExternalSourcePrompt),
|
|
}
|
|
|
|
impl From<ExternalSourcePrompt> for AgentInitialContent {
|
|
fn from(prompt: ExternalSourcePrompt) -> Self {
|
|
Self::FromExternalSource(prompt)
|
|
}
|
|
}
|
|
|
|
/// Opens the profile management interface for configuring agent tools and settings.
|
|
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = agent)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ManageProfiles {
|
|
#[serde(default)]
|
|
pub customize_tools: Option<AgentProfileId>,
|
|
}
|
|
|
|
impl ManageProfiles {
|
|
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
|
|
Self {
|
|
customize_tools: Some(profile_id),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum ModelUsageContext {
|
|
InlineAssistant,
|
|
}
|
|
|
|
impl ModelUsageContext {
|
|
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
|
match self {
|
|
Self::InlineAssistant => {
|
|
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn humanize_token_count(count: u64) -> String {
|
|
match count {
|
|
0..=999 => count.to_string(),
|
|
1000..=9999 => {
|
|
let thousands = count / 1000;
|
|
let hundreds = (count % 1000 + 50) / 100;
|
|
if hundreds == 0 {
|
|
format!("{}k", thousands)
|
|
} else if hundreds == 10 {
|
|
format!("{}k", thousands + 1)
|
|
} else {
|
|
format!("{}.{}k", thousands, hundreds)
|
|
}
|
|
}
|
|
10_000..=999_999 => format!("{}k", (count + 500) / 1000),
|
|
1_000_000..=9_999_999 => {
|
|
let millions = count / 1_000_000;
|
|
let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
|
|
if hundred_thousands == 0 {
|
|
format!("{}M", millions)
|
|
} else if hundred_thousands == 10 {
|
|
format!("{}M", millions + 1)
|
|
} else {
|
|
format!("{}.{}M", millions, hundred_thousands)
|
|
}
|
|
}
|
|
10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
|
|
}
|
|
}
|
|
|
|
/// Initializes the `agent` crate.
|
|
pub fn init(
|
|
fs: Arc<dyn Fs>,
|
|
prompt_builder: Arc<PromptBuilder>,
|
|
language_registry: Arc<LanguageRegistry>,
|
|
is_new_install: bool,
|
|
is_eval: bool,
|
|
cx: &mut App,
|
|
) {
|
|
agent::ThreadStore::init_global(cx);
|
|
prompt_store::init(cx);
|
|
skill_creator::init(cx);
|
|
if !is_eval {
|
|
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
|
// we're not running inside of the eval.
|
|
init_language_model_settings(cx);
|
|
}
|
|
agent_panel::init(cx);
|
|
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
|
|
thread_metadata_store::init(cx);
|
|
terminal_thread_metadata_store::init(cx);
|
|
|
|
inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
|
|
terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
|
|
cx.observe_new(move |workspace, window, cx| {
|
|
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
|
|
})
|
|
.detach();
|
|
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
|
workspace.register_action(
|
|
move |workspace: &mut Workspace,
|
|
_: &zed_actions::AcpRegistry,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>| {
|
|
let existing = workspace
|
|
.active_pane()
|
|
.read(cx)
|
|
.items()
|
|
.find_map(|item| item.downcast::<AgentRegistryPage>());
|
|
|
|
if let Some(existing) = existing {
|
|
existing.update(cx, |_, cx| {
|
|
project::AgentRegistryStore::global(cx)
|
|
.update(cx, |store, cx| store.refresh(cx));
|
|
});
|
|
workspace.activate_item(&existing, true, true, window, cx);
|
|
} else {
|
|
let registry_page = AgentRegistryPage::new(workspace, window, cx);
|
|
workspace.add_item_to_active_pane(
|
|
Box::new(registry_page),
|
|
None,
|
|
true,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
})
|
|
.detach();
|
|
cx.observe_new(ManageProfilesModal::register).detach();
|
|
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
|
workspace.register_action(
|
|
|workspace: &mut Workspace,
|
|
_: &ImportThreadsFromOtherChannels,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Workspace>| {
|
|
import_threads_from_other_channels(workspace, cx);
|
|
},
|
|
);
|
|
})
|
|
.detach();
|
|
|
|
// Update command palette filter based on AI settings
|
|
update_command_palette_filter(cx);
|
|
|
|
// Watch for settings changes
|
|
cx.observe_global::<SettingsStore>(|app_cx| {
|
|
// When settings change, update the command palette filter
|
|
update_command_palette_filter(app_cx);
|
|
})
|
|
.detach();
|
|
|
|
cx.on_flags_ready(|_, cx| {
|
|
update_command_palette_filter(cx);
|
|
})
|
|
.detach();
|
|
|
|
// Kick off the one-time migration of non-Default Rules to global
|
|
// Skills, deferred until server feature flags arrive.
|
|
//
|
|
// The migration itself is idempotent and no longer gated on a flag,
|
|
// but the deferral via `on_flags_ready` still matters: the migration
|
|
// writes to the on-disk `GlobalKeyValueStore`, which dispatches work
|
|
// on the `sqlezWorker` background thread. In `gpui::test` contexts,
|
|
// server flags are never received, so this callback never fires —
|
|
// which keeps that sqlite worker activity from racing with the
|
|
// `TestScheduler` and tripping its non-determinism panic.
|
|
{
|
|
let fs = fs.clone();
|
|
cx.on_flags_ready(move |_, cx| {
|
|
rules_to_skills_migration::migrate_rules_to_skills_if_needed(fs.clone(), cx);
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
maybe_backfill_editor_layout(fs, is_new_install, cx);
|
|
}
|
|
|
|
fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut App) {
|
|
let kvp = db::kvp::KeyValueStore::global(cx);
|
|
let already_backfilled =
|
|
util::ResultExt::log_err(kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY))
|
|
.flatten()
|
|
.is_some();
|
|
|
|
if !already_backfilled {
|
|
if !is_new_install {
|
|
AgentSettings::backfill_editor_layout(fs, cx);
|
|
}
|
|
|
|
db::write_and_log(cx, move || async move {
|
|
kvp.write_kvp(
|
|
PARALLEL_AGENT_LAYOUT_BACKFILL_KEY.to_string(),
|
|
"1".to_string(),
|
|
)
|
|
.await
|
|
});
|
|
}
|
|
}
|
|
|
|
fn update_command_palette_filter(cx: &mut App) {
|
|
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
|
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
|
|
|
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
|
.edit_predictions
|
|
.provider;
|
|
|
|
CommandPaletteFilter::update_global(cx, |filter, _| {
|
|
use editor::actions::{
|
|
AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
|
|
NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
|
};
|
|
let edit_prediction_actions = [
|
|
TypeId::of::<AcceptEditPrediction>(),
|
|
TypeId::of::<AcceptNextWordEditPrediction>(),
|
|
TypeId::of::<AcceptNextLineEditPrediction>(),
|
|
TypeId::of::<ShowEditPrediction>(),
|
|
TypeId::of::<NextEditPrediction>(),
|
|
TypeId::of::<PreviousEditPrediction>(),
|
|
TypeId::of::<ToggleEditPrediction>(),
|
|
];
|
|
|
|
let open_rules_library_action = [TypeId::of::<zed_actions::assistant::OpenRulesLibrary>()];
|
|
let skill_creator_actions = [
|
|
TypeId::of::<zed_actions::assistant::OpenSkillCreator>(),
|
|
TypeId::of::<zed_actions::assistant::CreateSkillFromUrl>(),
|
|
];
|
|
|
|
if disable_ai {
|
|
filter.hide_namespace("agent");
|
|
filter.hide_namespace("agents");
|
|
filter.hide_namespace("assistant");
|
|
filter.hide_namespace("copilot");
|
|
filter.hide_namespace("zed_predict_onboarding");
|
|
filter.hide_namespace("edit_prediction");
|
|
|
|
filter.hide_action_types(&edit_prediction_actions);
|
|
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
|
} else {
|
|
if agent_enabled {
|
|
filter.show_namespace("agent");
|
|
filter.show_namespace("agents");
|
|
filter.show_namespace("assistant");
|
|
} else {
|
|
filter.hide_namespace("agent");
|
|
filter.hide_namespace("agents");
|
|
filter.hide_namespace("assistant");
|
|
}
|
|
|
|
match edit_prediction_provider {
|
|
EditPredictionProvider::None => {
|
|
filter.hide_namespace("edit_prediction");
|
|
filter.hide_namespace("copilot");
|
|
filter.hide_action_types(&edit_prediction_actions);
|
|
}
|
|
EditPredictionProvider::Copilot => {
|
|
filter.show_namespace("edit_prediction");
|
|
filter.show_namespace("copilot");
|
|
filter.show_action_types(edit_prediction_actions.iter());
|
|
}
|
|
EditPredictionProvider::Zed
|
|
| EditPredictionProvider::Codestral
|
|
| EditPredictionProvider::Ollama
|
|
| EditPredictionProvider::OpenAiCompatibleApi
|
|
| EditPredictionProvider::Mercury => {
|
|
filter.show_namespace("edit_prediction");
|
|
filter.hide_namespace("copilot");
|
|
filter.show_action_types(edit_prediction_actions.iter());
|
|
}
|
|
}
|
|
|
|
filter.show_namespace("zed_predict_onboarding");
|
|
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
|
|
|
filter.show_namespace("multi_workspace");
|
|
}
|
|
|
|
// Hide `assistant: open rules library` — Rules are surfaced
|
|
// through the Skills UI now. Applied after the disable-ai /
|
|
// agent-enabled branches so it overrides the
|
|
// `show_namespace("assistant")` call above without affecting the
|
|
// rest of that namespace's actions.
|
|
if !disable_ai {
|
|
filter.hide_action_types(&open_rules_library_action);
|
|
filter.show_action_types(skill_creator_actions.iter());
|
|
} else {
|
|
filter.show_action_types(open_rules_library_action.iter());
|
|
filter.hide_action_types(&skill_creator_actions);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn init_language_model_settings(cx: &mut App) {
|
|
update_active_language_model_from_settings(cx);
|
|
|
|
cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
|
|
.detach();
|
|
cx.subscribe(
|
|
&LanguageModelRegistry::global(cx),
|
|
|_, event: &language_model::Event, cx| match event {
|
|
language_model::Event::ProviderStateChanged(_)
|
|
| language_model::Event::AddedProvider(_)
|
|
| language_model::Event::RemovedProvider(_)
|
|
| language_model::Event::ProvidersChanged => {
|
|
update_active_language_model_from_settings(cx);
|
|
}
|
|
_ => {}
|
|
},
|
|
)
|
|
.detach();
|
|
}
|
|
|
|
fn update_active_language_model_from_settings(cx: &mut App) {
|
|
let settings = AgentSettings::get_global(cx);
|
|
|
|
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
|
language_model::SelectedModel {
|
|
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
|
|
model: LanguageModelId::from(selection.model.clone()),
|
|
}
|
|
}
|
|
|
|
let default = settings.default_model.as_ref().map(to_selected_model);
|
|
let inline_assistant = settings
|
|
.inline_assistant_model
|
|
.as_ref()
|
|
.map(to_selected_model);
|
|
let commit_message = settings
|
|
.commit_message_model
|
|
.as_ref()
|
|
.map(to_selected_model);
|
|
let thread_summary = settings
|
|
.thread_summary_model
|
|
.as_ref()
|
|
.map(to_selected_model);
|
|
let inline_alternatives = settings
|
|
.inline_alternatives
|
|
.iter()
|
|
.map(to_selected_model)
|
|
.collect::<Vec<_>>();
|
|
|
|
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
|
registry.select_default_model(default.as_ref(), cx);
|
|
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
|
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
|
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
|
registry.select_inline_alternative_models(inline_alternatives, cx);
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use agent_settings::{AgentProfileId, AgentSettings};
|
|
use command_palette_hooks::CommandPaletteFilter;
|
|
use db::kvp::KeyValueStore;
|
|
use editor::actions::AcceptEditPrediction;
|
|
use gpui::{BorrowAppContext, TestAppContext, px};
|
|
use project::DisableAiSettings;
|
|
use settings::{
|
|
DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings, SettingsStore,
|
|
};
|
|
|
|
#[gpui::test]
|
|
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
|
|
// Init settings
|
|
cx.update(|cx| {
|
|
let store = SettingsStore::test(cx);
|
|
cx.set_global(store);
|
|
command_palette_hooks::init(cx);
|
|
AgentSettings::register(cx);
|
|
DisableAiSettings::register(cx);
|
|
AllLanguageSettings::register(cx);
|
|
});
|
|
|
|
let agent_settings = AgentSettings {
|
|
enabled: true,
|
|
button: true,
|
|
dock: DockPosition::Right,
|
|
flexible: true,
|
|
default_width: px(300.),
|
|
default_height: px(600.),
|
|
max_content_width: Some(px(850.)),
|
|
default_model: None,
|
|
subagent_model: None,
|
|
inline_assistant_model: None,
|
|
inline_assistant_use_streaming_tools: false,
|
|
commit_message_model: None,
|
|
thread_summary_model: None,
|
|
inline_alternatives: vec![],
|
|
favorite_models: vec![],
|
|
default_profile: AgentProfileId::default(),
|
|
profiles: Default::default(),
|
|
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
|
|
play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
|
|
single_file_review: false,
|
|
model_parameters: vec![],
|
|
enable_feedback: false,
|
|
expand_edit_card: true,
|
|
expand_terminal_card: true,
|
|
cancel_generation_on_terminal_stop: true,
|
|
use_modifier_to_send: true,
|
|
message_editor_min_lines: 1,
|
|
tool_permissions: Default::default(),
|
|
show_turn_stats: false,
|
|
show_merge_conflict_indicator: true,
|
|
sidebar_side: Default::default(),
|
|
thinking_display: Default::default(),
|
|
};
|
|
|
|
cx.update(|cx| {
|
|
AgentSettings::override_global(agent_settings.clone(), cx);
|
|
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
|
|
|
|
// Initial update
|
|
update_command_palette_filter(cx);
|
|
});
|
|
|
|
// Assert visible
|
|
cx.update(|cx| {
|
|
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
|
assert!(
|
|
!filter.is_hidden(&NewThread),
|
|
"NewThread should be visible by default"
|
|
);
|
|
assert!(
|
|
!filter.is_hidden(&NewTerminalThread),
|
|
"NewTerminalThread should be visible by default"
|
|
);
|
|
assert!(
|
|
!filter.is_hidden(&zed_actions::assistant::OpenSkillCreator),
|
|
"OpenSkillCreator should be visible by default"
|
|
);
|
|
assert!(
|
|
!filter.is_hidden(&zed_actions::assistant::CreateSkillFromUrl),
|
|
"CreateSkillFromUrl should be visible by default"
|
|
);
|
|
assert!(
|
|
!filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
|
|
"OpenGlobalAgentsMdRules should be visible by default"
|
|
);
|
|
assert!(
|
|
!filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
|
|
"OpenProjectAgentsMdRules should be visible by default"
|
|
);
|
|
});
|
|
|
|
// Disable agent
|
|
cx.update(|cx| {
|
|
let mut new_settings = agent_settings.clone();
|
|
new_settings.enabled = false;
|
|
AgentSettings::override_global(new_settings, cx);
|
|
|
|
// Trigger update
|
|
update_command_palette_filter(cx);
|
|
});
|
|
|
|
// Assert hidden
|
|
cx.update(|cx| {
|
|
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
|
assert!(
|
|
filter.is_hidden(&NewThread),
|
|
"NewThread should be hidden when agent is disabled"
|
|
);
|
|
assert!(
|
|
filter.is_hidden(&NewTerminalThread),
|
|
"NewTerminalThread should be hidden when agent is disabled"
|
|
);
|
|
assert!(
|
|
filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
|
|
"OpenGlobalAgentsMdRules should be hidden when agent is disabled"
|
|
);
|
|
assert!(
|
|
filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
|
|
"OpenProjectAgentsMdRules should be hidden when agent is disabled"
|
|
);
|
|
});
|
|
|
|
// Test EditPredictionProvider
|
|
// Enable EditPredictionProvider::Copilot
|
|
cx.update(|cx| {
|
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
store.update_user_settings(cx, |s| {
|
|
s.project
|
|
.all_languages
|
|
.edit_predictions
|
|
.get_or_insert(Default::default())
|
|
.provider = Some(EditPredictionProvider::Copilot);
|
|
});
|
|
});
|
|
update_command_palette_filter(cx);
|
|
});
|
|
|
|
cx.update(|cx| {
|
|
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
|
assert!(
|
|
!filter.is_hidden(&AcceptEditPrediction),
|
|
"EditPrediction should be visible when provider is Copilot"
|
|
);
|
|
});
|
|
|
|
// Disable EditPredictionProvider (None)
|
|
cx.update(|cx| {
|
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
store.update_user_settings(cx, |s| {
|
|
s.project
|
|
.all_languages
|
|
.edit_predictions
|
|
.get_or_insert(Default::default())
|
|
.provider = Some(EditPredictionProvider::None);
|
|
});
|
|
});
|
|
update_command_palette_filter(cx);
|
|
});
|
|
|
|
cx.update(|cx| {
|
|
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
|
assert!(
|
|
filter.is_hidden(&AcceptEditPrediction),
|
|
"EditPrediction should be hidden when provider is None"
|
|
);
|
|
});
|
|
}
|
|
|
|
async fn setup_backfill_test(cx: &mut TestAppContext) -> Arc<dyn Fs> {
|
|
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
|
fs.save(
|
|
paths::settings_file().as_path(),
|
|
&"{}".into(),
|
|
Default::default(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
cx.update(|cx| {
|
|
cx.set_global(db::AppDatabase::test_new());
|
|
let store = SettingsStore::test(cx);
|
|
cx.set_global(store);
|
|
AgentSettings::register(cx);
|
|
DisableAiSettings::register(cx);
|
|
cx.set_staff(true);
|
|
});
|
|
|
|
fs
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_backfill_sets_kvp_flag(cx: &mut TestAppContext) {
|
|
let fs = setup_backfill_test(cx).await;
|
|
|
|
cx.update(|cx| {
|
|
let kvp = KeyValueStore::global(cx);
|
|
assert!(
|
|
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
|
|
maybe_backfill_editor_layout(fs.clone(), false, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
|
assert!(
|
|
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
|
.unwrap()
|
|
.is_some(),
|
|
"flag should be set after backfill"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_backfill_new_install_sets_flag_without_writing_settings(cx: &mut TestAppContext) {
|
|
let fs = setup_backfill_test(cx).await;
|
|
|
|
cx.update(|cx| {
|
|
maybe_backfill_editor_layout(fs.clone(), true, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
|
assert!(
|
|
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
|
.unwrap()
|
|
.is_some(),
|
|
"flag should be set even for new installs"
|
|
);
|
|
|
|
let written = fs.load(paths::settings_file().as_path()).await.unwrap();
|
|
assert_eq!(written.trim(), "{}", "settings file should be unchanged");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_backfill_is_idempotent(cx: &mut TestAppContext) {
|
|
let fs = setup_backfill_test(cx).await;
|
|
|
|
cx.update(|cx| {
|
|
maybe_backfill_editor_layout(fs.clone(), false, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let after_first = fs.load(paths::settings_file().as_path()).await.unwrap();
|
|
|
|
cx.update(|cx| {
|
|
maybe_backfill_editor_layout(fs.clone(), false, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let after_second = fs.load(paths::settings_file().as_path()).await.unwrap();
|
|
assert_eq!(
|
|
after_first, after_second,
|
|
"second call should not change settings"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_external_agent_variants() {
|
|
assert_eq!(
|
|
serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
|
|
Agent::NativeAgent,
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
|
|
Agent::Custom {
|
|
id: "my-agent".into(),
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_new_external_agent_thread() {
|
|
let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"gemini"}"#)
|
|
.expect("should deserialize agent id");
|
|
assert_eq!(action.agent, AgentId::from("gemini"));
|
|
|
|
let action = serde_json::from_str::<NewExternalAgentThread>(
|
|
r#"{"agent":{"custom":{"name":"gemini"}}}"#,
|
|
)
|
|
.expect("should deserialize legacy custom agent payload");
|
|
assert_eq!(action.agent, AgentId::from("gemini"));
|
|
|
|
let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"NativeAgent"}"#)
|
|
.expect("should deserialize legacy native agent payload");
|
|
assert_eq!(action.agent, Agent::NativeAgent.id());
|
|
|
|
assert!(serde_json::from_str::<NewExternalAgentThread>(r#"{}"#).is_err());
|
|
}
|
|
}
|