assistant_panel.rs

  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}