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