1use std::ops::Range;
2use std::path::Path;
3use std::sync::Arc;
4use std::time::Duration;
5
6use db::kvp::KEY_VALUE_STORE;
7use markdown::Markdown;
8use serde::{Deserialize, Serialize};
9
10use anyhow::{Result, anyhow};
11use assistant_context_editor::{
12 AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
13 SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
14 render_remaining_tokens,
15};
16use assistant_settings::{AssistantDockPosition, AssistantSettings};
17use assistant_slash_command::SlashCommandWorkingSet;
18use assistant_tool::ToolWorkingSet;
19
20use client::{UserStore, zed_urls};
21use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
22use fs::Fs;
23use gpui::{
24 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
25 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
26 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
27 linear_gradient, prelude::*, pulsating_between,
28};
29use language::LanguageRegistry;
30use language_model::{
31 LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
32};
33use language_model_selector::ToggleModelSelector;
34use project::{Project, ProjectPath, Worktree};
35use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
36use proto::Plan;
37use rules_library::{RulesLibrary, open_rules_library};
38use search::{BufferSearchBar, buffer_search};
39use settings::{Settings, update_settings_file};
40use theme::ThemeSettings;
41use time::UtcOffset;
42use ui::{
43 Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
44 ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
45};
46use util::{ResultExt as _, maybe};
47use workspace::dock::{DockPosition, Panel, PanelEvent};
48use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
49use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
50use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
51use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
52use zed_llm_client::UsageLimit;
53
54use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
55use crate::agent_diff::AgentDiff;
56use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
57use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
58use crate::message_editor::{MessageEditor, MessageEditorEvent};
59use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
60use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
61use crate::thread_store::ThreadStore;
62use crate::ui::AgentOnboardingModal;
63use crate::{
64 AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
65 Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
66 OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
67 ToggleNavigationMenu, ToggleOptionsMenu,
68};
69
70const AGENT_PANEL_KEY: &str = "agent_panel";
71
72#[derive(Serialize, Deserialize)]
73struct SerializedAssistantPanel {
74 width: Option<Pixels>,
75}
76
77pub fn init(cx: &mut App) {
78 cx.observe_new(
79 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
80 workspace
81 .register_action(|workspace, action: &NewThread, window, cx| {
82 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
83 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
84 workspace.focus_panel::<AssistantPanel>(window, cx);
85 }
86 })
87 .register_action(|workspace, _: &OpenHistory, window, cx| {
88 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
89 workspace.focus_panel::<AssistantPanel>(window, cx);
90 panel.update(cx, |panel, cx| panel.open_history(window, cx));
91 }
92 })
93 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
94 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
95 workspace.focus_panel::<AssistantPanel>(window, cx);
96 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
97 }
98 })
99 .register_action(|workspace, _: &NewTextThread, window, cx| {
100 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
101 workspace.focus_panel::<AssistantPanel>(window, cx);
102 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
103 }
104 })
105 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
106 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
107 workspace.focus_panel::<AssistantPanel>(window, cx);
108 panel.update(cx, |panel, cx| {
109 panel.deploy_rules_library(action, window, cx)
110 });
111 }
112 })
113 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
114 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
115 workspace.focus_panel::<AssistantPanel>(window, cx);
116 let thread = panel.read(cx).thread.read(cx).thread().clone();
117 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
118 }
119 })
120 .register_action(|workspace, _: &Follow, window, cx| {
121 workspace.follow(CollaboratorId::Agent, window, cx);
122 })
123 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
124 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
125 workspace.focus_panel::<AssistantPanel>(window, cx);
126 panel.update(cx, |panel, cx| {
127 panel.message_editor.update(cx, |editor, cx| {
128 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
129 });
130 });
131 }
132 })
133 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
134 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
135 workspace.focus_panel::<AssistantPanel>(window, cx);
136 panel.update(cx, |panel, cx| {
137 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
138 });
139 }
140 })
141 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
142 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
143 workspace.focus_panel::<AssistantPanel>(window, cx);
144 panel.update(cx, |panel, cx| {
145 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
146 });
147 }
148 })
149 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
150 AgentOnboardingModal::toggle(workspace, window, cx)
151 })
152 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
153 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
154 window.refresh();
155 })
156 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
157 set_trial_upsell_dismissed(false, cx);
158 });
159 },
160 )
161 .detach();
162}
163
164enum ActiveView {
165 Thread {
166 change_title_editor: Entity<Editor>,
167 thread: WeakEntity<Thread>,
168 _subscriptions: Vec<gpui::Subscription>,
169 },
170 PromptEditor {
171 context_editor: Entity<ContextEditor>,
172 title_editor: Entity<Editor>,
173 buffer_search_bar: Entity<BufferSearchBar>,
174 _subscriptions: Vec<gpui::Subscription>,
175 },
176 History,
177 Configuration,
178}
179
180impl ActiveView {
181 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
182 let summary = thread.read(cx).summary_or_default();
183
184 let editor = cx.new(|cx| {
185 let mut editor = Editor::single_line(window, cx);
186 editor.set_text(summary.clone(), window, cx);
187 editor
188 });
189
190 let subscriptions = vec![
191 window.subscribe(&editor, cx, {
192 {
193 let thread = thread.clone();
194 move |editor, event, window, cx| match event {
195 EditorEvent::BufferEdited => {
196 let new_summary = editor.read(cx).text(cx);
197
198 thread.update(cx, |thread, cx| {
199 thread.set_summary(new_summary, cx);
200 })
201 }
202 EditorEvent::Blurred => {
203 if editor.read(cx).text(cx).is_empty() {
204 let summary = thread.read(cx).summary_or_default();
205
206 editor.update(cx, |editor, cx| {
207 editor.set_text(summary, window, cx);
208 });
209 }
210 }
211 _ => {}
212 }
213 }
214 }),
215 window.subscribe(&thread, cx, {
216 let editor = editor.clone();
217 move |thread, event, window, cx| match event {
218 ThreadEvent::SummaryGenerated => {
219 let summary = thread.read(cx).summary_or_default();
220
221 editor.update(cx, |editor, cx| {
222 editor.set_text(summary, window, cx);
223 })
224 }
225 _ => {}
226 }
227 }),
228 ];
229
230 Self::Thread {
231 change_title_editor: editor,
232 thread: thread.downgrade(),
233 _subscriptions: subscriptions,
234 }
235 }
236
237 pub fn prompt_editor(
238 context_editor: Entity<ContextEditor>,
239 language_registry: Arc<LanguageRegistry>,
240 window: &mut Window,
241 cx: &mut App,
242 ) -> Self {
243 let title = context_editor.read(cx).title(cx).to_string();
244
245 let editor = cx.new(|cx| {
246 let mut editor = Editor::single_line(window, cx);
247 editor.set_text(title, window, cx);
248 editor
249 });
250
251 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
252 // cause a custom summary to be set. The presence of this custom summary would cause
253 // summarization to not happen.
254 let mut suppress_first_edit = true;
255
256 let subscriptions = vec![
257 window.subscribe(&editor, cx, {
258 {
259 let context_editor = context_editor.clone();
260 move |editor, event, window, cx| match event {
261 EditorEvent::BufferEdited => {
262 if suppress_first_edit {
263 suppress_first_edit = false;
264 return;
265 }
266 let new_summary = editor.read(cx).text(cx);
267
268 context_editor.update(cx, |context_editor, cx| {
269 context_editor
270 .context()
271 .update(cx, |assistant_context, cx| {
272 assistant_context.set_custom_summary(new_summary, cx);
273 })
274 })
275 }
276 EditorEvent::Blurred => {
277 if editor.read(cx).text(cx).is_empty() {
278 let summary = context_editor
279 .read(cx)
280 .context()
281 .read(cx)
282 .summary_or_default();
283
284 editor.update(cx, |editor, cx| {
285 editor.set_text(summary, window, cx);
286 });
287 }
288 }
289 _ => {}
290 }
291 }
292 }),
293 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
294 let editor = editor.clone();
295 move |assistant_context, event, window, cx| match event {
296 ContextEvent::SummaryGenerated => {
297 let summary = assistant_context.read(cx).summary_or_default();
298
299 editor.update(cx, |editor, cx| {
300 editor.set_text(summary, window, cx);
301 })
302 }
303 _ => {}
304 }
305 }),
306 ];
307
308 let buffer_search_bar =
309 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
310 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
311 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
312 });
313
314 Self::PromptEditor {
315 context_editor,
316 title_editor: editor,
317 buffer_search_bar,
318 _subscriptions: subscriptions,
319 }
320 }
321}
322
323pub struct AssistantPanel {
324 workspace: WeakEntity<Workspace>,
325 user_store: Entity<UserStore>,
326 project: Entity<Project>,
327 fs: Arc<dyn Fs>,
328 language_registry: Arc<LanguageRegistry>,
329 thread_store: Entity<ThreadStore>,
330 thread: Entity<ActiveThread>,
331 message_editor: Entity<MessageEditor>,
332 _active_thread_subscriptions: Vec<Subscription>,
333 _default_model_subscription: Subscription,
334 context_store: Entity<TextThreadStore>,
335 prompt_store: Option<Entity<PromptStore>>,
336 inline_assist_context_store: Entity<crate::context_store::ContextStore>,
337 configuration: Option<Entity<AssistantConfiguration>>,
338 configuration_subscription: Option<Subscription>,
339 local_timezone: UtcOffset,
340 active_view: ActiveView,
341 previous_view: Option<ActiveView>,
342 history_store: Entity<HistoryStore>,
343 history: Entity<ThreadHistory>,
344 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
345 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
346 assistant_navigation_menu: Option<Entity<ContextMenu>>,
347 width: Option<Pixels>,
348 height: Option<Pixels>,
349 pending_serialization: Option<Task<Result<()>>>,
350 hide_trial_upsell: bool,
351 _trial_markdown: Entity<Markdown>,
352}
353
354impl AssistantPanel {
355 fn serialize(&mut self, cx: &mut Context<Self>) {
356 let width = self.width;
357 self.pending_serialization = Some(cx.background_spawn(async move {
358 KEY_VALUE_STORE
359 .write_kvp(
360 AGENT_PANEL_KEY.into(),
361 serde_json::to_string(&SerializedAssistantPanel { width })?,
362 )
363 .await?;
364 anyhow::Ok(())
365 }));
366 }
367 pub fn load(
368 workspace: WeakEntity<Workspace>,
369 prompt_builder: Arc<PromptBuilder>,
370 mut cx: AsyncWindowContext,
371 ) -> Task<Result<Entity<Self>>> {
372 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
373 cx.spawn(async move |cx| {
374 let prompt_store = match prompt_store {
375 Ok(prompt_store) => prompt_store.await.ok(),
376 Err(_) => None,
377 };
378 let tools = cx.new(|_| ToolWorkingSet::default())?;
379 let thread_store = workspace
380 .update(cx, |workspace, cx| {
381 let project = workspace.project().clone();
382 ThreadStore::load(
383 project,
384 tools.clone(),
385 prompt_store.clone(),
386 prompt_builder.clone(),
387 cx,
388 )
389 })?
390 .await?;
391
392 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
393 let context_store = workspace
394 .update(cx, |workspace, cx| {
395 let project = workspace.project().clone();
396 assistant_context_editor::ContextStore::new(
397 project,
398 prompt_builder.clone(),
399 slash_commands,
400 cx,
401 )
402 })?
403 .await?;
404
405 let serialized_panel = if let Some(panel) = cx
406 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
407 .await
408 .log_err()
409 .flatten()
410 {
411 Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
412 } else {
413 None
414 };
415
416 let panel = workspace.update_in(cx, |workspace, window, cx| {
417 let panel = cx.new(|cx| {
418 Self::new(
419 workspace,
420 thread_store,
421 context_store,
422 prompt_store,
423 window,
424 cx,
425 )
426 });
427 if let Some(serialized_panel) = serialized_panel {
428 panel.update(cx, |panel, cx| {
429 panel.width = serialized_panel.width.map(|w| w.round());
430 cx.notify();
431 });
432 }
433 panel
434 })?;
435
436 Ok(panel)
437 })
438 }
439
440 fn new(
441 workspace: &Workspace,
442 thread_store: Entity<ThreadStore>,
443 context_store: Entity<TextThreadStore>,
444 prompt_store: Option<Entity<PromptStore>>,
445 window: &mut Window,
446 cx: &mut Context<Self>,
447 ) -> Self {
448 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
449 let fs = workspace.app_state().fs.clone();
450 let user_store = workspace.app_state().user_store.clone();
451 let project = workspace.project();
452 let language_registry = project.read(cx).languages().clone();
453 let workspace = workspace.weak_handle();
454 let weak_self = cx.entity().downgrade();
455
456 let message_editor_context_store = cx.new(|_cx| {
457 crate::context_store::ContextStore::new(
458 project.downgrade(),
459 Some(thread_store.downgrade()),
460 )
461 });
462 let inline_assist_context_store = cx.new(|_cx| {
463 crate::context_store::ContextStore::new(
464 project.downgrade(),
465 Some(thread_store.downgrade()),
466 )
467 });
468
469 let message_editor = cx.new(|cx| {
470 MessageEditor::new(
471 fs.clone(),
472 workspace.clone(),
473 user_store.clone(),
474 message_editor_context_store.clone(),
475 prompt_store.clone(),
476 thread_store.downgrade(),
477 context_store.downgrade(),
478 thread.clone(),
479 agent_panel_dock_position(cx),
480 window,
481 cx,
482 )
483 });
484
485 let message_editor_subscription =
486 cx.subscribe(&message_editor, |_, _, event, cx| match event {
487 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
488 cx.notify();
489 }
490 });
491
492 let thread_id = thread.read(cx).id().clone();
493 let history_store = cx.new(|cx| {
494 HistoryStore::new(
495 thread_store.clone(),
496 context_store.clone(),
497 [RecentEntry::Thread(thread_id, thread.clone())],
498 cx,
499 )
500 });
501
502 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
503
504 let active_view = ActiveView::thread(thread.clone(), window, cx);
505 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
506 if let ThreadEvent::MessageAdded(_) = &event {
507 // needed to leave empty state
508 cx.notify();
509 }
510 });
511 let active_thread = cx.new(|cx| {
512 ActiveThread::new(
513 thread.clone(),
514 thread_store.clone(),
515 context_store.clone(),
516 message_editor_context_store.clone(),
517 language_registry.clone(),
518 workspace.clone(),
519 window,
520 cx,
521 )
522 });
523 AgentDiff::set_active_thread(&workspace, &thread, window, cx);
524
525 let active_thread_subscription =
526 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
527 ActiveThreadEvent::EditingMessageTokenCountChanged => {
528 cx.notify();
529 }
530 });
531
532 let weak_panel = weak_self.clone();
533
534 window.defer(cx, move |window, cx| {
535 let panel = weak_panel.clone();
536 let assistant_navigation_menu =
537 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
538 let recently_opened = panel
539 .update(cx, |this, cx| {
540 this.history_store.update(cx, |history_store, cx| {
541 history_store.recently_opened_entries(cx)
542 })
543 })
544 .unwrap_or_default();
545
546 if !recently_opened.is_empty() {
547 menu = menu.header("Recently Opened");
548
549 for entry in recently_opened.iter() {
550 let summary = entry.summary(cx);
551
552 menu = menu.entry_with_end_slot_on_hover(
553 summary,
554 None,
555 {
556 let panel = panel.clone();
557 let entry = entry.clone();
558 move |window, cx| {
559 panel
560 .update(cx, {
561 let entry = entry.clone();
562 move |this, cx| match entry {
563 RecentEntry::Thread(_, thread) => {
564 this.open_thread(thread, window, cx)
565 }
566 RecentEntry::Context(context) => {
567 let Some(path) = context.read(cx).path()
568 else {
569 return;
570 };
571 this.open_saved_prompt_editor(
572 path.clone(),
573 window,
574 cx,
575 )
576 .detach_and_log_err(cx)
577 }
578 }
579 })
580 .ok();
581 }
582 },
583 IconName::Close,
584 "Close Entry".into(),
585 {
586 let panel = panel.clone();
587 let entry = entry.clone();
588 move |_window, cx| {
589 panel
590 .update(cx, |this, cx| {
591 this.history_store.update(
592 cx,
593 |history_store, cx| {
594 history_store.remove_recently_opened_entry(
595 &entry, cx,
596 );
597 },
598 );
599 })
600 .ok();
601 }
602 },
603 );
604 }
605
606 menu = menu.separator();
607 }
608
609 menu.action("View All", Box::new(OpenHistory))
610 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
611 .fixed_width(px(320.).into())
612 .keep_open_on_confirm(false)
613 .key_context("NavigationMenu")
614 });
615 weak_panel
616 .update(cx, |panel, cx| {
617 cx.subscribe_in(
618 &assistant_navigation_menu,
619 window,
620 |_, menu, _: &DismissEvent, window, cx| {
621 menu.update(cx, |menu, _| {
622 menu.clear_selected();
623 });
624 cx.focus_self(window);
625 },
626 )
627 .detach();
628 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
629 })
630 .ok();
631 });
632
633 let _default_model_subscription = cx.subscribe(
634 &LanguageModelRegistry::global(cx),
635 |this, _, event: &language_model::Event, cx| match event {
636 language_model::Event::DefaultModelChanged => {
637 this.thread
638 .read(cx)
639 .thread()
640 .clone()
641 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
642 }
643 _ => {}
644 },
645 );
646
647 let trial_markdown = cx.new(|cx| {
648 Markdown::new(
649 include_str!("trial_markdown.md").into(),
650 Some(language_registry.clone()),
651 None,
652 cx,
653 )
654 });
655
656 Self {
657 active_view,
658 workspace,
659 user_store,
660 project: project.clone(),
661 fs: fs.clone(),
662 language_registry,
663 thread_store: thread_store.clone(),
664 thread: active_thread,
665 message_editor,
666 _active_thread_subscriptions: vec![
667 thread_subscription,
668 active_thread_subscription,
669 message_editor_subscription,
670 ],
671 _default_model_subscription,
672 context_store,
673 prompt_store,
674 configuration: None,
675 configuration_subscription: None,
676 local_timezone: UtcOffset::from_whole_seconds(
677 chrono::Local::now().offset().local_minus_utc(),
678 )
679 .unwrap(),
680 inline_assist_context_store,
681 previous_view: None,
682 history_store: history_store.clone(),
683 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
684 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
685 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
686 assistant_navigation_menu: None,
687 width: None,
688 height: None,
689 pending_serialization: None,
690 hide_trial_upsell: false,
691 _trial_markdown: trial_markdown,
692 }
693 }
694
695 pub fn toggle_focus(
696 workspace: &mut Workspace,
697 _: &ToggleFocus,
698 window: &mut Window,
699 cx: &mut Context<Workspace>,
700 ) {
701 if workspace
702 .panel::<Self>(cx)
703 .is_some_and(|panel| panel.read(cx).enabled(cx))
704 {
705 workspace.toggle_panel_focus::<Self>(window, cx);
706 }
707 }
708
709 pub(crate) fn local_timezone(&self) -> UtcOffset {
710 self.local_timezone
711 }
712
713 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
714 &self.prompt_store
715 }
716
717 pub(crate) fn inline_assist_context_store(
718 &self,
719 ) -> &Entity<crate::context_store::ContextStore> {
720 &self.inline_assist_context_store
721 }
722
723 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
724 &self.thread_store
725 }
726
727 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
728 &self.context_store
729 }
730
731 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
732 self.thread
733 .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
734 }
735
736 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
737 let thread = self
738 .thread_store
739 .update(cx, |this, cx| this.create_thread(cx));
740
741 let thread_view = ActiveView::thread(thread.clone(), window, cx);
742 self.set_active_view(thread_view, window, cx);
743
744 let context_store = cx.new(|_cx| {
745 crate::context_store::ContextStore::new(
746 self.project.downgrade(),
747 Some(self.thread_store.downgrade()),
748 )
749 });
750
751 if let Some(other_thread_id) = action.from_thread_id.clone() {
752 let other_thread_task = self
753 .thread_store
754 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
755
756 cx.spawn({
757 let context_store = context_store.clone();
758
759 async move |_panel, cx| {
760 let other_thread = other_thread_task.await?;
761
762 context_store.update(cx, |this, cx| {
763 this.add_thread(other_thread, false, cx);
764 })?;
765 anyhow::Ok(())
766 }
767 })
768 .detach_and_log_err(cx);
769 }
770
771 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
772 if let ThreadEvent::MessageAdded(_) = &event {
773 // needed to leave empty state
774 cx.notify();
775 }
776 });
777
778 self.thread = cx.new(|cx| {
779 ActiveThread::new(
780 thread.clone(),
781 self.thread_store.clone(),
782 self.context_store.clone(),
783 context_store.clone(),
784 self.language_registry.clone(),
785 self.workspace.clone(),
786 window,
787 cx,
788 )
789 });
790 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
791
792 let active_thread_subscription =
793 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
794 ActiveThreadEvent::EditingMessageTokenCountChanged => {
795 cx.notify();
796 }
797 });
798
799 self.message_editor = cx.new(|cx| {
800 MessageEditor::new(
801 self.fs.clone(),
802 self.workspace.clone(),
803 self.user_store.clone(),
804 context_store,
805 self.prompt_store.clone(),
806 self.thread_store.downgrade(),
807 self.context_store.downgrade(),
808 thread,
809 agent_panel_dock_position(cx),
810 window,
811 cx,
812 )
813 });
814 self.message_editor.focus_handle(cx).focus(window);
815
816 let message_editor_subscription =
817 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
818 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
819 cx.notify();
820 }
821 });
822
823 self._active_thread_subscriptions = vec![
824 thread_subscription,
825 active_thread_subscription,
826 message_editor_subscription,
827 ];
828 }
829
830 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
831 let context = self
832 .context_store
833 .update(cx, |context_store, cx| context_store.create(cx));
834 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
835 .log_err()
836 .flatten();
837
838 let context_editor = cx.new(|cx| {
839 let mut editor = ContextEditor::for_context(
840 context,
841 self.fs.clone(),
842 self.workspace.clone(),
843 self.project.clone(),
844 lsp_adapter_delegate,
845 window,
846 cx,
847 );
848 editor.insert_default_prompt(window, cx);
849 editor
850 });
851
852 self.set_active_view(
853 ActiveView::prompt_editor(
854 context_editor.clone(),
855 self.language_registry.clone(),
856 window,
857 cx,
858 ),
859 window,
860 cx,
861 );
862 context_editor.focus_handle(cx).focus(window);
863 }
864
865 fn deploy_rules_library(
866 &mut self,
867 action: &OpenRulesLibrary,
868 _window: &mut Window,
869 cx: &mut Context<Self>,
870 ) {
871 open_rules_library(
872 self.language_registry.clone(),
873 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
874 Arc::new(|| {
875 Box::new(SlashCommandCompletionProvider::new(
876 Arc::new(SlashCommandWorkingSet::default()),
877 None,
878 None,
879 ))
880 }),
881 action
882 .prompt_to_select
883 .map(|uuid| UserPromptId(uuid).into()),
884 cx,
885 )
886 .detach_and_log_err(cx);
887 }
888
889 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
890 if matches!(self.active_view, ActiveView::History) {
891 if let Some(previous_view) = self.previous_view.take() {
892 self.set_active_view(previous_view, window, cx);
893 }
894 } else {
895 self.thread_store
896 .update(cx, |thread_store, cx| thread_store.reload(cx))
897 .detach_and_log_err(cx);
898 self.set_active_view(ActiveView::History, window, cx);
899 }
900 cx.notify();
901 }
902
903 pub(crate) fn open_saved_prompt_editor(
904 &mut self,
905 path: Arc<Path>,
906 window: &mut Window,
907 cx: &mut Context<Self>,
908 ) -> Task<Result<()>> {
909 let context = self
910 .context_store
911 .update(cx, |store, cx| store.open_local_context(path, cx));
912 cx.spawn_in(window, async move |this, cx| {
913 let context = context.await?;
914 this.update_in(cx, |this, window, cx| {
915 this.open_prompt_editor(context, window, cx);
916 })
917 })
918 }
919
920 pub(crate) fn open_prompt_editor(
921 &mut self,
922 context: Entity<AssistantContext>,
923 window: &mut Window,
924 cx: &mut Context<Self>,
925 ) {
926 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
927 .log_err()
928 .flatten();
929 let editor = cx.new(|cx| {
930 ContextEditor::for_context(
931 context,
932 self.fs.clone(),
933 self.workspace.clone(),
934 self.project.clone(),
935 lsp_adapter_delegate,
936 window,
937 cx,
938 )
939 });
940 self.set_active_view(
941 ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
942 window,
943 cx,
944 );
945 }
946
947 pub(crate) fn open_thread_by_id(
948 &mut self,
949 thread_id: &ThreadId,
950 window: &mut Window,
951 cx: &mut Context<Self>,
952 ) -> Task<Result<()>> {
953 let open_thread_task = self
954 .thread_store
955 .update(cx, |this, cx| this.open_thread(thread_id, cx));
956 cx.spawn_in(window, async move |this, cx| {
957 let thread = open_thread_task.await?;
958 this.update_in(cx, |this, window, cx| {
959 this.open_thread(thread, window, cx);
960 anyhow::Ok(())
961 })??;
962 Ok(())
963 })
964 }
965
966 pub(crate) fn open_thread(
967 &mut self,
968 thread: Entity<Thread>,
969 window: &mut Window,
970 cx: &mut Context<Self>,
971 ) {
972 let thread_view = ActiveView::thread(thread.clone(), window, cx);
973 self.set_active_view(thread_view, window, cx);
974 let context_store = cx.new(|_cx| {
975 crate::context_store::ContextStore::new(
976 self.project.downgrade(),
977 Some(self.thread_store.downgrade()),
978 )
979 });
980 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
981 if let ThreadEvent::MessageAdded(_) = &event {
982 // needed to leave empty state
983 cx.notify();
984 }
985 });
986
987 self.thread = cx.new(|cx| {
988 ActiveThread::new(
989 thread.clone(),
990 self.thread_store.clone(),
991 self.context_store.clone(),
992 context_store.clone(),
993 self.language_registry.clone(),
994 self.workspace.clone(),
995 window,
996 cx,
997 )
998 });
999 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
1000
1001 let active_thread_subscription =
1002 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
1003 ActiveThreadEvent::EditingMessageTokenCountChanged => {
1004 cx.notify();
1005 }
1006 });
1007
1008 self.message_editor = cx.new(|cx| {
1009 MessageEditor::new(
1010 self.fs.clone(),
1011 self.workspace.clone(),
1012 self.user_store.clone(),
1013 context_store,
1014 self.prompt_store.clone(),
1015 self.thread_store.downgrade(),
1016 self.context_store.downgrade(),
1017 thread,
1018 agent_panel_dock_position(cx),
1019 window,
1020 cx,
1021 )
1022 });
1023 self.message_editor.focus_handle(cx).focus(window);
1024
1025 let message_editor_subscription =
1026 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
1027 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
1028 cx.notify();
1029 }
1030 });
1031
1032 self._active_thread_subscriptions = vec![
1033 thread_subscription,
1034 active_thread_subscription,
1035 message_editor_subscription,
1036 ];
1037 }
1038
1039 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1040 match self.active_view {
1041 ActiveView::Configuration | ActiveView::History => {
1042 self.active_view =
1043 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
1044 self.message_editor.focus_handle(cx).focus(window);
1045 cx.notify();
1046 }
1047 _ => {}
1048 }
1049 }
1050
1051 pub fn toggle_navigation_menu(
1052 &mut self,
1053 _: &ToggleNavigationMenu,
1054 window: &mut Window,
1055 cx: &mut Context<Self>,
1056 ) {
1057 self.assistant_navigation_menu_handle.toggle(window, cx);
1058 }
1059
1060 pub fn toggle_options_menu(
1061 &mut self,
1062 _: &ToggleOptionsMenu,
1063 window: &mut Window,
1064 cx: &mut Context<Self>,
1065 ) {
1066 self.assistant_dropdown_menu_handle.toggle(window, cx);
1067 }
1068
1069 pub fn increase_font_size(
1070 &mut self,
1071 action: &IncreaseBufferFontSize,
1072 _: &mut Window,
1073 cx: &mut Context<Self>,
1074 ) {
1075 self.adjust_font_size(action.persist, px(1.0), cx);
1076 }
1077
1078 pub fn decrease_font_size(
1079 &mut self,
1080 action: &DecreaseBufferFontSize,
1081 _: &mut Window,
1082 cx: &mut Context<Self>,
1083 ) {
1084 self.adjust_font_size(action.persist, px(-1.0), cx);
1085 }
1086
1087 fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1088 if persist {
1089 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, cx| {
1090 let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1091 let _ = settings
1092 .agent_font_size
1093 .insert(theme::clamp_font_size(agent_font_size).0);
1094 });
1095 } else {
1096 theme::adjust_agent_font_size(cx, |size| {
1097 *size += delta;
1098 });
1099 }
1100 }
1101
1102 pub fn reset_font_size(
1103 &mut self,
1104 action: &ResetBufferFontSize,
1105 _: &mut Window,
1106 cx: &mut Context<Self>,
1107 ) {
1108 if action.persist {
1109 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1110 settings.agent_font_size = None;
1111 });
1112 } else {
1113 theme::reset_agent_font_size(cx);
1114 }
1115 }
1116
1117 pub fn open_agent_diff(
1118 &mut self,
1119 _: &OpenAgentDiff,
1120 window: &mut Window,
1121 cx: &mut Context<Self>,
1122 ) {
1123 let thread = self.thread.read(cx).thread().clone();
1124 self.workspace
1125 .update(cx, |workspace, cx| {
1126 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1127 })
1128 .log_err();
1129 }
1130
1131 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1132 let context_server_store = self.project.read(cx).context_server_store();
1133 let tools = self.thread_store.read(cx).tools();
1134 let fs = self.fs.clone();
1135
1136 self.set_active_view(ActiveView::Configuration, window, cx);
1137 self.configuration =
1138 Some(cx.new(|cx| {
1139 AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
1140 }));
1141
1142 if let Some(configuration) = self.configuration.as_ref() {
1143 self.configuration_subscription = Some(cx.subscribe_in(
1144 configuration,
1145 window,
1146 Self::handle_assistant_configuration_event,
1147 ));
1148
1149 configuration.focus_handle(cx).focus(window);
1150 }
1151 }
1152
1153 pub(crate) fn open_active_thread_as_markdown(
1154 &mut self,
1155 _: &OpenActiveThreadAsMarkdown,
1156 window: &mut Window,
1157 cx: &mut Context<Self>,
1158 ) {
1159 let Some(workspace) = self
1160 .workspace
1161 .upgrade()
1162 .ok_or_else(|| anyhow!("workspace dropped"))
1163 .log_err()
1164 else {
1165 return;
1166 };
1167
1168 let Some(thread) = self.active_thread() else {
1169 return;
1170 };
1171
1172 active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
1173 .detach_and_log_err(cx);
1174 }
1175
1176 fn handle_assistant_configuration_event(
1177 &mut self,
1178 _entity: &Entity<AssistantConfiguration>,
1179 event: &AssistantConfigurationEvent,
1180 window: &mut Window,
1181 cx: &mut Context<Self>,
1182 ) {
1183 match event {
1184 AssistantConfigurationEvent::NewThread(provider) => {
1185 if LanguageModelRegistry::read_global(cx)
1186 .default_model()
1187 .map_or(true, |model| model.provider.id() != provider.id())
1188 {
1189 if let Some(model) = provider.default_model(cx) {
1190 update_settings_file::<AssistantSettings>(
1191 self.fs.clone(),
1192 cx,
1193 move |settings, _| settings.set_model(model),
1194 );
1195 }
1196 }
1197
1198 self.new_thread(&NewThread::default(), window, cx);
1199 }
1200 }
1201 }
1202
1203 pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1204 match &self.active_view {
1205 ActiveView::Thread { thread, .. } => thread.upgrade(),
1206 _ => None,
1207 }
1208 }
1209
1210 pub(crate) fn delete_thread(
1211 &mut self,
1212 thread_id: &ThreadId,
1213 cx: &mut Context<Self>,
1214 ) -> Task<Result<()>> {
1215 self.thread_store
1216 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1217 }
1218
1219 pub(crate) fn has_active_thread(&self) -> bool {
1220 matches!(self.active_view, ActiveView::Thread { .. })
1221 }
1222
1223 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1224 match &self.active_view {
1225 ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1226 _ => None,
1227 }
1228 }
1229
1230 pub(crate) fn delete_context(
1231 &mut self,
1232 path: Arc<Path>,
1233 cx: &mut Context<Self>,
1234 ) -> Task<Result<()>> {
1235 self.context_store
1236 .update(cx, |this, cx| this.delete_local_context(path, cx))
1237 }
1238
1239 fn set_active_view(
1240 &mut self,
1241 new_view: ActiveView,
1242 window: &mut Window,
1243 cx: &mut Context<Self>,
1244 ) {
1245 let current_is_history = matches!(self.active_view, ActiveView::History);
1246 let new_is_history = matches!(new_view, ActiveView::History);
1247
1248 match &self.active_view {
1249 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1250 if let Some(thread) = thread.upgrade() {
1251 if thread.read(cx).is_empty() {
1252 let id = thread.read(cx).id().clone();
1253 store.remove_recently_opened_thread(id, cx);
1254 }
1255 }
1256 }),
1257 _ => {}
1258 }
1259
1260 match &new_view {
1261 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1262 if let Some(thread) = thread.upgrade() {
1263 let id = thread.read(cx).id().clone();
1264 store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1265 }
1266 }),
1267 ActiveView::PromptEditor { context_editor, .. } => {
1268 self.history_store.update(cx, |store, cx| {
1269 let context = context_editor.read(cx).context().clone();
1270 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1271 })
1272 }
1273 _ => {}
1274 }
1275
1276 if current_is_history && !new_is_history {
1277 self.active_view = new_view;
1278 } else if !current_is_history && new_is_history {
1279 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1280 } else {
1281 if !new_is_history {
1282 self.previous_view = None;
1283 }
1284 self.active_view = new_view;
1285 }
1286
1287 self.focus_handle(cx).focus(window);
1288 }
1289}
1290
1291impl Focusable for AssistantPanel {
1292 fn focus_handle(&self, cx: &App) -> FocusHandle {
1293 match &self.active_view {
1294 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1295 ActiveView::History => self.history.focus_handle(cx),
1296 ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1297 ActiveView::Configuration => {
1298 if let Some(configuration) = self.configuration.as_ref() {
1299 configuration.focus_handle(cx)
1300 } else {
1301 cx.focus_handle()
1302 }
1303 }
1304 }
1305 }
1306}
1307
1308fn agent_panel_dock_position(cx: &App) -> DockPosition {
1309 match AssistantSettings::get_global(cx).dock {
1310 AssistantDockPosition::Left => DockPosition::Left,
1311 AssistantDockPosition::Bottom => DockPosition::Bottom,
1312 AssistantDockPosition::Right => DockPosition::Right,
1313 }
1314}
1315
1316impl EventEmitter<PanelEvent> for AssistantPanel {}
1317
1318impl Panel for AssistantPanel {
1319 fn persistent_name() -> &'static str {
1320 "AgentPanel"
1321 }
1322
1323 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1324 agent_panel_dock_position(cx)
1325 }
1326
1327 fn position_is_valid(&self, position: DockPosition) -> bool {
1328 position != DockPosition::Bottom
1329 }
1330
1331 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1332 self.message_editor.update(cx, |message_editor, cx| {
1333 message_editor.set_dock_position(position, cx);
1334 });
1335
1336 settings::update_settings_file::<AssistantSettings>(
1337 self.fs.clone(),
1338 cx,
1339 move |settings, _| {
1340 let dock = match position {
1341 DockPosition::Left => AssistantDockPosition::Left,
1342 DockPosition::Bottom => AssistantDockPosition::Bottom,
1343 DockPosition::Right => AssistantDockPosition::Right,
1344 };
1345 settings.set_dock(dock);
1346 },
1347 );
1348 }
1349
1350 fn size(&self, window: &Window, cx: &App) -> Pixels {
1351 let settings = AssistantSettings::get_global(cx);
1352 match self.position(window, cx) {
1353 DockPosition::Left | DockPosition::Right => {
1354 self.width.unwrap_or(settings.default_width)
1355 }
1356 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1357 }
1358 }
1359
1360 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1361 match self.position(window, cx) {
1362 DockPosition::Left | DockPosition::Right => self.width = size,
1363 DockPosition::Bottom => self.height = size,
1364 }
1365 self.serialize(cx);
1366 cx.notify();
1367 }
1368
1369 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1370
1371 fn remote_id() -> Option<proto::PanelId> {
1372 Some(proto::PanelId::AssistantPanel)
1373 }
1374
1375 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1376 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1377 .then_some(IconName::ZedAssistant)
1378 }
1379
1380 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1381 Some("Agent Panel")
1382 }
1383
1384 fn toggle_action(&self) -> Box<dyn Action> {
1385 Box::new(ToggleFocus)
1386 }
1387
1388 fn activation_priority(&self) -> u32 {
1389 3
1390 }
1391
1392 fn enabled(&self, cx: &App) -> bool {
1393 AssistantSettings::get_global(cx).enabled
1394 }
1395}
1396
1397impl AssistantPanel {
1398 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1399 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1400
1401 let content = match &self.active_view {
1402 ActiveView::Thread {
1403 change_title_editor,
1404 ..
1405 } => {
1406 let active_thread = self.thread.read(cx);
1407 let is_empty = active_thread.is_empty();
1408
1409 let summary = active_thread.summary(cx);
1410
1411 if is_empty {
1412 Label::new(Thread::DEFAULT_SUMMARY.clone())
1413 .truncate()
1414 .into_any_element()
1415 } else if summary.is_none() {
1416 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1417 .truncate()
1418 .into_any_element()
1419 } else {
1420 div()
1421 .w_full()
1422 .child(change_title_editor.clone())
1423 .into_any_element()
1424 }
1425 }
1426 ActiveView::PromptEditor {
1427 title_editor,
1428 context_editor,
1429 ..
1430 } => {
1431 let context_editor = context_editor.read(cx);
1432 let summary = context_editor.context().read(cx).summary();
1433
1434 match summary {
1435 None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1436 .truncate()
1437 .into_any_element(),
1438 Some(summary) => {
1439 if summary.done {
1440 div()
1441 .w_full()
1442 .child(title_editor.clone())
1443 .into_any_element()
1444 } else {
1445 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1446 .truncate()
1447 .into_any_element()
1448 }
1449 }
1450 }
1451 }
1452 ActiveView::History => Label::new("History").truncate().into_any_element(),
1453 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1454 };
1455
1456 h_flex()
1457 .key_context("TitleEditor")
1458 .id("TitleEditor")
1459 .flex_grow()
1460 .w_full()
1461 .max_w_full()
1462 .overflow_x_scroll()
1463 .child(content)
1464 .into_any()
1465 }
1466
1467 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1468 let active_thread = self.thread.read(cx);
1469 let user_store = self.user_store.read(cx);
1470 let thread = active_thread.thread().read(cx);
1471 let thread_id = thread.id().clone();
1472 let is_empty = active_thread.is_empty();
1473 let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
1474 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1475 maybe!({
1476 let amount = user_store.model_request_usage_amount()?;
1477 let limit = user_store.model_request_usage_limit()?.variant?;
1478
1479 Some(RequestUsage {
1480 amount: amount as i32,
1481 limit: match limit {
1482 proto::usage_limit::Variant::Limited(limited) => {
1483 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1484 }
1485 proto::usage_limit::Variant::Unlimited(_) => {
1486 zed_llm_client::UsageLimit::Unlimited
1487 }
1488 },
1489 })
1490 })
1491 });
1492
1493 let account_url = zed_urls::account_url(cx);
1494
1495 let show_token_count = match &self.active_view {
1496 ActiveView::Thread { .. } => !is_empty || !editor_empty,
1497 ActiveView::PromptEditor { .. } => true,
1498 _ => false,
1499 };
1500
1501 let focus_handle = self.focus_handle(cx);
1502
1503 let go_back_button = div().child(
1504 IconButton::new("go-back", IconName::ArrowLeft)
1505 .icon_size(IconSize::Small)
1506 .on_click(cx.listener(|this, _, window, cx| {
1507 this.go_back(&workspace::GoBack, window, cx);
1508 }))
1509 .tooltip({
1510 let focus_handle = focus_handle.clone();
1511 move |window, cx| {
1512 Tooltip::for_action_in(
1513 "Go Back",
1514 &workspace::GoBack,
1515 &focus_handle,
1516 window,
1517 cx,
1518 )
1519 }
1520 }),
1521 );
1522
1523 let recent_entries_menu = div().child(
1524 PopoverMenu::new("agent-nav-menu")
1525 .trigger_with_tooltip(
1526 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1527 .icon_size(IconSize::Small)
1528 .style(ui::ButtonStyle::Subtle),
1529 {
1530 let focus_handle = focus_handle.clone();
1531 move |window, cx| {
1532 Tooltip::for_action_in(
1533 "Toggle Panel Menu",
1534 &ToggleNavigationMenu,
1535 &focus_handle,
1536 window,
1537 cx,
1538 )
1539 }
1540 },
1541 )
1542 .anchor(Corner::TopLeft)
1543 .with_handle(self.assistant_navigation_menu_handle.clone())
1544 .menu({
1545 let menu = self.assistant_navigation_menu.clone();
1546 move |window, cx| {
1547 if let Some(menu) = menu.as_ref() {
1548 menu.update(cx, |_, cx| {
1549 cx.defer_in(window, |menu, window, cx| {
1550 menu.rebuild(window, cx);
1551 });
1552 })
1553 }
1554 menu.clone()
1555 }
1556 }),
1557 );
1558
1559 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1560 .trigger_with_tooltip(
1561 IconButton::new("agent-options-menu", IconName::Ellipsis)
1562 .icon_size(IconSize::Small),
1563 {
1564 let focus_handle = focus_handle.clone();
1565 move |window, cx| {
1566 Tooltip::for_action_in(
1567 "Toggle Agent Menu",
1568 &ToggleOptionsMenu,
1569 &focus_handle,
1570 window,
1571 cx,
1572 )
1573 }
1574 },
1575 )
1576 .anchor(Corner::TopRight)
1577 .with_handle(self.assistant_dropdown_menu_handle.clone())
1578 .menu(move |window, cx| {
1579 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1580 menu = menu
1581 .action("New Thread", NewThread::default().boxed_clone())
1582 .action("New Text Thread", NewTextThread.boxed_clone())
1583 .when(!is_empty, |menu| {
1584 menu.action(
1585 "New From Summary",
1586 Box::new(NewThread {
1587 from_thread_id: Some(thread_id.clone()),
1588 }),
1589 )
1590 })
1591 .separator();
1592
1593 menu = menu
1594 .header("MCP Servers")
1595 .action(
1596 "View Server Extensions",
1597 Box::new(zed_actions::Extensions {
1598 category_filter: Some(
1599 zed_actions::ExtensionCategoryFilter::ContextServers,
1600 ),
1601 }),
1602 )
1603 .action("Add Custom Server…", Box::new(AddContextServer))
1604 .separator();
1605
1606 if let Some(usage) = last_usage {
1607 menu = menu
1608 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1609 .custom_entry(
1610 move |_window, cx| {
1611 let used_percentage = match usage.limit {
1612 UsageLimit::Limited(limit) => {
1613 Some((usage.amount as f32 / limit as f32) * 100.)
1614 }
1615 UsageLimit::Unlimited => None,
1616 };
1617
1618 h_flex()
1619 .flex_1()
1620 .gap_1p5()
1621 .children(used_percentage.map(|percent| {
1622 ProgressBar::new("usage", percent, 100., cx)
1623 }))
1624 .child(
1625 Label::new(match usage.limit {
1626 UsageLimit::Limited(limit) => {
1627 format!("{} / {limit}", usage.amount)
1628 }
1629 UsageLimit::Unlimited => {
1630 format!("{} / ∞", usage.amount)
1631 }
1632 })
1633 .size(LabelSize::Small)
1634 .color(Color::Muted),
1635 )
1636 .into_any_element()
1637 },
1638 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1639 )
1640 .separator()
1641 }
1642
1643 menu = menu
1644 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1645 .action("Settings", Box::new(OpenConfiguration));
1646 menu
1647 }))
1648 });
1649
1650 h_flex()
1651 .id("assistant-toolbar")
1652 .h(Tab::container_height(cx))
1653 .max_w_full()
1654 .flex_none()
1655 .justify_between()
1656 .gap_2()
1657 .bg(cx.theme().colors().tab_bar_background)
1658 .border_b_1()
1659 .border_color(cx.theme().colors().border)
1660 .child(
1661 h_flex()
1662 .size_full()
1663 .pl_1()
1664 .gap_1()
1665 .child(match &self.active_view {
1666 ActiveView::History | ActiveView::Configuration => go_back_button,
1667 _ => recent_entries_menu,
1668 })
1669 .child(self.render_title_view(window, cx)),
1670 )
1671 .child(
1672 h_flex()
1673 .h_full()
1674 .gap_2()
1675 .when(show_token_count, |parent| {
1676 parent.children(self.render_token_count(&thread, cx))
1677 })
1678 .child(
1679 h_flex()
1680 .h_full()
1681 .gap(DynamicSpacing::Base02.rems(cx))
1682 .px(DynamicSpacing::Base08.rems(cx))
1683 .border_l_1()
1684 .border_color(cx.theme().colors().border)
1685 .child(
1686 IconButton::new("new", IconName::Plus)
1687 .icon_size(IconSize::Small)
1688 .style(ButtonStyle::Subtle)
1689 .tooltip(move |window, cx| {
1690 Tooltip::for_action_in(
1691 "New Thread",
1692 &NewThread::default(),
1693 &focus_handle,
1694 window,
1695 cx,
1696 )
1697 })
1698 .on_click(move |_event, window, cx| {
1699 window.dispatch_action(
1700 NewThread::default().boxed_clone(),
1701 cx,
1702 );
1703 }),
1704 )
1705 .child(agent_extra_menu),
1706 ),
1707 )
1708 }
1709
1710 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1711 let is_generating = thread.is_generating();
1712 let message_editor = self.message_editor.read(cx);
1713
1714 let conversation_token_usage = thread.total_token_usage()?;
1715
1716 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1717 self.thread.read(cx).editing_message_id()
1718 {
1719 let combined = thread
1720 .token_usage_up_to_message(editing_message_id)
1721 .add(unsent_tokens);
1722
1723 (combined, unsent_tokens > 0)
1724 } else {
1725 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1726 let combined = conversation_token_usage.add(unsent_tokens);
1727
1728 (combined, unsent_tokens > 0)
1729 };
1730
1731 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1732
1733 match &self.active_view {
1734 ActiveView::Thread { .. } => {
1735 if total_token_usage.total == 0 {
1736 return None;
1737 }
1738
1739 let token_color = match total_token_usage.ratio() {
1740 TokenUsageRatio::Normal if is_estimating => Color::Default,
1741 TokenUsageRatio::Normal => Color::Muted,
1742 TokenUsageRatio::Warning => Color::Warning,
1743 TokenUsageRatio::Exceeded => Color::Error,
1744 };
1745
1746 let token_count = h_flex()
1747 .id("token-count")
1748 .flex_shrink_0()
1749 .gap_0p5()
1750 .when(!is_generating && is_estimating, |parent| {
1751 parent
1752 .child(
1753 h_flex()
1754 .mr_1()
1755 .size_2p5()
1756 .justify_center()
1757 .rounded_full()
1758 .bg(cx.theme().colors().text.opacity(0.1))
1759 .child(
1760 div().size_1().rounded_full().bg(cx.theme().colors().text),
1761 ),
1762 )
1763 .tooltip(move |window, cx| {
1764 Tooltip::with_meta(
1765 "Estimated New Token Count",
1766 None,
1767 format!(
1768 "Current Conversation Tokens: {}",
1769 humanize_token_count(conversation_token_usage.total)
1770 ),
1771 window,
1772 cx,
1773 )
1774 })
1775 })
1776 .child(
1777 Label::new(humanize_token_count(total_token_usage.total))
1778 .size(LabelSize::Small)
1779 .color(token_color)
1780 .map(|label| {
1781 if is_generating || is_waiting_to_update_token_count {
1782 label
1783 .with_animation(
1784 "used-tokens-label",
1785 Animation::new(Duration::from_secs(2))
1786 .repeat()
1787 .with_easing(pulsating_between(0.6, 1.)),
1788 |label, delta| label.alpha(delta),
1789 )
1790 .into_any()
1791 } else {
1792 label.into_any_element()
1793 }
1794 }),
1795 )
1796 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1797 .child(
1798 Label::new(humanize_token_count(total_token_usage.max))
1799 .size(LabelSize::Small)
1800 .color(Color::Muted),
1801 )
1802 .into_any();
1803
1804 Some(token_count)
1805 }
1806 ActiveView::PromptEditor { context_editor, .. } => {
1807 let element = render_remaining_tokens(context_editor, cx)?;
1808
1809 Some(element.into_any_element())
1810 }
1811 _ => None,
1812 }
1813 }
1814
1815 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1816 if !matches!(self.active_view, ActiveView::Thread { .. }) {
1817 return false;
1818 }
1819
1820 if self.hide_trial_upsell || dismissed_trial_upsell() {
1821 return false;
1822 }
1823
1824 let is_using_zed_provider = self
1825 .thread
1826 .read(cx)
1827 .thread()
1828 .read(cx)
1829 .configured_model()
1830 .map_or(false, |model| {
1831 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1832 });
1833 if !is_using_zed_provider {
1834 return false;
1835 }
1836
1837 let plan = self.user_store.read(cx).current_plan();
1838 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
1839 return false;
1840 }
1841
1842 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1843 if has_previous_trial {
1844 return false;
1845 }
1846
1847 true
1848 }
1849
1850 fn render_trial_upsell(
1851 &self,
1852 _window: &mut Window,
1853 cx: &mut Context<Self>,
1854 ) -> Option<impl IntoElement> {
1855 if !self.should_render_upsell(cx) {
1856 return None;
1857 }
1858
1859 let checkbox = CheckboxWithLabel::new(
1860 "dont-show-again",
1861 Label::new("Don't show again").color(Color::Muted),
1862 ToggleState::Unselected,
1863 move |toggle_state, _window, cx| {
1864 let toggle_state_bool = toggle_state.selected();
1865
1866 set_trial_upsell_dismissed(toggle_state_bool, cx);
1867 },
1868 );
1869
1870 Some(
1871 div().p_2().child(
1872 v_flex()
1873 .w_full()
1874 .elevation_2(cx)
1875 .rounded(px(8.))
1876 .bg(cx.theme().colors().background.alpha(0.5))
1877 .p(px(3.))
1878
1879 .child(
1880 div()
1881 .gap_2()
1882 .flex()
1883 .flex_col()
1884 .size_full()
1885 .border_1()
1886 .rounded(px(5.))
1887 .border_color(cx.theme().colors().text.alpha(0.1))
1888 .overflow_hidden()
1889 .relative()
1890 .bg(cx.theme().colors().panel_background)
1891 .px_4()
1892 .py_3()
1893 .child(
1894 div()
1895 .absolute()
1896 .top_0()
1897 .right(px(-1.0))
1898 .w(px(441.))
1899 .h(px(167.))
1900 .child(
1901 Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
1902 )
1903 )
1904 .child(
1905 div()
1906 .absolute()
1907 .top(px(-8.0))
1908 .right_0()
1909 .w(px(400.))
1910 .h(px(92.))
1911 .child(
1912 Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
1913 )
1914 )
1915 // .child(
1916 // div()
1917 // .absolute()
1918 // .top_0()
1919 // .right(px(360.))
1920 // .size(px(401.))
1921 // .overflow_hidden()
1922 // .bg(cx.theme().colors().panel_background)
1923 // )
1924 .child(
1925 div()
1926 .absolute()
1927 .top_0()
1928 .right_0()
1929 .w(px(660.))
1930 .h(px(401.))
1931 .overflow_hidden()
1932 .bg(linear_gradient(
1933 75.,
1934 linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
1935 linear_color_stop(cx.theme().colors().panel_background, 0.45),
1936 ))
1937 )
1938 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
1939 .child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
1940 .child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
1941 .child(
1942 h_flex()
1943 .w_full()
1944 .px_neg_1()
1945 .justify_between()
1946 .items_center()
1947 .child(h_flex().items_center().gap_1().child(checkbox))
1948 .child(
1949 h_flex()
1950 .gap_2()
1951 .child(
1952 Button::new("dismiss-button", "Not Now")
1953 .style(ButtonStyle::Transparent)
1954 .color(Color::Muted)
1955 .on_click({
1956 let assistant_panel = cx.entity();
1957 move |_, _, cx| {
1958 assistant_panel.update(
1959 cx,
1960 |this, cx| {
1961 let hidden =
1962 this.hide_trial_upsell;
1963 println!("hidden: {}", hidden);
1964 this.hide_trial_upsell = true;
1965 let new_hidden =
1966 this.hide_trial_upsell;
1967 println!(
1968 "new_hidden: {}",
1969 new_hidden
1970 );
1971
1972 cx.notify();
1973 },
1974 );
1975 }
1976 }),
1977 )
1978 .child(
1979 Button::new("cta-button", "Start Trial")
1980 .style(ButtonStyle::Transparent)
1981 .on_click(|_, _, cx| {
1982 cx.open_url(&zed_urls::account_url(cx))
1983 }),
1984 ),
1985 ),
1986 ),
1987 ),
1988 ),
1989 )
1990 }
1991
1992 fn render_active_thread_or_empty_state(
1993 &self,
1994 window: &mut Window,
1995 cx: &mut Context<Self>,
1996 ) -> AnyElement {
1997 if self.thread.read(cx).is_empty() {
1998 return self
1999 .render_thread_empty_state(window, cx)
2000 .into_any_element();
2001 }
2002
2003 self.thread.clone().into_any_element()
2004 }
2005
2006 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2007 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2008 return Some(ConfigurationError::NoProvider);
2009 };
2010
2011 if !model.provider.is_authenticated(cx) {
2012 return Some(ConfigurationError::ProviderNotAuthenticated);
2013 }
2014
2015 if model.provider.must_accept_terms(cx) {
2016 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2017 model.provider,
2018 ));
2019 }
2020
2021 None
2022 }
2023
2024 fn render_thread_empty_state(
2025 &self,
2026 window: &mut Window,
2027 cx: &mut Context<Self>,
2028 ) -> impl IntoElement {
2029 let recent_history = self
2030 .history_store
2031 .update(cx, |this, cx| this.recent_entries(6, cx));
2032
2033 let configuration_error = self.configuration_error(cx);
2034 let no_error = configuration_error.is_none();
2035 let focus_handle = self.focus_handle(cx);
2036
2037 v_flex()
2038 .size_full()
2039 .when(recent_history.is_empty(), |this| {
2040 let configuration_error_ref = &configuration_error;
2041 this.child(
2042 v_flex()
2043 .size_full()
2044 .max_w_80()
2045 .mx_auto()
2046 .justify_center()
2047 .items_center()
2048 .gap_1()
2049 .child(
2050 h_flex().child(
2051 Headline::new("Welcome to the Agent Panel")
2052 ),
2053 )
2054 .when(no_error, |parent| {
2055 parent
2056 .child(
2057 h_flex().child(
2058 Label::new("Ask and build anything.")
2059 .color(Color::Muted)
2060 .mb_2p5(),
2061 ),
2062 )
2063 .child(
2064 Button::new("new-thread", "Start New Thread")
2065 .icon(IconName::Plus)
2066 .icon_position(IconPosition::Start)
2067 .icon_size(IconSize::Small)
2068 .icon_color(Color::Muted)
2069 .full_width()
2070 .key_binding(KeyBinding::for_action_in(
2071 &NewThread::default(),
2072 &focus_handle,
2073 window,
2074 cx,
2075 ))
2076 .on_click(|_event, window, cx| {
2077 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2078 }),
2079 )
2080 .child(
2081 Button::new("context", "Add Context")
2082 .icon(IconName::FileCode)
2083 .icon_position(IconPosition::Start)
2084 .icon_size(IconSize::Small)
2085 .icon_color(Color::Muted)
2086 .full_width()
2087 .key_binding(KeyBinding::for_action_in(
2088 &ToggleContextPicker,
2089 &focus_handle,
2090 window,
2091 cx,
2092 ))
2093 .on_click(|_event, window, cx| {
2094 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2095 }),
2096 )
2097 .child(
2098 Button::new("mode", "Switch Model")
2099 .icon(IconName::DatabaseZap)
2100 .icon_position(IconPosition::Start)
2101 .icon_size(IconSize::Small)
2102 .icon_color(Color::Muted)
2103 .full_width()
2104 .key_binding(KeyBinding::for_action_in(
2105 &ToggleModelSelector,
2106 &focus_handle,
2107 window,
2108 cx,
2109 ))
2110 .on_click(|_event, window, cx| {
2111 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2112 }),
2113 )
2114 .child(
2115 Button::new("settings", "View Settings")
2116 .icon(IconName::Settings)
2117 .icon_position(IconPosition::Start)
2118 .icon_size(IconSize::Small)
2119 .icon_color(Color::Muted)
2120 .full_width()
2121 .key_binding(KeyBinding::for_action_in(
2122 &OpenConfiguration,
2123 &focus_handle,
2124 window,
2125 cx,
2126 ))
2127 .on_click(|_event, window, cx| {
2128 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2129 }),
2130 )
2131 })
2132 .map(|parent| {
2133 match configuration_error_ref {
2134 Some(ConfigurationError::ProviderNotAuthenticated)
2135 | Some(ConfigurationError::NoProvider) => {
2136 parent
2137 .child(
2138 h_flex().child(
2139 Label::new("To start using the agent, configure at least one LLM provider.")
2140 .color(Color::Muted)
2141 .mb_2p5()
2142 )
2143 )
2144 .child(
2145 Button::new("settings", "Configure a Provider")
2146 .icon(IconName::Settings)
2147 .icon_position(IconPosition::Start)
2148 .icon_size(IconSize::Small)
2149 .icon_color(Color::Muted)
2150 .full_width()
2151 .key_binding(KeyBinding::for_action_in(
2152 &OpenConfiguration,
2153 &focus_handle,
2154 window,
2155 cx,
2156 ))
2157 .on_click(|_event, window, cx| {
2158 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2159 }),
2160 )
2161 }
2162 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2163 parent.children(
2164 provider.render_accept_terms(
2165 LanguageModelProviderTosView::ThreadFreshStart,
2166 cx,
2167 ),
2168 )
2169 }
2170 None => parent,
2171 }
2172 })
2173 )
2174 })
2175 .when(!recent_history.is_empty(), |parent| {
2176 let focus_handle = focus_handle.clone();
2177 let configuration_error_ref = &configuration_error;
2178
2179 parent
2180 .overflow_hidden()
2181 .p_1p5()
2182 .justify_end()
2183 .gap_1()
2184 .child(
2185 h_flex()
2186 .pl_1p5()
2187 .pb_1()
2188 .w_full()
2189 .justify_between()
2190 .border_b_1()
2191 .border_color(cx.theme().colors().border_variant)
2192 .child(
2193 Label::new("Past Interactions")
2194 .size(LabelSize::Small)
2195 .color(Color::Muted),
2196 )
2197 .child(
2198 Button::new("view-history", "View All")
2199 .style(ButtonStyle::Subtle)
2200 .label_size(LabelSize::Small)
2201 .key_binding(
2202 KeyBinding::for_action_in(
2203 &OpenHistory,
2204 &self.focus_handle(cx),
2205 window,
2206 cx,
2207 ).map(|kb| kb.size(rems_from_px(12.))),
2208 )
2209 .on_click(move |_event, window, cx| {
2210 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2211 }),
2212 ),
2213 )
2214 .child(
2215 v_flex()
2216 .gap_1()
2217 .children(
2218 recent_history.into_iter().map(|entry| {
2219 // TODO: Add keyboard navigation.
2220 match entry {
2221 HistoryEntry::Thread(thread) => {
2222 PastThread::new(thread, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
2223 .into_any_element()
2224 }
2225 HistoryEntry::Context(context) => {
2226 PastContext::new(context, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
2227 .into_any_element()
2228 }
2229 }
2230 }),
2231 )
2232 )
2233 .map(|parent| {
2234 match configuration_error_ref {
2235 Some(ConfigurationError::ProviderNotAuthenticated)
2236 | Some(ConfigurationError::NoProvider) => {
2237 parent
2238 .child(
2239 Banner::new()
2240 .severity(ui::Severity::Warning)
2241 .child(
2242 Label::new(
2243 "Configure at least one LLM provider to start using the panel.",
2244 )
2245 .size(LabelSize::Small),
2246 )
2247 .action_slot(
2248 Button::new("settings", "Configure Provider")
2249 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2250 .label_size(LabelSize::Small)
2251 .key_binding(
2252 KeyBinding::for_action_in(
2253 &OpenConfiguration,
2254 &focus_handle,
2255 window,
2256 cx,
2257 )
2258 .map(|kb| kb.size(rems_from_px(12.))),
2259 )
2260 .on_click(|_event, window, cx| {
2261 window.dispatch_action(
2262 OpenConfiguration.boxed_clone(),
2263 cx,
2264 )
2265 }),
2266 ),
2267 )
2268 }
2269 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2270 parent
2271 .child(
2272 Banner::new()
2273 .severity(ui::Severity::Warning)
2274 .child(
2275 h_flex()
2276 .w_full()
2277 .children(
2278 provider.render_accept_terms(
2279 LanguageModelProviderTosView::ThreadtEmptyState,
2280 cx,
2281 ),
2282 ),
2283 ),
2284 )
2285 }
2286 None => parent,
2287 }
2288 })
2289 })
2290 }
2291
2292 fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2293 let tool_use_limit_reached = self
2294 .thread
2295 .read(cx)
2296 .thread()
2297 .read(cx)
2298 .tool_use_limit_reached();
2299 if !tool_use_limit_reached {
2300 return None;
2301 }
2302
2303 let model = self
2304 .thread
2305 .read(cx)
2306 .thread()
2307 .read(cx)
2308 .configured_model()?
2309 .model;
2310
2311 let max_mode_upsell = if model.supports_max_mode() {
2312 " Enable max mode for unlimited tool use."
2313 } else {
2314 ""
2315 };
2316
2317 Some(
2318 Banner::new()
2319 .severity(ui::Severity::Info)
2320 .child(h_flex().child(Label::new(format!(
2321 "Consecutive tool use limit reached.{max_mode_upsell}"
2322 ))))
2323 .into_any_element(),
2324 )
2325 }
2326
2327 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2328 let last_error = self.thread.read(cx).last_error()?;
2329
2330 Some(
2331 div()
2332 .absolute()
2333 .right_3()
2334 .bottom_12()
2335 .max_w_96()
2336 .py_2()
2337 .px_3()
2338 .elevation_2(cx)
2339 .occlude()
2340 .child(match last_error {
2341 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2342 ThreadError::MaxMonthlySpendReached => {
2343 self.render_max_monthly_spend_reached_error(cx)
2344 }
2345 ThreadError::ModelRequestLimitReached { plan } => {
2346 self.render_model_request_limit_reached_error(plan, cx)
2347 }
2348 ThreadError::Message { header, message } => {
2349 self.render_error_message(header, message, cx)
2350 }
2351 })
2352 .into_any(),
2353 )
2354 }
2355
2356 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2357 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.";
2358
2359 v_flex()
2360 .gap_0p5()
2361 .child(
2362 h_flex()
2363 .gap_1p5()
2364 .items_center()
2365 .child(Icon::new(IconName::XCircle).color(Color::Error))
2366 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2367 )
2368 .child(
2369 div()
2370 .id("error-message")
2371 .max_h_24()
2372 .overflow_y_scroll()
2373 .child(Label::new(ERROR_MESSAGE)),
2374 )
2375 .child(
2376 h_flex()
2377 .justify_end()
2378 .mt_1()
2379 .gap_1()
2380 .child(self.create_copy_button(ERROR_MESSAGE))
2381 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2382 |this, _, _, cx| {
2383 this.thread.update(cx, |this, _cx| {
2384 this.clear_last_error();
2385 });
2386
2387 cx.open_url(&zed_urls::account_url(cx));
2388 cx.notify();
2389 },
2390 )))
2391 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2392 |this, _, _, cx| {
2393 this.thread.update(cx, |this, _cx| {
2394 this.clear_last_error();
2395 });
2396
2397 cx.notify();
2398 },
2399 ))),
2400 )
2401 .into_any()
2402 }
2403
2404 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2405 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2406
2407 v_flex()
2408 .gap_0p5()
2409 .child(
2410 h_flex()
2411 .gap_1p5()
2412 .items_center()
2413 .child(Icon::new(IconName::XCircle).color(Color::Error))
2414 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2415 )
2416 .child(
2417 div()
2418 .id("error-message")
2419 .max_h_24()
2420 .overflow_y_scroll()
2421 .child(Label::new(ERROR_MESSAGE)),
2422 )
2423 .child(
2424 h_flex()
2425 .justify_end()
2426 .mt_1()
2427 .gap_1()
2428 .child(self.create_copy_button(ERROR_MESSAGE))
2429 .child(
2430 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2431 cx.listener(|this, _, _, cx| {
2432 this.thread.update(cx, |this, _cx| {
2433 this.clear_last_error();
2434 });
2435
2436 cx.open_url(&zed_urls::account_url(cx));
2437 cx.notify();
2438 }),
2439 ),
2440 )
2441 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2442 |this, _, _, cx| {
2443 this.thread.update(cx, |this, _cx| {
2444 this.clear_last_error();
2445 });
2446
2447 cx.notify();
2448 },
2449 ))),
2450 )
2451 .into_any()
2452 }
2453
2454 fn render_model_request_limit_reached_error(
2455 &self,
2456 plan: Plan,
2457 cx: &mut Context<Self>,
2458 ) -> AnyElement {
2459 let error_message = match plan {
2460 Plan::ZedPro => {
2461 "Model request limit reached. Upgrade to usage-based billing for more requests."
2462 }
2463 Plan::ZedProTrial => {
2464 "Model request limit reached. Upgrade to Zed Pro for more requests."
2465 }
2466 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2467 };
2468 let call_to_action = match plan {
2469 Plan::ZedPro => "Upgrade to usage-based billing",
2470 Plan::ZedProTrial => "Upgrade to Zed Pro",
2471 Plan::Free => "Upgrade to Zed Pro",
2472 };
2473
2474 v_flex()
2475 .gap_0p5()
2476 .child(
2477 h_flex()
2478 .gap_1p5()
2479 .items_center()
2480 .child(Icon::new(IconName::XCircle).color(Color::Error))
2481 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2482 )
2483 .child(
2484 div()
2485 .id("error-message")
2486 .max_h_24()
2487 .overflow_y_scroll()
2488 .child(Label::new(error_message)),
2489 )
2490 .child(
2491 h_flex()
2492 .justify_end()
2493 .mt_1()
2494 .gap_1()
2495 .child(self.create_copy_button(error_message))
2496 .child(
2497 Button::new("subscribe", call_to_action).on_click(cx.listener(
2498 |this, _, _, cx| {
2499 this.thread.update(cx, |this, _cx| {
2500 this.clear_last_error();
2501 });
2502
2503 cx.open_url(&zed_urls::account_url(cx));
2504 cx.notify();
2505 },
2506 )),
2507 )
2508 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2509 |this, _, _, cx| {
2510 this.thread.update(cx, |this, _cx| {
2511 this.clear_last_error();
2512 });
2513
2514 cx.notify();
2515 },
2516 ))),
2517 )
2518 .into_any()
2519 }
2520
2521 fn render_error_message(
2522 &self,
2523 header: SharedString,
2524 message: SharedString,
2525 cx: &mut Context<Self>,
2526 ) -> AnyElement {
2527 let message_with_header = format!("{}\n{}", header, message);
2528 v_flex()
2529 .gap_0p5()
2530 .child(
2531 h_flex()
2532 .gap_1p5()
2533 .items_center()
2534 .child(Icon::new(IconName::XCircle).color(Color::Error))
2535 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2536 )
2537 .child(
2538 div()
2539 .id("error-message")
2540 .max_h_32()
2541 .overflow_y_scroll()
2542 .child(Label::new(message.clone())),
2543 )
2544 .child(
2545 h_flex()
2546 .justify_end()
2547 .mt_1()
2548 .gap_1()
2549 .child(self.create_copy_button(message_with_header))
2550 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2551 |this, _, _, cx| {
2552 this.thread.update(cx, |this, _cx| {
2553 this.clear_last_error();
2554 });
2555
2556 cx.notify();
2557 },
2558 ))),
2559 )
2560 .into_any()
2561 }
2562
2563 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2564 let is_local = self.project.read(cx).is_local();
2565 div()
2566 .invisible()
2567 .absolute()
2568 .top_0()
2569 .right_0()
2570 .bottom_0()
2571 .left_0()
2572 .bg(cx.theme().colors().drop_target_background)
2573 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2574 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2575 .when(is_local, |this| {
2576 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2577 })
2578 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2579 let item = tab.pane.read(cx).item_for_index(tab.ix);
2580 let project_paths = item
2581 .and_then(|item| item.project_path(cx))
2582 .into_iter()
2583 .collect::<Vec<_>>();
2584 this.handle_drop(project_paths, vec![], window, cx);
2585 }))
2586 .on_drop(
2587 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2588 let project_paths = selection
2589 .items()
2590 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2591 .collect::<Vec<_>>();
2592 this.handle_drop(project_paths, vec![], window, cx);
2593 }),
2594 )
2595 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2596 let tasks = paths
2597 .paths()
2598 .into_iter()
2599 .map(|path| {
2600 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2601 })
2602 .collect::<Vec<_>>();
2603 cx.spawn_in(window, async move |this, cx| {
2604 let mut paths = vec![];
2605 let mut added_worktrees = vec![];
2606 let opened_paths = futures::future::join_all(tasks).await;
2607 for entry in opened_paths {
2608 if let Some((worktree, project_path)) = entry.log_err() {
2609 added_worktrees.push(worktree);
2610 paths.push(project_path);
2611 }
2612 }
2613 this.update_in(cx, |this, window, cx| {
2614 this.handle_drop(paths, added_worktrees, window, cx);
2615 })
2616 .ok();
2617 })
2618 .detach();
2619 }))
2620 }
2621
2622 fn handle_drop(
2623 &mut self,
2624 paths: Vec<ProjectPath>,
2625 added_worktrees: Vec<Entity<Worktree>>,
2626 window: &mut Window,
2627 cx: &mut Context<Self>,
2628 ) {
2629 match &self.active_view {
2630 ActiveView::Thread { .. } => {
2631 let context_store = self.thread.read(cx).context_store().clone();
2632 context_store.update(cx, move |context_store, cx| {
2633 let mut tasks = Vec::new();
2634 for project_path in &paths {
2635 tasks.push(context_store.add_file_from_path(
2636 project_path.clone(),
2637 false,
2638 cx,
2639 ));
2640 }
2641 cx.background_spawn(async move {
2642 futures::future::join_all(tasks).await;
2643 // Need to hold onto the worktrees until they have already been used when
2644 // opening the buffers.
2645 drop(added_worktrees);
2646 })
2647 .detach();
2648 });
2649 }
2650 ActiveView::PromptEditor { context_editor, .. } => {
2651 context_editor.update(cx, |context_editor, cx| {
2652 ContextEditor::insert_dragged_files(
2653 context_editor,
2654 paths,
2655 added_worktrees,
2656 window,
2657 cx,
2658 );
2659 });
2660 }
2661 ActiveView::History | ActiveView::Configuration => {}
2662 }
2663 }
2664
2665 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2666 let message = message.into();
2667 IconButton::new("copy", IconName::Copy)
2668 .on_click(move |_, _, cx| {
2669 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2670 })
2671 .tooltip(Tooltip::text("Copy Error Message"))
2672 }
2673
2674 fn key_context(&self) -> KeyContext {
2675 let mut key_context = KeyContext::new_with_defaults();
2676 key_context.add("AgentPanel");
2677 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2678 key_context.add("prompt_editor");
2679 }
2680 key_context
2681 }
2682}
2683
2684impl Render for AssistantPanel {
2685 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2686 v_flex()
2687 .key_context(self.key_context())
2688 .justify_between()
2689 .size_full()
2690 .on_action(cx.listener(Self::cancel))
2691 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2692 this.new_thread(action, window, cx);
2693 }))
2694 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2695 this.open_history(window, cx);
2696 }))
2697 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2698 this.open_configuration(window, cx);
2699 }))
2700 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2701 .on_action(cx.listener(Self::deploy_rules_library))
2702 .on_action(cx.listener(Self::open_agent_diff))
2703 .on_action(cx.listener(Self::go_back))
2704 .on_action(cx.listener(Self::toggle_navigation_menu))
2705 .on_action(cx.listener(Self::toggle_options_menu))
2706 .on_action(cx.listener(Self::increase_font_size))
2707 .on_action(cx.listener(Self::decrease_font_size))
2708 .on_action(cx.listener(Self::reset_font_size))
2709 .child(self.render_toolbar(window, cx))
2710 .children(self.render_trial_upsell(window, cx))
2711 .map(|parent| match &self.active_view {
2712 ActiveView::Thread { .. } => parent.child(
2713 v_flex()
2714 .relative()
2715 .justify_between()
2716 .size_full()
2717 .child(self.render_active_thread_or_empty_state(window, cx))
2718 .children(self.render_tool_use_limit_reached(cx))
2719 .child(h_flex().child(self.message_editor.clone()))
2720 .children(self.render_last_error(cx))
2721 .child(self.render_drag_target(cx)),
2722 ),
2723 ActiveView::History => parent.child(self.history.clone()),
2724 ActiveView::PromptEditor {
2725 context_editor,
2726 buffer_search_bar,
2727 ..
2728 } => {
2729 let mut registrar = buffer_search::DivRegistrar::new(
2730 |this, _, _cx| match &this.active_view {
2731 ActiveView::PromptEditor {
2732 buffer_search_bar, ..
2733 } => Some(buffer_search_bar.clone()),
2734 _ => None,
2735 },
2736 cx,
2737 );
2738 BufferSearchBar::register(&mut registrar);
2739 parent.child(
2740 registrar
2741 .into_div()
2742 .size_full()
2743 .relative()
2744 .map(|parent| {
2745 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2746 if buffer_search_bar.is_dismissed() {
2747 return parent;
2748 }
2749 parent.child(
2750 div()
2751 .p(DynamicSpacing::Base08.rems(cx))
2752 .border_b_1()
2753 .border_color(cx.theme().colors().border_variant)
2754 .bg(cx.theme().colors().editor_background)
2755 .child(buffer_search_bar.render(window, cx)),
2756 )
2757 })
2758 })
2759 .child(context_editor.clone())
2760 .child(self.render_drag_target(cx)),
2761 )
2762 }
2763 ActiveView::Configuration => parent.children(self.configuration.clone()),
2764 })
2765 }
2766}
2767
2768struct PromptLibraryInlineAssist {
2769 workspace: WeakEntity<Workspace>,
2770}
2771
2772impl PromptLibraryInlineAssist {
2773 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2774 Self { workspace }
2775 }
2776}
2777
2778impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2779 fn assist(
2780 &self,
2781 prompt_editor: &Entity<Editor>,
2782 initial_prompt: Option<String>,
2783 window: &mut Window,
2784 cx: &mut Context<RulesLibrary>,
2785 ) {
2786 InlineAssistant::update_global(cx, |assistant, cx| {
2787 let Some(project) = self
2788 .workspace
2789 .upgrade()
2790 .map(|workspace| workspace.read(cx).project().downgrade())
2791 else {
2792 return;
2793 };
2794 let prompt_store = None;
2795 let thread_store = None;
2796 let text_thread_store = None;
2797 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
2798 assistant.assist(
2799 &prompt_editor,
2800 self.workspace.clone(),
2801 context_store,
2802 project,
2803 prompt_store,
2804 thread_store,
2805 text_thread_store,
2806 initial_prompt,
2807 window,
2808 cx,
2809 )
2810 })
2811 }
2812
2813 fn focus_assistant_panel(
2814 &self,
2815 workspace: &mut Workspace,
2816 window: &mut Window,
2817 cx: &mut Context<Workspace>,
2818 ) -> bool {
2819 workspace
2820 .focus_panel::<AssistantPanel>(window, cx)
2821 .is_some()
2822 }
2823}
2824
2825pub struct ConcreteAssistantPanelDelegate;
2826
2827impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2828 fn active_context_editor(
2829 &self,
2830 workspace: &mut Workspace,
2831 _window: &mut Window,
2832 cx: &mut Context<Workspace>,
2833 ) -> Option<Entity<ContextEditor>> {
2834 let panel = workspace.panel::<AssistantPanel>(cx)?;
2835 panel.read(cx).active_context_editor()
2836 }
2837
2838 fn open_saved_context(
2839 &self,
2840 workspace: &mut Workspace,
2841 path: Arc<Path>,
2842 window: &mut Window,
2843 cx: &mut Context<Workspace>,
2844 ) -> Task<Result<()>> {
2845 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2846 return Task::ready(Err(anyhow!("Agent panel not found")));
2847 };
2848
2849 panel.update(cx, |panel, cx| {
2850 panel.open_saved_prompt_editor(path, window, cx)
2851 })
2852 }
2853
2854 fn open_remote_context(
2855 &self,
2856 _workspace: &mut Workspace,
2857 _context_id: assistant_context_editor::ContextId,
2858 _window: &mut Window,
2859 _cx: &mut Context<Workspace>,
2860 ) -> Task<Result<Entity<ContextEditor>>> {
2861 Task::ready(Err(anyhow!("opening remote context not implemented")))
2862 }
2863
2864 fn quote_selection(
2865 &self,
2866 workspace: &mut Workspace,
2867 selection_ranges: Vec<Range<Anchor>>,
2868 buffer: Entity<MultiBuffer>,
2869 window: &mut Window,
2870 cx: &mut Context<Workspace>,
2871 ) {
2872 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2873 return;
2874 };
2875
2876 if !panel.focus_handle(cx).contains_focused(window, cx) {
2877 workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2878 }
2879
2880 panel.update(cx, |_, cx| {
2881 // Wait to create a new context until the workspace is no longer
2882 // being updated.
2883 cx.defer_in(window, move |panel, window, cx| {
2884 if panel.has_active_thread() {
2885 panel.message_editor.update(cx, |message_editor, cx| {
2886 message_editor.context_store().update(cx, |store, cx| {
2887 let buffer = buffer.read(cx);
2888 let selection_ranges = selection_ranges
2889 .into_iter()
2890 .flat_map(|range| {
2891 let (start_buffer, start) =
2892 buffer.text_anchor_for_position(range.start, cx)?;
2893 let (end_buffer, end) =
2894 buffer.text_anchor_for_position(range.end, cx)?;
2895 if start_buffer != end_buffer {
2896 return None;
2897 }
2898 Some((start_buffer, start..end))
2899 })
2900 .collect::<Vec<_>>();
2901
2902 for (buffer, range) in selection_ranges {
2903 store.add_selection(buffer, range, cx);
2904 }
2905 })
2906 })
2907 } else if let Some(context_editor) = panel.active_context_editor() {
2908 let snapshot = buffer.read(cx).snapshot(cx);
2909 let selection_ranges = selection_ranges
2910 .into_iter()
2911 .map(|range| range.to_point(&snapshot))
2912 .collect::<Vec<_>>();
2913
2914 context_editor.update(cx, |context_editor, cx| {
2915 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2916 });
2917 }
2918 });
2919 });
2920 }
2921}
2922
2923const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
2924
2925fn dismissed_trial_upsell() -> bool {
2926 db::kvp::KEY_VALUE_STORE
2927 .read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
2928 .log_err()
2929 .map_or(false, |s| s.is_some())
2930}
2931
2932fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
2933 db::write_and_log(cx, move || async move {
2934 if is_dismissed {
2935 db::kvp::KEY_VALUE_STORE
2936 .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
2937 .await
2938 } else {
2939 db::kvp::KEY_VALUE_STORE
2940 .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
2941 .await
2942 }
2943 })
2944}