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