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