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