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