assistant_panel.rs

  1use std::path::PathBuf;
  2use std::sync::Arc;
  3
  4use anyhow::{anyhow, Result};
  5use assistant_context_editor::{
  6    make_lsp_adapter_delegate, AssistantPanelDelegate, ContextEditor, ContextHistory,
  7};
  8use assistant_settings::{AssistantDockPosition, AssistantSettings};
  9use assistant_slash_command::SlashCommandWorkingSet;
 10use assistant_tool::ToolWorkingSet;
 11use client::zed_urls;
 12use fs::Fs;
 13use gpui::{
 14    prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Corner, EventEmitter,
 15    FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
 16    WindowContext,
 17};
 18use language::LanguageRegistry;
 19use project::Project;
 20use prompt_library::PromptBuilder;
 21use settings::Settings;
 22use time::UtcOffset;
 23use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
 24use util::ResultExt as _;
 25use workspace::dock::{DockPosition, Panel, PanelEvent};
 26use workspace::Workspace;
 27use zed_actions::assistant::ToggleFocus;
 28
 29use crate::active_thread::ActiveThread;
 30use crate::message_editor::MessageEditor;
 31use crate::thread::{Thread, ThreadError, ThreadId};
 32use crate::thread_history::{PastThread, ThreadHistory};
 33use crate::thread_store::ThreadStore;
 34use crate::{NewPromptEditor, NewThread, OpenHistory, OpenPromptEditorHistory};
 35
 36pub fn init(cx: &mut AppContext) {
 37    cx.observe_new_views(
 38        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 39            workspace
 40                .register_action(|workspace, _: &NewThread, cx| {
 41                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 42                        panel.update(cx, |panel, cx| panel.new_thread(cx));
 43                        workspace.focus_panel::<AssistantPanel>(cx);
 44                    }
 45                })
 46                .register_action(|workspace, _: &OpenHistory, cx| {
 47                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 48                        workspace.focus_panel::<AssistantPanel>(cx);
 49                        panel.update(cx, |panel, cx| panel.open_history(cx));
 50                    }
 51                })
 52                .register_action(|workspace, _: &NewPromptEditor, cx| {
 53                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 54                        workspace.focus_panel::<AssistantPanel>(cx);
 55                        panel.update(cx, |panel, cx| panel.new_prompt_editor(cx));
 56                    }
 57                })
 58                .register_action(|workspace, _: &OpenPromptEditorHistory, cx| {
 59                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 60                        workspace.focus_panel::<AssistantPanel>(cx);
 61                        panel.update(cx, |panel, cx| panel.open_prompt_editor_history(cx));
 62                    }
 63                });
 64        },
 65    )
 66    .detach();
 67}
 68
 69enum ActiveView {
 70    Thread,
 71    PromptEditor,
 72    History,
 73    PromptEditorHistory,
 74}
 75
 76pub struct AssistantPanel {
 77    workspace: WeakView<Workspace>,
 78    project: Model<Project>,
 79    fs: Arc<dyn Fs>,
 80    language_registry: Arc<LanguageRegistry>,
 81    thread_store: Model<ThreadStore>,
 82    thread: View<ActiveThread>,
 83    message_editor: View<MessageEditor>,
 84    context_store: Model<assistant_context_editor::ContextStore>,
 85    context_editor: Option<View<ContextEditor>>,
 86    context_history: Option<View<ContextHistory>>,
 87    tools: Arc<ToolWorkingSet>,
 88    local_timezone: UtcOffset,
 89    active_view: ActiveView,
 90    history: View<ThreadHistory>,
 91    new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 92    open_history_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 93    width: Option<Pixels>,
 94    height: Option<Pixels>,
 95}
 96
 97impl AssistantPanel {
 98    pub fn load(
 99        workspace: WeakView<Workspace>,
100        prompt_builder: Arc<PromptBuilder>,
101        cx: AsyncWindowContext,
102    ) -> Task<Result<View<Self>>> {
103        cx.spawn(|mut cx| async move {
104            let tools = Arc::new(ToolWorkingSet::default());
105            let thread_store = workspace
106                .update(&mut cx, |workspace, cx| {
107                    let project = workspace.project().clone();
108                    ThreadStore::new(project, tools.clone(), cx)
109                })?
110                .await?;
111
112            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
113            let context_store = workspace
114                .update(&mut cx, |workspace, cx| {
115                    let project = workspace.project().clone();
116                    assistant_context_editor::ContextStore::new(
117                        project,
118                        prompt_builder.clone(),
119                        slash_commands,
120                        tools.clone(),
121                        cx,
122                    )
123                })?
124                .await?;
125
126            workspace.update(&mut cx, |workspace, cx| {
127                cx.new_view(|cx| Self::new(workspace, thread_store, context_store, tools, cx))
128            })
129        })
130    }
131
132    fn new(
133        workspace: &Workspace,
134        thread_store: Model<ThreadStore>,
135        context_store: Model<assistant_context_editor::ContextStore>,
136        tools: Arc<ToolWorkingSet>,
137        cx: &mut ViewContext<Self>,
138    ) -> Self {
139        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
140        let fs = workspace.app_state().fs.clone();
141        let project = workspace.project().clone();
142        let language_registry = project.read(cx).languages().clone();
143        let workspace = workspace.weak_handle();
144        let weak_self = cx.view().downgrade();
145
146        let message_editor = cx.new_view(|cx| {
147            MessageEditor::new(
148                fs.clone(),
149                workspace.clone(),
150                thread_store.downgrade(),
151                thread.clone(),
152                cx,
153            )
154        });
155
156        Self {
157            active_view: ActiveView::Thread,
158            workspace: workspace.clone(),
159            project,
160            fs: fs.clone(),
161            language_registry: language_registry.clone(),
162            thread_store: thread_store.clone(),
163            thread: cx.new_view(|cx| {
164                ActiveThread::new(
165                    thread.clone(),
166                    workspace,
167                    language_registry,
168                    tools.clone(),
169                    cx,
170                )
171            }),
172            message_editor,
173            context_store,
174            context_editor: None,
175            context_history: None,
176            tools,
177            local_timezone: UtcOffset::from_whole_seconds(
178                chrono::Local::now().offset().local_minus_utc(),
179            )
180            .unwrap(),
181            history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
182            new_item_context_menu_handle: PopoverMenuHandle::default(),
183            open_history_context_menu_handle: PopoverMenuHandle::default(),
184            width: None,
185            height: None,
186        }
187    }
188
189    pub fn toggle_focus(
190        workspace: &mut Workspace,
191        _: &ToggleFocus,
192        cx: &mut ViewContext<Workspace>,
193    ) {
194        let settings = AssistantSettings::get_global(cx);
195        if !settings.enabled {
196            return;
197        }
198
199        workspace.toggle_panel_focus::<Self>(cx);
200    }
201
202    pub(crate) fn local_timezone(&self) -> UtcOffset {
203        self.local_timezone
204    }
205
206    pub(crate) fn thread_store(&self) -> &Model<ThreadStore> {
207        &self.thread_store
208    }
209
210    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
211        self.thread
212            .update(cx, |thread, cx| thread.cancel_last_completion(cx));
213    }
214
215    fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
216        let thread = self
217            .thread_store
218            .update(cx, |this, cx| this.create_thread(cx));
219
220        self.active_view = ActiveView::Thread;
221        self.thread = cx.new_view(|cx| {
222            ActiveThread::new(
223                thread.clone(),
224                self.workspace.clone(),
225                self.language_registry.clone(),
226                self.tools.clone(),
227                cx,
228            )
229        });
230        self.message_editor = cx.new_view(|cx| {
231            MessageEditor::new(
232                self.fs.clone(),
233                self.workspace.clone(),
234                self.thread_store.downgrade(),
235                thread,
236                cx,
237            )
238        });
239        self.message_editor.focus_handle(cx).focus(cx);
240    }
241
242    fn new_prompt_editor(&mut self, cx: &mut ViewContext<Self>) {
243        self.active_view = ActiveView::PromptEditor;
244
245        let context = self
246            .context_store
247            .update(cx, |context_store, cx| context_store.create(cx));
248        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
249            .log_err()
250            .flatten();
251
252        self.context_editor = Some(cx.new_view(|cx| {
253            let mut editor = ContextEditor::for_context(
254                context,
255                self.fs.clone(),
256                self.workspace.clone(),
257                self.project.clone(),
258                lsp_adapter_delegate,
259                cx,
260            );
261            editor.insert_default_prompt(cx);
262            editor
263        }));
264
265        if let Some(context_editor) = self.context_editor.as_ref() {
266            context_editor.focus_handle(cx).focus(cx);
267        }
268    }
269
270    fn open_history(&mut self, cx: &mut ViewContext<Self>) {
271        self.active_view = ActiveView::History;
272        self.history.focus_handle(cx).focus(cx);
273        cx.notify();
274    }
275
276    fn open_prompt_editor_history(&mut self, cx: &mut ViewContext<Self>) {
277        self.active_view = ActiveView::PromptEditorHistory;
278        self.context_history = Some(cx.new_view(|cx| {
279            ContextHistory::new(
280                self.project.clone(),
281                self.context_store.clone(),
282                self.workspace.clone(),
283                cx,
284            )
285        }));
286
287        if let Some(context_history) = self.context_history.as_ref() {
288            context_history.focus_handle(cx).focus(cx);
289        }
290
291        cx.notify();
292    }
293
294    fn open_saved_prompt_editor(
295        &mut self,
296        path: PathBuf,
297        cx: &mut ViewContext<Self>,
298    ) -> Task<Result<()>> {
299        let context = self
300            .context_store
301            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
302        let fs = self.fs.clone();
303        let project = self.project.clone();
304        let workspace = self.workspace.clone();
305
306        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
307
308        cx.spawn(|this, mut cx| async move {
309            let context = context.await?;
310            this.update(&mut cx, |this, cx| {
311                let editor = cx.new_view(|cx| {
312                    ContextEditor::for_context(
313                        context,
314                        fs,
315                        workspace,
316                        project,
317                        lsp_adapter_delegate,
318                        cx,
319                    )
320                });
321                this.active_view = ActiveView::PromptEditor;
322                this.context_editor = Some(editor);
323
324                anyhow::Ok(())
325            })??;
326            Ok(())
327        })
328    }
329
330    pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
331        let Some(thread) = self
332            .thread_store
333            .update(cx, |this, cx| this.open_thread(thread_id, cx))
334        else {
335            return;
336        };
337
338        self.active_view = ActiveView::Thread;
339        self.thread = cx.new_view(|cx| {
340            ActiveThread::new(
341                thread.clone(),
342                self.workspace.clone(),
343                self.language_registry.clone(),
344                self.tools.clone(),
345                cx,
346            )
347        });
348        self.message_editor = cx.new_view(|cx| {
349            MessageEditor::new(
350                self.fs.clone(),
351                self.workspace.clone(),
352                self.thread_store.downgrade(),
353                thread,
354                cx,
355            )
356        });
357        self.message_editor.focus_handle(cx).focus(cx);
358    }
359
360    pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
361        self.thread.read(cx).thread.clone()
362    }
363
364    pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
365        self.thread_store
366            .update(cx, |this, cx| this.delete_thread(thread_id, cx));
367    }
368}
369
370impl FocusableView for AssistantPanel {
371    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
372        match self.active_view {
373            ActiveView::Thread => self.message_editor.focus_handle(cx),
374            ActiveView::History => self.history.focus_handle(cx),
375            ActiveView::PromptEditor => {
376                if let Some(context_editor) = self.context_editor.as_ref() {
377                    context_editor.focus_handle(cx)
378                } else {
379                    cx.focus_handle()
380                }
381            }
382            ActiveView::PromptEditorHistory => {
383                if let Some(context_history) = self.context_history.as_ref() {
384                    context_history.focus_handle(cx)
385                } else {
386                    cx.focus_handle()
387                }
388            }
389        }
390    }
391}
392
393impl EventEmitter<PanelEvent> for AssistantPanel {}
394
395impl Panel for AssistantPanel {
396    fn persistent_name() -> &'static str {
397        "AssistantPanel2"
398    }
399
400    fn position(&self, cx: &WindowContext) -> DockPosition {
401        match AssistantSettings::get_global(cx).dock {
402            AssistantDockPosition::Left => DockPosition::Left,
403            AssistantDockPosition::Bottom => DockPosition::Bottom,
404            AssistantDockPosition::Right => DockPosition::Right,
405        }
406    }
407
408    fn position_is_valid(&self, _: DockPosition) -> bool {
409        true
410    }
411
412    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
413        settings::update_settings_file::<AssistantSettings>(
414            self.fs.clone(),
415            cx,
416            move |settings, _| {
417                let dock = match position {
418                    DockPosition::Left => AssistantDockPosition::Left,
419                    DockPosition::Bottom => AssistantDockPosition::Bottom,
420                    DockPosition::Right => AssistantDockPosition::Right,
421                };
422                settings.set_dock(dock);
423            },
424        );
425    }
426
427    fn size(&self, cx: &WindowContext) -> Pixels {
428        let settings = AssistantSettings::get_global(cx);
429        match self.position(cx) {
430            DockPosition::Left | DockPosition::Right => {
431                self.width.unwrap_or(settings.default_width)
432            }
433            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
434        }
435    }
436
437    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
438        match self.position(cx) {
439            DockPosition::Left | DockPosition::Right => self.width = size,
440            DockPosition::Bottom => self.height = size,
441        }
442        cx.notify();
443    }
444
445    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
446
447    fn remote_id() -> Option<proto::PanelId> {
448        Some(proto::PanelId::AssistantPanel)
449    }
450
451    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
452        let settings = AssistantSettings::get_global(cx);
453        if !settings.enabled || !settings.button {
454            return None;
455        }
456
457        Some(IconName::ZedAssistant)
458    }
459
460    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
461        Some("Assistant Panel")
462    }
463
464    fn toggle_action(&self) -> Box<dyn Action> {
465        Box::new(ToggleFocus)
466    }
467
468    fn activation_priority(&self) -> u32 {
469        3
470    }
471}
472
473impl AssistantPanel {
474    fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
475        let thread = self.thread.read(cx);
476
477        let title = match self.active_view {
478            ActiveView::Thread => {
479                if thread.is_empty() {
480                    thread.summary_or_default(cx)
481                } else {
482                    thread
483                        .summary(cx)
484                        .unwrap_or_else(|| SharedString::from("Loading Summary…"))
485                }
486            }
487            ActiveView::PromptEditor => self
488                .context_editor
489                .as_ref()
490                .map(|context_editor| {
491                    SharedString::from(context_editor.read(cx).title(cx).to_string())
492                })
493                .unwrap_or_else(|| SharedString::from("Loading Summary…")),
494            ActiveView::History => "History / Thread".into(),
495            ActiveView::PromptEditorHistory => "History / Prompt Editor".into(),
496        };
497
498        h_flex()
499            .id("assistant-toolbar")
500            .px(DynamicSpacing::Base08.rems(cx))
501            .h(Tab::container_height(cx))
502            .flex_none()
503            .justify_between()
504            .gap(DynamicSpacing::Base08.rems(cx))
505            .bg(cx.theme().colors().tab_bar_background)
506            .border_b_1()
507            .border_color(cx.theme().colors().border)
508            .child(h_flex().child(Label::new(title)))
509            .child(
510                h_flex()
511                    .h_full()
512                    .pl_1p5()
513                    .border_l_1()
514                    .border_color(cx.theme().colors().border)
515                    .gap(DynamicSpacing::Base02.rems(cx))
516                    .child(
517                        PopoverMenu::new("assistant-toolbar-new-popover-menu")
518                            .trigger(
519                                IconButton::new("new", IconName::Plus)
520                                    .icon_size(IconSize::Small)
521                                    .style(ButtonStyle::Subtle)
522                                    .tooltip(|cx| Tooltip::text("New…", cx)),
523                            )
524                            .anchor(Corner::TopRight)
525                            .with_handle(self.new_item_context_menu_handle.clone())
526                            .menu(move |cx| {
527                                Some(ContextMenu::build(cx, |menu, _| {
528                                    menu.action("New Thread", NewThread.boxed_clone())
529                                        .action("New Prompt Editor", NewPromptEditor.boxed_clone())
530                                }))
531                            }),
532                    )
533                    .child(
534                        PopoverMenu::new("assistant-toolbar-history-popover-menu")
535                            .trigger(
536                                IconButton::new("open-history", IconName::HistoryRerun)
537                                    .icon_size(IconSize::Small)
538                                    .style(ButtonStyle::Subtle)
539                                    .tooltip(|cx| Tooltip::text("History…", cx)),
540                            )
541                            .anchor(Corner::TopRight)
542                            .with_handle(self.open_history_context_menu_handle.clone())
543                            .menu(move |cx| {
544                                Some(ContextMenu::build(cx, |menu, _| {
545                                    menu.action("Thread History", OpenHistory.boxed_clone())
546                                        .action(
547                                            "Prompt Editor History",
548                                            OpenPromptEditorHistory.boxed_clone(),
549                                        )
550                                }))
551                            }),
552                    )
553                    .child(
554                        IconButton::new("configure-assistant", IconName::Settings)
555                            .icon_size(IconSize::Small)
556                            .style(ButtonStyle::Subtle)
557                            .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
558                            .on_click(move |_event, _cx| {
559                                println!("Configure Assistant");
560                            }),
561                    ),
562            )
563    }
564
565    fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
566        if self.thread.read(cx).is_empty() {
567            return self.render_thread_empty_state(cx).into_any_element();
568        }
569
570        self.thread.clone().into_any()
571    }
572
573    fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
574        let recent_threads = self
575            .thread_store
576            .update(cx, |this, cx| this.recent_threads(3, cx));
577
578        v_flex()
579            .gap_2()
580            .child(
581                v_flex().w_full().child(
582                    svg()
583                        .path("icons/logo_96.svg")
584                        .text_color(cx.theme().colors().text)
585                        .w(px(40.))
586                        .h(px(40.))
587                        .mx_auto()
588                        .mb_4(),
589                ),
590            )
591            .when(!recent_threads.is_empty(), |parent| {
592                parent
593                    .child(
594                        h_flex().w_full().justify_center().child(
595                            Label::new("Recent Threads:")
596                                .size(LabelSize::Small)
597                                .color(Color::Muted),
598                        ),
599                    )
600                    .child(v_flex().mx_auto().w_4_5().gap_2().children(
601                        recent_threads.into_iter().map(|thread| {
602                            // TODO: keyboard navigation
603                            PastThread::new(thread, cx.view().downgrade(), false)
604                        }),
605                    ))
606                    .child(
607                        h_flex().w_full().justify_center().child(
608                            Button::new("view-all-past-threads", "View All Past Threads")
609                                .style(ButtonStyle::Subtle)
610                                .label_size(LabelSize::Small)
611                                .key_binding(KeyBinding::for_action_in(
612                                    &OpenHistory,
613                                    &self.focus_handle(cx),
614                                    cx,
615                                ))
616                                .on_click(move |_event, cx| {
617                                    cx.dispatch_action(OpenHistory.boxed_clone());
618                                }),
619                        ),
620                    )
621            })
622    }
623
624    fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
625        let last_error = self.thread.read(cx).last_error()?;
626
627        Some(
628            div()
629                .absolute()
630                .right_3()
631                .bottom_12()
632                .max_w_96()
633                .py_2()
634                .px_3()
635                .elevation_2(cx)
636                .occlude()
637                .child(match last_error {
638                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
639                    ThreadError::MaxMonthlySpendReached => {
640                        self.render_max_monthly_spend_reached_error(cx)
641                    }
642                    ThreadError::Message(error_message) => {
643                        self.render_error_message(&error_message, cx)
644                    }
645                })
646                .into_any(),
647        )
648    }
649
650    fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
651        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.";
652
653        v_flex()
654            .gap_0p5()
655            .child(
656                h_flex()
657                    .gap_1p5()
658                    .items_center()
659                    .child(Icon::new(IconName::XCircle).color(Color::Error))
660                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
661            )
662            .child(
663                div()
664                    .id("error-message")
665                    .max_h_24()
666                    .overflow_y_scroll()
667                    .child(Label::new(ERROR_MESSAGE)),
668            )
669            .child(
670                h_flex()
671                    .justify_end()
672                    .mt_1()
673                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
674                        |this, _, cx| {
675                            this.thread.update(cx, |this, _cx| {
676                                this.clear_last_error();
677                            });
678
679                            cx.open_url(&zed_urls::account_url(cx));
680                            cx.notify();
681                        },
682                    )))
683                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
684                        |this, _, cx| {
685                            this.thread.update(cx, |this, _cx| {
686                                this.clear_last_error();
687                            });
688
689                            cx.notify();
690                        },
691                    ))),
692            )
693            .into_any()
694    }
695
696    fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
697        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
698
699        v_flex()
700            .gap_0p5()
701            .child(
702                h_flex()
703                    .gap_1p5()
704                    .items_center()
705                    .child(Icon::new(IconName::XCircle).color(Color::Error))
706                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
707            )
708            .child(
709                div()
710                    .id("error-message")
711                    .max_h_24()
712                    .overflow_y_scroll()
713                    .child(Label::new(ERROR_MESSAGE)),
714            )
715            .child(
716                h_flex()
717                    .justify_end()
718                    .mt_1()
719                    .child(
720                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
721                            cx.listener(|this, _, cx| {
722                                this.thread.update(cx, |this, _cx| {
723                                    this.clear_last_error();
724                                });
725
726                                cx.open_url(&zed_urls::account_url(cx));
727                                cx.notify();
728                            }),
729                        ),
730                    )
731                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
732                        |this, _, cx| {
733                            this.thread.update(cx, |this, _cx| {
734                                this.clear_last_error();
735                            });
736
737                            cx.notify();
738                        },
739                    ))),
740            )
741            .into_any()
742    }
743
744    fn render_error_message(
745        &self,
746        error_message: &SharedString,
747        cx: &mut ViewContext<Self>,
748    ) -> AnyElement {
749        v_flex()
750            .gap_0p5()
751            .child(
752                h_flex()
753                    .gap_1p5()
754                    .items_center()
755                    .child(Icon::new(IconName::XCircle).color(Color::Error))
756                    .child(
757                        Label::new("Error interacting with language model")
758                            .weight(FontWeight::MEDIUM),
759                    ),
760            )
761            .child(
762                div()
763                    .id("error-message")
764                    .max_h_32()
765                    .overflow_y_scroll()
766                    .child(Label::new(error_message.clone())),
767            )
768            .child(
769                h_flex()
770                    .justify_end()
771                    .mt_1()
772                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
773                        |this, _, cx| {
774                            this.thread.update(cx, |this, _cx| {
775                                this.clear_last_error();
776                            });
777
778                            cx.notify();
779                        },
780                    ))),
781            )
782            .into_any()
783    }
784}
785
786impl Render for AssistantPanel {
787    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
788        v_flex()
789            .key_context("AssistantPanel2")
790            .justify_between()
791            .size_full()
792            .on_action(cx.listener(Self::cancel))
793            .on_action(cx.listener(|this, _: &NewThread, cx| {
794                this.new_thread(cx);
795            }))
796            .on_action(cx.listener(|this, _: &OpenHistory, cx| {
797                this.open_history(cx);
798            }))
799            .child(self.render_toolbar(cx))
800            .map(|parent| match self.active_view {
801                ActiveView::Thread => parent
802                    .child(self.render_active_thread_or_empty_state(cx))
803                    .child(
804                        h_flex()
805                            .border_t_1()
806                            .border_color(cx.theme().colors().border)
807                            .child(self.message_editor.clone()),
808                    )
809                    .children(self.render_last_error(cx)),
810                ActiveView::History => parent.child(self.history.clone()),
811                ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
812                ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
813            })
814    }
815}
816
817pub struct ConcreteAssistantPanelDelegate;
818
819impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
820    fn active_context_editor(
821        &self,
822        workspace: &mut Workspace,
823        cx: &mut ViewContext<Workspace>,
824    ) -> Option<View<ContextEditor>> {
825        let panel = workspace.panel::<AssistantPanel>(cx)?;
826        panel.update(cx, |panel, _cx| panel.context_editor.clone())
827    }
828
829    fn open_saved_context(
830        &self,
831        workspace: &mut Workspace,
832        path: std::path::PathBuf,
833        cx: &mut ViewContext<Workspace>,
834    ) -> Task<Result<()>> {
835        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
836            return Task::ready(Err(anyhow!("Assistant panel not found")));
837        };
838
839        panel.update(cx, |panel, cx| panel.open_saved_prompt_editor(path, cx))
840    }
841
842    fn open_remote_context(
843        &self,
844        _workspace: &mut Workspace,
845        _context_id: assistant_context_editor::ContextId,
846        _cx: &mut ViewContext<Workspace>,
847    ) -> Task<Result<View<ContextEditor>>> {
848        Task::ready(Err(anyhow!("opening remote context not implemented")))
849    }
850
851    fn quote_selection(
852        &self,
853        _workspace: &mut Workspace,
854        _creases: Vec<(String, String)>,
855        _cx: &mut ViewContext<Workspace>,
856    ) {
857    }
858}