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