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