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