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