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}