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.action(
1092 "New Text Thread",
1093 NewTextThread.boxed_clone(),
1094 )
1095 .when(!is_empty, |menu| {
1096 menu.action(
1097 "Continue in New Thread",
1098 Box::new(NewThread {
1099 from_thread_id: Some(thread_id.clone()),
1100 }),
1101 )
1102 })
1103 .separator()
1104 .action("Settings", OpenConfiguration.boxed_clone())
1105 },
1106 ))
1107 }),
1108 ),
1109 ),
1110 )
1111 }
1112
1113 fn render_active_thread_or_empty_state(
1114 &self,
1115 window: &mut Window,
1116 cx: &mut Context<Self>,
1117 ) -> AnyElement {
1118 if self.thread.read(cx).is_empty() {
1119 return self
1120 .render_thread_empty_state(window, cx)
1121 .into_any_element();
1122 }
1123
1124 self.thread.clone().into_any_element()
1125 }
1126
1127 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1128 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1129 return Some(ConfigurationError::NoProvider);
1130 };
1131
1132 if !model.provider.is_authenticated(cx) {
1133 return Some(ConfigurationError::ProviderNotAuthenticated);
1134 }
1135
1136 if model.provider.must_accept_terms(cx) {
1137 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1138 model.provider,
1139 ));
1140 }
1141
1142 None
1143 }
1144
1145 fn render_thread_empty_state(
1146 &self,
1147 window: &mut Window,
1148 cx: &mut Context<Self>,
1149 ) -> impl IntoElement {
1150 let recent_history = self
1151 .history_store
1152 .update(cx, |this, cx| this.recent_entries(6, cx));
1153
1154 let configuration_error = self.configuration_error(cx);
1155 let no_error = configuration_error.is_none();
1156 let focus_handle = self.focus_handle(cx);
1157
1158 v_flex()
1159 .size_full()
1160 .when(recent_history.is_empty(), |this| {
1161 let configuration_error_ref = &configuration_error;
1162 this.child(
1163 v_flex()
1164 .size_full()
1165 .max_w_80()
1166 .mx_auto()
1167 .justify_center()
1168 .items_center()
1169 .gap_1()
1170 .child(
1171 h_flex().child(
1172 Headline::new("Welcome to the Agent Panel")
1173 ),
1174 )
1175 .when(no_error, |parent| {
1176 parent
1177 .child(
1178 h_flex().child(
1179 Label::new("Ask and build anything.")
1180 .color(Color::Muted)
1181 .mb_2p5(),
1182 ),
1183 )
1184 .child(
1185 Button::new("new-thread", "Start New Thread")
1186 .icon(IconName::Plus)
1187 .icon_position(IconPosition::Start)
1188 .icon_size(IconSize::Small)
1189 .icon_color(Color::Muted)
1190 .full_width()
1191 .key_binding(KeyBinding::for_action_in(
1192 &NewThread::default(),
1193 &focus_handle,
1194 window,
1195 cx,
1196 ))
1197 .on_click(|_event, window, cx| {
1198 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1199 }),
1200 )
1201 .child(
1202 Button::new("context", "Add Context")
1203 .icon(IconName::FileCode)
1204 .icon_position(IconPosition::Start)
1205 .icon_size(IconSize::Small)
1206 .icon_color(Color::Muted)
1207 .full_width()
1208 .key_binding(KeyBinding::for_action_in(
1209 &ToggleContextPicker,
1210 &focus_handle,
1211 window,
1212 cx,
1213 ))
1214 .on_click(|_event, window, cx| {
1215 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1216 }),
1217 )
1218 .child(
1219 Button::new("mode", "Switch Model")
1220 .icon(IconName::DatabaseZap)
1221 .icon_position(IconPosition::Start)
1222 .icon_size(IconSize::Small)
1223 .icon_color(Color::Muted)
1224 .full_width()
1225 .key_binding(KeyBinding::for_action_in(
1226 &ToggleModelSelector,
1227 &focus_handle,
1228 window,
1229 cx,
1230 ))
1231 .on_click(|_event, window, cx| {
1232 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1233 }),
1234 )
1235 .child(
1236 Button::new("settings", "View Settings")
1237 .icon(IconName::Settings)
1238 .icon_position(IconPosition::Start)
1239 .icon_size(IconSize::Small)
1240 .icon_color(Color::Muted)
1241 .full_width()
1242 .key_binding(KeyBinding::for_action_in(
1243 &OpenConfiguration,
1244 &focus_handle,
1245 window,
1246 cx,
1247 ))
1248 .on_click(|_event, window, cx| {
1249 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1250 }),
1251 )
1252 })
1253 .map(|parent| {
1254 match configuration_error_ref {
1255 Some(ConfigurationError::ProviderNotAuthenticated)
1256 | Some(ConfigurationError::NoProvider) => {
1257 parent
1258 .child(
1259 h_flex().child(
1260 Label::new("To start using the agent, configure at least one LLM provider.")
1261 .color(Color::Muted)
1262 .mb_2p5()
1263 )
1264 )
1265 .child(
1266 Button::new("settings", "Configure a Provider")
1267 .icon(IconName::Settings)
1268 .icon_position(IconPosition::Start)
1269 .icon_size(IconSize::Small)
1270 .icon_color(Color::Muted)
1271 .full_width()
1272 .key_binding(KeyBinding::for_action_in(
1273 &OpenConfiguration,
1274 &focus_handle,
1275 window,
1276 cx,
1277 ))
1278 .on_click(|_event, window, cx| {
1279 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1280 }),
1281 )
1282 }
1283 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1284 parent.children(
1285 provider.render_accept_terms(
1286 LanguageModelProviderTosView::ThreadFreshStart,
1287 cx,
1288 ),
1289 )
1290 }
1291 None => parent,
1292 }
1293 })
1294 )
1295 })
1296 .when(!recent_history.is_empty(), |parent| {
1297 let focus_handle = focus_handle.clone();
1298 let configuration_error_ref = &configuration_error;
1299
1300 parent
1301 .overflow_hidden()
1302 .p_1p5()
1303 .justify_end()
1304 .gap_1()
1305 .child(
1306 h_flex()
1307 .pl_1p5()
1308 .pb_1()
1309 .w_full()
1310 .justify_between()
1311 .border_b_1()
1312 .border_color(cx.theme().colors().border_variant)
1313 .child(
1314 Label::new("Past Interactions")
1315 .size(LabelSize::Small)
1316 .color(Color::Muted),
1317 )
1318 .child(
1319 Button::new("view-history", "View All")
1320 .style(ButtonStyle::Subtle)
1321 .label_size(LabelSize::Small)
1322 .key_binding(
1323 KeyBinding::for_action_in(
1324 &OpenHistory,
1325 &self.focus_handle(cx),
1326 window,
1327 cx,
1328 ).map(|kb| kb.size(rems_from_px(12.))),
1329 )
1330 .on_click(move |_event, window, cx| {
1331 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1332 }),
1333 ),
1334 )
1335 .child(
1336 v_flex()
1337 .gap_1()
1338 .children(
1339 recent_history.into_iter().map(|entry| {
1340 // TODO: Add keyboard navigation.
1341 match entry {
1342 HistoryEntry::Thread(thread) => {
1343 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1344 .into_any_element()
1345 }
1346 HistoryEntry::Context(context) => {
1347 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1348 .into_any_element()
1349 }
1350 }
1351 }),
1352 )
1353 )
1354 .map(|parent| {
1355 match configuration_error_ref {
1356 Some(ConfigurationError::ProviderNotAuthenticated)
1357 | Some(ConfigurationError::NoProvider) => {
1358 parent
1359 .child(
1360 Banner::new()
1361 .severity(ui::Severity::Warning)
1362 .children(
1363 Label::new(
1364 "Configure at least one LLM provider to start using the panel.",
1365 )
1366 .size(LabelSize::Small),
1367 )
1368 .action_slot(
1369 Button::new("settings", "Configure Provider")
1370 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1371 .label_size(LabelSize::Small)
1372 .key_binding(
1373 KeyBinding::for_action_in(
1374 &OpenConfiguration,
1375 &focus_handle,
1376 window,
1377 cx,
1378 )
1379 .map(|kb| kb.size(rems_from_px(12.))),
1380 )
1381 .on_click(|_event, window, cx| {
1382 window.dispatch_action(
1383 OpenConfiguration.boxed_clone(),
1384 cx,
1385 )
1386 }),
1387 ),
1388 )
1389 }
1390 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1391 parent
1392 .child(
1393 Banner::new()
1394 .severity(ui::Severity::Warning)
1395 .children(
1396 h_flex()
1397 .w_full()
1398 .children(
1399 provider.render_accept_terms(
1400 LanguageModelProviderTosView::ThreadtEmptyState,
1401 cx,
1402 ),
1403 ),
1404 ),
1405 )
1406 }
1407 None => parent,
1408 }
1409 })
1410 })
1411 }
1412
1413 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1414 let last_error = self.thread.read(cx).last_error()?;
1415
1416 Some(
1417 div()
1418 .absolute()
1419 .right_3()
1420 .bottom_12()
1421 .max_w_96()
1422 .py_2()
1423 .px_3()
1424 .elevation_2(cx)
1425 .occlude()
1426 .child(match last_error {
1427 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1428 ThreadError::MaxMonthlySpendReached => {
1429 self.render_max_monthly_spend_reached_error(cx)
1430 }
1431 ThreadError::Message { header, message } => {
1432 self.render_error_message(header, message, cx)
1433 }
1434 })
1435 .into_any(),
1436 )
1437 }
1438
1439 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1440 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.";
1441
1442 v_flex()
1443 .gap_0p5()
1444 .child(
1445 h_flex()
1446 .gap_1p5()
1447 .items_center()
1448 .child(Icon::new(IconName::XCircle).color(Color::Error))
1449 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1450 )
1451 .child(
1452 div()
1453 .id("error-message")
1454 .max_h_24()
1455 .overflow_y_scroll()
1456 .child(Label::new(ERROR_MESSAGE)),
1457 )
1458 .child(
1459 h_flex()
1460 .justify_end()
1461 .mt_1()
1462 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1463 |this, _, _, cx| {
1464 this.thread.update(cx, |this, _cx| {
1465 this.clear_last_error();
1466 });
1467
1468 cx.open_url(&zed_urls::account_url(cx));
1469 cx.notify();
1470 },
1471 )))
1472 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1473 |this, _, _, cx| {
1474 this.thread.update(cx, |this, _cx| {
1475 this.clear_last_error();
1476 });
1477
1478 cx.notify();
1479 },
1480 ))),
1481 )
1482 .into_any()
1483 }
1484
1485 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1486 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1487
1488 v_flex()
1489 .gap_0p5()
1490 .child(
1491 h_flex()
1492 .gap_1p5()
1493 .items_center()
1494 .child(Icon::new(IconName::XCircle).color(Color::Error))
1495 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1496 )
1497 .child(
1498 div()
1499 .id("error-message")
1500 .max_h_24()
1501 .overflow_y_scroll()
1502 .child(Label::new(ERROR_MESSAGE)),
1503 )
1504 .child(
1505 h_flex()
1506 .justify_end()
1507 .mt_1()
1508 .child(
1509 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1510 cx.listener(|this, _, _, cx| {
1511 this.thread.update(cx, |this, _cx| {
1512 this.clear_last_error();
1513 });
1514
1515 cx.open_url(&zed_urls::account_url(cx));
1516 cx.notify();
1517 }),
1518 ),
1519 )
1520 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1521 |this, _, _, cx| {
1522 this.thread.update(cx, |this, _cx| {
1523 this.clear_last_error();
1524 });
1525
1526 cx.notify();
1527 },
1528 ))),
1529 )
1530 .into_any()
1531 }
1532
1533 fn render_error_message(
1534 &self,
1535 header: SharedString,
1536 message: SharedString,
1537 cx: &mut Context<Self>,
1538 ) -> AnyElement {
1539 v_flex()
1540 .gap_0p5()
1541 .child(
1542 h_flex()
1543 .gap_1p5()
1544 .items_center()
1545 .child(Icon::new(IconName::XCircle).color(Color::Error))
1546 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1547 )
1548 .child(
1549 div()
1550 .id("error-message")
1551 .max_h_32()
1552 .overflow_y_scroll()
1553 .child(Label::new(message)),
1554 )
1555 .child(
1556 h_flex()
1557 .justify_end()
1558 .mt_1()
1559 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1560 |this, _, _, cx| {
1561 this.thread.update(cx, |this, _cx| {
1562 this.clear_last_error();
1563 });
1564
1565 cx.notify();
1566 },
1567 ))),
1568 )
1569 .into_any()
1570 }
1571
1572 fn key_context(&self) -> KeyContext {
1573 let mut key_context = KeyContext::new_with_defaults();
1574 key_context.add("AgentPanel");
1575 if matches!(self.active_view, ActiveView::PromptEditor) {
1576 key_context.add("prompt_editor");
1577 }
1578 key_context
1579 }
1580}
1581
1582impl Render for AssistantPanel {
1583 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1584 v_flex()
1585 .key_context(self.key_context())
1586 .justify_between()
1587 .size_full()
1588 .on_action(cx.listener(Self::cancel))
1589 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1590 this.new_thread(action, window, cx);
1591 }))
1592 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1593 this.open_history(window, cx);
1594 }))
1595 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1596 this.open_configuration(window, cx);
1597 }))
1598 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1599 .on_action(cx.listener(Self::deploy_prompt_library))
1600 .on_action(cx.listener(Self::open_agent_diff))
1601 .on_action(cx.listener(Self::go_back))
1602 .child(self.render_toolbar(window, cx))
1603 .map(|parent| match self.active_view {
1604 ActiveView::Thread { .. } => parent
1605 .child(self.render_active_thread_or_empty_state(window, cx))
1606 .child(h_flex().child(self.message_editor.clone()))
1607 .children(self.render_last_error(cx)),
1608 ActiveView::History => parent.child(self.history.clone()),
1609 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1610 ActiveView::Configuration => parent.children(self.configuration.clone()),
1611 })
1612 }
1613}
1614
1615struct PromptLibraryInlineAssist {
1616 workspace: WeakEntity<Workspace>,
1617}
1618
1619impl PromptLibraryInlineAssist {
1620 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1621 Self { workspace }
1622 }
1623}
1624
1625impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1626 fn assist(
1627 &self,
1628 prompt_editor: &Entity<Editor>,
1629 _initial_prompt: Option<String>,
1630 window: &mut Window,
1631 cx: &mut Context<PromptLibrary>,
1632 ) {
1633 InlineAssistant::update_global(cx, |assistant, cx| {
1634 let Some(project) = self
1635 .workspace
1636 .upgrade()
1637 .map(|workspace| workspace.read(cx).project().downgrade())
1638 else {
1639 return;
1640 };
1641 assistant.assist(
1642 &prompt_editor,
1643 self.workspace.clone(),
1644 project,
1645 None,
1646 window,
1647 cx,
1648 )
1649 })
1650 }
1651
1652 fn focus_assistant_panel(
1653 &self,
1654 workspace: &mut Workspace,
1655 window: &mut Window,
1656 cx: &mut Context<Workspace>,
1657 ) -> bool {
1658 workspace
1659 .focus_panel::<AssistantPanel>(window, cx)
1660 .is_some()
1661 }
1662}
1663
1664pub struct ConcreteAssistantPanelDelegate;
1665
1666impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1667 fn active_context_editor(
1668 &self,
1669 workspace: &mut Workspace,
1670 _window: &mut Window,
1671 cx: &mut Context<Workspace>,
1672 ) -> Option<Entity<ContextEditor>> {
1673 let panel = workspace.panel::<AssistantPanel>(cx)?;
1674 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1675 }
1676
1677 fn open_saved_context(
1678 &self,
1679 workspace: &mut Workspace,
1680 path: std::path::PathBuf,
1681 window: &mut Window,
1682 cx: &mut Context<Workspace>,
1683 ) -> Task<Result<()>> {
1684 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1685 return Task::ready(Err(anyhow!("Agent panel not found")));
1686 };
1687
1688 panel.update(cx, |panel, cx| {
1689 panel.open_saved_prompt_editor(path, window, cx)
1690 })
1691 }
1692
1693 fn open_remote_context(
1694 &self,
1695 _workspace: &mut Workspace,
1696 _context_id: assistant_context_editor::ContextId,
1697 _window: &mut Window,
1698 _cx: &mut Context<Workspace>,
1699 ) -> Task<Result<Entity<ContextEditor>>> {
1700 Task::ready(Err(anyhow!("opening remote context not implemented")))
1701 }
1702
1703 fn quote_selection(
1704 &self,
1705 _workspace: &mut Workspace,
1706 _creases: Vec<(String, String)>,
1707 _window: &mut Window,
1708 _cx: &mut Context<Workspace>,
1709 ) {
1710 }
1711}