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