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