assistant_panel.rs

  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}