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