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