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 = cx.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 self.message_editor.focus_handle(cx).focus(window);
573 cx.notify();
574 }
575 _ => {}
576 }
577 }
578
579 pub fn open_agent_diff(
580 &mut self,
581 _: &OpenAgentDiff,
582 window: &mut Window,
583 cx: &mut Context<Self>,
584 ) {
585 let thread = self.thread.read(cx).thread().clone();
586 self.workspace
587 .update(cx, |workspace, cx| {
588 AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
589 })
590 .log_err();
591 }
592
593 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
594 let context_server_manager = self.thread_store.read(cx).context_server_manager();
595 let tools = self.thread_store.read(cx).tools();
596 let fs = self.fs.clone();
597
598 self.set_active_view(ActiveView::Configuration, window, cx);
599 self.configuration =
600 Some(cx.new(|cx| {
601 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
602 }));
603
604 if let Some(configuration) = self.configuration.as_ref() {
605 self.configuration_subscription = Some(cx.subscribe_in(
606 configuration,
607 window,
608 Self::handle_assistant_configuration_event,
609 ));
610
611 configuration.focus_handle(cx).focus(window);
612 }
613 }
614
615 pub(crate) fn open_active_thread_as_markdown(
616 &mut self,
617 _: &OpenActiveThreadAsMarkdown,
618 window: &mut Window,
619 cx: &mut Context<Self>,
620 ) {
621 let Some(workspace) = self
622 .workspace
623 .upgrade()
624 .ok_or_else(|| anyhow!("workspace dropped"))
625 .log_err()
626 else {
627 return;
628 };
629
630 let markdown_language_task = workspace
631 .read(cx)
632 .app_state()
633 .languages
634 .language_for_name("Markdown");
635 let thread = self.active_thread(cx);
636 cx.spawn_in(window, async move |_this, cx| {
637 let markdown_language = markdown_language_task.await?;
638
639 workspace.update_in(cx, |workspace, window, cx| {
640 let thread = thread.read(cx);
641 let markdown = thread.to_markdown(cx)?;
642 let thread_summary = thread
643 .summary()
644 .map(|summary| summary.to_string())
645 .unwrap_or_else(|| "Thread".to_string());
646
647 let project = workspace.project().clone();
648 let buffer = project.update(cx, |project, cx| {
649 project.create_local_buffer(&markdown, Some(markdown_language), cx)
650 });
651 let buffer = cx.new(|cx| {
652 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
653 });
654
655 workspace.add_item_to_active_pane(
656 Box::new(cx.new(|cx| {
657 let mut editor =
658 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
659 editor.set_breadcrumb_header(thread_summary);
660 editor
661 })),
662 None,
663 true,
664 window,
665 cx,
666 );
667
668 anyhow::Ok(())
669 })
670 })
671 .detach_and_log_err(cx);
672 }
673
674 fn handle_assistant_configuration_event(
675 &mut self,
676 _entity: &Entity<AssistantConfiguration>,
677 event: &AssistantConfigurationEvent,
678 window: &mut Window,
679 cx: &mut Context<Self>,
680 ) {
681 match event {
682 AssistantConfigurationEvent::NewThread(provider) => {
683 if LanguageModelRegistry::read_global(cx)
684 .default_model()
685 .map_or(true, |model| model.provider.id() != provider.id())
686 {
687 if let Some(model) = provider.default_model(cx) {
688 update_settings_file::<AssistantSettings>(
689 self.fs.clone(),
690 cx,
691 move |settings, _| settings.set_model(model),
692 );
693 }
694 }
695
696 self.new_thread(&NewThread::default(), window, cx);
697 }
698 }
699 }
700
701 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
702 self.thread.read(cx).thread().clone()
703 }
704
705 pub(crate) fn delete_thread(
706 &mut self,
707 thread_id: &ThreadId,
708 cx: &mut Context<Self>,
709 ) -> Task<Result<()>> {
710 self.thread_store
711 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
712 }
713
714 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
715 self.context_editor.clone()
716 }
717
718 pub(crate) fn delete_context(
719 &mut self,
720 path: PathBuf,
721 cx: &mut Context<Self>,
722 ) -> Task<Result<()>> {
723 self.context_store
724 .update(cx, |this, cx| this.delete_local_context(path, cx))
725 }
726
727 fn set_active_view(
728 &mut self,
729 new_view: ActiveView,
730 window: &mut Window,
731 cx: &mut Context<Self>,
732 ) {
733 let current_is_history = matches!(self.active_view, ActiveView::History);
734 let new_is_history = matches!(new_view, ActiveView::History);
735
736 if current_is_history && !new_is_history {
737 self.active_view = new_view;
738 } else if !current_is_history && new_is_history {
739 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
740 } else {
741 if !new_is_history {
742 self.previous_view = None;
743 }
744 self.active_view = new_view;
745 }
746
747 self.focus_handle(cx).focus(window);
748 }
749}
750
751impl Focusable for AssistantPanel {
752 fn focus_handle(&self, cx: &App) -> FocusHandle {
753 match self.active_view {
754 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
755 ActiveView::History => self.history.focus_handle(cx),
756 ActiveView::PromptEditor => {
757 if let Some(context_editor) = self.context_editor.as_ref() {
758 context_editor.focus_handle(cx)
759 } else {
760 cx.focus_handle()
761 }
762 }
763 ActiveView::Configuration => {
764 if let Some(configuration) = self.configuration.as_ref() {
765 configuration.focus_handle(cx)
766 } else {
767 cx.focus_handle()
768 }
769 }
770 }
771 }
772}
773
774impl EventEmitter<PanelEvent> for AssistantPanel {}
775
776impl Panel for AssistantPanel {
777 fn persistent_name() -> &'static str {
778 "AgentPanel"
779 }
780
781 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
782 match AssistantSettings::get_global(cx).dock {
783 AssistantDockPosition::Left => DockPosition::Left,
784 AssistantDockPosition::Bottom => DockPosition::Bottom,
785 AssistantDockPosition::Right => DockPosition::Right,
786 }
787 }
788
789 fn position_is_valid(&self, _: DockPosition) -> bool {
790 true
791 }
792
793 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
794 settings::update_settings_file::<AssistantSettings>(
795 self.fs.clone(),
796 cx,
797 move |settings, _| {
798 let dock = match position {
799 DockPosition::Left => AssistantDockPosition::Left,
800 DockPosition::Bottom => AssistantDockPosition::Bottom,
801 DockPosition::Right => AssistantDockPosition::Right,
802 };
803 settings.set_dock(dock);
804 },
805 );
806 }
807
808 fn size(&self, window: &Window, cx: &App) -> Pixels {
809 let settings = AssistantSettings::get_global(cx);
810 match self.position(window, cx) {
811 DockPosition::Left | DockPosition::Right => {
812 self.width.unwrap_or(settings.default_width)
813 }
814 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
815 }
816 }
817
818 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
819 match self.position(window, cx) {
820 DockPosition::Left | DockPosition::Right => self.width = size,
821 DockPosition::Bottom => self.height = size,
822 }
823 cx.notify();
824 }
825
826 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
827
828 fn remote_id() -> Option<proto::PanelId> {
829 Some(proto::PanelId::AssistantPanel)
830 }
831
832 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
833 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
834 .then_some(IconName::ZedAssistant)
835 }
836
837 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
838 Some("Agent Panel")
839 }
840
841 fn toggle_action(&self) -> Box<dyn Action> {
842 Box::new(ToggleFocus)
843 }
844
845 fn activation_priority(&self) -> u32 {
846 3
847 }
848
849 fn enabled(&self, cx: &App) -> bool {
850 AssistantSettings::get_global(cx).enabled
851 }
852}
853
854impl AssistantPanel {
855 fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
856 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
857
858 let content = match &self.active_view {
859 ActiveView::Thread {
860 change_title_editor,
861 ..
862 } => {
863 let active_thread = self.thread.read(cx);
864 let is_empty = active_thread.is_empty();
865
866 let summary = active_thread.summary(cx);
867
868 if is_empty {
869 Label::new(Thread::DEFAULT_SUMMARY.clone())
870 .truncate()
871 .ml_2()
872 .into_any_element()
873 } else if summary.is_none() {
874 Label::new(LOADING_SUMMARY_PLACEHOLDER)
875 .ml_2()
876 .truncate()
877 .into_any_element()
878 } else {
879 div()
880 .ml_2()
881 .w_full()
882 .child(change_title_editor.clone())
883 .into_any_element()
884 }
885 }
886 ActiveView::PromptEditor => {
887 let title = self
888 .context_editor
889 .as_ref()
890 .map(|context_editor| {
891 SharedString::from(context_editor.read(cx).title(cx).to_string())
892 })
893 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
894
895 Label::new(title).ml_2().truncate().into_any_element()
896 }
897 ActiveView::History => Label::new("History").truncate().into_any_element(),
898 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
899 };
900
901 h_flex()
902 .key_context("TitleEditor")
903 .id("TitleEditor")
904 .flex_grow()
905 .w_full()
906 .max_w_full()
907 .overflow_x_scroll()
908 .child(content)
909 .into_any()
910 }
911
912 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
913 let active_thread = self.thread.read(cx);
914 let thread = active_thread.thread().read(cx);
915 let token_usage = thread.total_token_usage(cx);
916 let thread_id = thread.id().clone();
917
918 let is_generating = thread.is_generating();
919 let is_empty = active_thread.is_empty();
920 let focus_handle = self.focus_handle(cx);
921
922 let is_history = matches!(self.active_view, ActiveView::History);
923
924 let show_token_count = match &self.active_view {
925 ActiveView::Thread { .. } => !is_empty,
926 ActiveView::PromptEditor => self.context_editor.is_some(),
927 _ => false,
928 };
929
930 let go_back_button = match &self.active_view {
931 ActiveView::History | ActiveView::Configuration => Some(
932 div().pl_1().child(
933 IconButton::new("go-back", IconName::ArrowLeft)
934 .icon_size(IconSize::Small)
935 .on_click(cx.listener(|this, _, window, cx| {
936 this.go_back(&workspace::GoBack, window, cx);
937 }))
938 .tooltip({
939 let focus_handle = focus_handle.clone();
940 move |window, cx| {
941 Tooltip::for_action_in(
942 "Go Back",
943 &workspace::GoBack,
944 &focus_handle,
945 window,
946 cx,
947 )
948 }
949 }),
950 ),
951 ),
952 _ => None,
953 };
954
955 h_flex()
956 .id("assistant-toolbar")
957 .h(Tab::container_height(cx))
958 .max_w_full()
959 .flex_none()
960 .justify_between()
961 .gap_2()
962 .bg(cx.theme().colors().tab_bar_background)
963 .border_b_1()
964 .border_color(cx.theme().colors().border)
965 .child(
966 h_flex()
967 .w_full()
968 .gap_1()
969 .children(go_back_button)
970 .child(self.render_title_view(window, cx)),
971 )
972 .child(
973 h_flex()
974 .h_full()
975 .gap_2()
976 .when(show_token_count, |parent| match self.active_view {
977 ActiveView::Thread { .. } => {
978 if token_usage.total == 0 {
979 return parent;
980 }
981
982 let token_color = match token_usage.ratio {
983 TokenUsageRatio::Normal => Color::Muted,
984 TokenUsageRatio::Warning => Color::Warning,
985 TokenUsageRatio::Exceeded => Color::Error,
986 };
987
988 parent.child(
989 h_flex()
990 .flex_shrink_0()
991 .gap_0p5()
992 .child(
993 Label::new(assistant_context_editor::humanize_token_count(
994 token_usage.total,
995 ))
996 .size(LabelSize::Small)
997 .color(token_color)
998 .map(|label| {
999 if is_generating {
1000 label
1001 .with_animation(
1002 "used-tokens-label",
1003 Animation::new(Duration::from_secs(2))
1004 .repeat()
1005 .with_easing(pulsating_between(
1006 0.6, 1.,
1007 )),
1008 |label, delta| label.alpha(delta),
1009 )
1010 .into_any()
1011 } else {
1012 label.into_any_element()
1013 }
1014 }),
1015 )
1016 .child(
1017 Label::new("/").size(LabelSize::Small).color(Color::Muted),
1018 )
1019 .child(
1020 Label::new(assistant_context_editor::humanize_token_count(
1021 token_usage.max,
1022 ))
1023 .size(LabelSize::Small)
1024 .color(Color::Muted),
1025 ),
1026 )
1027 }
1028 ActiveView::PromptEditor => {
1029 let Some(editor) = self.context_editor.as_ref() else {
1030 return parent;
1031 };
1032 let Some(element) = render_remaining_tokens(editor, cx) else {
1033 return parent;
1034 };
1035 parent.child(element)
1036 }
1037 _ => parent,
1038 })
1039 .child(
1040 h_flex()
1041 .h_full()
1042 .gap(DynamicSpacing::Base02.rems(cx))
1043 .px(DynamicSpacing::Base08.rems(cx))
1044 .border_l_1()
1045 .border_color(cx.theme().colors().border)
1046 .child(
1047 IconButton::new("new", IconName::Plus)
1048 .icon_size(IconSize::Small)
1049 .style(ButtonStyle::Subtle)
1050 .tooltip(move |window, cx| {
1051 Tooltip::for_action_in(
1052 "New Thread",
1053 &NewThread::default(),
1054 &focus_handle,
1055 window,
1056 cx,
1057 )
1058 })
1059 .on_click(move |_event, window, cx| {
1060 window.dispatch_action(
1061 NewThread::default().boxed_clone(),
1062 cx,
1063 );
1064 }),
1065 )
1066 .child(
1067 IconButton::new("open-history", IconName::HistoryRerun)
1068 .icon_size(IconSize::Small)
1069 .toggle_state(is_history)
1070 .selected_icon_color(Color::Accent)
1071 .tooltip({
1072 let focus_handle = self.focus_handle(cx);
1073 move |window, cx| {
1074 Tooltip::for_action_in(
1075 "History",
1076 &OpenHistory,
1077 &focus_handle,
1078 window,
1079 cx,
1080 )
1081 }
1082 })
1083 .on_click(move |_event, window, cx| {
1084 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1085 }),
1086 )
1087 .child(
1088 PopoverMenu::new("assistant-menu")
1089 .trigger_with_tooltip(
1090 IconButton::new("new", IconName::Ellipsis)
1091 .icon_size(IconSize::Small)
1092 .style(ButtonStyle::Subtle),
1093 Tooltip::text("Toggle Agent Menu"),
1094 )
1095 .anchor(Corner::TopRight)
1096 .with_handle(self.assistant_dropdown_menu_handle.clone())
1097 .menu(move |window, cx| {
1098 Some(ContextMenu::build(
1099 window,
1100 cx,
1101 |menu, _window, _cx| {
1102 menu
1103 .when(!is_empty, |menu| {
1104 menu.action(
1105 "Start New From Summary",
1106 Box::new(NewThread {
1107 from_thread_id: Some(thread_id.clone()),
1108 }),
1109 ).separator()
1110 })
1111 .action(
1112 "New Text Thread",
1113 NewTextThread.boxed_clone(),
1114 )
1115 .action("Settings", OpenConfiguration.boxed_clone())
1116 .separator()
1117 .action(
1118 "Install MCPs",
1119 zed_actions::Extensions {
1120 category_filter: Some(
1121 zed_actions::ExtensionCategoryFilter::ContextServers,
1122 ),
1123 }
1124 .boxed_clone(),
1125 )
1126 },
1127 ))
1128 }),
1129 ),
1130 ),
1131 )
1132 }
1133
1134 fn render_active_thread_or_empty_state(
1135 &self,
1136 window: &mut Window,
1137 cx: &mut Context<Self>,
1138 ) -> AnyElement {
1139 if self.thread.read(cx).is_empty() {
1140 return self
1141 .render_thread_empty_state(window, cx)
1142 .into_any_element();
1143 }
1144
1145 self.thread.clone().into_any_element()
1146 }
1147
1148 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1149 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1150 return Some(ConfigurationError::NoProvider);
1151 };
1152
1153 if !model.provider.is_authenticated(cx) {
1154 return Some(ConfigurationError::ProviderNotAuthenticated);
1155 }
1156
1157 if model.provider.must_accept_terms(cx) {
1158 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1159 model.provider,
1160 ));
1161 }
1162
1163 None
1164 }
1165
1166 fn render_thread_empty_state(
1167 &self,
1168 window: &mut Window,
1169 cx: &mut Context<Self>,
1170 ) -> impl IntoElement {
1171 let recent_history = self
1172 .history_store
1173 .update(cx, |this, cx| this.recent_entries(6, cx));
1174
1175 let configuration_error = self.configuration_error(cx);
1176 let no_error = configuration_error.is_none();
1177 let focus_handle = self.focus_handle(cx);
1178
1179 v_flex()
1180 .size_full()
1181 .when(recent_history.is_empty(), |this| {
1182 let configuration_error_ref = &configuration_error;
1183 this.child(
1184 v_flex()
1185 .size_full()
1186 .max_w_80()
1187 .mx_auto()
1188 .justify_center()
1189 .items_center()
1190 .gap_1()
1191 .child(
1192 h_flex().child(
1193 Headline::new("Welcome to the Agent Panel")
1194 ),
1195 )
1196 .when(no_error, |parent| {
1197 parent
1198 .child(
1199 h_flex().child(
1200 Label::new("Ask and build anything.")
1201 .color(Color::Muted)
1202 .mb_2p5(),
1203 ),
1204 )
1205 .child(
1206 Button::new("new-thread", "Start New Thread")
1207 .icon(IconName::Plus)
1208 .icon_position(IconPosition::Start)
1209 .icon_size(IconSize::Small)
1210 .icon_color(Color::Muted)
1211 .full_width()
1212 .key_binding(KeyBinding::for_action_in(
1213 &NewThread::default(),
1214 &focus_handle,
1215 window,
1216 cx,
1217 ))
1218 .on_click(|_event, window, cx| {
1219 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1220 }),
1221 )
1222 .child(
1223 Button::new("context", "Add Context")
1224 .icon(IconName::FileCode)
1225 .icon_position(IconPosition::Start)
1226 .icon_size(IconSize::Small)
1227 .icon_color(Color::Muted)
1228 .full_width()
1229 .key_binding(KeyBinding::for_action_in(
1230 &ToggleContextPicker,
1231 &focus_handle,
1232 window,
1233 cx,
1234 ))
1235 .on_click(|_event, window, cx| {
1236 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1237 }),
1238 )
1239 .child(
1240 Button::new("mode", "Switch Model")
1241 .icon(IconName::DatabaseZap)
1242 .icon_position(IconPosition::Start)
1243 .icon_size(IconSize::Small)
1244 .icon_color(Color::Muted)
1245 .full_width()
1246 .key_binding(KeyBinding::for_action_in(
1247 &ToggleModelSelector,
1248 &focus_handle,
1249 window,
1250 cx,
1251 ))
1252 .on_click(|_event, window, cx| {
1253 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1254 }),
1255 )
1256 .child(
1257 Button::new("settings", "View Settings")
1258 .icon(IconName::Settings)
1259 .icon_position(IconPosition::Start)
1260 .icon_size(IconSize::Small)
1261 .icon_color(Color::Muted)
1262 .full_width()
1263 .key_binding(KeyBinding::for_action_in(
1264 &OpenConfiguration,
1265 &focus_handle,
1266 window,
1267 cx,
1268 ))
1269 .on_click(|_event, window, cx| {
1270 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1271 }),
1272 )
1273 })
1274 .map(|parent| {
1275 match configuration_error_ref {
1276 Some(ConfigurationError::ProviderNotAuthenticated)
1277 | Some(ConfigurationError::NoProvider) => {
1278 parent
1279 .child(
1280 h_flex().child(
1281 Label::new("To start using the agent, configure at least one LLM provider.")
1282 .color(Color::Muted)
1283 .mb_2p5()
1284 )
1285 )
1286 .child(
1287 Button::new("settings", "Configure a Provider")
1288 .icon(IconName::Settings)
1289 .icon_position(IconPosition::Start)
1290 .icon_size(IconSize::Small)
1291 .icon_color(Color::Muted)
1292 .full_width()
1293 .key_binding(KeyBinding::for_action_in(
1294 &OpenConfiguration,
1295 &focus_handle,
1296 window,
1297 cx,
1298 ))
1299 .on_click(|_event, window, cx| {
1300 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1301 }),
1302 )
1303 }
1304 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1305 parent.children(
1306 provider.render_accept_terms(
1307 LanguageModelProviderTosView::ThreadFreshStart,
1308 cx,
1309 ),
1310 )
1311 }
1312 None => parent,
1313 }
1314 })
1315 )
1316 })
1317 .when(!recent_history.is_empty(), |parent| {
1318 let focus_handle = focus_handle.clone();
1319 let configuration_error_ref = &configuration_error;
1320
1321 parent
1322 .overflow_hidden()
1323 .p_1p5()
1324 .justify_end()
1325 .gap_1()
1326 .child(
1327 h_flex()
1328 .pl_1p5()
1329 .pb_1()
1330 .w_full()
1331 .justify_between()
1332 .border_b_1()
1333 .border_color(cx.theme().colors().border_variant)
1334 .child(
1335 Label::new("Past Interactions")
1336 .size(LabelSize::Small)
1337 .color(Color::Muted),
1338 )
1339 .child(
1340 Button::new("view-history", "View All")
1341 .style(ButtonStyle::Subtle)
1342 .label_size(LabelSize::Small)
1343 .key_binding(
1344 KeyBinding::for_action_in(
1345 &OpenHistory,
1346 &self.focus_handle(cx),
1347 window,
1348 cx,
1349 ).map(|kb| kb.size(rems_from_px(12.))),
1350 )
1351 .on_click(move |_event, window, cx| {
1352 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1353 }),
1354 ),
1355 )
1356 .child(
1357 v_flex()
1358 .gap_1()
1359 .children(
1360 recent_history.into_iter().map(|entry| {
1361 // TODO: Add keyboard navigation.
1362 match entry {
1363 HistoryEntry::Thread(thread) => {
1364 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1365 .into_any_element()
1366 }
1367 HistoryEntry::Context(context) => {
1368 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1369 .into_any_element()
1370 }
1371 }
1372 }),
1373 )
1374 )
1375 .map(|parent| {
1376 match configuration_error_ref {
1377 Some(ConfigurationError::ProviderNotAuthenticated)
1378 | Some(ConfigurationError::NoProvider) => {
1379 parent
1380 .child(
1381 Banner::new()
1382 .severity(ui::Severity::Warning)
1383 .children(
1384 Label::new(
1385 "Configure at least one LLM provider to start using the panel.",
1386 )
1387 .size(LabelSize::Small),
1388 )
1389 .action_slot(
1390 Button::new("settings", "Configure Provider")
1391 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1392 .label_size(LabelSize::Small)
1393 .key_binding(
1394 KeyBinding::for_action_in(
1395 &OpenConfiguration,
1396 &focus_handle,
1397 window,
1398 cx,
1399 )
1400 .map(|kb| kb.size(rems_from_px(12.))),
1401 )
1402 .on_click(|_event, window, cx| {
1403 window.dispatch_action(
1404 OpenConfiguration.boxed_clone(),
1405 cx,
1406 )
1407 }),
1408 ),
1409 )
1410 }
1411 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1412 parent
1413 .child(
1414 Banner::new()
1415 .severity(ui::Severity::Warning)
1416 .children(
1417 h_flex()
1418 .w_full()
1419 .children(
1420 provider.render_accept_terms(
1421 LanguageModelProviderTosView::ThreadtEmptyState,
1422 cx,
1423 ),
1424 ),
1425 ),
1426 )
1427 }
1428 None => parent,
1429 }
1430 })
1431 })
1432 }
1433
1434 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1435 let last_error = self.thread.read(cx).last_error()?;
1436
1437 Some(
1438 div()
1439 .absolute()
1440 .right_3()
1441 .bottom_12()
1442 .max_w_96()
1443 .py_2()
1444 .px_3()
1445 .elevation_2(cx)
1446 .occlude()
1447 .child(match last_error {
1448 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1449 ThreadError::MaxMonthlySpendReached => {
1450 self.render_max_monthly_spend_reached_error(cx)
1451 }
1452 ThreadError::Message { header, message } => {
1453 self.render_error_message(header, message, cx)
1454 }
1455 })
1456 .into_any(),
1457 )
1458 }
1459
1460 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1461 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.";
1462
1463 v_flex()
1464 .gap_0p5()
1465 .child(
1466 h_flex()
1467 .gap_1p5()
1468 .items_center()
1469 .child(Icon::new(IconName::XCircle).color(Color::Error))
1470 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1471 )
1472 .child(
1473 div()
1474 .id("error-message")
1475 .max_h_24()
1476 .overflow_y_scroll()
1477 .child(Label::new(ERROR_MESSAGE)),
1478 )
1479 .child(
1480 h_flex()
1481 .justify_end()
1482 .mt_1()
1483 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1484 |this, _, _, cx| {
1485 this.thread.update(cx, |this, _cx| {
1486 this.clear_last_error();
1487 });
1488
1489 cx.open_url(&zed_urls::account_url(cx));
1490 cx.notify();
1491 },
1492 )))
1493 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1494 |this, _, _, cx| {
1495 this.thread.update(cx, |this, _cx| {
1496 this.clear_last_error();
1497 });
1498
1499 cx.notify();
1500 },
1501 ))),
1502 )
1503 .into_any()
1504 }
1505
1506 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1507 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1508
1509 v_flex()
1510 .gap_0p5()
1511 .child(
1512 h_flex()
1513 .gap_1p5()
1514 .items_center()
1515 .child(Icon::new(IconName::XCircle).color(Color::Error))
1516 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1517 )
1518 .child(
1519 div()
1520 .id("error-message")
1521 .max_h_24()
1522 .overflow_y_scroll()
1523 .child(Label::new(ERROR_MESSAGE)),
1524 )
1525 .child(
1526 h_flex()
1527 .justify_end()
1528 .mt_1()
1529 .child(
1530 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1531 cx.listener(|this, _, _, cx| {
1532 this.thread.update(cx, |this, _cx| {
1533 this.clear_last_error();
1534 });
1535
1536 cx.open_url(&zed_urls::account_url(cx));
1537 cx.notify();
1538 }),
1539 ),
1540 )
1541 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1542 |this, _, _, cx| {
1543 this.thread.update(cx, |this, _cx| {
1544 this.clear_last_error();
1545 });
1546
1547 cx.notify();
1548 },
1549 ))),
1550 )
1551 .into_any()
1552 }
1553
1554 fn render_error_message(
1555 &self,
1556 header: SharedString,
1557 message: SharedString,
1558 cx: &mut Context<Self>,
1559 ) -> AnyElement {
1560 v_flex()
1561 .gap_0p5()
1562 .child(
1563 h_flex()
1564 .gap_1p5()
1565 .items_center()
1566 .child(Icon::new(IconName::XCircle).color(Color::Error))
1567 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1568 )
1569 .child(
1570 div()
1571 .id("error-message")
1572 .max_h_32()
1573 .overflow_y_scroll()
1574 .child(Label::new(message)),
1575 )
1576 .child(
1577 h_flex()
1578 .justify_end()
1579 .mt_1()
1580 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1581 |this, _, _, cx| {
1582 this.thread.update(cx, |this, _cx| {
1583 this.clear_last_error();
1584 });
1585
1586 cx.notify();
1587 },
1588 ))),
1589 )
1590 .into_any()
1591 }
1592
1593 fn key_context(&self) -> KeyContext {
1594 let mut key_context = KeyContext::new_with_defaults();
1595 key_context.add("AgentPanel");
1596 if matches!(self.active_view, ActiveView::PromptEditor) {
1597 key_context.add("prompt_editor");
1598 }
1599 key_context
1600 }
1601}
1602
1603impl Render for AssistantPanel {
1604 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1605 v_flex()
1606 .key_context(self.key_context())
1607 .justify_between()
1608 .size_full()
1609 .on_action(cx.listener(Self::cancel))
1610 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1611 this.new_thread(action, window, cx);
1612 }))
1613 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1614 this.open_history(window, cx);
1615 }))
1616 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1617 this.open_configuration(window, cx);
1618 }))
1619 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1620 .on_action(cx.listener(Self::deploy_prompt_library))
1621 .on_action(cx.listener(Self::open_agent_diff))
1622 .on_action(cx.listener(Self::go_back))
1623 .child(self.render_toolbar(window, cx))
1624 .map(|parent| match self.active_view {
1625 ActiveView::Thread { .. } => parent
1626 .child(self.render_active_thread_or_empty_state(window, cx))
1627 .child(h_flex().child(self.message_editor.clone()))
1628 .children(self.render_last_error(cx)),
1629 ActiveView::History => parent.child(self.history.clone()),
1630 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1631 ActiveView::Configuration => parent.children(self.configuration.clone()),
1632 })
1633 }
1634}
1635
1636struct PromptLibraryInlineAssist {
1637 workspace: WeakEntity<Workspace>,
1638}
1639
1640impl PromptLibraryInlineAssist {
1641 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1642 Self { workspace }
1643 }
1644}
1645
1646impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1647 fn assist(
1648 &self,
1649 prompt_editor: &Entity<Editor>,
1650 _initial_prompt: Option<String>,
1651 window: &mut Window,
1652 cx: &mut Context<PromptLibrary>,
1653 ) {
1654 InlineAssistant::update_global(cx, |assistant, cx| {
1655 let Some(project) = self
1656 .workspace
1657 .upgrade()
1658 .map(|workspace| workspace.read(cx).project().downgrade())
1659 else {
1660 return;
1661 };
1662 assistant.assist(
1663 &prompt_editor,
1664 self.workspace.clone(),
1665 project,
1666 None,
1667 window,
1668 cx,
1669 )
1670 })
1671 }
1672
1673 fn focus_assistant_panel(
1674 &self,
1675 workspace: &mut Workspace,
1676 window: &mut Window,
1677 cx: &mut Context<Workspace>,
1678 ) -> bool {
1679 workspace
1680 .focus_panel::<AssistantPanel>(window, cx)
1681 .is_some()
1682 }
1683}
1684
1685pub struct ConcreteAssistantPanelDelegate;
1686
1687impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1688 fn active_context_editor(
1689 &self,
1690 workspace: &mut Workspace,
1691 _window: &mut Window,
1692 cx: &mut Context<Workspace>,
1693 ) -> Option<Entity<ContextEditor>> {
1694 let panel = workspace.panel::<AssistantPanel>(cx)?;
1695 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1696 }
1697
1698 fn open_saved_context(
1699 &self,
1700 workspace: &mut Workspace,
1701 path: std::path::PathBuf,
1702 window: &mut Window,
1703 cx: &mut Context<Workspace>,
1704 ) -> Task<Result<()>> {
1705 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1706 return Task::ready(Err(anyhow!("Agent panel not found")));
1707 };
1708
1709 panel.update(cx, |panel, cx| {
1710 panel.open_saved_prompt_editor(path, window, cx)
1711 })
1712 }
1713
1714 fn open_remote_context(
1715 &self,
1716 _workspace: &mut Workspace,
1717 _context_id: assistant_context_editor::ContextId,
1718 _window: &mut Window,
1719 _cx: &mut Context<Workspace>,
1720 ) -> Task<Result<Entity<ContextEditor>>> {
1721 Task::ready(Err(anyhow!("opening remote context not implemented")))
1722 }
1723
1724 fn quote_selection(
1725 &self,
1726 _workspace: &mut Workspace,
1727 _creases: Vec<(String, String)>,
1728 _window: &mut Window,
1729 _cx: &mut Context<Workspace>,
1730 ) {
1731 }
1732}