1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use assistant_context_editor::{
7 AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
8 make_lsp_adapter_delegate, render_remaining_tokens,
9};
10use assistant_settings::{AssistantDockPosition, AssistantSettings};
11use assistant_slash_command::SlashCommandWorkingSet;
12use assistant_tool::ToolWorkingSet;
13
14use client::zed_urls;
15use editor::{Editor, MultiBuffer};
16use fs::Fs;
17use gpui::{
18 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
19 EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
20 UpdateGlobal, WeakEntity, action_with_deprecated_aliases, prelude::*, pulsating_between,
21};
22use language::LanguageRegistry;
23use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
24use language_model_selector::ToggleModelSelector;
25use project::Project;
26use prompt_library::{PromptLibrary, open_prompt_library};
27use prompt_store::PromptBuilder;
28use settings::{Settings, update_settings_file};
29use time::UtcOffset;
30use ui::{
31 Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
32};
33use util::ResultExt as _;
34use workspace::Workspace;
35use workspace::dock::{DockPosition, Panel, PanelEvent};
36use zed_actions::assistant::ToggleFocus;
37
38use crate::active_thread::ActiveThread;
39use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
40use crate::history_store::{HistoryEntry, HistoryStore};
41use crate::message_editor::MessageEditor;
42use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
43use crate::thread_history::{PastContext, PastThread, ThreadHistory};
44use crate::thread_store::ThreadStore;
45use crate::{
46 AgentDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
47 OpenAgentDiff, OpenConfiguration, OpenHistory, ToggleContextPicker,
48};
49
50action_with_deprecated_aliases!(
51 assistant,
52 OpenPromptLibrary,
53 ["assistant::DeployPromptLibrary"]
54);
55
56pub fn init(cx: &mut App) {
57 cx.observe_new(
58 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
59 workspace
60 .register_action(|workspace, action: &NewThread, window, cx| {
61 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
62 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
63 workspace.focus_panel::<AssistantPanel>(window, cx);
64 }
65 })
66 .register_action(|workspace, _: &OpenHistory, window, cx| {
67 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
68 workspace.focus_panel::<AssistantPanel>(window, cx);
69 panel.update(cx, |panel, cx| panel.open_history(window, cx));
70 }
71 })
72 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
73 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
74 workspace.focus_panel::<AssistantPanel>(window, cx);
75 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
76 }
77 })
78 .register_action(|workspace, _: &NewPromptEditor, window, cx| {
79 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
80 workspace.focus_panel::<AssistantPanel>(window, cx);
81 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
82 }
83 })
84 .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
85 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
86 workspace.focus_panel::<AssistantPanel>(window, cx);
87 panel.update(cx, |panel, cx| {
88 panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
89 });
90 }
91 })
92 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
93 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
94 workspace.focus_panel::<AssistantPanel>(window, cx);
95 panel.update(cx, |panel, cx| {
96 panel.open_agent_diff(&OpenAgentDiff, window, cx);
97 });
98 }
99 });
100 },
101 )
102 .detach();
103}
104
105enum ActiveView {
106 Thread,
107 PromptEditor,
108 History,
109 Configuration,
110}
111
112pub struct AssistantPanel {
113 workspace: WeakEntity<Workspace>,
114 project: Entity<Project>,
115 fs: Arc<dyn Fs>,
116 language_registry: Arc<LanguageRegistry>,
117 thread_store: Entity<ThreadStore>,
118 thread: Entity<ActiveThread>,
119 message_editor: Entity<MessageEditor>,
120 context_store: Entity<assistant_context_editor::ContextStore>,
121 context_editor: Option<Entity<ContextEditor>>,
122 configuration: Option<Entity<AssistantConfiguration>>,
123 configuration_subscription: Option<Subscription>,
124 local_timezone: UtcOffset,
125 active_view: ActiveView,
126 history_store: Entity<HistoryStore>,
127 history: Entity<ThreadHistory>,
128 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
129 width: Option<Pixels>,
130 height: Option<Pixels>,
131}
132
133impl AssistantPanel {
134 pub fn load(
135 workspace: WeakEntity<Workspace>,
136 prompt_builder: Arc<PromptBuilder>,
137 cx: AsyncWindowContext,
138 ) -> Task<Result<Entity<Self>>> {
139 cx.spawn(async move |cx| {
140 let tools = Arc::new(ToolWorkingSet::default());
141 let thread_store = workspace.update(cx, |workspace, cx| {
142 let project = workspace.project().clone();
143 ThreadStore::new(project, tools.clone(), prompt_builder.clone(), cx)
144 })??;
145
146 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
147 let context_store = workspace
148 .update(cx, |workspace, cx| {
149 let project = workspace.project().clone();
150 assistant_context_editor::ContextStore::new(
151 project,
152 prompt_builder.clone(),
153 slash_commands,
154 cx,
155 )
156 })?
157 .await?;
158
159 workspace.update_in(cx, |workspace, window, cx| {
160 cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
161 })
162 })
163 }
164
165 fn new(
166 workspace: &Workspace,
167 thread_store: Entity<ThreadStore>,
168 context_store: Entity<assistant_context_editor::ContextStore>,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) -> Self {
172 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
173 let fs = workspace.app_state().fs.clone();
174 let project = workspace.project().clone();
175 let language_registry = project.read(cx).languages().clone();
176 let workspace = workspace.weak_handle();
177 let weak_self = cx.entity().downgrade();
178
179 let message_editor_context_store = cx.new(|_cx| {
180 crate::context_store::ContextStore::new(
181 workspace.clone(),
182 Some(thread_store.downgrade()),
183 )
184 });
185
186 let message_editor = cx.new(|cx| {
187 MessageEditor::new(
188 fs.clone(),
189 workspace.clone(),
190 message_editor_context_store.clone(),
191 thread_store.downgrade(),
192 thread.clone(),
193 window,
194 cx,
195 )
196 });
197
198 let history_store =
199 cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
200
201 let thread = cx.new(|cx| {
202 ActiveThread::new(
203 thread.clone(),
204 thread_store.clone(),
205 language_registry.clone(),
206 message_editor_context_store.clone(),
207 workspace.clone(),
208 window,
209 cx,
210 )
211 });
212
213 Self {
214 active_view: ActiveView::Thread,
215 workspace,
216 project: project.clone(),
217 fs: fs.clone(),
218 language_registry,
219 thread_store: thread_store.clone(),
220 thread,
221 message_editor,
222 context_store,
223 context_editor: None,
224 configuration: None,
225 configuration_subscription: None,
226 local_timezone: UtcOffset::from_whole_seconds(
227 chrono::Local::now().offset().local_minus_utc(),
228 )
229 .unwrap(),
230 history_store: history_store.clone(),
231 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
232 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
233 width: None,
234 height: None,
235 }
236 }
237
238 pub fn toggle_focus(
239 workspace: &mut Workspace,
240 _: &ToggleFocus,
241 window: &mut Window,
242 cx: &mut Context<Workspace>,
243 ) {
244 if workspace
245 .panel::<Self>(cx)
246 .is_some_and(|panel| panel.read(cx).enabled(cx))
247 {
248 workspace.toggle_panel_focus::<Self>(window, cx);
249 }
250 }
251
252 pub(crate) fn local_timezone(&self) -> UtcOffset {
253 self.local_timezone
254 }
255
256 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
257 &self.thread_store
258 }
259
260 fn cancel(
261 &mut self,
262 _: &editor::actions::Cancel,
263 _window: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 self.thread
267 .update(cx, |thread, cx| thread.cancel_last_completion(cx));
268 }
269
270 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
271 let thread = self
272 .thread_store
273 .update(cx, |this, cx| this.create_thread(cx));
274
275 self.active_view = ActiveView::Thread;
276
277 let message_editor_context_store = cx.new(|_cx| {
278 crate::context_store::ContextStore::new(
279 self.workspace.clone(),
280 Some(self.thread_store.downgrade()),
281 )
282 });
283
284 if let Some(other_thread_id) = action.from_thread_id.clone() {
285 let other_thread_task = self
286 .thread_store
287 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
288
289 cx.spawn({
290 let context_store = message_editor_context_store.clone();
291
292 async move |_panel, cx| {
293 let other_thread = other_thread_task.await?;
294
295 context_store.update(cx, |this, cx| {
296 this.add_thread(other_thread, false, cx);
297 })?;
298 anyhow::Ok(())
299 }
300 })
301 .detach_and_log_err(cx);
302 }
303
304 self.thread = cx.new(|cx| {
305 ActiveThread::new(
306 thread.clone(),
307 self.thread_store.clone(),
308 self.language_registry.clone(),
309 message_editor_context_store.clone(),
310 self.workspace.clone(),
311 window,
312 cx,
313 )
314 });
315 self.message_editor = cx.new(|cx| {
316 MessageEditor::new(
317 self.fs.clone(),
318 self.workspace.clone(),
319 message_editor_context_store,
320 self.thread_store.downgrade(),
321 thread,
322 window,
323 cx,
324 )
325 });
326 self.message_editor.focus_handle(cx).focus(window);
327 }
328
329 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
330 self.active_view = ActiveView::PromptEditor;
331
332 let context = self
333 .context_store
334 .update(cx, |context_store, cx| context_store.create(cx));
335 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
336 .log_err()
337 .flatten();
338
339 self.context_editor = Some(cx.new(|cx| {
340 let mut editor = ContextEditor::for_context(
341 context,
342 self.fs.clone(),
343 self.workspace.clone(),
344 self.project.clone(),
345 lsp_adapter_delegate,
346 window,
347 cx,
348 );
349 editor.insert_default_prompt(window, cx);
350 editor
351 }));
352
353 if let Some(context_editor) = self.context_editor.as_ref() {
354 context_editor.focus_handle(cx).focus(window);
355 }
356 }
357
358 fn deploy_prompt_library(
359 &mut self,
360 _: &OpenPromptLibrary,
361 _window: &mut Window,
362 cx: &mut Context<Self>,
363 ) {
364 open_prompt_library(
365 self.language_registry.clone(),
366 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
367 Arc::new(|| {
368 Box::new(SlashCommandCompletionProvider::new(
369 Arc::new(SlashCommandWorkingSet::default()),
370 None,
371 None,
372 ))
373 }),
374 cx,
375 )
376 .detach_and_log_err(cx);
377 }
378
379 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
380 self.thread_store
381 .update(cx, |thread_store, cx| thread_store.reload(cx))
382 .detach_and_log_err(cx);
383 self.active_view = ActiveView::History;
384 self.history.focus_handle(cx).focus(window);
385 cx.notify();
386 }
387
388 pub(crate) fn open_saved_prompt_editor(
389 &mut self,
390 path: PathBuf,
391 window: &mut Window,
392 cx: &mut Context<Self>,
393 ) -> Task<Result<()>> {
394 let context = self
395 .context_store
396 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
397 let fs = self.fs.clone();
398 let project = self.project.clone();
399 let workspace = self.workspace.clone();
400
401 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
402
403 cx.spawn_in(window, async move |this, cx| {
404 let context = context.await?;
405 this.update_in(cx, |this, window, cx| {
406 let editor = cx.new(|cx| {
407 ContextEditor::for_context(
408 context,
409 fs,
410 workspace,
411 project,
412 lsp_adapter_delegate,
413 window,
414 cx,
415 )
416 });
417 this.active_view = ActiveView::PromptEditor;
418 this.context_editor = Some(editor);
419
420 anyhow::Ok(())
421 })??;
422 Ok(())
423 })
424 }
425
426 pub(crate) fn open_thread(
427 &mut self,
428 thread_id: &ThreadId,
429 window: &mut Window,
430 cx: &mut Context<Self>,
431 ) -> Task<Result<()>> {
432 let open_thread_task = self
433 .thread_store
434 .update(cx, |this, cx| this.open_thread(thread_id, cx));
435
436 cx.spawn_in(window, async move |this, cx| {
437 let thread = open_thread_task.await?;
438 this.update_in(cx, |this, window, cx| {
439 this.active_view = ActiveView::Thread;
440 let message_editor_context_store = cx.new(|_cx| {
441 crate::context_store::ContextStore::new(
442 this.workspace.clone(),
443 Some(this.thread_store.downgrade()),
444 )
445 });
446 this.thread = cx.new(|cx| {
447 ActiveThread::new(
448 thread.clone(),
449 this.thread_store.clone(),
450 this.language_registry.clone(),
451 message_editor_context_store.clone(),
452 this.workspace.clone(),
453 window,
454 cx,
455 )
456 });
457 this.message_editor = cx.new(|cx| {
458 MessageEditor::new(
459 this.fs.clone(),
460 this.workspace.clone(),
461 message_editor_context_store,
462 this.thread_store.downgrade(),
463 thread,
464 window,
465 cx,
466 )
467 });
468 this.message_editor.focus_handle(cx).focus(window);
469 })
470 })
471 }
472
473 pub fn open_agent_diff(
474 &mut self,
475 _: &OpenAgentDiff,
476 window: &mut Window,
477 cx: &mut Context<Self>,
478 ) {
479 let thread = self.thread.read(cx).thread().clone();
480 AgentDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
481 }
482
483 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
484 let context_server_manager = self.thread_store.read(cx).context_server_manager();
485 let tools = self.thread_store.read(cx).tools();
486 let fs = self.fs.clone();
487
488 self.active_view = ActiveView::Configuration;
489 self.configuration =
490 Some(cx.new(|cx| {
491 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
492 }));
493
494 if let Some(configuration) = self.configuration.as_ref() {
495 self.configuration_subscription = Some(cx.subscribe_in(
496 configuration,
497 window,
498 Self::handle_assistant_configuration_event,
499 ));
500
501 configuration.focus_handle(cx).focus(window);
502 }
503 }
504
505 pub(crate) fn open_active_thread_as_markdown(
506 &mut self,
507 _: &OpenActiveThreadAsMarkdown,
508 window: &mut Window,
509 cx: &mut Context<Self>,
510 ) {
511 let Some(workspace) = self
512 .workspace
513 .upgrade()
514 .ok_or_else(|| anyhow!("workspace dropped"))
515 .log_err()
516 else {
517 return;
518 };
519
520 let markdown_language_task = workspace
521 .read(cx)
522 .app_state()
523 .languages
524 .language_for_name("Markdown");
525 let thread = self.active_thread(cx);
526 cx.spawn_in(window, async move |_this, cx| {
527 let markdown_language = markdown_language_task.await?;
528
529 workspace.update_in(cx, |workspace, window, cx| {
530 let thread = thread.read(cx);
531 let markdown = thread.to_markdown(cx)?;
532 let thread_summary = thread
533 .summary()
534 .map(|summary| summary.to_string())
535 .unwrap_or_else(|| "Thread".to_string());
536
537 let project = workspace.project().clone();
538 let buffer = project.update(cx, |project, cx| {
539 project.create_local_buffer(&markdown, Some(markdown_language), cx)
540 });
541 let buffer = cx.new(|cx| {
542 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
543 });
544
545 workspace.add_item_to_active_pane(
546 Box::new(cx.new(|cx| {
547 let mut editor =
548 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
549 editor.set_breadcrumb_header(thread_summary);
550 editor
551 })),
552 None,
553 true,
554 window,
555 cx,
556 );
557
558 anyhow::Ok(())
559 })
560 })
561 .detach_and_log_err(cx);
562 }
563
564 fn handle_assistant_configuration_event(
565 &mut self,
566 _entity: &Entity<AssistantConfiguration>,
567 event: &AssistantConfigurationEvent,
568 window: &mut Window,
569 cx: &mut Context<Self>,
570 ) {
571 match event {
572 AssistantConfigurationEvent::NewThread(provider) => {
573 if LanguageModelRegistry::read_global(cx)
574 .default_model()
575 .map_or(true, |model| model.provider.id() != provider.id())
576 {
577 if let Some(model) = provider.default_model(cx) {
578 update_settings_file::<AssistantSettings>(
579 self.fs.clone(),
580 cx,
581 move |settings, _| settings.set_model(model),
582 );
583 }
584 }
585
586 self.new_thread(&NewThread::default(), window, cx);
587 }
588 }
589 }
590
591 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
592 self.thread.read(cx).thread().clone()
593 }
594
595 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {
596 self.thread_store
597 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
598 .detach_and_log_err(cx);
599 }
600
601 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
602 self.context_editor.clone()
603 }
604
605 pub(crate) fn delete_context(&mut self, path: PathBuf, cx: &mut Context<Self>) {
606 self.context_store
607 .update(cx, |this, cx| this.delete_local_context(path, cx))
608 .detach_and_log_err(cx);
609 }
610}
611
612impl Focusable for AssistantPanel {
613 fn focus_handle(&self, cx: &App) -> FocusHandle {
614 match self.active_view {
615 ActiveView::Thread => self.message_editor.focus_handle(cx),
616 ActiveView::History => self.history.focus_handle(cx),
617 ActiveView::PromptEditor => {
618 if let Some(context_editor) = self.context_editor.as_ref() {
619 context_editor.focus_handle(cx)
620 } else {
621 cx.focus_handle()
622 }
623 }
624 ActiveView::Configuration => {
625 if let Some(configuration) = self.configuration.as_ref() {
626 configuration.focus_handle(cx)
627 } else {
628 cx.focus_handle()
629 }
630 }
631 }
632 }
633}
634
635impl EventEmitter<PanelEvent> for AssistantPanel {}
636
637impl Panel for AssistantPanel {
638 fn persistent_name() -> &'static str {
639 "AgentPanel"
640 }
641
642 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
643 match AssistantSettings::get_global(cx).dock {
644 AssistantDockPosition::Left => DockPosition::Left,
645 AssistantDockPosition::Bottom => DockPosition::Bottom,
646 AssistantDockPosition::Right => DockPosition::Right,
647 }
648 }
649
650 fn position_is_valid(&self, _: DockPosition) -> bool {
651 true
652 }
653
654 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
655 settings::update_settings_file::<AssistantSettings>(
656 self.fs.clone(),
657 cx,
658 move |settings, _| {
659 let dock = match position {
660 DockPosition::Left => AssistantDockPosition::Left,
661 DockPosition::Bottom => AssistantDockPosition::Bottom,
662 DockPosition::Right => AssistantDockPosition::Right,
663 };
664 settings.set_dock(dock);
665 },
666 );
667 }
668
669 fn size(&self, window: &Window, cx: &App) -> Pixels {
670 let settings = AssistantSettings::get_global(cx);
671 match self.position(window, cx) {
672 DockPosition::Left | DockPosition::Right => {
673 self.width.unwrap_or(settings.default_width)
674 }
675 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
676 }
677 }
678
679 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
680 match self.position(window, cx) {
681 DockPosition::Left | DockPosition::Right => self.width = size,
682 DockPosition::Bottom => self.height = size,
683 }
684 cx.notify();
685 }
686
687 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
688
689 fn remote_id() -> Option<proto::PanelId> {
690 Some(proto::PanelId::AssistantPanel)
691 }
692
693 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
694 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
695 .then_some(IconName::ZedAssistant)
696 }
697
698 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
699 Some("Agent Panel")
700 }
701
702 fn toggle_action(&self) -> Box<dyn Action> {
703 Box::new(ToggleFocus)
704 }
705
706 fn activation_priority(&self) -> u32 {
707 3
708 }
709
710 fn enabled(&self, cx: &App) -> bool {
711 AssistantSettings::get_global(cx).enabled
712 }
713}
714
715impl AssistantPanel {
716 fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
717 let active_thread = self.thread.read(cx);
718 let thread = active_thread.thread().read(cx);
719 let token_usage = thread.total_token_usage(cx);
720 let thread_id = thread.id().clone();
721
722 let is_generating = thread.is_generating();
723 let is_empty = active_thread.is_empty();
724 let focus_handle = self.focus_handle(cx);
725
726 let title = match self.active_view {
727 ActiveView::Thread => {
728 if is_empty {
729 active_thread.summary_or_default(cx)
730 } else {
731 active_thread
732 .summary(cx)
733 .unwrap_or_else(|| SharedString::from("Loading Summary…"))
734 }
735 }
736 ActiveView::PromptEditor => self
737 .context_editor
738 .as_ref()
739 .map(|context_editor| {
740 SharedString::from(context_editor.read(cx).title(cx).to_string())
741 })
742 .unwrap_or_else(|| SharedString::from("Loading Summary…")),
743 ActiveView::History => "History".into(),
744 ActiveView::Configuration => "Settings".into(),
745 };
746
747 let show_token_count = match self.active_view {
748 ActiveView::Thread => !is_empty,
749 ActiveView::PromptEditor => self.context_editor.is_some(),
750 _ => false,
751 };
752
753 h_flex()
754 .id("assistant-toolbar")
755 .h(Tab::container_height(cx))
756 .flex_none()
757 .justify_between()
758 .gap(DynamicSpacing::Base08.rems(cx))
759 .bg(cx.theme().colors().tab_bar_background)
760 .border_b_1()
761 .border_color(cx.theme().colors().border)
762 .child(
763 div()
764 .id("title")
765 .overflow_x_scroll()
766 .px(DynamicSpacing::Base08.rems(cx))
767 .child(Label::new(title).truncate()),
768 )
769 .child(
770 h_flex()
771 .h_full()
772 .pl_2()
773 .gap_2()
774 .bg(cx.theme().colors().tab_bar_background)
775 .when(show_token_count, |parent| match self.active_view {
776 ActiveView::Thread => {
777 if token_usage.total == 0 {
778 return parent;
779 }
780
781 let token_color = match token_usage.ratio {
782 TokenUsageRatio::Normal => Color::Muted,
783 TokenUsageRatio::Warning => Color::Warning,
784 TokenUsageRatio::Exceeded => Color::Error,
785 };
786
787 parent.child(
788 h_flex()
789 .gap_0p5()
790 .child(
791 Label::new(assistant_context_editor::humanize_token_count(
792 token_usage.total,
793 ))
794 .size(LabelSize::Small)
795 .color(token_color)
796 .map(|label| {
797 if is_generating {
798 label
799 .with_animation(
800 "used-tokens-label",
801 Animation::new(Duration::from_secs(2))
802 .repeat()
803 .with_easing(pulsating_between(
804 0.6, 1.,
805 )),
806 |label, delta| label.alpha(delta),
807 )
808 .into_any()
809 } else {
810 label.into_any_element()
811 }
812 }),
813 )
814 .child(
815 Label::new("/").size(LabelSize::Small).color(Color::Muted),
816 )
817 .child(
818 Label::new(assistant_context_editor::humanize_token_count(
819 token_usage.max,
820 ))
821 .size(LabelSize::Small)
822 .color(Color::Muted),
823 ),
824 )
825 }
826 ActiveView::PromptEditor => {
827 let Some(editor) = self.context_editor.as_ref() else {
828 return parent;
829 };
830 let Some(element) = render_remaining_tokens(editor, cx) else {
831 return parent;
832 };
833 parent.child(element)
834 }
835 _ => parent,
836 })
837 .child(
838 h_flex()
839 .h_full()
840 .px(DynamicSpacing::Base08.rems(cx))
841 .border_l_1()
842 .border_color(cx.theme().colors().border)
843 .gap(DynamicSpacing::Base02.rems(cx))
844 .child(
845 IconButton::new("new", IconName::Plus)
846 .icon_size(IconSize::Small)
847 .style(ButtonStyle::Subtle)
848 .tooltip(move |window, cx| {
849 Tooltip::for_action_in(
850 "New Thread",
851 &NewThread::default(),
852 &focus_handle,
853 window,
854 cx,
855 )
856 })
857 .on_click(move |_event, window, cx| {
858 window.dispatch_action(
859 NewThread::default().boxed_clone(),
860 cx,
861 );
862 }),
863 )
864 .child(
865 PopoverMenu::new("assistant-menu")
866 .trigger_with_tooltip(
867 IconButton::new("new", IconName::Ellipsis)
868 .icon_size(IconSize::Small)
869 .style(ButtonStyle::Subtle),
870 Tooltip::text("Toggle Agent Menu"),
871 )
872 .anchor(Corner::TopRight)
873 .with_handle(self.assistant_dropdown_menu_handle.clone())
874 .menu(move |window, cx| {
875 Some(ContextMenu::build(
876 window,
877 cx,
878 |menu, _window, _cx| {
879 menu.action(
880 "New Thread",
881 Box::new(NewThread {
882 from_thread_id: None,
883 }),
884 )
885 .action(
886 "New Prompt Editor",
887 NewPromptEditor.boxed_clone(),
888 )
889 .when(!is_empty, |menu| {
890 menu.action(
891 "Continue in New Thread",
892 Box::new(NewThread {
893 from_thread_id: Some(thread_id.clone()),
894 }),
895 )
896 })
897 .separator()
898 .action("History", OpenHistory.boxed_clone())
899 .action("Settings", OpenConfiguration.boxed_clone())
900 },
901 ))
902 }),
903 ),
904 ),
905 )
906 }
907
908 fn render_active_thread_or_empty_state(
909 &self,
910 window: &mut Window,
911 cx: &mut Context<Self>,
912 ) -> AnyElement {
913 if self.thread.read(cx).is_empty() {
914 return self
915 .render_thread_empty_state(window, cx)
916 .into_any_element();
917 }
918
919 self.thread.clone().into_any_element()
920 }
921
922 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
923 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
924 return Some(ConfigurationError::NoProvider);
925 };
926
927 if !model.provider.is_authenticated(cx) {
928 return Some(ConfigurationError::ProviderNotAuthenticated);
929 }
930
931 if model.provider.must_accept_terms(cx) {
932 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
933 model.provider,
934 ));
935 }
936
937 None
938 }
939
940 fn render_thread_empty_state(
941 &self,
942 window: &mut Window,
943 cx: &mut Context<Self>,
944 ) -> impl IntoElement {
945 let recent_history = self
946 .history_store
947 .update(cx, |this, cx| this.recent_entries(6, cx));
948
949 let configuration_error = self.configuration_error(cx);
950 let no_error = configuration_error.is_none();
951 let focus_handle = self.focus_handle(cx);
952
953 v_flex()
954 .size_full()
955 .when(recent_history.is_empty(), |this| {
956 let configuration_error_ref = &configuration_error;
957 this.child(
958 v_flex()
959 .size_full()
960 .max_w_80()
961 .mx_auto()
962 .justify_center()
963 .items_center()
964 .gap_1()
965 .child(
966 h_flex().child(
967 Headline::new("Welcome to the Agent Panel")
968 ),
969 )
970 .when(no_error, |parent| {
971 parent
972 .child(
973 h_flex().child(
974 Label::new("Ask and build anything.")
975 .color(Color::Muted)
976 .mb_2p5(),
977 ),
978 )
979 .child(
980 Button::new("new-thread", "Start New Thread")
981 .icon(IconName::Plus)
982 .icon_position(IconPosition::Start)
983 .icon_size(IconSize::Small)
984 .icon_color(Color::Muted)
985 .full_width()
986 .key_binding(KeyBinding::for_action_in(
987 &NewThread::default(),
988 &focus_handle,
989 window,
990 cx,
991 ))
992 .on_click(|_event, window, cx| {
993 window.dispatch_action(NewThread::default().boxed_clone(), cx)
994 }),
995 )
996 .child(
997 Button::new("context", "Add Context")
998 .icon(IconName::FileCode)
999 .icon_position(IconPosition::Start)
1000 .icon_size(IconSize::Small)
1001 .icon_color(Color::Muted)
1002 .full_width()
1003 .key_binding(KeyBinding::for_action_in(
1004 &ToggleContextPicker,
1005 &focus_handle,
1006 window,
1007 cx,
1008 ))
1009 .on_click(|_event, window, cx| {
1010 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1011 }),
1012 )
1013 .child(
1014 Button::new("mode", "Switch Model")
1015 .icon(IconName::DatabaseZap)
1016 .icon_position(IconPosition::Start)
1017 .icon_size(IconSize::Small)
1018 .icon_color(Color::Muted)
1019 .full_width()
1020 .key_binding(KeyBinding::for_action_in(
1021 &ToggleModelSelector,
1022 &focus_handle,
1023 window,
1024 cx,
1025 ))
1026 .on_click(|_event, window, cx| {
1027 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1028 }),
1029 )
1030 .child(
1031 Button::new("settings", "View Settings")
1032 .icon(IconName::Settings)
1033 .icon_position(IconPosition::Start)
1034 .icon_size(IconSize::Small)
1035 .icon_color(Color::Muted)
1036 .full_width()
1037 .key_binding(KeyBinding::for_action_in(
1038 &OpenConfiguration,
1039 &focus_handle,
1040 window,
1041 cx,
1042 ))
1043 .on_click(|_event, window, cx| {
1044 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1045 }),
1046 )
1047 })
1048 .map(|parent| {
1049 match configuration_error_ref {
1050 Some(ConfigurationError::ProviderNotAuthenticated)
1051 | Some(ConfigurationError::NoProvider) => {
1052 parent
1053 .child(
1054 h_flex().child(
1055 Label::new("To start using the agent, configure at least one LLM provider.")
1056 .color(Color::Muted)
1057 .mb_2p5()
1058 )
1059 )
1060 .child(
1061 Button::new("settings", "Configure a Provider")
1062 .icon(IconName::Settings)
1063 .icon_position(IconPosition::Start)
1064 .icon_size(IconSize::Small)
1065 .icon_color(Color::Muted)
1066 .full_width()
1067 .key_binding(KeyBinding::for_action_in(
1068 &OpenConfiguration,
1069 &focus_handle,
1070 window,
1071 cx,
1072 ))
1073 .on_click(|_event, window, cx| {
1074 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1075 }),
1076 )
1077 }
1078 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1079 parent.children(
1080 provider.render_accept_terms(
1081 LanguageModelProviderTosView::ThreadFreshStart,
1082 cx,
1083 ),
1084 )
1085 }
1086 None => parent,
1087 }
1088 })
1089 )
1090 })
1091 .when(!recent_history.is_empty(), |parent| {
1092 let focus_handle = focus_handle.clone();
1093 let configuration_error_ref = &configuration_error;
1094
1095 parent
1096 .p_1p5()
1097 .justify_end()
1098 .gap_1()
1099 .child(
1100 h_flex()
1101 .pl_1p5()
1102 .pb_1()
1103 .w_full()
1104 .justify_between()
1105 .border_b_1()
1106 .border_color(cx.theme().colors().border_variant)
1107 .child(
1108 Label::new("Past Interactions")
1109 .size(LabelSize::Small)
1110 .color(Color::Muted),
1111 )
1112 .child(
1113 Button::new("view-history", "View All")
1114 .style(ButtonStyle::Subtle)
1115 .label_size(LabelSize::Small)
1116 .key_binding(
1117 KeyBinding::for_action_in(
1118 &OpenHistory,
1119 &self.focus_handle(cx),
1120 window,
1121 cx,
1122 ).map(|kb| kb.size(rems_from_px(12.))),
1123 )
1124 .on_click(move |_event, window, cx| {
1125 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1126 }),
1127 ),
1128 )
1129 .child(
1130 v_flex()
1131 .gap_1()
1132 .children(
1133 recent_history.into_iter().map(|entry| {
1134 // TODO: Add keyboard navigation.
1135 match entry {
1136 HistoryEntry::Thread(thread) => {
1137 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1138 .into_any_element()
1139 }
1140 HistoryEntry::Context(context) => {
1141 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1142 .into_any_element()
1143 }
1144 }
1145 }),
1146 )
1147 )
1148 .map(|parent| {
1149 match configuration_error_ref {
1150 Some(ConfigurationError::ProviderNotAuthenticated)
1151 | Some(ConfigurationError::NoProvider) => {
1152 parent
1153 .child(
1154 Banner::new()
1155 .severity(ui::Severity::Warning)
1156 .children(
1157 Label::new(
1158 "Configure at least one LLM provider to start using the panel.",
1159 )
1160 .size(LabelSize::Small),
1161 )
1162 .action_slot(
1163 Button::new("settings", "Configure Provider")
1164 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1165 .label_size(LabelSize::Small)
1166 .key_binding(
1167 KeyBinding::for_action_in(
1168 &OpenConfiguration,
1169 &focus_handle,
1170 window,
1171 cx,
1172 )
1173 .map(|kb| kb.size(rems_from_px(12.))),
1174 )
1175 .on_click(|_event, window, cx| {
1176 window.dispatch_action(
1177 OpenConfiguration.boxed_clone(),
1178 cx,
1179 )
1180 }),
1181 ),
1182 )
1183 }
1184 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1185 parent
1186 .child(
1187 Banner::new()
1188 .severity(ui::Severity::Warning)
1189 .children(
1190 h_flex()
1191 .w_full()
1192 .children(
1193 provider.render_accept_terms(
1194 LanguageModelProviderTosView::ThreadtEmptyState,
1195 cx,
1196 ),
1197 ),
1198 ),
1199 )
1200 }
1201 None => parent,
1202 }
1203 })
1204 })
1205 }
1206
1207 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1208 let last_error = self.thread.read(cx).last_error()?;
1209
1210 Some(
1211 div()
1212 .absolute()
1213 .right_3()
1214 .bottom_12()
1215 .max_w_96()
1216 .py_2()
1217 .px_3()
1218 .elevation_2(cx)
1219 .occlude()
1220 .child(match last_error {
1221 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1222 ThreadError::MaxMonthlySpendReached => {
1223 self.render_max_monthly_spend_reached_error(cx)
1224 }
1225 ThreadError::Message { header, message } => {
1226 self.render_error_message(header, message, cx)
1227 }
1228 })
1229 .into_any(),
1230 )
1231 }
1232
1233 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1234 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.";
1235
1236 v_flex()
1237 .gap_0p5()
1238 .child(
1239 h_flex()
1240 .gap_1p5()
1241 .items_center()
1242 .child(Icon::new(IconName::XCircle).color(Color::Error))
1243 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1244 )
1245 .child(
1246 div()
1247 .id("error-message")
1248 .max_h_24()
1249 .overflow_y_scroll()
1250 .child(Label::new(ERROR_MESSAGE)),
1251 )
1252 .child(
1253 h_flex()
1254 .justify_end()
1255 .mt_1()
1256 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1257 |this, _, _, cx| {
1258 this.thread.update(cx, |this, _cx| {
1259 this.clear_last_error();
1260 });
1261
1262 cx.open_url(&zed_urls::account_url(cx));
1263 cx.notify();
1264 },
1265 )))
1266 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1267 |this, _, _, cx| {
1268 this.thread.update(cx, |this, _cx| {
1269 this.clear_last_error();
1270 });
1271
1272 cx.notify();
1273 },
1274 ))),
1275 )
1276 .into_any()
1277 }
1278
1279 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1280 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1281
1282 v_flex()
1283 .gap_0p5()
1284 .child(
1285 h_flex()
1286 .gap_1p5()
1287 .items_center()
1288 .child(Icon::new(IconName::XCircle).color(Color::Error))
1289 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1290 )
1291 .child(
1292 div()
1293 .id("error-message")
1294 .max_h_24()
1295 .overflow_y_scroll()
1296 .child(Label::new(ERROR_MESSAGE)),
1297 )
1298 .child(
1299 h_flex()
1300 .justify_end()
1301 .mt_1()
1302 .child(
1303 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1304 cx.listener(|this, _, _, cx| {
1305 this.thread.update(cx, |this, _cx| {
1306 this.clear_last_error();
1307 });
1308
1309 cx.open_url(&zed_urls::account_url(cx));
1310 cx.notify();
1311 }),
1312 ),
1313 )
1314 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1315 |this, _, _, cx| {
1316 this.thread.update(cx, |this, _cx| {
1317 this.clear_last_error();
1318 });
1319
1320 cx.notify();
1321 },
1322 ))),
1323 )
1324 .into_any()
1325 }
1326
1327 fn render_error_message(
1328 &self,
1329 header: SharedString,
1330 message: SharedString,
1331 cx: &mut Context<Self>,
1332 ) -> AnyElement {
1333 v_flex()
1334 .gap_0p5()
1335 .child(
1336 h_flex()
1337 .gap_1p5()
1338 .items_center()
1339 .child(Icon::new(IconName::XCircle).color(Color::Error))
1340 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1341 )
1342 .child(
1343 div()
1344 .id("error-message")
1345 .max_h_32()
1346 .overflow_y_scroll()
1347 .child(Label::new(message)),
1348 )
1349 .child(
1350 h_flex()
1351 .justify_end()
1352 .mt_1()
1353 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1354 |this, _, _, cx| {
1355 this.thread.update(cx, |this, _cx| {
1356 this.clear_last_error();
1357 });
1358
1359 cx.notify();
1360 },
1361 ))),
1362 )
1363 .into_any()
1364 }
1365
1366 fn key_context(&self) -> KeyContext {
1367 let mut key_context = KeyContext::new_with_defaults();
1368 key_context.add("AgentPanel");
1369 if matches!(self.active_view, ActiveView::PromptEditor) {
1370 key_context.add("prompt_editor");
1371 }
1372 key_context
1373 }
1374}
1375
1376impl Render for AssistantPanel {
1377 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1378 v_flex()
1379 .key_context(self.key_context())
1380 .justify_between()
1381 .size_full()
1382 .on_action(cx.listener(Self::cancel))
1383 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1384 this.new_thread(action, window, cx);
1385 }))
1386 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1387 this.open_history(window, cx);
1388 }))
1389 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1390 this.open_configuration(window, cx);
1391 }))
1392 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1393 .on_action(cx.listener(Self::deploy_prompt_library))
1394 .on_action(cx.listener(Self::open_agent_diff))
1395 .child(self.render_toolbar(window, cx))
1396 .map(|parent| match self.active_view {
1397 ActiveView::Thread => parent
1398 .child(self.render_active_thread_or_empty_state(window, cx))
1399 .child(h_flex().child(self.message_editor.clone()))
1400 .children(self.render_last_error(cx)),
1401 ActiveView::History => parent.child(self.history.clone()),
1402 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1403 ActiveView::Configuration => parent.children(self.configuration.clone()),
1404 })
1405 }
1406}
1407
1408struct PromptLibraryInlineAssist {
1409 workspace: WeakEntity<Workspace>,
1410}
1411
1412impl PromptLibraryInlineAssist {
1413 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1414 Self { workspace }
1415 }
1416}
1417
1418impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1419 fn assist(
1420 &self,
1421 prompt_editor: &Entity<Editor>,
1422 _initial_prompt: Option<String>,
1423 window: &mut Window,
1424 cx: &mut Context<PromptLibrary>,
1425 ) {
1426 InlineAssistant::update_global(cx, |assistant, cx| {
1427 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1428 })
1429 }
1430
1431 fn focus_assistant_panel(
1432 &self,
1433 workspace: &mut Workspace,
1434 window: &mut Window,
1435 cx: &mut Context<Workspace>,
1436 ) -> bool {
1437 workspace
1438 .focus_panel::<AssistantPanel>(window, cx)
1439 .is_some()
1440 }
1441}
1442
1443pub struct ConcreteAssistantPanelDelegate;
1444
1445impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1446 fn active_context_editor(
1447 &self,
1448 workspace: &mut Workspace,
1449 _window: &mut Window,
1450 cx: &mut Context<Workspace>,
1451 ) -> Option<Entity<ContextEditor>> {
1452 let panel = workspace.panel::<AssistantPanel>(cx)?;
1453 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1454 }
1455
1456 fn open_saved_context(
1457 &self,
1458 workspace: &mut Workspace,
1459 path: std::path::PathBuf,
1460 window: &mut Window,
1461 cx: &mut Context<Workspace>,
1462 ) -> Task<Result<()>> {
1463 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1464 return Task::ready(Err(anyhow!("Agent panel not found")));
1465 };
1466
1467 panel.update(cx, |panel, cx| {
1468 panel.open_saved_prompt_editor(path, window, cx)
1469 })
1470 }
1471
1472 fn open_remote_context(
1473 &self,
1474 _workspace: &mut Workspace,
1475 _context_id: assistant_context_editor::ContextId,
1476 _window: &mut Window,
1477 _cx: &mut Context<Workspace>,
1478 ) -> Task<Result<Entity<ContextEditor>>> {
1479 Task::ready(Err(anyhow!("opening remote context not implemented")))
1480 }
1481
1482 fn quote_selection(
1483 &self,
1484 _workspace: &mut Workspace,
1485 _creases: Vec<(String, String)>,
1486 _window: &mut Window,
1487 _cx: &mut Context<Workspace>,
1488 ) {
1489 }
1490}