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