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