Extract an agent_ui crate from agent (#33284)
This PR moves the UI-dependent logic in the `agent` crate into its own crate, `agent_ui`. The remaining `agent` crate no longer depends on `editor`, `picker`, `ui`, `workspace`, etc. This has compile time benefits, but the main motivation is to isolate our core agentic logic, so that we can make agents more pluggable/configurable. Release Notes: - N/A
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use context_server::{ContextServerCommand, ContextServerId};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
|
||||
WeakEntity, percentage, prelude::*,
|
||||
};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
context_server_store::{
|
||||
ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
|
||||
},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
enum ConfigurationTarget {
|
||||
New,
|
||||
Existing {
|
||||
id: ContextServerId,
|
||||
command: ContextServerCommand,
|
||||
},
|
||||
Extension {
|
||||
id: ContextServerId,
|
||||
repository_url: Option<SharedString>,
|
||||
installation: Option<extension::ContextServerConfiguration>,
|
||||
},
|
||||
}
|
||||
|
||||
enum ConfigurationSource {
|
||||
New {
|
||||
editor: Entity<Editor>,
|
||||
},
|
||||
Existing {
|
||||
editor: Entity<Editor>,
|
||||
},
|
||||
Extension {
|
||||
id: ContextServerId,
|
||||
editor: Option<Entity<Editor>>,
|
||||
repository_url: Option<SharedString>,
|
||||
installation_instructions: Option<Entity<markdown::Markdown>>,
|
||||
settings_validator: Option<jsonschema::Validator>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConfigurationSource {
|
||||
fn has_configuration_options(&self) -> bool {
|
||||
!matches!(self, ConfigurationSource::Extension { editor: None, .. })
|
||||
}
|
||||
|
||||
fn is_new(&self) -> bool {
|
||||
matches!(self, ConfigurationSource::New { .. })
|
||||
}
|
||||
|
||||
fn from_target(
|
||||
target: ConfigurationTarget,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
jsonc_language: Option<Arc<Language>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
fn create_editor(
|
||||
json: String,
|
||||
jsonc_language: Option<Arc<Language>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Editor> {
|
||||
cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(4, 16, window, cx);
|
||||
editor.set_text(json, window, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
|
||||
}
|
||||
editor
|
||||
})
|
||||
}
|
||||
|
||||
match target {
|
||||
ConfigurationTarget::New => ConfigurationSource::New {
|
||||
editor: create_editor(context_server_input(None), jsonc_language, window, cx),
|
||||
},
|
||||
ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
|
||||
editor: create_editor(
|
||||
context_server_input(Some((id, command))),
|
||||
jsonc_language,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
},
|
||||
ConfigurationTarget::Extension {
|
||||
id,
|
||||
repository_url,
|
||||
installation,
|
||||
} => {
|
||||
let settings_validator = installation.as_ref().and_then(|installation| {
|
||||
jsonschema::validator_for(&installation.settings_schema)
|
||||
.context("Failed to load JSON schema for context server settings")
|
||||
.log_err()
|
||||
});
|
||||
let installation_instructions = installation.as_ref().map(|installation| {
|
||||
cx.new(|cx| {
|
||||
Markdown::new(
|
||||
installation.installation_instructions.clone().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
ConfigurationSource::Extension {
|
||||
id,
|
||||
repository_url,
|
||||
installation_instructions,
|
||||
settings_validator,
|
||||
editor: installation.map(|installation| {
|
||||
create_editor(installation.default_settings, jsonc_language, window, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
|
||||
match self {
|
||||
ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
|
||||
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
|
||||
(
|
||||
id,
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
ConfigurationSource::Extension {
|
||||
id,
|
||||
editor,
|
||||
settings_validator,
|
||||
..
|
||||
} => {
|
||||
let text = editor
|
||||
.as_ref()
|
||||
.context("No output available")?
|
||||
.read(cx)
|
||||
.text(cx);
|
||||
let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
|
||||
if let Some(settings_validator) = settings_validator {
|
||||
if let Err(error) = settings_validator.validate(&settings) {
|
||||
return Err(anyhow::anyhow!(error.to_string()));
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
id.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
enabled: true,
|
||||
settings,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
|
||||
let (name, path, args, env) = match existing {
|
||||
Some((id, cmd)) => {
|
||||
let args = serde_json::to_string(&cmd.args).unwrap();
|
||||
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
|
||||
(id.0.to_string(), cmd.path, args, env)
|
||||
}
|
||||
None => (
|
||||
"some-mcp-server".to_string(),
|
||||
"".to_string(),
|
||||
"[]".to_string(),
|
||||
"{}".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"{{
|
||||
/// The name of your MCP server
|
||||
"{name}": {{
|
||||
"command": {{
|
||||
/// The path to the executable
|
||||
"path": "{path}",
|
||||
/// The arguments to pass to the executable
|
||||
"args": {args},
|
||||
/// The environment variables to set for the executable
|
||||
"env": {env}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_context_server_extension(
|
||||
id: ContextServerId,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<ConfigurationTarget>> {
|
||||
let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
|
||||
|
||||
let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
|
||||
let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let installation = descriptor
|
||||
.configuration(worktree_store, cx)
|
||||
.await
|
||||
.context("Failed to resolve context server configuration")
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
Some(ConfigurationTarget::Extension {
|
||||
id,
|
||||
repository_url: extension
|
||||
.and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
|
||||
installation,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
Waiting,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
pub struct ConfigureContextServerModal {
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
pub fn register(
|
||||
workspace: &mut Workspace,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action({
|
||||
let language_registry = language_registry.clone();
|
||||
move |_workspace, _: &AddContextServer, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
let language_registry = language_registry.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
Self::show_modal(
|
||||
ConfigurationTarget::New,
|
||||
language_registry,
|
||||
workspace_handle,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_modal_for_existing_server(
|
||||
server_id: ContextServerId,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(settings) = ProjectSettings::get_global(cx)
|
||||
.context_servers
|
||||
.get(&server_id.0)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
ContextServerDescriptorRegistry::default_global(cx)
|
||||
.read(cx)
|
||||
.context_server_descriptor(&server_id.0)
|
||||
.map(|_| ContextServerSettings::Extension {
|
||||
enabled: true,
|
||||
settings: serde_json::json!({}),
|
||||
})
|
||||
})
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
|
||||
};
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let target = match settings {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: _,
|
||||
command,
|
||||
} => Some(ConfigurationTarget::Existing {
|
||||
id: server_id,
|
||||
command,
|
||||
}),
|
||||
ContextServerSettings::Extension { .. } => {
|
||||
match workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
resolve_context_server_extension(
|
||||
server_id,
|
||||
workspace.project().read(cx).worktree_store(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
Some(task) => task.await,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match target {
|
||||
Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
|
||||
None => Err(anyhow::anyhow!("Failed to resolve context server")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn show_modal(
|
||||
target: ConfigurationTarget,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
let context_server_store = workspace.project().read(cx).context_server_store();
|
||||
workspace.toggle_modal(window, cx, |window, cx| Self {
|
||||
context_server_store,
|
||||
workspace: workspace_handle,
|
||||
state: State::Idle,
|
||||
source: ConfigurationSource::from_target(
|
||||
target,
|
||||
language_registry,
|
||||
jsonc_language,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
self.state = State::Error(err.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
|
||||
self.state = State::Idle;
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (id, settings) = match self.source.output(cx) {
|
||||
Ok(val) => val,
|
||||
Err(error) => {
|
||||
self.set_error(error.to_string(), cx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.state = State::Waiting;
|
||||
let wait_for_context_server_task =
|
||||
wait_for_context_server(&self.context_server_store, id.clone(), cx);
|
||||
cx.spawn({
|
||||
let id = id.clone();
|
||||
async move |this, cx| {
|
||||
let result = wait_for_context_server_task.await;
|
||||
this.update(cx, |this, cx| match result {
|
||||
Ok(_) => {
|
||||
this.state = State::Idle;
|
||||
this.show_configured_context_server_toast(id, cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
Err(err) => {
|
||||
this.set_error(err, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
|
||||
self.workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let status_toast = StatusToast::new(
|
||||
format!("{} configured successfully.", id.0),
|
||||
cx,
|
||||
|this, _cx| {
|
||||
this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
|
||||
.action("Dismiss", |_, _| {})
|
||||
},
|
||||
);
|
||||
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
|
||||
let value: serde_json::Value = serde_json_lenient::from_str(text)?;
|
||||
let object = value.as_object().context("Expected object")?;
|
||||
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
|
||||
let (context_server_name, value) = object.into_iter().next().unwrap();
|
||||
let command = value.get("command").context("Expected command")?;
|
||||
let command: ContextServerCommand = serde_json::from_value(command.clone())?;
|
||||
Ok((ContextServerId(context_server_name.clone().into()), command))
|
||||
}
|
||||
|
||||
impl ModalView for ConfigureContextServerModal {}
|
||||
|
||||
impl Focusable for ConfigureContextServerModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.source {
|
||||
ConfigurationSource::New { editor } => editor.focus_handle(cx),
|
||||
ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
|
||||
ConfigurationSource::Extension { editor, .. } => editor
|
||||
.as_ref()
|
||||
.map(|editor| editor.focus_handle(cx))
|
||||
.unwrap_or_else(|| cx.focus_handle()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
fn render_modal_header(&self) -> ModalHeader {
|
||||
let text: SharedString = match &self.source {
|
||||
ConfigurationSource::New { .. } => "Add MCP Server".into(),
|
||||
ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
|
||||
ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
|
||||
};
|
||||
ModalHeader::new().headline(text)
|
||||
}
|
||||
|
||||
fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
|
||||
|
||||
if let ConfigurationSource::Extension {
|
||||
installation_instructions: Some(installation_instructions),
|
||||
..
|
||||
} = &self.source
|
||||
{
|
||||
div()
|
||||
.pb_2()
|
||||
.text_sm()
|
||||
.child(MarkdownElement::new(
|
||||
installation_instructions.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(MODAL_DESCRIPTION)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_modal_content(&self, cx: &App) -> AnyElement {
|
||||
let editor = match &self.source {
|
||||
ConfigurationSource::New { editor } => editor,
|
||||
ConfigurationSource::Existing { editor } => editor,
|
||||
ConfigurationSource::Extension { editor, .. } => {
|
||||
let Some(editor) = editor else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
editor
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_modal_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> ModalFooter {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
let is_connecting = matches!(self.state, State::Waiting);
|
||||
|
||||
ModalFooter::new()
|
||||
.start_slot::<Button>(
|
||||
if let ConfigurationSource::Extension {
|
||||
repository_url: Some(repository_url),
|
||||
..
|
||||
} = &self.source
|
||||
{
|
||||
Some(
|
||||
Button::new("open-repository", "Open Repository")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip({
|
||||
let repository_url = repository_url.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Open Repository",
|
||||
None,
|
||||
repository_url.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let repository_url = repository_url.clone();
|
||||
move |_, _, cx| cx.open_url(&repository_url)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new(
|
||||
"cancel",
|
||||
if self.source.has_configuration_options() {
|
||||
"Cancel"
|
||||
} else {
|
||||
"Dismiss"
|
||||
},
|
||||
)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
),
|
||||
)
|
||||
.children(self.source.has_configuration_options().then(|| {
|
||||
Button::new(
|
||||
"add-server",
|
||||
if self.source.is_new() {
|
||||
"Add Server"
|
||||
} else {
|
||||
"Configure Server"
|
||||
},
|
||||
)
|
||||
.disabled(is_connecting)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
}),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_waiting_for_context_server() -> Div {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
Label::new("Waiting for Context Server")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_modal_error(error: SharedString) -> Div {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigureContextServerModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("ConfigureContextServerModal")
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Confirm, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
.header(self.render_modal_header())
|
||||
.section(
|
||||
Section::new()
|
||||
.child(self.render_modal_description(window, cx))
|
||||
.child(self.render_modal_content(cx))
|
||||
.child(match &self.state {
|
||||
State::Idle => div(),
|
||||
State::Waiting => Self::render_waiting_for_context_server(),
|
||||
State::Error(error) => Self::render_modal_error(error.clone()),
|
||||
}),
|
||||
)
|
||||
.footer(self.render_modal_footer(window, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_context_server(
|
||||
context_server_store: &Entity<ContextServerStore>,
|
||||
context_server_id: ContextServerId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(), Arc<str>>> {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let tx = Arc::new(Mutex::new(Some(tx)));
|
||||
|
||||
let subscription = cx.subscribe(context_server_store, move |_, event, _cx| match event {
|
||||
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
||||
match status {
|
||||
ContextServerStatus::Running => {
|
||||
if server_id == &context_server_id {
|
||||
if let Some(tx) = tx.lock().unwrap().take() {
|
||||
let _ = tx.send(Ok(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextServerStatus::Stopped => {
|
||||
if server_id == &context_server_id {
|
||||
if let Some(tx) = tx.lock().unwrap().take() {
|
||||
let _ = tx.send(Err("Context server stopped running".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextServerStatus::Error(error) => {
|
||||
if server_id == &context_server_id {
|
||||
if let Some(tx) = tx.lock().unwrap().take() {
|
||||
let _ = tx.send(Err(error.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
let result = rx.await.unwrap();
|
||||
drop(subscription);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let mut text_style = window.text_style();
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.ui_font.features.clone()),
|
||||
font_size: Some(TextSize::XSmall.rems(cx).into()),
|
||||
color: Some(colors.text_muted),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(colors.text_accent.opacity(0.5)),
|
||||
thickness: px(1.),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
|
||||
use settings::Settings as _;
|
||||
use ui::{
|
||||
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
|
||||
};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AgentPanel, ManageProfiles};
|
||||
use agent::agent_profile::AgentProfile;
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
enum Mode {
|
||||
ChooseProfile(ChooseProfileMode),
|
||||
NewProfile(NewProfileMode),
|
||||
ViewProfile(ViewProfileMode),
|
||||
ConfigureTools {
|
||||
profile_id: AgentProfileId,
|
||||
tool_picker: Entity<ToolPicker>,
|
||||
_subscription: Subscription,
|
||||
},
|
||||
ConfigureMcps {
|
||||
profile_id: AgentProfileId,
|
||||
tool_picker: Entity<ToolPicker>,
|
||||
_subscription: Subscription,
|
||||
},
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let mut builtin_profiles = Vec::new();
|
||||
let mut custom_profiles = Vec::new();
|
||||
|
||||
for (profile_id, profile) in settings.profiles.iter() {
|
||||
let entry = ProfileEntry {
|
||||
id: profile_id.clone(),
|
||||
name: profile.name.clone(),
|
||||
navigation: NavigableEntry::focusable(cx),
|
||||
};
|
||||
if builtin_profiles::is_builtin(profile_id) {
|
||||
builtin_profiles.push(entry);
|
||||
} else {
|
||||
custom_profiles.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
builtin_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
custom_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Self::ChooseProfile(ChooseProfileMode {
|
||||
builtin_profiles,
|
||||
custom_profiles,
|
||||
add_new_profile: NavigableEntry::focusable(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProfileEntry {
|
||||
pub id: AgentProfileId,
|
||||
pub name: SharedString,
|
||||
pub navigation: NavigableEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChooseProfileMode {
|
||||
builtin_profiles: Vec<ProfileEntry>,
|
||||
custom_profiles: Vec<ProfileEntry>,
|
||||
add_new_profile: NavigableEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewProfileMode {
|
||||
profile_id: AgentProfileId,
|
||||
fork_profile: NavigableEntry,
|
||||
configure_tools: NavigableEntry,
|
||||
configure_mcps: NavigableEntry,
|
||||
cancel_item: NavigableEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewProfileMode {
|
||||
name_editor: Entity<Editor>,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
pub struct ManageProfilesModal {
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
focus_handle: FocusHandle,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl ManageProfilesModal {
|
||||
pub fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let thread_store = panel.read(cx).thread_store();
|
||||
let tools = thread_store.read(cx).tools();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let mut this = Self::new(fs, tools, window, cx);
|
||||
|
||||
if let Some(profile_id) = action.customize_tools.clone() {
|
||||
this.configure_builtin_tools(profile_id, window, cx);
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
Self {
|
||||
fs,
|
||||
tools,
|
||||
focus_handle,
|
||||
mode: Mode::choose_profile(window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mode = Mode::choose_profile(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_profile(
|
||||
&mut self,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
name_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Profile name", cx);
|
||||
});
|
||||
|
||||
self.mode = Mode::NewProfile(NewProfileMode {
|
||||
name_editor,
|
||||
base_profile_id,
|
||||
});
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub fn view_profile(
|
||||
&mut self,
|
||||
profile_id: AgentProfileId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = Mode::ViewProfile(ViewProfileMode {
|
||||
profile_id,
|
||||
fork_profile: NavigableEntry::focusable(cx),
|
||||
configure_tools: NavigableEntry::focusable(cx),
|
||||
configure_mcps: NavigableEntry::focusable(cx),
|
||||
cancel_item: NavigableEntry::focusable(cx),
|
||||
});
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_mcp_tools(
|
||||
&mut self,
|
||||
profile_id: AgentProfileId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let tool_picker = cx.new(|cx| {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
ToolPickerMode::McpTools,
|
||||
self.fs.clone(),
|
||||
self.tools.clone(),
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
cx,
|
||||
);
|
||||
ToolPicker::mcp_tools(delegate, window, cx)
|
||||
});
|
||||
let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |this, _tool_picker, _: &DismissEvent, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
self.mode = Mode::ConfigureMcps {
|
||||
profile_id,
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_builtin_tools(
|
||||
&mut self,
|
||||
profile_id: AgentProfileId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let tool_picker = cx.new(|cx| {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
ToolPickerMode::BuiltinTools,
|
||||
self.fs.clone(),
|
||||
self.tools.clone(),
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
cx,
|
||||
);
|
||||
ToolPicker::builtin_tools(delegate, window, cx)
|
||||
});
|
||||
let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |this, _tool_picker, _: &DismissEvent, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
self.mode = Mode::ConfigureTools {
|
||||
profile_id,
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { .. } => {}
|
||||
Mode::NewProfile(mode) => {
|
||||
let name = mode.name_editor.read(cx).text(cx);
|
||||
|
||||
let profile_id =
|
||||
AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
|
||||
self.view_profile(profile_id, window, cx);
|
||||
}
|
||||
Mode::ViewProfile(_) => {}
|
||||
Mode::ConfigureTools { .. } => {}
|
||||
Mode::ConfigureMcps { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { .. } => {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
Mode::NewProfile(mode) => {
|
||||
if let Some(profile_id) = mode.base_profile_id.clone() {
|
||||
self.view_profile(profile_id, window, cx);
|
||||
} else {
|
||||
self.choose_profile(window, cx);
|
||||
}
|
||||
}
|
||||
Mode::ViewProfile(_) => self.choose_profile(window, cx),
|
||||
Mode::ConfigureTools { profile_id, .. } => {
|
||||
self.view_profile(profile_id.clone(), window, cx)
|
||||
}
|
||||
Mode::ConfigureMcps { profile_id, .. } => {
|
||||
self.view_profile(profile_id.clone(), window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ManageProfilesModal {}
|
||||
|
||||
impl Focusable for ManageProfilesModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile(_) => self.focus_handle.clone(),
|
||||
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
|
||||
Mode::ViewProfile(_) => self.focus_handle.clone(),
|
||||
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
|
||||
Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
|
||||
|
||||
impl ManageProfilesModal {
|
||||
fn render_profile(
|
||||
&self,
|
||||
profile: &ProfileEntry,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
div()
|
||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.track_focus(&profile.navigation.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(profile.name.clone()))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Customize")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.on_click({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_choose_profile(
|
||||
&mut self,
|
||||
mode: ChooseProfileMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Navigable::new(
|
||||
div()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(ProfileModalHeader::new("Agent Profiles", None))
|
||||
.child(
|
||||
v_flex()
|
||||
.pb_1()
|
||||
.child(ListSeparator)
|
||||
.children(
|
||||
mode.builtin_profiles
|
||||
.iter()
|
||||
.map(|profile| self.render_profile(profile, window, cx)),
|
||||
)
|
||||
.when(!mode.custom_profiles.is_empty(), |this| {
|
||||
this.child(ListSeparator)
|
||||
.child(
|
||||
div().pl_2().pb_1().child(
|
||||
Label::new("Custom Profiles")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
mode.custom_profiles
|
||||
.iter()
|
||||
.map(|profile| self.render_profile(profile, window, cx)),
|
||||
)
|
||||
})
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div()
|
||||
.id("new-profile")
|
||||
.track_focus(&mode.add_new_profile.focus_handle)
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||
this.new_profile(None, window, cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new("new-profile")
|
||||
.toggle_state(
|
||||
mode.add_new_profile
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Plus))
|
||||
.child(Label::new("Add New Profile"))
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.new_profile(None, window, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.map(|mut navigable| {
|
||||
for profile in mode.builtin_profiles {
|
||||
navigable = navigable.entry(profile.navigation);
|
||||
}
|
||||
for profile in mode.custom_profiles {
|
||||
navigable = navigable.entry(profile.navigation);
|
||||
}
|
||||
|
||||
navigable
|
||||
})
|
||||
.entry(mode.add_new_profile)
|
||||
}
|
||||
|
||||
fn render_new_profile(
|
||||
&mut self,
|
||||
mode: NewProfileMode,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
|
||||
settings
|
||||
.profiles
|
||||
.get(base_profile_id)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into())
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.id("new-profile")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.child(ProfileModalHeader::new(
|
||||
match &base_profile_name {
|
||||
Some(base_profile) => format!("Fork {base_profile}"),
|
||||
None => "New Profile".into(),
|
||||
},
|
||||
match base_profile_name {
|
||||
Some(_) => Some(IconName::Scissors),
|
||||
None => Some(IconName::Plus),
|
||||
},
|
||||
))
|
||||
.child(ListSeparator)
|
||||
.child(h_flex().p_2().child(mode.name_editor.clone()))
|
||||
}
|
||||
|
||||
fn render_view_profile(
|
||||
&mut self,
|
||||
mode: ViewProfileMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let profile_name = settings
|
||||
.profiles
|
||||
.get(&mode.profile_id)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let icon = match mode.profile_id.as_str() {
|
||||
"write" => IconName::Pencil,
|
||||
"ask" => IconName::MessageBubbles,
|
||||
_ => IconName::UserRoundPen,
|
||||
};
|
||||
|
||||
Navigable::new(
|
||||
div()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(ProfileModalHeader::new(profile_name, Some(icon)))
|
||||
.child(
|
||||
v_flex()
|
||||
.pb_1()
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div()
|
||||
.id("fork-profile")
|
||||
.track_focus(&mode.fork_profile.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.new_profile(Some(profile_id.clone()), window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("fork-profile")
|
||||
.toggle_state(
|
||||
mode.fork_profile
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Scissors)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Fork Profile"))
|
||||
.on_click({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.new_profile(
|
||||
Some(profile_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("configure-builtin-tools")
|
||||
.track_focus(&mode.configure_tools.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.configure_builtin_tools(
|
||||
profile_id.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("configure-builtin-tools-item")
|
||||
.toggle_state(
|
||||
mode.configure_tools
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Settings)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Configure Built-in Tools"))
|
||||
.on_click({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.configure_builtin_tools(
|
||||
profile_id.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("configure-mcps")
|
||||
.track_focus(&mode.configure_mcps.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.configure_mcp_tools(profile_id.clone(), window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("configure-mcp-tools")
|
||||
.toggle_state(
|
||||
mode.configure_mcps
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Hammer)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Configure MCP Tools"))
|
||||
.on_click({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.configure_mcp_tools(
|
||||
profile_id.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div()
|
||||
.id("cancel-item")
|
||||
.track_focus(&mode.cancel_item.focus_handle)
|
||||
.on_action({
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.cancel(window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("cancel-item")
|
||||
.toggle_state(
|
||||
mode.cancel_item
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
Icon::new(IconName::ArrowLeft)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Go Back"))
|
||||
.end_slot(
|
||||
div().children(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.cancel(window, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.entry(mode.fork_profile)
|
||||
.entry(mode.configure_tools)
|
||||
.entry(mode.configure_mcps)
|
||||
.entry(mode.cancel_item)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ManageProfilesModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let go_back_item = div()
|
||||
.id("cancel-item")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action({
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.cancel(window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("cancel-item")
|
||||
.toggle_state(self.focus_handle.contains_focused(window, cx))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
Icon::new(IconName::ArrowLeft)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Go Back"))
|
||||
.end_slot(
|
||||
div().children(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.cancel(window, cx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("ManageProfilesModal")
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
Mode::ChooseProfile(mode) => self
|
||||
.render_choose_profile(mode.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
Mode::NewProfile(mode) => self
|
||||
.render_new_profile(mode.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
Mode::ViewProfile(mode) => self
|
||||
.render_view_profile(mode.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
Mode::ConfigureTools {
|
||||
profile_id,
|
||||
tool_picker,
|
||||
..
|
||||
} => {
|
||||
let profile_name = settings
|
||||
.profiles
|
||||
.get(profile_id)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
v_flex()
|
||||
.pb_1()
|
||||
.child(ProfileModalHeader::new(
|
||||
format!("{profile_name} — Configure Built-in Tools"),
|
||||
Some(IconName::Cog),
|
||||
))
|
||||
.child(ListSeparator)
|
||||
.child(tool_picker.clone())
|
||||
.child(ListSeparator)
|
||||
.child(go_back_item)
|
||||
.into_any_element()
|
||||
}
|
||||
Mode::ConfigureMcps {
|
||||
profile_id,
|
||||
tool_picker,
|
||||
..
|
||||
} => {
|
||||
let profile_name = settings
|
||||
.profiles
|
||||
.get(profile_id)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
v_flex()
|
||||
.pb_1()
|
||||
.child(ProfileModalHeader::new(
|
||||
format!("{profile_name} — Configure MCP Tools"),
|
||||
Some(IconName::Hammer),
|
||||
))
|
||||
.child(ListSeparator)
|
||||
.child(tool_picker.clone())
|
||||
.child(ListSeparator)
|
||||
.child(go_back_item)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ProfileModalHeader {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
}
|
||||
|
||||
impl ProfileModalHeader {
|
||||
pub fn new(label: impl Into<SharedString>, icon: Option<IconName>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
icon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ProfileModalHeader {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let mut container = h_flex()
|
||||
.w_full()
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.pb(DynamicSpacing::Base04.rems(cx))
|
||||
.rounded_t_sm()
|
||||
.gap_1p5();
|
||||
|
||||
if let Some(icon) = self.icon {
|
||||
container = container.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
|
||||
}
|
||||
|
||||
container.child(
|
||||
h_flex().gap_1().overflow_x_hidden().child(
|
||||
div()
|
||||
.max_w_96()
|
||||
.overflow_x_hidden()
|
||||
.text_ellipsis()
|
||||
.child(Headline::new(self.label).size(HeadlineSize::XSmall)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use agent_settings::{
|
||||
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::update_settings_file;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct ToolPicker {
|
||||
picker: Entity<Picker<ToolPickerDelegate>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ToolPickerMode {
|
||||
BuiltinTools,
|
||||
McpTools,
|
||||
}
|
||||
|
||||
impl ToolPicker {
|
||||
pub fn builtin_tools(
|
||||
delegate: ToolPickerDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
|
||||
Self { picker }
|
||||
}
|
||||
|
||||
pub fn mcp_tools(
|
||||
delegate: ToolPickerDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::list(delegate, window, cx).modal(false));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ToolPicker {}
|
||||
|
||||
impl Focusable for ToolPicker {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ToolPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PickerItem {
|
||||
Tool {
|
||||
server_id: Option<Arc<str>>,
|
||||
name: Arc<str>,
|
||||
},
|
||||
ContextServer {
|
||||
server_id: Arc<str>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ToolPickerDelegate {
|
||||
tool_picker: WeakEntity<ToolPicker>,
|
||||
fs: Arc<dyn Fs>,
|
||||
items: Arc<Vec<PickerItem>>,
|
||||
profile_id: AgentProfileId,
|
||||
profile_settings: AgentProfileSettings,
|
||||
filtered_items: Vec<PickerItem>,
|
||||
selected_index: usize,
|
||||
mode: ToolPickerMode,
|
||||
}
|
||||
|
||||
impl ToolPickerDelegate {
|
||||
pub fn new(
|
||||
mode: ToolPickerMode,
|
||||
fs: Arc<dyn Fs>,
|
||||
tool_set: Entity<ToolWorkingSet>,
|
||||
profile_id: AgentProfileId,
|
||||
profile_settings: AgentProfileSettings,
|
||||
cx: &mut Context<ToolPicker>,
|
||||
) -> Self {
|
||||
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
|
||||
|
||||
Self {
|
||||
tool_picker: cx.entity().downgrade(),
|
||||
fs,
|
||||
items,
|
||||
profile_id,
|
||||
profile_settings,
|
||||
filtered_items: Vec::new(),
|
||||
selected_index: 0,
|
||||
mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_items(
|
||||
mode: ToolPickerMode,
|
||||
tool_set: &Entity<ToolWorkingSet>,
|
||||
cx: &mut App,
|
||||
) -> Vec<PickerItem> {
|
||||
let mut items = Vec::new();
|
||||
for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
|
||||
match source {
|
||||
ToolSource::Native => {
|
||||
if mode == ToolPickerMode::BuiltinTools {
|
||||
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
|
||||
name: tool.name().into(),
|
||||
server_id: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
if mode == ToolPickerMode::McpTools && !tools.is_empty() {
|
||||
let server_id: Arc<str> = id.clone().into();
|
||||
items.push(PickerItem::ContextServer {
|
||||
server_id: server_id.clone(),
|
||||
});
|
||||
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
|
||||
name: tool.name().into(),
|
||||
server_id: Some(server_id.clone()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ToolPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_items.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn can_select(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
let item = &self.filtered_items[ix];
|
||||
match item {
|
||||
PickerItem::Tool { .. } => true,
|
||||
PickerItem::ContextServer { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
match self.mode {
|
||||
ToolPickerMode::BuiltinTools => "Search built-in tools…",
|
||||
ToolPickerMode::McpTools => "Search MCP tools…",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_items = self.items.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_items = cx
|
||||
.background_spawn(async move {
|
||||
let mut tools_by_provider: BTreeMap<Option<Arc<str>>, Vec<Arc<str>>> =
|
||||
BTreeMap::default();
|
||||
|
||||
for item in all_items.iter() {
|
||||
if let PickerItem::Tool { server_id, name } = item.clone() {
|
||||
if name.contains(&query) {
|
||||
tools_by_provider.entry(server_id).or_default().push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (server_id, names) in tools_by_provider {
|
||||
if let Some(server_id) = server_id.clone() {
|
||||
items.push(PickerItem::ContextServer { server_id });
|
||||
}
|
||||
for name in names {
|
||||
items.push(PickerItem::Tool {
|
||||
server_id: server_id.clone(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.filtered_items = filtered_items;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.filtered_items.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.filtered_items.is_empty() {
|
||||
self.dismissed(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let item = &self.filtered_items[self.selected_index];
|
||||
|
||||
let PickerItem::Tool {
|
||||
name: tool_name,
|
||||
server_id,
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_currently_enabled = if let Some(server_id) = server_id.clone() {
|
||||
let preset = self
|
||||
.profile_settings
|
||||
.context_servers
|
||||
.entry(server_id)
|
||||
.or_default();
|
||||
let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
|
||||
*preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
|
||||
is_enabled
|
||||
} else {
|
||||
let is_enabled = *self
|
||||
.profile_settings
|
||||
.tools
|
||||
.entry(tool_name.clone())
|
||||
.or_default();
|
||||
*self
|
||||
.profile_settings
|
||||
.tools
|
||||
.entry(tool_name.clone())
|
||||
.or_default() = !is_enabled;
|
||||
is_enabled
|
||||
};
|
||||
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile_settings.clone();
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AgentSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
|
||||
if let Some(server_id) = server_id {
|
||||
let preset = profile.context_servers.entry(server_id).or_default();
|
||||
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
} else {
|
||||
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.tool_picker
|
||||
.update(cx, |_this, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let item = &self.filtered_items[ix];
|
||||
match item {
|
||||
PickerItem::ContextServer { server_id, .. } => Some(
|
||||
div()
|
||||
.px_2()
|
||||
.pb_1()
|
||||
.when(ix > 1, |this| {
|
||||
this.mt_1()
|
||||
.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
})
|
||||
.child(
|
||||
Label::new(server_id)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
PickerItem::Tool { name, server_id } => {
|
||||
let is_enabled = if let Some(server_id) = server_id {
|
||||
self.profile_settings
|
||||
.context_servers
|
||||
.get(server_id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(name))
|
||||
.copied()
|
||||
.unwrap_or(self.profile_settings.enable_all_context_servers)
|
||||
} else {
|
||||
self.profile_settings
|
||||
.tools
|
||||
.get(name)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(name.clone()))
|
||||
.end_slot::<Icon>(is_enabled.then(|| {
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success)
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user