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