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