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;
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 configuration_error(&self, cx: &AppContext) -> Option<ConfigurationError> {
666 let provider = LanguageModelRegistry::read_global(cx).active_provider();
667 let is_authenticated = provider
668 .as_ref()
669 .map_or(false, |provider| provider.is_authenticated(cx));
670
671 if provider.is_some() && is_authenticated {
672 return None;
673 }
674
675 if !is_authenticated {
676 return Some(ConfigurationError::ProviderNotAuthenticated);
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 v_flex()
695 .gap_2()
696 .child(
697 v_flex().w_full().child(
698 svg()
699 .path("icons/logo_96.svg")
700 .text_color(cx.theme().colors().text)
701 .w(px(40.))
702 .h(px(40.))
703 .mx_auto()
704 .mb_4(),
705 ),
706 )
707 .when(
708 matches!(
709 self.configuration_error(cx),
710 Some(ConfigurationError::ProviderNotAuthenticated)
711 ),
712 |parent| {
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 )
740 .when(
741 recent_threads.is_empty() && self.configuration_error(cx).is_none(),
742 |parent| {
743 parent.child(
744 v_flex().gap_0p5().child(create_welcome_heading()).child(
745 h_flex().w_full().justify_center().child(
746 Label::new("Start typing to chat with your codebase")
747 .color(Color::Muted),
748 ),
749 ),
750 )
751 },
752 )
753 .when(!recent_threads.is_empty(), |parent| {
754 parent
755 .child(
756 h_flex().w_full().justify_center().child(
757 Label::new("Recent Threads:")
758 .size(LabelSize::Small)
759 .color(Color::Muted),
760 ),
761 )
762 .child(v_flex().mx_auto().w_4_5().gap_2().children(
763 recent_threads.into_iter().map(|thread| {
764 // TODO: keyboard navigation
765 PastThread::new(thread, cx.view().downgrade(), false)
766 }),
767 ))
768 .child(
769 h_flex().w_full().justify_center().child(
770 Button::new("view-all-past-threads", "View All Past Threads")
771 .style(ButtonStyle::Subtle)
772 .label_size(LabelSize::Small)
773 .key_binding(KeyBinding::for_action_in(
774 &OpenHistory,
775 &self.focus_handle(cx),
776 cx,
777 ))
778 .on_click(move |_event, cx| {
779 cx.dispatch_action(OpenHistory.boxed_clone());
780 }),
781 ),
782 )
783 })
784 }
785
786 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
787 let last_error = self.thread.read(cx).last_error()?;
788
789 Some(
790 div()
791 .absolute()
792 .right_3()
793 .bottom_12()
794 .max_w_96()
795 .py_2()
796 .px_3()
797 .elevation_2(cx)
798 .occlude()
799 .child(match last_error {
800 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
801 ThreadError::MaxMonthlySpendReached => {
802 self.render_max_monthly_spend_reached_error(cx)
803 }
804 ThreadError::Message(error_message) => {
805 self.render_error_message(&error_message, cx)
806 }
807 })
808 .into_any(),
809 )
810 }
811
812 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
813 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.";
814
815 v_flex()
816 .gap_0p5()
817 .child(
818 h_flex()
819 .gap_1p5()
820 .items_center()
821 .child(Icon::new(IconName::XCircle).color(Color::Error))
822 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
823 )
824 .child(
825 div()
826 .id("error-message")
827 .max_h_24()
828 .overflow_y_scroll()
829 .child(Label::new(ERROR_MESSAGE)),
830 )
831 .child(
832 h_flex()
833 .justify_end()
834 .mt_1()
835 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
836 |this, _, cx| {
837 this.thread.update(cx, |this, _cx| {
838 this.clear_last_error();
839 });
840
841 cx.open_url(&zed_urls::account_url(cx));
842 cx.notify();
843 },
844 )))
845 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
846 |this, _, cx| {
847 this.thread.update(cx, |this, _cx| {
848 this.clear_last_error();
849 });
850
851 cx.notify();
852 },
853 ))),
854 )
855 .into_any()
856 }
857
858 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
859 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
860
861 v_flex()
862 .gap_0p5()
863 .child(
864 h_flex()
865 .gap_1p5()
866 .items_center()
867 .child(Icon::new(IconName::XCircle).color(Color::Error))
868 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
869 )
870 .child(
871 div()
872 .id("error-message")
873 .max_h_24()
874 .overflow_y_scroll()
875 .child(Label::new(ERROR_MESSAGE)),
876 )
877 .child(
878 h_flex()
879 .justify_end()
880 .mt_1()
881 .child(
882 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
883 cx.listener(|this, _, cx| {
884 this.thread.update(cx, |this, _cx| {
885 this.clear_last_error();
886 });
887
888 cx.open_url(&zed_urls::account_url(cx));
889 cx.notify();
890 }),
891 ),
892 )
893 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
894 |this, _, cx| {
895 this.thread.update(cx, |this, _cx| {
896 this.clear_last_error();
897 });
898
899 cx.notify();
900 },
901 ))),
902 )
903 .into_any()
904 }
905
906 fn render_error_message(
907 &self,
908 error_message: &SharedString,
909 cx: &mut ViewContext<Self>,
910 ) -> AnyElement {
911 v_flex()
912 .gap_0p5()
913 .child(
914 h_flex()
915 .gap_1p5()
916 .items_center()
917 .child(Icon::new(IconName::XCircle).color(Color::Error))
918 .child(
919 Label::new("Error interacting with language model")
920 .weight(FontWeight::MEDIUM),
921 ),
922 )
923 .child(
924 div()
925 .id("error-message")
926 .max_h_32()
927 .overflow_y_scroll()
928 .child(Label::new(error_message.clone())),
929 )
930 .child(
931 h_flex()
932 .justify_end()
933 .mt_1()
934 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
935 |this, _, cx| {
936 this.thread.update(cx, |this, _cx| {
937 this.clear_last_error();
938 });
939
940 cx.notify();
941 },
942 ))),
943 )
944 .into_any()
945 }
946}
947
948impl Render for AssistantPanel {
949 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
950 v_flex()
951 .key_context("AssistantPanel2")
952 .justify_between()
953 .size_full()
954 .on_action(cx.listener(Self::cancel))
955 .on_action(cx.listener(|this, _: &NewThread, cx| {
956 this.new_thread(cx);
957 }))
958 .on_action(cx.listener(|this, _: &OpenHistory, cx| {
959 this.open_history(cx);
960 }))
961 .on_action(cx.listener(Self::deploy_prompt_library))
962 .child(self.render_toolbar(cx))
963 .map(|parent| match self.active_view {
964 ActiveView::Thread => parent
965 .child(self.render_active_thread_or_empty_state(cx))
966 .child(
967 h_flex()
968 .border_t_1()
969 .border_color(cx.theme().colors().border)
970 .child(self.message_editor.clone()),
971 )
972 .children(self.render_last_error(cx)),
973 ActiveView::History => parent.child(self.history.clone()),
974 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
975 ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
976 ActiveView::Configuration => parent.children(self.configuration.clone()),
977 })
978 }
979}
980
981struct PromptLibraryInlineAssist {
982 workspace: WeakView<Workspace>,
983}
984
985impl PromptLibraryInlineAssist {
986 pub fn new(workspace: WeakView<Workspace>) -> Self {
987 Self { workspace }
988 }
989}
990
991impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
992 fn assist(
993 &self,
994 prompt_editor: &View<Editor>,
995 _initial_prompt: Option<String>,
996 cx: &mut ViewContext<PromptLibrary>,
997 ) {
998 InlineAssistant::update_global(cx, |assistant, cx| {
999 assistant.assist(&prompt_editor, self.workspace.clone(), None, cx)
1000 })
1001 }
1002
1003 fn focus_assistant_panel(
1004 &self,
1005 workspace: &mut Workspace,
1006 cx: &mut ViewContext<Workspace>,
1007 ) -> bool {
1008 workspace.focus_panel::<AssistantPanel>(cx).is_some()
1009 }
1010}
1011
1012pub struct ConcreteAssistantPanelDelegate;
1013
1014impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1015 fn active_context_editor(
1016 &self,
1017 workspace: &mut Workspace,
1018 cx: &mut ViewContext<Workspace>,
1019 ) -> Option<View<ContextEditor>> {
1020 let panel = workspace.panel::<AssistantPanel>(cx)?;
1021 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1022 }
1023
1024 fn open_saved_context(
1025 &self,
1026 workspace: &mut Workspace,
1027 path: std::path::PathBuf,
1028 cx: &mut ViewContext<Workspace>,
1029 ) -> Task<Result<()>> {
1030 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1031 return Task::ready(Err(anyhow!("Assistant panel not found")));
1032 };
1033
1034 panel.update(cx, |panel, cx| panel.open_saved_prompt_editor(path, cx))
1035 }
1036
1037 fn open_remote_context(
1038 &self,
1039 _workspace: &mut Workspace,
1040 _context_id: assistant_context_editor::ContextId,
1041 _cx: &mut ViewContext<Workspace>,
1042 ) -> Task<Result<View<ContextEditor>>> {
1043 Task::ready(Err(anyhow!("opening remote context not implemented")))
1044 }
1045
1046 fn quote_selection(
1047 &self,
1048 _workspace: &mut Workspace,
1049 _creases: Vec<(String, String)>,
1050 _cx: &mut ViewContext<Workspace>,
1051 ) {
1052 }
1053}