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