assistant_panel.rs

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