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