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().clone();
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 workspace.clone(),
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.workspace.clone(),
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.workspace.clone(),
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 .into_any_element()
859 } else if summary.is_none() {
860 Label::new(LOADING_SUMMARY_PLACEHOLDER)
861 .truncate()
862 .into_any_element()
863 } else {
864 change_title_editor.clone().into_any_element()
865 }
866 }
867 ActiveView::PromptEditor => {
868 let title = self
869 .context_editor
870 .as_ref()
871 .map(|context_editor| {
872 SharedString::from(context_editor.read(cx).title(cx).to_string())
873 })
874 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
875
876 Label::new(title).truncate().into_any_element()
877 }
878 ActiveView::History => Label::new("History").truncate().into_any_element(),
879 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
880 };
881
882 h_flex()
883 .key_context("TitleEditor")
884 .id("TitleEditor")
885 .flex_grow()
886 .w_full()
887 .max_w_full()
888 .overflow_x_scroll()
889 .child(content)
890 .into_any()
891 }
892
893 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
894 let active_thread = self.thread.read(cx);
895 let thread = active_thread.thread().read(cx);
896 let token_usage = thread.total_token_usage(cx);
897 let thread_id = thread.id().clone();
898
899 let is_generating = thread.is_generating();
900 let is_empty = active_thread.is_empty();
901 let focus_handle = self.focus_handle(cx);
902
903 let is_history = matches!(self.active_view, ActiveView::History);
904
905 let show_token_count = match &self.active_view {
906 ActiveView::Thread { .. } => !is_empty,
907 ActiveView::PromptEditor => self.context_editor.is_some(),
908 _ => false,
909 };
910
911 let go_back_button = match &self.active_view {
912 ActiveView::History | ActiveView::Configuration => Some(
913 IconButton::new("go-back", IconName::ArrowLeft)
914 .icon_size(IconSize::Small)
915 .on_click(cx.listener(|this, _, window, cx| {
916 this.go_back(&workspace::GoBack, window, cx);
917 }))
918 .tooltip({
919 let focus_handle = focus_handle.clone();
920 move |window, cx| {
921 Tooltip::for_action_in(
922 "Go Back",
923 &workspace::GoBack,
924 &focus_handle,
925 window,
926 cx,
927 )
928 }
929 }),
930 ),
931 _ => None,
932 };
933
934 h_flex()
935 .id("assistant-toolbar")
936 .h(Tab::container_height(cx))
937 .max_w_full()
938 .flex_none()
939 .justify_between()
940 .gap_2()
941 .bg(cx.theme().colors().tab_bar_background)
942 .border_b_1()
943 .border_color(cx.theme().colors().border)
944 .child(
945 h_flex()
946 .w_full()
947 .pl_2()
948 .gap_2()
949 .children(go_back_button)
950 .child(self.render_title_view(window, cx)),
951 )
952 .child(
953 h_flex()
954 .h_full()
955 .gap_2()
956 .when(show_token_count, |parent| match self.active_view {
957 ActiveView::Thread { .. } => {
958 if token_usage.total == 0 {
959 return parent;
960 }
961
962 let token_color = match token_usage.ratio {
963 TokenUsageRatio::Normal => Color::Muted,
964 TokenUsageRatio::Warning => Color::Warning,
965 TokenUsageRatio::Exceeded => Color::Error,
966 };
967
968 parent.child(
969 h_flex()
970 .flex_shrink_0()
971 .gap_0p5()
972 .child(
973 Label::new(assistant_context_editor::humanize_token_count(
974 token_usage.total,
975 ))
976 .size(LabelSize::Small)
977 .color(token_color)
978 .map(|label| {
979 if is_generating {
980 label
981 .with_animation(
982 "used-tokens-label",
983 Animation::new(Duration::from_secs(2))
984 .repeat()
985 .with_easing(pulsating_between(
986 0.6, 1.,
987 )),
988 |label, delta| label.alpha(delta),
989 )
990 .into_any()
991 } else {
992 label.into_any_element()
993 }
994 }),
995 )
996 .child(
997 Label::new("/").size(LabelSize::Small).color(Color::Muted),
998 )
999 .child(
1000 Label::new(assistant_context_editor::humanize_token_count(
1001 token_usage.max,
1002 ))
1003 .size(LabelSize::Small)
1004 .color(Color::Muted),
1005 ),
1006 )
1007 }
1008 ActiveView::PromptEditor => {
1009 let Some(editor) = self.context_editor.as_ref() else {
1010 return parent;
1011 };
1012 let Some(element) = render_remaining_tokens(editor, cx) else {
1013 return parent;
1014 };
1015 parent.child(element)
1016 }
1017 _ => parent,
1018 })
1019 .child(
1020 h_flex()
1021 .h_full()
1022 .gap(DynamicSpacing::Base02.rems(cx))
1023 .px(DynamicSpacing::Base08.rems(cx))
1024 .border_l_1()
1025 .border_color(cx.theme().colors().border)
1026 .child(
1027 IconButton::new("new", IconName::Plus)
1028 .icon_size(IconSize::Small)
1029 .style(ButtonStyle::Subtle)
1030 .tooltip(move |window, cx| {
1031 Tooltip::for_action_in(
1032 "New Thread",
1033 &NewThread::default(),
1034 &focus_handle,
1035 window,
1036 cx,
1037 )
1038 })
1039 .on_click(move |_event, window, cx| {
1040 window.dispatch_action(
1041 NewThread::default().boxed_clone(),
1042 cx,
1043 );
1044 }),
1045 )
1046 .child(
1047 IconButton::new("open-history", IconName::HistoryRerun)
1048 .icon_size(IconSize::Small)
1049 .toggle_state(is_history)
1050 .selected_icon_color(Color::Accent)
1051 .tooltip({
1052 let focus_handle = self.focus_handle(cx);
1053 move |window, cx| {
1054 Tooltip::for_action_in(
1055 "History",
1056 &OpenHistory,
1057 &focus_handle,
1058 window,
1059 cx,
1060 )
1061 }
1062 })
1063 .on_click(move |_event, window, cx| {
1064 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1065 }),
1066 )
1067 .child(
1068 PopoverMenu::new("assistant-menu")
1069 .trigger_with_tooltip(
1070 IconButton::new("new", IconName::Ellipsis)
1071 .icon_size(IconSize::Small)
1072 .style(ButtonStyle::Subtle),
1073 Tooltip::text("Toggle Agent Menu"),
1074 )
1075 .anchor(Corner::TopRight)
1076 .with_handle(self.assistant_dropdown_menu_handle.clone())
1077 .menu(move |window, cx| {
1078 Some(ContextMenu::build(
1079 window,
1080 cx,
1081 |menu, _window, _cx| {
1082 menu.action(
1083 "New Text Thread",
1084 NewPromptEditor.boxed_clone(),
1085 )
1086 .when(!is_empty, |menu| {
1087 menu.action(
1088 "Continue in New Thread",
1089 Box::new(NewThread {
1090 from_thread_id: Some(thread_id.clone()),
1091 }),
1092 )
1093 })
1094 .separator()
1095 .action("Settings", OpenConfiguration.boxed_clone())
1096 },
1097 ))
1098 }),
1099 ),
1100 ),
1101 )
1102 }
1103
1104 fn render_active_thread_or_empty_state(
1105 &self,
1106 window: &mut Window,
1107 cx: &mut Context<Self>,
1108 ) -> AnyElement {
1109 if self.thread.read(cx).is_empty() {
1110 return self
1111 .render_thread_empty_state(window, cx)
1112 .into_any_element();
1113 }
1114
1115 self.thread.clone().into_any_element()
1116 }
1117
1118 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1119 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1120 return Some(ConfigurationError::NoProvider);
1121 };
1122
1123 if !model.provider.is_authenticated(cx) {
1124 return Some(ConfigurationError::ProviderNotAuthenticated);
1125 }
1126
1127 if model.provider.must_accept_terms(cx) {
1128 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1129 model.provider,
1130 ));
1131 }
1132
1133 None
1134 }
1135
1136 fn render_thread_empty_state(
1137 &self,
1138 window: &mut Window,
1139 cx: &mut Context<Self>,
1140 ) -> impl IntoElement {
1141 let recent_history = self
1142 .history_store
1143 .update(cx, |this, cx| this.recent_entries(6, cx));
1144
1145 let configuration_error = self.configuration_error(cx);
1146 let no_error = configuration_error.is_none();
1147 let focus_handle = self.focus_handle(cx);
1148
1149 v_flex()
1150 .size_full()
1151 .when(recent_history.is_empty(), |this| {
1152 let configuration_error_ref = &configuration_error;
1153 this.child(
1154 v_flex()
1155 .size_full()
1156 .max_w_80()
1157 .mx_auto()
1158 .justify_center()
1159 .items_center()
1160 .gap_1()
1161 .child(
1162 h_flex().child(
1163 Headline::new("Welcome to the Agent Panel")
1164 ),
1165 )
1166 .when(no_error, |parent| {
1167 parent
1168 .child(
1169 h_flex().child(
1170 Label::new("Ask and build anything.")
1171 .color(Color::Muted)
1172 .mb_2p5(),
1173 ),
1174 )
1175 .child(
1176 Button::new("new-thread", "Start New Thread")
1177 .icon(IconName::Plus)
1178 .icon_position(IconPosition::Start)
1179 .icon_size(IconSize::Small)
1180 .icon_color(Color::Muted)
1181 .full_width()
1182 .key_binding(KeyBinding::for_action_in(
1183 &NewThread::default(),
1184 &focus_handle,
1185 window,
1186 cx,
1187 ))
1188 .on_click(|_event, window, cx| {
1189 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1190 }),
1191 )
1192 .child(
1193 Button::new("context", "Add Context")
1194 .icon(IconName::FileCode)
1195 .icon_position(IconPosition::Start)
1196 .icon_size(IconSize::Small)
1197 .icon_color(Color::Muted)
1198 .full_width()
1199 .key_binding(KeyBinding::for_action_in(
1200 &ToggleContextPicker,
1201 &focus_handle,
1202 window,
1203 cx,
1204 ))
1205 .on_click(|_event, window, cx| {
1206 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1207 }),
1208 )
1209 .child(
1210 Button::new("mode", "Switch Model")
1211 .icon(IconName::DatabaseZap)
1212 .icon_position(IconPosition::Start)
1213 .icon_size(IconSize::Small)
1214 .icon_color(Color::Muted)
1215 .full_width()
1216 .key_binding(KeyBinding::for_action_in(
1217 &ToggleModelSelector,
1218 &focus_handle,
1219 window,
1220 cx,
1221 ))
1222 .on_click(|_event, window, cx| {
1223 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1224 }),
1225 )
1226 .child(
1227 Button::new("settings", "View Settings")
1228 .icon(IconName::Settings)
1229 .icon_position(IconPosition::Start)
1230 .icon_size(IconSize::Small)
1231 .icon_color(Color::Muted)
1232 .full_width()
1233 .key_binding(KeyBinding::for_action_in(
1234 &OpenConfiguration,
1235 &focus_handle,
1236 window,
1237 cx,
1238 ))
1239 .on_click(|_event, window, cx| {
1240 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1241 }),
1242 )
1243 })
1244 .map(|parent| {
1245 match configuration_error_ref {
1246 Some(ConfigurationError::ProviderNotAuthenticated)
1247 | Some(ConfigurationError::NoProvider) => {
1248 parent
1249 .child(
1250 h_flex().child(
1251 Label::new("To start using the agent, configure at least one LLM provider.")
1252 .color(Color::Muted)
1253 .mb_2p5()
1254 )
1255 )
1256 .child(
1257 Button::new("settings", "Configure a Provider")
1258 .icon(IconName::Settings)
1259 .icon_position(IconPosition::Start)
1260 .icon_size(IconSize::Small)
1261 .icon_color(Color::Muted)
1262 .full_width()
1263 .key_binding(KeyBinding::for_action_in(
1264 &OpenConfiguration,
1265 &focus_handle,
1266 window,
1267 cx,
1268 ))
1269 .on_click(|_event, window, cx| {
1270 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1271 }),
1272 )
1273 }
1274 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1275 parent.children(
1276 provider.render_accept_terms(
1277 LanguageModelProviderTosView::ThreadFreshStart,
1278 cx,
1279 ),
1280 )
1281 }
1282 None => parent,
1283 }
1284 })
1285 )
1286 })
1287 .when(!recent_history.is_empty(), |parent| {
1288 let focus_handle = focus_handle.clone();
1289 let configuration_error_ref = &configuration_error;
1290
1291 parent
1292 .p_1p5()
1293 .justify_end()
1294 .gap_1()
1295 .child(
1296 h_flex()
1297 .pl_1p5()
1298 .pb_1()
1299 .w_full()
1300 .justify_between()
1301 .border_b_1()
1302 .border_color(cx.theme().colors().border_variant)
1303 .child(
1304 Label::new("Past Interactions")
1305 .size(LabelSize::Small)
1306 .color(Color::Muted),
1307 )
1308 .child(
1309 Button::new("view-history", "View All")
1310 .style(ButtonStyle::Subtle)
1311 .label_size(LabelSize::Small)
1312 .key_binding(
1313 KeyBinding::for_action_in(
1314 &OpenHistory,
1315 &self.focus_handle(cx),
1316 window,
1317 cx,
1318 ).map(|kb| kb.size(rems_from_px(12.))),
1319 )
1320 .on_click(move |_event, window, cx| {
1321 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1322 }),
1323 ),
1324 )
1325 .child(
1326 v_flex()
1327 .gap_1()
1328 .children(
1329 recent_history.into_iter().map(|entry| {
1330 // TODO: Add keyboard navigation.
1331 match entry {
1332 HistoryEntry::Thread(thread) => {
1333 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1334 .into_any_element()
1335 }
1336 HistoryEntry::Context(context) => {
1337 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1338 .into_any_element()
1339 }
1340 }
1341 }),
1342 )
1343 )
1344 .map(|parent| {
1345 match configuration_error_ref {
1346 Some(ConfigurationError::ProviderNotAuthenticated)
1347 | Some(ConfigurationError::NoProvider) => {
1348 parent
1349 .child(
1350 Banner::new()
1351 .severity(ui::Severity::Warning)
1352 .children(
1353 Label::new(
1354 "Configure at least one LLM provider to start using the panel.",
1355 )
1356 .size(LabelSize::Small),
1357 )
1358 .action_slot(
1359 Button::new("settings", "Configure Provider")
1360 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1361 .label_size(LabelSize::Small)
1362 .key_binding(
1363 KeyBinding::for_action_in(
1364 &OpenConfiguration,
1365 &focus_handle,
1366 window,
1367 cx,
1368 )
1369 .map(|kb| kb.size(rems_from_px(12.))),
1370 )
1371 .on_click(|_event, window, cx| {
1372 window.dispatch_action(
1373 OpenConfiguration.boxed_clone(),
1374 cx,
1375 )
1376 }),
1377 ),
1378 )
1379 }
1380 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1381 parent
1382 .child(
1383 Banner::new()
1384 .severity(ui::Severity::Warning)
1385 .children(
1386 h_flex()
1387 .w_full()
1388 .children(
1389 provider.render_accept_terms(
1390 LanguageModelProviderTosView::ThreadtEmptyState,
1391 cx,
1392 ),
1393 ),
1394 ),
1395 )
1396 }
1397 None => parent,
1398 }
1399 })
1400 })
1401 }
1402
1403 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1404 let last_error = self.thread.read(cx).last_error()?;
1405
1406 Some(
1407 div()
1408 .absolute()
1409 .right_3()
1410 .bottom_12()
1411 .max_w_96()
1412 .py_2()
1413 .px_3()
1414 .elevation_2(cx)
1415 .occlude()
1416 .child(match last_error {
1417 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1418 ThreadError::MaxMonthlySpendReached => {
1419 self.render_max_monthly_spend_reached_error(cx)
1420 }
1421 ThreadError::Message { header, message } => {
1422 self.render_error_message(header, message, cx)
1423 }
1424 })
1425 .into_any(),
1426 )
1427 }
1428
1429 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1430 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.";
1431
1432 v_flex()
1433 .gap_0p5()
1434 .child(
1435 h_flex()
1436 .gap_1p5()
1437 .items_center()
1438 .child(Icon::new(IconName::XCircle).color(Color::Error))
1439 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1440 )
1441 .child(
1442 div()
1443 .id("error-message")
1444 .max_h_24()
1445 .overflow_y_scroll()
1446 .child(Label::new(ERROR_MESSAGE)),
1447 )
1448 .child(
1449 h_flex()
1450 .justify_end()
1451 .mt_1()
1452 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1453 |this, _, _, cx| {
1454 this.thread.update(cx, |this, _cx| {
1455 this.clear_last_error();
1456 });
1457
1458 cx.open_url(&zed_urls::account_url(cx));
1459 cx.notify();
1460 },
1461 )))
1462 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1463 |this, _, _, cx| {
1464 this.thread.update(cx, |this, _cx| {
1465 this.clear_last_error();
1466 });
1467
1468 cx.notify();
1469 },
1470 ))),
1471 )
1472 .into_any()
1473 }
1474
1475 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1476 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1477
1478 v_flex()
1479 .gap_0p5()
1480 .child(
1481 h_flex()
1482 .gap_1p5()
1483 .items_center()
1484 .child(Icon::new(IconName::XCircle).color(Color::Error))
1485 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1486 )
1487 .child(
1488 div()
1489 .id("error-message")
1490 .max_h_24()
1491 .overflow_y_scroll()
1492 .child(Label::new(ERROR_MESSAGE)),
1493 )
1494 .child(
1495 h_flex()
1496 .justify_end()
1497 .mt_1()
1498 .child(
1499 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1500 cx.listener(|this, _, _, cx| {
1501 this.thread.update(cx, |this, _cx| {
1502 this.clear_last_error();
1503 });
1504
1505 cx.open_url(&zed_urls::account_url(cx));
1506 cx.notify();
1507 }),
1508 ),
1509 )
1510 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1511 |this, _, _, cx| {
1512 this.thread.update(cx, |this, _cx| {
1513 this.clear_last_error();
1514 });
1515
1516 cx.notify();
1517 },
1518 ))),
1519 )
1520 .into_any()
1521 }
1522
1523 fn render_error_message(
1524 &self,
1525 header: SharedString,
1526 message: SharedString,
1527 cx: &mut Context<Self>,
1528 ) -> AnyElement {
1529 v_flex()
1530 .gap_0p5()
1531 .child(
1532 h_flex()
1533 .gap_1p5()
1534 .items_center()
1535 .child(Icon::new(IconName::XCircle).color(Color::Error))
1536 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1537 )
1538 .child(
1539 div()
1540 .id("error-message")
1541 .max_h_32()
1542 .overflow_y_scroll()
1543 .child(Label::new(message)),
1544 )
1545 .child(
1546 h_flex()
1547 .justify_end()
1548 .mt_1()
1549 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1550 |this, _, _, cx| {
1551 this.thread.update(cx, |this, _cx| {
1552 this.clear_last_error();
1553 });
1554
1555 cx.notify();
1556 },
1557 ))),
1558 )
1559 .into_any()
1560 }
1561
1562 fn key_context(&self) -> KeyContext {
1563 let mut key_context = KeyContext::new_with_defaults();
1564 key_context.add("AgentPanel");
1565 if matches!(self.active_view, ActiveView::PromptEditor) {
1566 key_context.add("prompt_editor");
1567 }
1568 key_context
1569 }
1570}
1571
1572impl Render for AssistantPanel {
1573 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1574 v_flex()
1575 .key_context(self.key_context())
1576 .justify_between()
1577 .size_full()
1578 .on_action(cx.listener(Self::cancel))
1579 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1580 this.new_thread(action, window, cx);
1581 }))
1582 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1583 this.open_history(window, cx);
1584 }))
1585 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1586 this.open_configuration(window, cx);
1587 }))
1588 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1589 .on_action(cx.listener(Self::deploy_prompt_library))
1590 .on_action(cx.listener(Self::open_agent_diff))
1591 .on_action(cx.listener(Self::go_back))
1592 .child(self.render_toolbar(window, cx))
1593 .map(|parent| match self.active_view {
1594 ActiveView::Thread { .. } => parent
1595 .child(self.render_active_thread_or_empty_state(window, cx))
1596 .child(h_flex().child(self.message_editor.clone()))
1597 .children(self.render_last_error(cx)),
1598 ActiveView::History => parent.child(self.history.clone()),
1599 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1600 ActiveView::Configuration => parent.children(self.configuration.clone()),
1601 })
1602 }
1603}
1604
1605struct PromptLibraryInlineAssist {
1606 workspace: WeakEntity<Workspace>,
1607}
1608
1609impl PromptLibraryInlineAssist {
1610 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1611 Self { workspace }
1612 }
1613}
1614
1615impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1616 fn assist(
1617 &self,
1618 prompt_editor: &Entity<Editor>,
1619 _initial_prompt: Option<String>,
1620 window: &mut Window,
1621 cx: &mut Context<PromptLibrary>,
1622 ) {
1623 InlineAssistant::update_global(cx, |assistant, cx| {
1624 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1625 })
1626 }
1627
1628 fn focus_assistant_panel(
1629 &self,
1630 workspace: &mut Workspace,
1631 window: &mut Window,
1632 cx: &mut Context<Workspace>,
1633 ) -> bool {
1634 workspace
1635 .focus_panel::<AssistantPanel>(window, cx)
1636 .is_some()
1637 }
1638}
1639
1640pub struct ConcreteAssistantPanelDelegate;
1641
1642impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1643 fn active_context_editor(
1644 &self,
1645 workspace: &mut Workspace,
1646 _window: &mut Window,
1647 cx: &mut Context<Workspace>,
1648 ) -> Option<Entity<ContextEditor>> {
1649 let panel = workspace.panel::<AssistantPanel>(cx)?;
1650 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1651 }
1652
1653 fn open_saved_context(
1654 &self,
1655 workspace: &mut Workspace,
1656 path: std::path::PathBuf,
1657 window: &mut Window,
1658 cx: &mut Context<Workspace>,
1659 ) -> Task<Result<()>> {
1660 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1661 return Task::ready(Err(anyhow!("Agent panel not found")));
1662 };
1663
1664 panel.update(cx, |panel, cx| {
1665 panel.open_saved_prompt_editor(path, window, cx)
1666 })
1667 }
1668
1669 fn open_remote_context(
1670 &self,
1671 _workspace: &mut Workspace,
1672 _context_id: assistant_context_editor::ContextId,
1673 _window: &mut Window,
1674 _cx: &mut Context<Workspace>,
1675 ) -> Task<Result<Entity<ContextEditor>>> {
1676 Task::ready(Err(anyhow!("opening remote context not implemented")))
1677 }
1678
1679 fn quote_selection(
1680 &self,
1681 _workspace: &mut Workspace,
1682 _creases: Vec<(String, String)>,
1683 _window: &mut Window,
1684 _cx: &mut Context<Workspace>,
1685 ) {
1686 }
1687}