1use std::sync::Arc;
2
3use anyhow::Result;
4use assistant_tool::ToolWorkingSet;
5use client::zed_urls;
6use gpui::{
7 prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
8 FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
9 WindowContext,
10};
11use language::LanguageRegistry;
12use language_model::LanguageModelRegistry;
13use language_model_selector::LanguageModelSelector;
14use time::UtcOffset;
15use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
16use workspace::dock::{DockPosition, Panel, PanelEvent};
17use workspace::Workspace;
18
19use crate::active_thread::ActiveThread;
20use crate::message_editor::MessageEditor;
21use crate::thread::{ThreadError, ThreadId};
22use crate::thread_history::{PastThread, ThreadHistory};
23use crate::thread_store::ThreadStore;
24use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
25
26pub fn init(cx: &mut AppContext) {
27 cx.observe_new_views(
28 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
29 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
30 workspace.toggle_panel_focus::<AssistantPanel>(cx);
31 });
32 },
33 )
34 .detach();
35}
36
37enum ActiveView {
38 Thread,
39 History,
40}
41
42pub struct AssistantPanel {
43 workspace: WeakView<Workspace>,
44 language_registry: Arc<LanguageRegistry>,
45 thread_store: Model<ThreadStore>,
46 thread: View<ActiveThread>,
47 message_editor: View<MessageEditor>,
48 tools: Arc<ToolWorkingSet>,
49 local_timezone: UtcOffset,
50 active_view: ActiveView,
51 history: View<ThreadHistory>,
52}
53
54impl AssistantPanel {
55 pub fn load(
56 workspace: WeakView<Workspace>,
57 cx: AsyncWindowContext,
58 ) -> Task<Result<View<Self>>> {
59 cx.spawn(|mut cx| async move {
60 let tools = Arc::new(ToolWorkingSet::default());
61 let thread_store = workspace
62 .update(&mut cx, |workspace, cx| {
63 let project = workspace.project().clone();
64 ThreadStore::new(project, tools.clone(), cx)
65 })?
66 .await?;
67
68 workspace.update(&mut cx, |workspace, cx| {
69 cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
70 })
71 })
72 }
73
74 fn new(
75 workspace: &Workspace,
76 thread_store: Model<ThreadStore>,
77 tools: Arc<ToolWorkingSet>,
78 cx: &mut ViewContext<Self>,
79 ) -> Self {
80 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
81 let language_registry = workspace.project().read(cx).languages().clone();
82 let workspace = workspace.weak_handle();
83 let weak_self = cx.view().downgrade();
84
85 Self {
86 active_view: ActiveView::Thread,
87 workspace: workspace.clone(),
88 language_registry: language_registry.clone(),
89 thread_store: thread_store.clone(),
90 thread: cx.new_view(|cx| {
91 ActiveThread::new(
92 thread.clone(),
93 workspace,
94 language_registry,
95 tools.clone(),
96 cx,
97 )
98 }),
99 message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
100 tools,
101 local_timezone: UtcOffset::from_whole_seconds(
102 chrono::Local::now().offset().local_minus_utc(),
103 )
104 .unwrap(),
105 history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
106 }
107 }
108
109 pub(crate) fn local_timezone(&self) -> UtcOffset {
110 self.local_timezone
111 }
112
113 fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
114 let thread = self
115 .thread_store
116 .update(cx, |this, cx| this.create_thread(cx));
117
118 self.active_view = ActiveView::Thread;
119 self.thread = cx.new_view(|cx| {
120 ActiveThread::new(
121 thread.clone(),
122 self.workspace.clone(),
123 self.language_registry.clone(),
124 self.tools.clone(),
125 cx,
126 )
127 });
128 self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
129 self.message_editor.focus_handle(cx).focus(cx);
130 }
131
132 pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
133 let Some(thread) = self
134 .thread_store
135 .update(cx, |this, cx| this.open_thread(thread_id, cx))
136 else {
137 return;
138 };
139
140 self.active_view = ActiveView::Thread;
141 self.thread = cx.new_view(|cx| {
142 ActiveThread::new(
143 thread.clone(),
144 self.workspace.clone(),
145 self.language_registry.clone(),
146 self.tools.clone(),
147 cx,
148 )
149 });
150 self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
151 self.message_editor.focus_handle(cx).focus(cx);
152 }
153
154 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
155 self.thread_store
156 .update(cx, |this, cx| this.delete_thread(thread_id, cx));
157 }
158}
159
160impl FocusableView for AssistantPanel {
161 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
162 match self.active_view {
163 ActiveView::Thread => self.message_editor.focus_handle(cx),
164 ActiveView::History => self.history.focus_handle(cx),
165 }
166 }
167}
168
169impl EventEmitter<PanelEvent> for AssistantPanel {}
170
171impl Panel for AssistantPanel {
172 fn persistent_name() -> &'static str {
173 "AssistantPanel2"
174 }
175
176 fn position(&self, _cx: &WindowContext) -> DockPosition {
177 DockPosition::Right
178 }
179
180 fn position_is_valid(&self, _: DockPosition) -> bool {
181 true
182 }
183
184 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
185
186 fn size(&self, _cx: &WindowContext) -> Pixels {
187 px(640.)
188 }
189
190 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
191
192 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
193
194 fn remote_id() -> Option<proto::PanelId> {
195 Some(proto::PanelId::AssistantPanel)
196 }
197
198 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
199 Some(IconName::ZedAssistant)
200 }
201
202 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
203 Some("Assistant Panel")
204 }
205
206 fn toggle_action(&self) -> Box<dyn Action> {
207 Box::new(ToggleFocus)
208 }
209}
210
211impl AssistantPanel {
212 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
213 let focus_handle = self.focus_handle(cx);
214
215 h_flex()
216 .id("assistant-toolbar")
217 .justify_between()
218 .gap(DynamicSpacing::Base08.rems(cx))
219 .h(Tab::container_height(cx))
220 .px(DynamicSpacing::Base08.rems(cx))
221 .bg(cx.theme().colors().tab_bar_background)
222 .border_b_1()
223 .border_color(cx.theme().colors().border_variant)
224 .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
225 .child(
226 h_flex()
227 .gap(DynamicSpacing::Base08.rems(cx))
228 .child(self.render_language_model_selector(cx))
229 .child(Divider::vertical())
230 .child(
231 IconButton::new("new-thread", IconName::Plus)
232 .shape(IconButtonShape::Square)
233 .icon_size(IconSize::Small)
234 .style(ButtonStyle::Subtle)
235 .tooltip({
236 let focus_handle = focus_handle.clone();
237 move |cx| {
238 Tooltip::for_action_in(
239 "New Thread",
240 &NewThread,
241 &focus_handle,
242 cx,
243 )
244 }
245 })
246 .on_click(move |_event, cx| {
247 cx.dispatch_action(NewThread.boxed_clone());
248 }),
249 )
250 .child(
251 IconButton::new("open-history", IconName::HistoryRerun)
252 .shape(IconButtonShape::Square)
253 .icon_size(IconSize::Small)
254 .style(ButtonStyle::Subtle)
255 .tooltip({
256 let focus_handle = focus_handle.clone();
257 move |cx| {
258 Tooltip::for_action_in(
259 "Open History",
260 &OpenHistory,
261 &focus_handle,
262 cx,
263 )
264 }
265 })
266 .on_click(move |_event, cx| {
267 cx.dispatch_action(OpenHistory.boxed_clone());
268 }),
269 )
270 .child(
271 IconButton::new("configure-assistant", IconName::Settings)
272 .shape(IconButtonShape::Square)
273 .icon_size(IconSize::Small)
274 .style(ButtonStyle::Subtle)
275 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
276 .on_click(move |_event, _cx| {
277 println!("Configure Assistant");
278 }),
279 ),
280 )
281 }
282
283 fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
284 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
285 let active_model = LanguageModelRegistry::read_global(cx).active_model();
286
287 LanguageModelSelector::new(
288 |model, _cx| {
289 println!("Selected {:?}", model.name());
290 },
291 ButtonLike::new("active-model")
292 .style(ButtonStyle::Subtle)
293 .child(
294 h_flex()
295 .w_full()
296 .gap_0p5()
297 .child(
298 div()
299 .overflow_x_hidden()
300 .flex_grow()
301 .whitespace_nowrap()
302 .child(match (active_provider, active_model) {
303 (Some(provider), Some(model)) => h_flex()
304 .gap_1()
305 .child(
306 Icon::new(
307 model.icon().unwrap_or_else(|| provider.icon()),
308 )
309 .color(Color::Muted)
310 .size(IconSize::XSmall),
311 )
312 .child(
313 Label::new(model.name().0)
314 .size(LabelSize::Small)
315 .color(Color::Muted),
316 )
317 .into_any_element(),
318 _ => Label::new("No model selected")
319 .size(LabelSize::Small)
320 .color(Color::Muted)
321 .into_any_element(),
322 }),
323 )
324 .child(
325 Icon::new(IconName::ChevronDown)
326 .color(Color::Muted)
327 .size(IconSize::XSmall),
328 ),
329 )
330 .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
331 )
332 }
333
334 fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
335 if self.thread.read(cx).is_empty() {
336 return self.render_thread_empty_state(cx).into_any_element();
337 }
338
339 self.thread.clone().into_any()
340 }
341
342 fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
343 let recent_threads = self
344 .thread_store
345 .update(cx, |this, cx| this.recent_threads(3, cx));
346
347 v_flex()
348 .gap_2()
349 .mx_auto()
350 .child(
351 v_flex().w_full().child(
352 svg()
353 .path("icons/logo_96.svg")
354 .text_color(cx.theme().colors().text)
355 .w(px(40.))
356 .h(px(40.))
357 .mx_auto()
358 .mb_4(),
359 ),
360 )
361 .child(v_flex())
362 .child(
363 h_flex()
364 .w_full()
365 .justify_center()
366 .child(Label::new("Context Examples:").size(LabelSize::Small)),
367 )
368 .child(
369 h_flex()
370 .gap_2()
371 .justify_center()
372 .child(
373 h_flex()
374 .gap_1()
375 .p_0p5()
376 .rounded_md()
377 .border_1()
378 .border_color(cx.theme().colors().border_variant)
379 .child(
380 Icon::new(IconName::Terminal)
381 .size(IconSize::Small)
382 .color(Color::Disabled),
383 )
384 .child(Label::new("Terminal").size(LabelSize::Small)),
385 )
386 .child(
387 h_flex()
388 .gap_1()
389 .p_0p5()
390 .rounded_md()
391 .border_1()
392 .border_color(cx.theme().colors().border_variant)
393 .child(
394 Icon::new(IconName::Folder)
395 .size(IconSize::Small)
396 .color(Color::Disabled),
397 )
398 .child(Label::new("/src/components").size(LabelSize::Small)),
399 ),
400 )
401 .when(!recent_threads.is_empty(), |parent| {
402 parent
403 .child(
404 h_flex()
405 .w_full()
406 .justify_center()
407 .child(Label::new("Recent Threads:").size(LabelSize::Small)),
408 )
409 .child(
410 v_flex().gap_2().children(
411 recent_threads
412 .into_iter()
413 .map(|thread| PastThread::new(thread, cx.view().downgrade())),
414 ),
415 )
416 .child(
417 h_flex().w_full().justify_center().child(
418 Button::new("view-all-past-threads", "View All Past Threads")
419 .style(ButtonStyle::Subtle)
420 .label_size(LabelSize::Small)
421 .key_binding(KeyBinding::for_action_in(
422 &OpenHistory,
423 &self.focus_handle(cx),
424 cx,
425 ))
426 .on_click(move |_event, cx| {
427 cx.dispatch_action(OpenHistory.boxed_clone());
428 }),
429 ),
430 )
431 })
432 }
433
434 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
435 let last_error = self.thread.read(cx).last_error()?;
436
437 Some(
438 div()
439 .absolute()
440 .right_3()
441 .bottom_12()
442 .max_w_96()
443 .py_2()
444 .px_3()
445 .elevation_2(cx)
446 .occlude()
447 .child(match last_error {
448 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
449 ThreadError::MaxMonthlySpendReached => {
450 self.render_max_monthly_spend_reached_error(cx)
451 }
452 ThreadError::Message(error_message) => {
453 self.render_error_message(&error_message, cx)
454 }
455 })
456 .into_any(),
457 )
458 }
459
460 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
461 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
462
463 v_flex()
464 .gap_0p5()
465 .child(
466 h_flex()
467 .gap_1p5()
468 .items_center()
469 .child(Icon::new(IconName::XCircle).color(Color::Error))
470 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
471 )
472 .child(
473 div()
474 .id("error-message")
475 .max_h_24()
476 .overflow_y_scroll()
477 .child(Label::new(ERROR_MESSAGE)),
478 )
479 .child(
480 h_flex()
481 .justify_end()
482 .mt_1()
483 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
484 |this, _, cx| {
485 this.thread.update(cx, |this, _cx| {
486 this.clear_last_error();
487 });
488
489 cx.open_url(&zed_urls::account_url(cx));
490 cx.notify();
491 },
492 )))
493 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
494 |this, _, cx| {
495 this.thread.update(cx, |this, _cx| {
496 this.clear_last_error();
497 });
498
499 cx.notify();
500 },
501 ))),
502 )
503 .into_any()
504 }
505
506 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
507 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
508
509 v_flex()
510 .gap_0p5()
511 .child(
512 h_flex()
513 .gap_1p5()
514 .items_center()
515 .child(Icon::new(IconName::XCircle).color(Color::Error))
516 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
517 )
518 .child(
519 div()
520 .id("error-message")
521 .max_h_24()
522 .overflow_y_scroll()
523 .child(Label::new(ERROR_MESSAGE)),
524 )
525 .child(
526 h_flex()
527 .justify_end()
528 .mt_1()
529 .child(
530 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
531 cx.listener(|this, _, cx| {
532 this.thread.update(cx, |this, _cx| {
533 this.clear_last_error();
534 });
535
536 cx.open_url(&zed_urls::account_url(cx));
537 cx.notify();
538 }),
539 ),
540 )
541 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
542 |this, _, cx| {
543 this.thread.update(cx, |this, _cx| {
544 this.clear_last_error();
545 });
546
547 cx.notify();
548 },
549 ))),
550 )
551 .into_any()
552 }
553
554 fn render_error_message(
555 &self,
556 error_message: &SharedString,
557 cx: &mut ViewContext<Self>,
558 ) -> AnyElement {
559 v_flex()
560 .gap_0p5()
561 .child(
562 h_flex()
563 .gap_1p5()
564 .items_center()
565 .child(Icon::new(IconName::XCircle).color(Color::Error))
566 .child(
567 Label::new("Error interacting with language model")
568 .weight(FontWeight::MEDIUM),
569 ),
570 )
571 .child(
572 div()
573 .id("error-message")
574 .max_h_32()
575 .overflow_y_scroll()
576 .child(Label::new(error_message.clone())),
577 )
578 .child(
579 h_flex()
580 .justify_end()
581 .mt_1()
582 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
583 |this, _, cx| {
584 this.thread.update(cx, |this, _cx| {
585 this.clear_last_error();
586 });
587
588 cx.notify();
589 },
590 ))),
591 )
592 .into_any()
593 }
594}
595
596impl Render for AssistantPanel {
597 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
598 v_flex()
599 .key_context("AssistantPanel2")
600 .justify_between()
601 .size_full()
602 .on_action(cx.listener(|this, _: &NewThread, cx| {
603 this.new_thread(cx);
604 }))
605 .on_action(cx.listener(|this, _: &OpenHistory, cx| {
606 this.active_view = ActiveView::History;
607 this.history.focus_handle(cx).focus(cx);
608 cx.notify();
609 }))
610 .child(self.render_toolbar(cx))
611 .map(|parent| match self.active_view {
612 ActiveView::Thread => parent
613 .child(self.render_active_thread_or_empty_state(cx))
614 .child(
615 h_flex()
616 .border_t_1()
617 .border_color(cx.theme().colors().border_variant)
618 .child(self.message_editor.clone()),
619 )
620 .children(self.render_last_error(cx)),
621 ActiveView::History => parent.child(self.history.clone()),
622 })
623 }
624}