1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use assistant_context_editor::{
7 AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
8 make_lsp_adapter_delegate, render_remaining_tokens,
9};
10use assistant_settings::{AssistantDockPosition, AssistantSettings};
11use assistant_slash_command::SlashCommandWorkingSet;
12use assistant_tool::ToolWorkingSet;
13
14use client::zed_urls;
15use editor::{Editor, EditorEvent, MultiBuffer};
16use fs::Fs;
17use gpui::{
18 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
19 EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
20 UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
21};
22use language::LanguageRegistry;
23use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
24use language_model_selector::ToggleModelSelector;
25use project::Project;
26use prompt_library::{PromptLibrary, open_prompt_library};
27use prompt_store::PromptBuilder;
28use settings::{Settings, update_settings_file};
29use time::UtcOffset;
30use ui::{
31 Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
32};
33use util::ResultExt as _;
34use workspace::Workspace;
35use workspace::dock::{DockPosition, Panel, PanelEvent};
36use zed_actions::agent::OpenConfiguration;
37use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
38
39use crate::active_thread::ActiveThread;
40use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
41use crate::history_store::{HistoryEntry, HistoryStore};
42use crate::message_editor::MessageEditor;
43use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
44use crate::thread_history::{PastContext, PastThread, ThreadHistory};
45use crate::thread_store::ThreadStore;
46use crate::{
47 AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
48 OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
49};
50
51pub fn init(cx: &mut App) {
52 cx.observe_new(
53 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
54 workspace
55 .register_action(|workspace, action: &NewThread, window, cx| {
56 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
57 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
58 workspace.focus_panel::<AssistantPanel>(window, cx);
59 }
60 })
61 .register_action(|workspace, _: &OpenHistory, window, cx| {
62 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
63 workspace.focus_panel::<AssistantPanel>(window, cx);
64 panel.update(cx, |panel, cx| panel.open_history(window, cx));
65 }
66 })
67 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
68 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
69 workspace.focus_panel::<AssistantPanel>(window, cx);
70 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
71 }
72 })
73 .register_action(|workspace, _: &NewTextThread, window, cx| {
74 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
75 workspace.focus_panel::<AssistantPanel>(window, cx);
76 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
77 }
78 })
79 .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
80 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
81 workspace.focus_panel::<AssistantPanel>(window, cx);
82 panel.update(cx, |panel, cx| {
83 panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
84 });
85 }
86 })
87 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
88 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
89 workspace.focus_panel::<AssistantPanel>(window, cx);
90 let thread = panel.read(cx).thread.read(cx).thread().clone();
91 AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
92 }
93 })
94 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
95 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
96 workspace.focus_panel::<AssistantPanel>(window, cx);
97 panel.update(cx, |panel, cx| {
98 panel.message_editor.update(cx, |editor, cx| {
99 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
100 });
101 });
102 }
103 });
104 },
105 )
106 .detach();
107}
108
109enum ActiveView {
110 Thread {
111 change_title_editor: Entity<Editor>,
112 _subscriptions: Vec<gpui::Subscription>,
113 },
114 PromptEditor,
115 History,
116 Configuration,
117}
118
119impl ActiveView {
120 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
121 let summary = thread.read(cx).summary_or_default();
122
123 let editor = cx.new(|cx| {
124 let mut editor = Editor::single_line(window, cx);
125 editor.set_text(summary, window, cx);
126 editor
127 });
128
129 let subscriptions = vec![
130 window.subscribe(&editor, cx, {
131 {
132 let thread = thread.clone();
133 move |editor, event, window, cx| match event {
134 EditorEvent::BufferEdited => {
135 let new_summary = editor.read(cx).text(cx);
136
137 thread.update(cx, |thread, cx| {
138 thread.set_summary(new_summary, cx);
139 })
140 }
141 EditorEvent::Blurred => {
142 if editor.read(cx).text(cx).is_empty() {
143 let summary = thread.read(cx).summary_or_default();
144
145 editor.update(cx, |editor, cx| {
146 editor.set_text(summary, window, cx);
147 });
148 }
149 }
150 _ => {}
151 }
152 }
153 }),
154 window.subscribe(&thread, cx, {
155 let editor = editor.clone();
156 move |thread, event, window, cx| match event {
157 ThreadEvent::SummaryGenerated => {
158 let summary = thread.read(cx).summary_or_default();
159
160 editor.update(cx, |editor, cx| {
161 editor.set_text(summary, window, cx);
162 })
163 }
164 _ => {}
165 }
166 }),
167 ];
168
169 Self::Thread {
170 change_title_editor: editor,
171 _subscriptions: subscriptions,
172 }
173 }
174}
175
176pub struct AssistantPanel {
177 workspace: WeakEntity<Workspace>,
178 project: Entity<Project>,
179 fs: Arc<dyn Fs>,
180 language_registry: Arc<LanguageRegistry>,
181 thread_store: Entity<ThreadStore>,
182 thread: Entity<ActiveThread>,
183 _thread_subscription: Subscription,
184 message_editor: Entity<MessageEditor>,
185 context_store: Entity<assistant_context_editor::ContextStore>,
186 context_editor: Option<Entity<ContextEditor>>,
187 configuration: Option<Entity<AssistantConfiguration>>,
188 configuration_subscription: Option<Subscription>,
189 local_timezone: UtcOffset,
190 active_view: ActiveView,
191 previous_view: Option<ActiveView>,
192 history_store: Entity<HistoryStore>,
193 history: Entity<ThreadHistory>,
194 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
195 width: Option<Pixels>,
196 height: Option<Pixels>,
197}
198
199impl AssistantPanel {
200 pub fn load(
201 workspace: WeakEntity<Workspace>,
202 prompt_builder: Arc<PromptBuilder>,
203 cx: AsyncWindowContext,
204 ) -> Task<Result<Entity<Self>>> {
205 cx.spawn(async move |cx| {
206 let tools = Arc::new(ToolWorkingSet::default());
207 let thread_store = workspace
208 .update(cx, |workspace, cx| {
209 let project = workspace.project().clone();
210 ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
211 })?
212 .await;
213
214 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
215 let context_store = workspace
216 .update(cx, |workspace, cx| {
217 let project = workspace.project().clone();
218 assistant_context_editor::ContextStore::new(
219 project,
220 prompt_builder.clone(),
221 slash_commands,
222 cx,
223 )
224 })?
225 .await?;
226
227 workspace.update_in(cx, |workspace, window, cx| {
228 cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
229 })
230 })
231 }
232
233 fn new(
234 workspace: &Workspace,
235 thread_store: Entity<ThreadStore>,
236 context_store: Entity<assistant_context_editor::ContextStore>,
237 window: &mut Window,
238 cx: &mut Context<Self>,
239 ) -> Self {
240 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
241 let fs = workspace.app_state().fs.clone();
242 let project = workspace.project();
243 let language_registry = project.read(cx).languages().clone();
244 let workspace = workspace.weak_handle();
245 let weak_self = cx.entity().downgrade();
246
247 let message_editor_context_store = cx.new(|_cx| {
248 crate::context_store::ContextStore::new(
249 project.downgrade(),
250 Some(thread_store.downgrade()),
251 )
252 });
253
254 let message_editor = cx.new(|cx| {
255 MessageEditor::new(
256 fs.clone(),
257 workspace.clone(),
258 message_editor_context_store.clone(),
259 thread_store.downgrade(),
260 thread.clone(),
261 window,
262 cx,
263 )
264 });
265
266 let history_store =
267 cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
268
269 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
270
271 let active_view = ActiveView::thread(thread.clone(), window, cx);
272 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
273 if let ThreadEvent::MessageAdded(_) = &event {
274 // needed to leave empty state
275 cx.notify();
276 }
277 });
278 let thread = cx.new(|cx| {
279 ActiveThread::new(
280 thread.clone(),
281 thread_store.clone(),
282 language_registry.clone(),
283 message_editor_context_store.clone(),
284 workspace.clone(),
285 window,
286 cx,
287 )
288 });
289
290 Self {
291 active_view,
292 workspace,
293 project: project.clone(),
294 fs: fs.clone(),
295 language_registry,
296 thread_store: thread_store.clone(),
297 thread,
298 _thread_subscription: thread_subscription,
299 message_editor,
300 context_store,
301 context_editor: None,
302 configuration: None,
303 configuration_subscription: None,
304 local_timezone: UtcOffset::from_whole_seconds(
305 chrono::Local::now().offset().local_minus_utc(),
306 )
307 .unwrap(),
308 previous_view: None,
309 history_store: history_store.clone(),
310 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
311 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
312 width: None,
313 height: None,
314 }
315 }
316
317 pub fn toggle_focus(
318 workspace: &mut Workspace,
319 _: &ToggleFocus,
320 window: &mut Window,
321 cx: &mut Context<Workspace>,
322 ) {
323 if workspace
324 .panel::<Self>(cx)
325 .is_some_and(|panel| panel.read(cx).enabled(cx))
326 {
327 workspace.toggle_panel_focus::<Self>(window, cx);
328 }
329 }
330
331 pub(crate) fn local_timezone(&self) -> UtcOffset {
332 self.local_timezone
333 }
334
335 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
336 &self.thread_store
337 }
338
339 fn cancel(
340 &mut self,
341 _: &editor::actions::Cancel,
342 _window: &mut Window,
343 cx: &mut Context<Self>,
344 ) {
345 self.thread
346 .update(cx, |thread, cx| thread.cancel_last_completion(cx));
347 }
348
349 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
350 let thread = self
351 .thread_store
352 .update(cx, |this, cx| this.create_thread(cx));
353
354 let thread_view = ActiveView::thread(thread.clone(), window, cx);
355 self.set_active_view(thread_view, window, cx);
356
357 let message_editor_context_store = cx.new(|_cx| {
358 crate::context_store::ContextStore::new(
359 self.project.downgrade(),
360 Some(self.thread_store.downgrade()),
361 )
362 });
363
364 if let Some(other_thread_id) = action.from_thread_id.clone() {
365 let other_thread_task = self
366 .thread_store
367 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
368
369 cx.spawn({
370 let context_store = message_editor_context_store.clone();
371
372 async move |_panel, cx| {
373 let other_thread = other_thread_task.await?;
374
375 context_store.update(cx, |this, cx| {
376 this.add_thread(other_thread, false, cx);
377 })?;
378 anyhow::Ok(())
379 }
380 })
381 .detach_and_log_err(cx);
382 }
383
384 self.thread = cx.new(|cx| {
385 ActiveThread::new(
386 thread.clone(),
387 self.thread_store.clone(),
388 self.language_registry.clone(),
389 message_editor_context_store.clone(),
390 self.workspace.clone(),
391 window,
392 cx,
393 )
394 });
395
396 self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
397 if let ThreadEvent::MessageAdded(_) = &event {
398 // needed to leave empty state
399 cx.notify();
400 }
401 });
402
403 self.message_editor = cx.new(|cx| {
404 MessageEditor::new(
405 self.fs.clone(),
406 self.workspace.clone(),
407 message_editor_context_store,
408 self.thread_store.downgrade(),
409 thread,
410 window,
411 cx,
412 )
413 });
414 self.message_editor.focus_handle(cx).focus(window);
415 }
416
417 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
418 self.set_active_view(ActiveView::PromptEditor, window, cx);
419
420 let context = self
421 .context_store
422 .update(cx, |context_store, cx| context_store.create(cx));
423 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
424 .log_err()
425 .flatten();
426
427 self.context_editor = Some(cx.new(|cx| {
428 let mut editor = ContextEditor::for_context(
429 context,
430 self.fs.clone(),
431 self.workspace.clone(),
432 self.project.clone(),
433 lsp_adapter_delegate,
434 window,
435 cx,
436 );
437 editor.insert_default_prompt(window, cx);
438 editor
439 }));
440
441 if let Some(context_editor) = self.context_editor.as_ref() {
442 context_editor.focus_handle(cx).focus(window);
443 }
444 }
445
446 fn deploy_prompt_library(
447 &mut self,
448 _: &OpenPromptLibrary,
449 _window: &mut Window,
450 cx: &mut Context<Self>,
451 ) {
452 open_prompt_library(
453 self.language_registry.clone(),
454 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
455 Arc::new(|| {
456 Box::new(SlashCommandCompletionProvider::new(
457 Arc::new(SlashCommandWorkingSet::default()),
458 None,
459 None,
460 ))
461 }),
462 cx,
463 )
464 .detach_and_log_err(cx);
465 }
466
467 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
468 if matches!(self.active_view, ActiveView::History) {
469 if let Some(previous_view) = self.previous_view.take() {
470 self.set_active_view(previous_view, window, cx);
471 }
472 } else {
473 self.thread_store
474 .update(cx, |thread_store, cx| thread_store.reload(cx))
475 .detach_and_log_err(cx);
476 self.set_active_view(ActiveView::History, window, cx);
477 }
478 cx.notify();
479 }
480
481 pub(crate) fn open_saved_prompt_editor(
482 &mut self,
483 path: PathBuf,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) -> Task<Result<()>> {
487 let context = self
488 .context_store
489 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
490 let fs = self.fs.clone();
491 let project = self.project.clone();
492 let workspace = self.workspace.clone();
493
494 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
495
496 cx.spawn_in(window, async move |this, cx| {
497 let context = context.await?;
498 this.update_in(cx, |this, window, cx| {
499 let editor = cx.new(|cx| {
500 ContextEditor::for_context(
501 context,
502 fs,
503 workspace,
504 project,
505 lsp_adapter_delegate,
506 window,
507 cx,
508 )
509 });
510 this.set_active_view(ActiveView::PromptEditor, window, cx);
511 this.context_editor = Some(editor);
512
513 anyhow::Ok(())
514 })??;
515 Ok(())
516 })
517 }
518
519 pub(crate) fn open_thread(
520 &mut self,
521 thread_id: &ThreadId,
522 window: &mut Window,
523 cx: &mut Context<Self>,
524 ) -> Task<Result<()>> {
525 let open_thread_task = self
526 .thread_store
527 .update(cx, |this, cx| this.open_thread(thread_id, cx));
528
529 cx.spawn_in(window, async move |this, cx| {
530 let thread = open_thread_task.await?;
531 this.update_in(cx, |this, window, cx| {
532 let thread_view = ActiveView::thread(thread.clone(), window, cx);
533 this.set_active_view(thread_view, window, cx);
534 let message_editor_context_store = cx.new(|_cx| {
535 crate::context_store::ContextStore::new(
536 this.project.downgrade(),
537 Some(this.thread_store.downgrade()),
538 )
539 });
540 this.thread = cx.new(|cx| {
541 ActiveThread::new(
542 thread.clone(),
543 this.thread_store.clone(),
544 this.language_registry.clone(),
545 message_editor_context_store.clone(),
546 this.workspace.clone(),
547 window,
548 cx,
549 )
550 });
551 this.message_editor = cx.new(|cx| {
552 MessageEditor::new(
553 this.fs.clone(),
554 this.workspace.clone(),
555 message_editor_context_store,
556 this.thread_store.downgrade(),
557 thread,
558 window,
559 cx,
560 )
561 });
562 this.message_editor.focus_handle(cx).focus(window);
563 })
564 })
565 }
566
567 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
568 match self.active_view {
569 ActiveView::Configuration | ActiveView::History => {
570 self.active_view =
571 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
572 cx.notify();
573 }
574 _ => {}
575 }
576 }
577
578 pub fn open_agent_diff(
579 &mut self,
580 _: &OpenAgentDiff,
581 window: &mut Window,
582 cx: &mut Context<Self>,
583 ) {
584 let thread = self.thread.read(cx).thread().clone();
585 self.workspace
586 .update(cx, |workspace, cx| {
587 AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
588 })
589 .log_err();
590 }
591
592 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
593 let context_server_manager = self.thread_store.read(cx).context_server_manager();
594 let tools = self.thread_store.read(cx).tools();
595 let fs = self.fs.clone();
596
597 self.set_active_view(ActiveView::Configuration, window, cx);
598 self.configuration =
599 Some(cx.new(|cx| {
600 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
601 }));
602
603 if let Some(configuration) = self.configuration.as_ref() {
604 self.configuration_subscription = Some(cx.subscribe_in(
605 configuration,
606 window,
607 Self::handle_assistant_configuration_event,
608 ));
609
610 configuration.focus_handle(cx).focus(window);
611 }
612 }
613
614 pub(crate) fn open_active_thread_as_markdown(
615 &mut self,
616 _: &OpenActiveThreadAsMarkdown,
617 window: &mut Window,
618 cx: &mut Context<Self>,
619 ) {
620 let Some(workspace) = self
621 .workspace
622 .upgrade()
623 .ok_or_else(|| anyhow!("workspace dropped"))
624 .log_err()
625 else {
626 return;
627 };
628
629 let markdown_language_task = workspace
630 .read(cx)
631 .app_state()
632 .languages
633 .language_for_name("Markdown");
634 let thread = self.active_thread(cx);
635 cx.spawn_in(window, async move |_this, cx| {
636 let markdown_language = markdown_language_task.await?;
637
638 workspace.update_in(cx, |workspace, window, cx| {
639 let thread = thread.read(cx);
640 let markdown = thread.to_markdown(cx)?;
641 let thread_summary = thread
642 .summary()
643 .map(|summary| summary.to_string())
644 .unwrap_or_else(|| "Thread".to_string());
645
646 let project = workspace.project().clone();
647 let buffer = project.update(cx, |project, cx| {
648 project.create_local_buffer(&markdown, Some(markdown_language), cx)
649 });
650 let buffer = cx.new(|cx| {
651 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
652 });
653
654 workspace.add_item_to_active_pane(
655 Box::new(cx.new(|cx| {
656 let mut editor =
657 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
658 editor.set_breadcrumb_header(thread_summary);
659 editor
660 })),
661 None,
662 true,
663 window,
664 cx,
665 );
666
667 anyhow::Ok(())
668 })
669 })
670 .detach_and_log_err(cx);
671 }
672
673 fn handle_assistant_configuration_event(
674 &mut self,
675 _entity: &Entity<AssistantConfiguration>,
676 event: &AssistantConfigurationEvent,
677 window: &mut Window,
678 cx: &mut Context<Self>,
679 ) {
680 match event {
681 AssistantConfigurationEvent::NewThread(provider) => {
682 if LanguageModelRegistry::read_global(cx)
683 .default_model()
684 .map_or(true, |model| model.provider.id() != provider.id())
685 {
686 if let Some(model) = provider.default_model(cx) {
687 update_settings_file::<AssistantSettings>(
688 self.fs.clone(),
689 cx,
690 move |settings, _| settings.set_model(model),
691 );
692 }
693 }
694
695 self.new_thread(&NewThread::default(), window, cx);
696 }
697 }
698 }
699
700 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
701 self.thread.read(cx).thread().clone()
702 }
703
704 pub(crate) fn delete_thread(
705 &mut self,
706 thread_id: &ThreadId,
707 cx: &mut Context<Self>,
708 ) -> Task<Result<()>> {
709 self.thread_store
710 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
711 }
712
713 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
714 self.context_editor.clone()
715 }
716
717 pub(crate) fn delete_context(
718 &mut self,
719 path: PathBuf,
720 cx: &mut Context<Self>,
721 ) -> Task<Result<()>> {
722 self.context_store
723 .update(cx, |this, cx| this.delete_local_context(path, cx))
724 }
725
726 fn set_active_view(
727 &mut self,
728 new_view: ActiveView,
729 window: &mut Window,
730 cx: &mut Context<Self>,
731 ) {
732 let current_is_history = matches!(self.active_view, ActiveView::History);
733 let new_is_history = matches!(new_view, ActiveView::History);
734
735 if current_is_history && !new_is_history {
736 self.active_view = new_view;
737 } else if !current_is_history && new_is_history {
738 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
739 } else {
740 if !new_is_history {
741 self.previous_view = None;
742 }
743 self.active_view = new_view;
744 }
745
746 self.focus_handle(cx).focus(window);
747 }
748}
749
750impl Focusable for AssistantPanel {
751 fn focus_handle(&self, cx: &App) -> FocusHandle {
752 match self.active_view {
753 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
754 ActiveView::History => self.history.focus_handle(cx),
755 ActiveView::PromptEditor => {
756 if let Some(context_editor) = self.context_editor.as_ref() {
757 context_editor.focus_handle(cx)
758 } else {
759 cx.focus_handle()
760 }
761 }
762 ActiveView::Configuration => {
763 if let Some(configuration) = self.configuration.as_ref() {
764 configuration.focus_handle(cx)
765 } else {
766 cx.focus_handle()
767 }
768 }
769 }
770 }
771}
772
773impl EventEmitter<PanelEvent> for AssistantPanel {}
774
775impl Panel for AssistantPanel {
776 fn persistent_name() -> &'static str {
777 "AgentPanel"
778 }
779
780 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
781 match AssistantSettings::get_global(cx).dock {
782 AssistantDockPosition::Left => DockPosition::Left,
783 AssistantDockPosition::Bottom => DockPosition::Bottom,
784 AssistantDockPosition::Right => DockPosition::Right,
785 }
786 }
787
788 fn position_is_valid(&self, _: DockPosition) -> bool {
789 true
790 }
791
792 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
793 settings::update_settings_file::<AssistantSettings>(
794 self.fs.clone(),
795 cx,
796 move |settings, _| {
797 let dock = match position {
798 DockPosition::Left => AssistantDockPosition::Left,
799 DockPosition::Bottom => AssistantDockPosition::Bottom,
800 DockPosition::Right => AssistantDockPosition::Right,
801 };
802 settings.set_dock(dock);
803 },
804 );
805 }
806
807 fn size(&self, window: &Window, cx: &App) -> Pixels {
808 let settings = AssistantSettings::get_global(cx);
809 match self.position(window, cx) {
810 DockPosition::Left | DockPosition::Right => {
811 self.width.unwrap_or(settings.default_width)
812 }
813 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
814 }
815 }
816
817 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
818 match self.position(window, cx) {
819 DockPosition::Left | DockPosition::Right => self.width = size,
820 DockPosition::Bottom => self.height = size,
821 }
822 cx.notify();
823 }
824
825 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
826
827 fn remote_id() -> Option<proto::PanelId> {
828 Some(proto::PanelId::AssistantPanel)
829 }
830
831 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
832 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
833 .then_some(IconName::ZedAssistant)
834 }
835
836 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
837 Some("Agent Panel")
838 }
839
840 fn toggle_action(&self) -> Box<dyn Action> {
841 Box::new(ToggleFocus)
842 }
843
844 fn activation_priority(&self) -> u32 {
845 3
846 }
847
848 fn enabled(&self, cx: &App) -> bool {
849 AssistantSettings::get_global(cx).enabled
850 }
851}
852
853impl AssistantPanel {
854 fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
855 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
856
857 let content = match &self.active_view {
858 ActiveView::Thread {
859 change_title_editor,
860 ..
861 } => {
862 let active_thread = self.thread.read(cx);
863 let is_empty = active_thread.is_empty();
864
865 let summary = active_thread.summary(cx);
866
867 if is_empty {
868 Label::new(Thread::DEFAULT_SUMMARY.clone())
869 .truncate()
870 .ml_2()
871 .into_any_element()
872 } else if summary.is_none() {
873 Label::new(LOADING_SUMMARY_PLACEHOLDER)
874 .ml_2()
875 .truncate()
876 .into_any_element()
877 } else {
878 div()
879 .ml_2()
880 .w_full()
881 .child(change_title_editor.clone())
882 .into_any_element()
883 }
884 }
885 ActiveView::PromptEditor => {
886 let title = self
887 .context_editor
888 .as_ref()
889 .map(|context_editor| {
890 SharedString::from(context_editor.read(cx).title(cx).to_string())
891 })
892 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
893
894 Label::new(title).ml_2().truncate().into_any_element()
895 }
896 ActiveView::History => Label::new("History").truncate().into_any_element(),
897 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
898 };
899
900 h_flex()
901 .key_context("TitleEditor")
902 .id("TitleEditor")
903 .flex_grow()
904 .w_full()
905 .max_w_full()
906 .overflow_x_scroll()
907 .child(content)
908 .into_any()
909 }
910
911 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
912 let active_thread = self.thread.read(cx);
913 let thread = active_thread.thread().read(cx);
914 let token_usage = thread.total_token_usage(cx);
915 let thread_id = thread.id().clone();
916
917 let is_generating = thread.is_generating();
918 let is_empty = active_thread.is_empty();
919 let focus_handle = self.focus_handle(cx);
920
921 let is_history = matches!(self.active_view, ActiveView::History);
922
923 let show_token_count = match &self.active_view {
924 ActiveView::Thread { .. } => !is_empty,
925 ActiveView::PromptEditor => self.context_editor.is_some(),
926 _ => false,
927 };
928
929 let go_back_button = match &self.active_view {
930 ActiveView::History | ActiveView::Configuration => Some(
931 div().pl_1().child(
932 IconButton::new("go-back", IconName::ArrowLeft)
933 .icon_size(IconSize::Small)
934 .on_click(cx.listener(|this, _, window, cx| {
935 this.go_back(&workspace::GoBack, window, cx);
936 }))
937 .tooltip({
938 let focus_handle = focus_handle.clone();
939 move |window, cx| {
940 Tooltip::for_action_in(
941 "Go Back",
942 &workspace::GoBack,
943 &focus_handle,
944 window,
945 cx,
946 )
947 }
948 }),
949 ),
950 ),
951 _ => None,
952 };
953
954 h_flex()
955 .id("assistant-toolbar")
956 .h(Tab::container_height(cx))
957 .max_w_full()
958 .flex_none()
959 .justify_between()
960 .gap_2()
961 .bg(cx.theme().colors().tab_bar_background)
962 .border_b_1()
963 .border_color(cx.theme().colors().border)
964 .child(
965 h_flex()
966 .w_full()
967 .gap_1()
968 .children(go_back_button)
969 .child(self.render_title_view(window, cx)),
970 )
971 .child(
972 h_flex()
973 .h_full()
974 .gap_2()
975 .when(show_token_count, |parent| match self.active_view {
976 ActiveView::Thread { .. } => {
977 if token_usage.total == 0 {
978 return parent;
979 }
980
981 let token_color = match token_usage.ratio {
982 TokenUsageRatio::Normal => Color::Muted,
983 TokenUsageRatio::Warning => Color::Warning,
984 TokenUsageRatio::Exceeded => Color::Error,
985 };
986
987 parent.child(
988 h_flex()
989 .flex_shrink_0()
990 .gap_0p5()
991 .child(
992 Label::new(assistant_context_editor::humanize_token_count(
993 token_usage.total,
994 ))
995 .size(LabelSize::Small)
996 .color(token_color)
997 .map(|label| {
998 if is_generating {
999 label
1000 .with_animation(
1001 "used-tokens-label",
1002 Animation::new(Duration::from_secs(2))
1003 .repeat()
1004 .with_easing(pulsating_between(
1005 0.6, 1.,
1006 )),
1007 |label, delta| label.alpha(delta),
1008 )
1009 .into_any()
1010 } else {
1011 label.into_any_element()
1012 }
1013 }),
1014 )
1015 .child(
1016 Label::new("/").size(LabelSize::Small).color(Color::Muted),
1017 )
1018 .child(
1019 Label::new(assistant_context_editor::humanize_token_count(
1020 token_usage.max,
1021 ))
1022 .size(LabelSize::Small)
1023 .color(Color::Muted),
1024 ),
1025 )
1026 }
1027 ActiveView::PromptEditor => {
1028 let Some(editor) = self.context_editor.as_ref() else {
1029 return parent;
1030 };
1031 let Some(element) = render_remaining_tokens(editor, cx) else {
1032 return parent;
1033 };
1034 parent.child(element)
1035 }
1036 _ => parent,
1037 })
1038 .child(
1039 h_flex()
1040 .h_full()
1041 .gap(DynamicSpacing::Base02.rems(cx))
1042 .px(DynamicSpacing::Base08.rems(cx))
1043 .border_l_1()
1044 .border_color(cx.theme().colors().border)
1045 .child(
1046 IconButton::new("new", IconName::Plus)
1047 .icon_size(IconSize::Small)
1048 .style(ButtonStyle::Subtle)
1049 .tooltip(move |window, cx| {
1050 Tooltip::for_action_in(
1051 "New Thread",
1052 &NewThread::default(),
1053 &focus_handle,
1054 window,
1055 cx,
1056 )
1057 })
1058 .on_click(move |_event, window, cx| {
1059 window.dispatch_action(
1060 NewThread::default().boxed_clone(),
1061 cx,
1062 );
1063 }),
1064 )
1065 .child(
1066 IconButton::new("open-history", IconName::HistoryRerun)
1067 .icon_size(IconSize::Small)
1068 .toggle_state(is_history)
1069 .selected_icon_color(Color::Accent)
1070 .tooltip({
1071 let focus_handle = self.focus_handle(cx);
1072 move |window, cx| {
1073 Tooltip::for_action_in(
1074 "History",
1075 &OpenHistory,
1076 &focus_handle,
1077 window,
1078 cx,
1079 )
1080 }
1081 })
1082 .on_click(move |_event, window, cx| {
1083 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1084 }),
1085 )
1086 .child(
1087 PopoverMenu::new("assistant-menu")
1088 .trigger_with_tooltip(
1089 IconButton::new("new", IconName::Ellipsis)
1090 .icon_size(IconSize::Small)
1091 .style(ButtonStyle::Subtle),
1092 Tooltip::text("Toggle Agent Menu"),
1093 )
1094 .anchor(Corner::TopRight)
1095 .with_handle(self.assistant_dropdown_menu_handle.clone())
1096 .menu(move |window, cx| {
1097 Some(ContextMenu::build(
1098 window,
1099 cx,
1100 |menu, _window, _cx| {
1101 menu
1102 .when(!is_empty, |menu| {
1103 menu.action(
1104 "Start New From Summary",
1105 Box::new(NewThread {
1106 from_thread_id: Some(thread_id.clone()),
1107 }),
1108 ).separator()
1109 })
1110 .action(
1111 "New Text Thread",
1112 NewTextThread.boxed_clone(),
1113 )
1114 .action("Settings", OpenConfiguration.boxed_clone())
1115 .separator()
1116 .action(
1117 "Install MCPs",
1118 zed_actions::Extensions {
1119 category_filter: Some(
1120 zed_actions::ExtensionCategoryFilter::ContextServers,
1121 ),
1122 }
1123 .boxed_clone(),
1124 )
1125 },
1126 ))
1127 }),
1128 ),
1129 ),
1130 )
1131 }
1132
1133 fn render_active_thread_or_empty_state(
1134 &self,
1135 window: &mut Window,
1136 cx: &mut Context<Self>,
1137 ) -> AnyElement {
1138 if self.thread.read(cx).is_empty() {
1139 return self
1140 .render_thread_empty_state(window, cx)
1141 .into_any_element();
1142 }
1143
1144 self.thread.clone().into_any_element()
1145 }
1146
1147 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1148 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1149 return Some(ConfigurationError::NoProvider);
1150 };
1151
1152 if !model.provider.is_authenticated(cx) {
1153 return Some(ConfigurationError::ProviderNotAuthenticated);
1154 }
1155
1156 if model.provider.must_accept_terms(cx) {
1157 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1158 model.provider,
1159 ));
1160 }
1161
1162 None
1163 }
1164
1165 fn render_thread_empty_state(
1166 &self,
1167 window: &mut Window,
1168 cx: &mut Context<Self>,
1169 ) -> impl IntoElement {
1170 let recent_history = self
1171 .history_store
1172 .update(cx, |this, cx| this.recent_entries(6, cx));
1173
1174 let configuration_error = self.configuration_error(cx);
1175 let no_error = configuration_error.is_none();
1176 let focus_handle = self.focus_handle(cx);
1177
1178 v_flex()
1179 .size_full()
1180 .when(recent_history.is_empty(), |this| {
1181 let configuration_error_ref = &configuration_error;
1182 this.child(
1183 v_flex()
1184 .size_full()
1185 .max_w_80()
1186 .mx_auto()
1187 .justify_center()
1188 .items_center()
1189 .gap_1()
1190 .child(
1191 h_flex().child(
1192 Headline::new("Welcome to the Agent Panel")
1193 ),
1194 )
1195 .when(no_error, |parent| {
1196 parent
1197 .child(
1198 h_flex().child(
1199 Label::new("Ask and build anything.")
1200 .color(Color::Muted)
1201 .mb_2p5(),
1202 ),
1203 )
1204 .child(
1205 Button::new("new-thread", "Start New Thread")
1206 .icon(IconName::Plus)
1207 .icon_position(IconPosition::Start)
1208 .icon_size(IconSize::Small)
1209 .icon_color(Color::Muted)
1210 .full_width()
1211 .key_binding(KeyBinding::for_action_in(
1212 &NewThread::default(),
1213 &focus_handle,
1214 window,
1215 cx,
1216 ))
1217 .on_click(|_event, window, cx| {
1218 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1219 }),
1220 )
1221 .child(
1222 Button::new("context", "Add Context")
1223 .icon(IconName::FileCode)
1224 .icon_position(IconPosition::Start)
1225 .icon_size(IconSize::Small)
1226 .icon_color(Color::Muted)
1227 .full_width()
1228 .key_binding(KeyBinding::for_action_in(
1229 &ToggleContextPicker,
1230 &focus_handle,
1231 window,
1232 cx,
1233 ))
1234 .on_click(|_event, window, cx| {
1235 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1236 }),
1237 )
1238 .child(
1239 Button::new("mode", "Switch Model")
1240 .icon(IconName::DatabaseZap)
1241 .icon_position(IconPosition::Start)
1242 .icon_size(IconSize::Small)
1243 .icon_color(Color::Muted)
1244 .full_width()
1245 .key_binding(KeyBinding::for_action_in(
1246 &ToggleModelSelector,
1247 &focus_handle,
1248 window,
1249 cx,
1250 ))
1251 .on_click(|_event, window, cx| {
1252 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1253 }),
1254 )
1255 .child(
1256 Button::new("settings", "View Settings")
1257 .icon(IconName::Settings)
1258 .icon_position(IconPosition::Start)
1259 .icon_size(IconSize::Small)
1260 .icon_color(Color::Muted)
1261 .full_width()
1262 .key_binding(KeyBinding::for_action_in(
1263 &OpenConfiguration,
1264 &focus_handle,
1265 window,
1266 cx,
1267 ))
1268 .on_click(|_event, window, cx| {
1269 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1270 }),
1271 )
1272 })
1273 .map(|parent| {
1274 match configuration_error_ref {
1275 Some(ConfigurationError::ProviderNotAuthenticated)
1276 | Some(ConfigurationError::NoProvider) => {
1277 parent
1278 .child(
1279 h_flex().child(
1280 Label::new("To start using the agent, configure at least one LLM provider.")
1281 .color(Color::Muted)
1282 .mb_2p5()
1283 )
1284 )
1285 .child(
1286 Button::new("settings", "Configure a Provider")
1287 .icon(IconName::Settings)
1288 .icon_position(IconPosition::Start)
1289 .icon_size(IconSize::Small)
1290 .icon_color(Color::Muted)
1291 .full_width()
1292 .key_binding(KeyBinding::for_action_in(
1293 &OpenConfiguration,
1294 &focus_handle,
1295 window,
1296 cx,
1297 ))
1298 .on_click(|_event, window, cx| {
1299 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1300 }),
1301 )
1302 }
1303 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1304 parent.children(
1305 provider.render_accept_terms(
1306 LanguageModelProviderTosView::ThreadFreshStart,
1307 cx,
1308 ),
1309 )
1310 }
1311 None => parent,
1312 }
1313 })
1314 )
1315 })
1316 .when(!recent_history.is_empty(), |parent| {
1317 let focus_handle = focus_handle.clone();
1318 let configuration_error_ref = &configuration_error;
1319
1320 parent
1321 .overflow_hidden()
1322 .p_1p5()
1323 .justify_end()
1324 .gap_1()
1325 .child(
1326 h_flex()
1327 .pl_1p5()
1328 .pb_1()
1329 .w_full()
1330 .justify_between()
1331 .border_b_1()
1332 .border_color(cx.theme().colors().border_variant)
1333 .child(
1334 Label::new("Past Interactions")
1335 .size(LabelSize::Small)
1336 .color(Color::Muted),
1337 )
1338 .child(
1339 Button::new("view-history", "View All")
1340 .style(ButtonStyle::Subtle)
1341 .label_size(LabelSize::Small)
1342 .key_binding(
1343 KeyBinding::for_action_in(
1344 &OpenHistory,
1345 &self.focus_handle(cx),
1346 window,
1347 cx,
1348 ).map(|kb| kb.size(rems_from_px(12.))),
1349 )
1350 .on_click(move |_event, window, cx| {
1351 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1352 }),
1353 ),
1354 )
1355 .child(
1356 v_flex()
1357 .gap_1()
1358 .children(
1359 recent_history.into_iter().map(|entry| {
1360 // TODO: Add keyboard navigation.
1361 match entry {
1362 HistoryEntry::Thread(thread) => {
1363 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1364 .into_any_element()
1365 }
1366 HistoryEntry::Context(context) => {
1367 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1368 .into_any_element()
1369 }
1370 }
1371 }),
1372 )
1373 )
1374 .map(|parent| {
1375 match configuration_error_ref {
1376 Some(ConfigurationError::ProviderNotAuthenticated)
1377 | Some(ConfigurationError::NoProvider) => {
1378 parent
1379 .child(
1380 Banner::new()
1381 .severity(ui::Severity::Warning)
1382 .children(
1383 Label::new(
1384 "Configure at least one LLM provider to start using the panel.",
1385 )
1386 .size(LabelSize::Small),
1387 )
1388 .action_slot(
1389 Button::new("settings", "Configure Provider")
1390 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1391 .label_size(LabelSize::Small)
1392 .key_binding(
1393 KeyBinding::for_action_in(
1394 &OpenConfiguration,
1395 &focus_handle,
1396 window,
1397 cx,
1398 )
1399 .map(|kb| kb.size(rems_from_px(12.))),
1400 )
1401 .on_click(|_event, window, cx| {
1402 window.dispatch_action(
1403 OpenConfiguration.boxed_clone(),
1404 cx,
1405 )
1406 }),
1407 ),
1408 )
1409 }
1410 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1411 parent
1412 .child(
1413 Banner::new()
1414 .severity(ui::Severity::Warning)
1415 .children(
1416 h_flex()
1417 .w_full()
1418 .children(
1419 provider.render_accept_terms(
1420 LanguageModelProviderTosView::ThreadtEmptyState,
1421 cx,
1422 ),
1423 ),
1424 ),
1425 )
1426 }
1427 None => parent,
1428 }
1429 })
1430 })
1431 }
1432
1433 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1434 let last_error = self.thread.read(cx).last_error()?;
1435
1436 Some(
1437 div()
1438 .absolute()
1439 .right_3()
1440 .bottom_12()
1441 .max_w_96()
1442 .py_2()
1443 .px_3()
1444 .elevation_2(cx)
1445 .occlude()
1446 .child(match last_error {
1447 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1448 ThreadError::MaxMonthlySpendReached => {
1449 self.render_max_monthly_spend_reached_error(cx)
1450 }
1451 ThreadError::Message { header, message } => {
1452 self.render_error_message(header, message, cx)
1453 }
1454 })
1455 .into_any(),
1456 )
1457 }
1458
1459 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1460 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.";
1461
1462 v_flex()
1463 .gap_0p5()
1464 .child(
1465 h_flex()
1466 .gap_1p5()
1467 .items_center()
1468 .child(Icon::new(IconName::XCircle).color(Color::Error))
1469 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1470 )
1471 .child(
1472 div()
1473 .id("error-message")
1474 .max_h_24()
1475 .overflow_y_scroll()
1476 .child(Label::new(ERROR_MESSAGE)),
1477 )
1478 .child(
1479 h_flex()
1480 .justify_end()
1481 .mt_1()
1482 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1483 |this, _, _, cx| {
1484 this.thread.update(cx, |this, _cx| {
1485 this.clear_last_error();
1486 });
1487
1488 cx.open_url(&zed_urls::account_url(cx));
1489 cx.notify();
1490 },
1491 )))
1492 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1493 |this, _, _, cx| {
1494 this.thread.update(cx, |this, _cx| {
1495 this.clear_last_error();
1496 });
1497
1498 cx.notify();
1499 },
1500 ))),
1501 )
1502 .into_any()
1503 }
1504
1505 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1506 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1507
1508 v_flex()
1509 .gap_0p5()
1510 .child(
1511 h_flex()
1512 .gap_1p5()
1513 .items_center()
1514 .child(Icon::new(IconName::XCircle).color(Color::Error))
1515 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1516 )
1517 .child(
1518 div()
1519 .id("error-message")
1520 .max_h_24()
1521 .overflow_y_scroll()
1522 .child(Label::new(ERROR_MESSAGE)),
1523 )
1524 .child(
1525 h_flex()
1526 .justify_end()
1527 .mt_1()
1528 .child(
1529 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1530 cx.listener(|this, _, _, cx| {
1531 this.thread.update(cx, |this, _cx| {
1532 this.clear_last_error();
1533 });
1534
1535 cx.open_url(&zed_urls::account_url(cx));
1536 cx.notify();
1537 }),
1538 ),
1539 )
1540 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1541 |this, _, _, cx| {
1542 this.thread.update(cx, |this, _cx| {
1543 this.clear_last_error();
1544 });
1545
1546 cx.notify();
1547 },
1548 ))),
1549 )
1550 .into_any()
1551 }
1552
1553 fn render_error_message(
1554 &self,
1555 header: SharedString,
1556 message: SharedString,
1557 cx: &mut Context<Self>,
1558 ) -> AnyElement {
1559 v_flex()
1560 .gap_0p5()
1561 .child(
1562 h_flex()
1563 .gap_1p5()
1564 .items_center()
1565 .child(Icon::new(IconName::XCircle).color(Color::Error))
1566 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1567 )
1568 .child(
1569 div()
1570 .id("error-message")
1571 .max_h_32()
1572 .overflow_y_scroll()
1573 .child(Label::new(message)),
1574 )
1575 .child(
1576 h_flex()
1577 .justify_end()
1578 .mt_1()
1579 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1580 |this, _, _, cx| {
1581 this.thread.update(cx, |this, _cx| {
1582 this.clear_last_error();
1583 });
1584
1585 cx.notify();
1586 },
1587 ))),
1588 )
1589 .into_any()
1590 }
1591
1592 fn key_context(&self) -> KeyContext {
1593 let mut key_context = KeyContext::new_with_defaults();
1594 key_context.add("AgentPanel");
1595 if matches!(self.active_view, ActiveView::PromptEditor) {
1596 key_context.add("prompt_editor");
1597 }
1598 key_context
1599 }
1600}
1601
1602impl Render for AssistantPanel {
1603 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1604 v_flex()
1605 .key_context(self.key_context())
1606 .justify_between()
1607 .size_full()
1608 .on_action(cx.listener(Self::cancel))
1609 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1610 this.new_thread(action, window, cx);
1611 }))
1612 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1613 this.open_history(window, cx);
1614 }))
1615 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1616 this.open_configuration(window, cx);
1617 }))
1618 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1619 .on_action(cx.listener(Self::deploy_prompt_library))
1620 .on_action(cx.listener(Self::open_agent_diff))
1621 .on_action(cx.listener(Self::go_back))
1622 .child(self.render_toolbar(window, cx))
1623 .map(|parent| match self.active_view {
1624 ActiveView::Thread { .. } => parent
1625 .child(self.render_active_thread_or_empty_state(window, cx))
1626 .child(h_flex().child(self.message_editor.clone()))
1627 .children(self.render_last_error(cx)),
1628 ActiveView::History => parent.child(self.history.clone()),
1629 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1630 ActiveView::Configuration => parent.children(self.configuration.clone()),
1631 })
1632 }
1633}
1634
1635struct PromptLibraryInlineAssist {
1636 workspace: WeakEntity<Workspace>,
1637}
1638
1639impl PromptLibraryInlineAssist {
1640 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1641 Self { workspace }
1642 }
1643}
1644
1645impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1646 fn assist(
1647 &self,
1648 prompt_editor: &Entity<Editor>,
1649 _initial_prompt: Option<String>,
1650 window: &mut Window,
1651 cx: &mut Context<PromptLibrary>,
1652 ) {
1653 InlineAssistant::update_global(cx, |assistant, cx| {
1654 let Some(project) = self
1655 .workspace
1656 .upgrade()
1657 .map(|workspace| workspace.read(cx).project().downgrade())
1658 else {
1659 return;
1660 };
1661 assistant.assist(
1662 &prompt_editor,
1663 self.workspace.clone(),
1664 project,
1665 None,
1666 window,
1667 cx,
1668 )
1669 })
1670 }
1671
1672 fn focus_assistant_panel(
1673 &self,
1674 workspace: &mut Workspace,
1675 window: &mut Window,
1676 cx: &mut Context<Workspace>,
1677 ) -> bool {
1678 workspace
1679 .focus_panel::<AssistantPanel>(window, cx)
1680 .is_some()
1681 }
1682}
1683
1684pub struct ConcreteAssistantPanelDelegate;
1685
1686impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1687 fn active_context_editor(
1688 &self,
1689 workspace: &mut Workspace,
1690 _window: &mut Window,
1691 cx: &mut Context<Workspace>,
1692 ) -> Option<Entity<ContextEditor>> {
1693 let panel = workspace.panel::<AssistantPanel>(cx)?;
1694 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1695 }
1696
1697 fn open_saved_context(
1698 &self,
1699 workspace: &mut Workspace,
1700 path: std::path::PathBuf,
1701 window: &mut Window,
1702 cx: &mut Context<Workspace>,
1703 ) -> Task<Result<()>> {
1704 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1705 return Task::ready(Err(anyhow!("Agent panel not found")));
1706 };
1707
1708 panel.update(cx, |panel, cx| {
1709 panel.open_saved_prompt_editor(path, window, cx)
1710 })
1711 }
1712
1713 fn open_remote_context(
1714 &self,
1715 _workspace: &mut Workspace,
1716 _context_id: assistant_context_editor::ContextId,
1717 _window: &mut Window,
1718 _cx: &mut Context<Workspace>,
1719 ) -> Task<Result<Entity<ContextEditor>>> {
1720 Task::ready(Err(anyhow!("opening remote context not implemented")))
1721 }
1722
1723 fn quote_selection(
1724 &self,
1725 _workspace: &mut Workspace,
1726 _creases: Vec<(String, String)>,
1727 _window: &mut Window,
1728 _cx: &mut Context<Workspace>,
1729 ) {
1730 }
1731}