1use std::sync::Arc;
2
3use anyhow::Result;
4use assistant_tool::ToolWorkingSet;
5use gpui::{
6 prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
7 FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext,
8};
9use language_model::{LanguageModelRegistry, Role};
10use language_model_selector::LanguageModelSelector;
11use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
12use workspace::dock::{DockPosition, Panel, PanelEvent};
13use workspace::Workspace;
14
15use crate::message_editor::MessageEditor;
16use crate::thread::{Message, Thread, ThreadEvent};
17use crate::{NewThread, ToggleFocus, ToggleModelSelector};
18
19pub fn init(cx: &mut AppContext) {
20 cx.observe_new_views(
21 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
22 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
23 workspace.toggle_panel_focus::<AssistantPanel>(cx);
24 });
25 },
26 )
27 .detach();
28}
29
30pub struct AssistantPanel {
31 workspace: WeakView<Workspace>,
32 thread: Model<Thread>,
33 message_editor: View<MessageEditor>,
34 tools: Arc<ToolWorkingSet>,
35 _subscriptions: Vec<Subscription>,
36}
37
38impl AssistantPanel {
39 pub fn load(
40 workspace: WeakView<Workspace>,
41 cx: AsyncWindowContext,
42 ) -> Task<Result<View<Self>>> {
43 cx.spawn(|mut cx| async move {
44 let tools = Arc::new(ToolWorkingSet::default());
45 workspace.update(&mut cx, |workspace, cx| {
46 cx.new_view(|cx| Self::new(workspace, tools, cx))
47 })
48 })
49 }
50
51 fn new(workspace: &Workspace, tools: Arc<ToolWorkingSet>, cx: &mut ViewContext<Self>) -> Self {
52 let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx));
53 let subscriptions = vec![
54 cx.observe(&thread, |_, _, cx| cx.notify()),
55 cx.subscribe(&thread, Self::handle_thread_event),
56 ];
57
58 Self {
59 workspace: workspace.weak_handle(),
60 thread: thread.clone(),
61 message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
62 tools,
63 _subscriptions: subscriptions,
64 }
65 }
66
67 fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
68 let tools = self.thread.read(cx).tools().clone();
69 let thread = cx.new_model(|cx| Thread::new(tools, cx));
70 let subscriptions = vec![
71 cx.observe(&thread, |_, _, cx| cx.notify()),
72 cx.subscribe(&thread, Self::handle_thread_event),
73 ];
74
75 self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
76 self.thread = thread;
77 self._subscriptions = subscriptions;
78
79 self.message_editor.focus_handle(cx).focus(cx);
80 }
81
82 fn handle_thread_event(
83 &mut self,
84 _: Model<Thread>,
85 event: &ThreadEvent,
86 cx: &mut ViewContext<Self>,
87 ) {
88 match event {
89 ThreadEvent::StreamedCompletion => {}
90 ThreadEvent::UsePendingTools => {
91 let pending_tool_uses = self
92 .thread
93 .read(cx)
94 .pending_tool_uses()
95 .into_iter()
96 .filter(|tool_use| tool_use.status.is_idle())
97 .cloned()
98 .collect::<Vec<_>>();
99
100 for tool_use in pending_tool_uses {
101 if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
102 let task = tool.run(tool_use.input, self.workspace.clone(), cx);
103
104 self.thread.update(cx, |thread, cx| {
105 thread.insert_tool_output(
106 tool_use.assistant_message_id,
107 tool_use.id.clone(),
108 task,
109 cx,
110 );
111 });
112 }
113 }
114 }
115 ThreadEvent::ToolFinished { .. } => {}
116 }
117 }
118}
119
120impl FocusableView for AssistantPanel {
121 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
122 self.message_editor.focus_handle(cx)
123 }
124}
125
126impl EventEmitter<PanelEvent> for AssistantPanel {}
127
128impl Panel for AssistantPanel {
129 fn persistent_name() -> &'static str {
130 "AssistantPanel2"
131 }
132
133 fn position(&self, _cx: &WindowContext) -> DockPosition {
134 DockPosition::Right
135 }
136
137 fn position_is_valid(&self, _: DockPosition) -> bool {
138 true
139 }
140
141 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
142
143 fn size(&self, _cx: &WindowContext) -> Pixels {
144 px(640.)
145 }
146
147 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
148
149 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
150
151 fn remote_id() -> Option<proto::PanelId> {
152 Some(proto::PanelId::AssistantPanel)
153 }
154
155 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
156 Some(IconName::ZedAssistant)
157 }
158
159 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
160 Some("Assistant Panel")
161 }
162
163 fn toggle_action(&self) -> Box<dyn Action> {
164 Box::new(ToggleFocus)
165 }
166}
167
168impl AssistantPanel {
169 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
170 let focus_handle = self.focus_handle(cx);
171
172 h_flex()
173 .id("assistant-toolbar")
174 .justify_between()
175 .gap(DynamicSpacing::Base08.rems(cx))
176 .h(Tab::container_height(cx))
177 .px(DynamicSpacing::Base08.rems(cx))
178 .bg(cx.theme().colors().tab_bar_background)
179 .border_b_1()
180 .border_color(cx.theme().colors().border_variant)
181 .child(h_flex().child(Label::new("Thread Title Goes Here")))
182 .child(
183 h_flex()
184 .gap(DynamicSpacing::Base08.rems(cx))
185 .child(self.render_language_model_selector(cx))
186 .child(Divider::vertical())
187 .child(
188 IconButton::new("new-thread", IconName::Plus)
189 .shape(IconButtonShape::Square)
190 .icon_size(IconSize::Small)
191 .style(ButtonStyle::Subtle)
192 .tooltip({
193 let focus_handle = focus_handle.clone();
194 move |cx| {
195 Tooltip::for_action_in(
196 "New Thread",
197 &NewThread,
198 &focus_handle,
199 cx,
200 )
201 }
202 })
203 .on_click(move |_event, _cx| {
204 println!("New Thread");
205 }),
206 )
207 .child(
208 IconButton::new("open-history", IconName::HistoryRerun)
209 .shape(IconButtonShape::Square)
210 .icon_size(IconSize::Small)
211 .style(ButtonStyle::Subtle)
212 .tooltip(move |cx| Tooltip::text("Open History", cx))
213 .on_click(move |_event, _cx| {
214 println!("Open History");
215 }),
216 )
217 .child(
218 IconButton::new("configure-assistant", IconName::Settings)
219 .shape(IconButtonShape::Square)
220 .icon_size(IconSize::Small)
221 .style(ButtonStyle::Subtle)
222 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
223 .on_click(move |_event, _cx| {
224 println!("Configure Assistant");
225 }),
226 ),
227 )
228 }
229
230 fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
231 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
232 let active_model = LanguageModelRegistry::read_global(cx).active_model();
233
234 LanguageModelSelector::new(
235 |model, _cx| {
236 println!("Selected {:?}", model.name());
237 },
238 ButtonLike::new("active-model")
239 .style(ButtonStyle::Subtle)
240 .child(
241 h_flex()
242 .w_full()
243 .gap_0p5()
244 .child(
245 div()
246 .overflow_x_hidden()
247 .flex_grow()
248 .whitespace_nowrap()
249 .child(match (active_provider, active_model) {
250 (Some(provider), Some(model)) => h_flex()
251 .gap_1()
252 .child(
253 Icon::new(
254 model.icon().unwrap_or_else(|| provider.icon()),
255 )
256 .color(Color::Muted)
257 .size(IconSize::XSmall),
258 )
259 .child(
260 Label::new(model.name().0)
261 .size(LabelSize::Small)
262 .color(Color::Muted),
263 )
264 .into_any_element(),
265 _ => Label::new("No model selected")
266 .size(LabelSize::Small)
267 .color(Color::Muted)
268 .into_any_element(),
269 }),
270 )
271 .child(
272 Icon::new(IconName::ChevronDown)
273 .color(Color::Muted)
274 .size(IconSize::XSmall),
275 ),
276 )
277 .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
278 )
279 }
280
281 fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
282 let (role_icon, role_name) = match message.role {
283 Role::User => (IconName::Person, "You"),
284 Role::Assistant => (IconName::ZedAssistant, "Assistant"),
285 Role::System => (IconName::Settings, "System"),
286 };
287
288 v_flex()
289 .border_1()
290 .border_color(cx.theme().colors().border_variant)
291 .rounded_md()
292 .child(
293 h_flex()
294 .justify_between()
295 .p_1p5()
296 .border_b_1()
297 .border_color(cx.theme().colors().border_variant)
298 .child(
299 h_flex()
300 .gap_2()
301 .child(Icon::new(role_icon).size(IconSize::Small))
302 .child(Label::new(role_name).size(LabelSize::Small)),
303 ),
304 )
305 .child(v_flex().p_1p5().child(Label::new(message.text.clone())))
306 }
307}
308
309impl Render for AssistantPanel {
310 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
311 let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
312
313 v_flex()
314 .key_context("AssistantPanel2")
315 .justify_between()
316 .size_full()
317 .on_action(cx.listener(|this, _: &NewThread, cx| {
318 this.new_thread(cx);
319 }))
320 .child(self.render_toolbar(cx))
321 .child(
322 v_flex()
323 .id("message-list")
324 .gap_2()
325 .size_full()
326 .p_2()
327 .overflow_y_scroll()
328 .bg(cx.theme().colors().panel_background)
329 .children(
330 messages
331 .into_iter()
332 .map(|message| self.render_message(message, cx)),
333 ),
334 )
335 .child(
336 h_flex()
337 .border_t_1()
338 .border_color(cx.theme().colors().border_variant)
339 .child(self.message_editor.clone()),
340 )
341 }
342}