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