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