1use std::sync::Arc;
2
3use assistant_settings::{AgentProfile, AssistantSettings};
4use assistant_tool::{ToolSource, ToolWorkingSet};
5use collections::HashMap;
6use gpui::{Entity, Subscription};
7use scripting_tool::ScriptingTool;
8use settings::{Settings as _, SettingsStore};
9use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
10
11pub struct ToolSelector {
12 profiles: HashMap<Arc<str>, AgentProfile>,
13 tools: Arc<ToolWorkingSet>,
14 _subscriptions: Vec<Subscription>,
15}
16
17impl ToolSelector {
18 pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
19 let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
20 this.refresh_profiles(cx);
21 });
22
23 let mut this = Self {
24 profiles: HashMap::default(),
25 tools,
26 _subscriptions: vec![settings_subscription],
27 };
28 this.refresh_profiles(cx);
29
30 this
31 }
32
33 fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
34 let settings = AssistantSettings::get_global(cx);
35 let mut profiles = settings.profiles.clone();
36
37 let read_only = AgentProfile::read_only();
38 if !profiles.contains_key(read_only.name.as_ref()) {
39 profiles.insert(read_only.name.clone().into(), read_only);
40 }
41
42 let code_writer = AgentProfile::code_writer();
43 if !profiles.contains_key(code_writer.name.as_ref()) {
44 profiles.insert(code_writer.name.clone().into(), code_writer);
45 }
46
47 self.profiles = profiles;
48 }
49
50 fn build_context_menu(
51 &self,
52 window: &mut Window,
53 cx: &mut Context<Self>,
54 ) -> Entity<ContextMenu> {
55 let profiles = self.profiles.clone();
56 let tool_set = self.tools.clone();
57 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
58 let icon_position = IconPosition::End;
59
60 menu = menu.header("Profiles");
61 for (_id, profile) in profiles.clone() {
62 menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
63 let tools = tool_set.clone();
64 move |_window, cx| {
65 tools.disable_source(ToolSource::Native, cx);
66 tools.disable_scripting_tool();
67 tools.enable(
68 ToolSource::Native,
69 &profile
70 .tools
71 .iter()
72 .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
73 .collect::<Vec<_>>(),
74 );
75
76 if profile.tools.contains_key(ScriptingTool::NAME) {
77 tools.enable_scripting_tool();
78 }
79 }
80 });
81 }
82
83 menu = menu.separator();
84
85 let tools_by_source = tool_set.tools_by_source(cx);
86
87 let all_tools_enabled = tool_set.are_all_tools_enabled();
88 menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
89 let tools = tool_set.clone();
90 move |_window, cx| {
91 if all_tools_enabled {
92 tools.disable_all_tools(cx);
93 } else {
94 tools.enable_all_tools();
95 }
96 }
97 });
98
99 for (source, tools) in tools_by_source {
100 let mut tools = tools
101 .into_iter()
102 .map(|tool| {
103 let source = tool.source();
104 let name = tool.name().into();
105 let is_enabled = tool_set.is_enabled(&source, &name);
106
107 (source, name, is_enabled)
108 })
109 .collect::<Vec<_>>();
110
111 if ToolSource::Native == source {
112 tools.push((
113 ToolSource::Native,
114 ScriptingTool::NAME.into(),
115 tool_set.is_scripting_tool_enabled(),
116 ));
117 tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
118 }
119
120 menu = match &source {
121 ToolSource::Native => menu.separator().header("Zed Tools"),
122 ToolSource::ContextServer { id } => {
123 let all_tools_from_source_enabled =
124 tool_set.are_all_tools_from_source_enabled(&source);
125
126 menu.separator().header(id).toggleable_entry(
127 "All Tools",
128 all_tools_from_source_enabled,
129 icon_position,
130 None,
131 {
132 let tools = tool_set.clone();
133 let source = source.clone();
134 move |_window, cx| {
135 if all_tools_from_source_enabled {
136 tools.disable_source(source.clone(), cx);
137 } else {
138 tools.enable_source(&source);
139 }
140 }
141 },
142 )
143 }
144 };
145
146 for (source, name, is_enabled) in tools {
147 menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
148 let tools = tool_set.clone();
149 move |_window, _cx| {
150 if name.as_ref() == ScriptingTool::NAME {
151 if is_enabled {
152 tools.disable_scripting_tool();
153 } else {
154 tools.enable_scripting_tool();
155 }
156 } else {
157 if is_enabled {
158 tools.disable(source.clone(), &[name.clone()]);
159 } else {
160 tools.enable(source.clone(), &[name.clone()]);
161 }
162 }
163 }
164 });
165 }
166 }
167
168 menu
169 })
170 }
171}
172
173impl Render for ToolSelector {
174 fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
175 let this = cx.entity().clone();
176 PopoverMenu::new("tool-selector")
177 .menu(move |window, cx| {
178 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
179 })
180 .trigger_with_tooltip(
181 IconButton::new("tool-selector-button", IconName::SettingsAlt)
182 .icon_size(IconSize::Small)
183 .icon_color(Color::Muted),
184 Tooltip::text("Customize Tools"),
185 )
186 .anchor(gpui::Corner::BottomLeft)
187 }
188}