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