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