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