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(tool_use.id.clone(), task, cx);
106 });
107 }
108 }
109 }
110 ThreadEvent::ToolFinished { .. } => {}
111 }
112 }
113}
114
115impl FocusableView for AssistantPanel {
116 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
117 self.message_editor.focus_handle(cx)
118 }
119}
120
121impl EventEmitter<PanelEvent> for AssistantPanel {}
122
123impl Panel for AssistantPanel {
124 fn persistent_name() -> &'static str {
125 "AssistantPanel2"
126 }
127
128 fn position(&self, _cx: &WindowContext) -> DockPosition {
129 DockPosition::Right
130 }
131
132 fn position_is_valid(&self, _: DockPosition) -> bool {
133 true
134 }
135
136 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
137
138 fn size(&self, _cx: &WindowContext) -> Pixels {
139 px(640.)
140 }
141
142 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
143
144 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
145
146 fn remote_id() -> Option<proto::PanelId> {
147 Some(proto::PanelId::AssistantPanel)
148 }
149
150 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
151 Some(IconName::ZedAssistant)
152 }
153
154 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
155 Some("Assistant Panel")
156 }
157
158 fn toggle_action(&self) -> Box<dyn Action> {
159 Box::new(ToggleFocus)
160 }
161}
162
163impl AssistantPanel {
164 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
165 let focus_handle = self.focus_handle(cx);
166
167 h_flex()
168 .id("assistant-toolbar")
169 .justify_between()
170 .gap(DynamicSpacing::Base08.rems(cx))
171 .h(Tab::container_height(cx))
172 .px(DynamicSpacing::Base08.rems(cx))
173 .bg(cx.theme().colors().tab_bar_background)
174 .border_b_1()
175 .border_color(cx.theme().colors().border_variant)
176 .child(h_flex().child(Label::new("Thread Title Goes Here")))
177 .child(
178 h_flex()
179 .gap(DynamicSpacing::Base08.rems(cx))
180 .child(self.render_language_model_selector(cx))
181 .child(Divider::vertical())
182 .child(
183 IconButton::new("new-thread", IconName::Plus)
184 .shape(IconButtonShape::Square)
185 .icon_size(IconSize::Small)
186 .style(ButtonStyle::Subtle)
187 .tooltip({
188 let focus_handle = focus_handle.clone();
189 move |cx| {
190 Tooltip::for_action_in(
191 "New Thread",
192 &NewThread,
193 &focus_handle,
194 cx,
195 )
196 }
197 })
198 .on_click(move |_event, _cx| {
199 println!("New Thread");
200 }),
201 )
202 .child(
203 IconButton::new("open-history", IconName::HistoryRerun)
204 .shape(IconButtonShape::Square)
205 .icon_size(IconSize::Small)
206 .style(ButtonStyle::Subtle)
207 .tooltip(move |cx| Tooltip::text("Open History", cx))
208 .on_click(move |_event, _cx| {
209 println!("Open History");
210 }),
211 )
212 .child(
213 IconButton::new("configure-assistant", IconName::Settings)
214 .shape(IconButtonShape::Square)
215 .icon_size(IconSize::Small)
216 .style(ButtonStyle::Subtle)
217 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
218 .on_click(move |_event, _cx| {
219 println!("Configure Assistant");
220 }),
221 ),
222 )
223 }
224
225 fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
226 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
227 let active_model = LanguageModelRegistry::read_global(cx).active_model();
228
229 LanguageModelSelector::new(
230 |model, _cx| {
231 println!("Selected {:?}", model.name());
232 },
233 ButtonLike::new("active-model")
234 .style(ButtonStyle::Subtle)
235 .child(
236 h_flex()
237 .w_full()
238 .gap_0p5()
239 .child(
240 div()
241 .overflow_x_hidden()
242 .flex_grow()
243 .whitespace_nowrap()
244 .child(match (active_provider, active_model) {
245 (Some(provider), Some(model)) => h_flex()
246 .gap_1()
247 .child(
248 Icon::new(
249 model.icon().unwrap_or_else(|| provider.icon()),
250 )
251 .color(Color::Muted)
252 .size(IconSize::XSmall),
253 )
254 .child(
255 Label::new(model.name().0)
256 .size(LabelSize::Small)
257 .color(Color::Muted),
258 )
259 .into_any_element(),
260 _ => Label::new("No model selected")
261 .size(LabelSize::Small)
262 .color(Color::Muted)
263 .into_any_element(),
264 }),
265 )
266 .child(
267 Icon::new(IconName::ChevronDown)
268 .color(Color::Muted)
269 .size(IconSize::XSmall),
270 ),
271 )
272 .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
273 )
274 }
275
276 fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
277 let (role_icon, role_name) = match message.role {
278 Role::User => (IconName::Person, "You"),
279 Role::Assistant => (IconName::ZedAssistant, "Assistant"),
280 Role::System => (IconName::Settings, "System"),
281 };
282
283 v_flex()
284 .border_1()
285 .border_color(cx.theme().colors().border_variant)
286 .rounded_md()
287 .child(
288 h_flex()
289 .justify_between()
290 .p_1p5()
291 .border_b_1()
292 .border_color(cx.theme().colors().border_variant)
293 .child(
294 h_flex()
295 .gap_2()
296 .child(Icon::new(role_icon).size(IconSize::Small))
297 .child(Label::new(role_name).size(LabelSize::Small)),
298 ),
299 )
300 .child(v_flex().p_1p5().child(Label::new(message.text.clone())))
301 }
302}
303
304impl Render for AssistantPanel {
305 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
306 let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
307
308 v_flex()
309 .key_context("AssistantPanel2")
310 .justify_between()
311 .size_full()
312 .on_action(cx.listener(|this, _: &NewThread, cx| {
313 this.new_thread(cx);
314 }))
315 .child(self.render_toolbar(cx))
316 .child(
317 v_flex()
318 .id("message-list")
319 .gap_2()
320 .size_full()
321 .p_2()
322 .overflow_y_scroll()
323 .bg(cx.theme().colors().panel_background)
324 .children(
325 messages
326 .into_iter()
327 .map(|message| self.render_message(message, cx)),
328 ),
329 )
330 .child(
331 h_flex()
332 .border_t_1()
333 .border_color(cx.theme().colors().border_variant)
334 .child(self.message_editor.clone()),
335 )
336 }
337}