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::{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 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 markdown_language_task = workspace
1159 .read(cx)
1160 .app_state()
1161 .languages
1162 .language_for_name("Markdown");
1163 let Some(thread) = self.active_thread() else {
1164 return;
1165 };
1166 cx.spawn_in(window, async move |_this, cx| {
1167 let markdown_language = markdown_language_task.await?;
1168
1169 workspace.update_in(cx, |workspace, window, cx| {
1170 let thread = thread.read(cx);
1171 let markdown = thread.to_markdown(cx)?;
1172 let thread_summary = thread
1173 .summary()
1174 .map(|summary| summary.to_string())
1175 .unwrap_or_else(|| "Thread".to_string());
1176
1177 let project = workspace.project().clone();
1178 let buffer = project.update(cx, |project, cx| {
1179 project.create_local_buffer(&markdown, Some(markdown_language), cx)
1180 });
1181 let buffer = cx.new(|cx| {
1182 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
1183 });
1184
1185 workspace.add_item_to_active_pane(
1186 Box::new(cx.new(|cx| {
1187 let mut editor =
1188 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1189 editor.set_breadcrumb_header(thread_summary);
1190 editor
1191 })),
1192 None,
1193 true,
1194 window,
1195 cx,
1196 );
1197
1198 anyhow::Ok(())
1199 })
1200 })
1201 .detach_and_log_err(cx);
1202 }
1203
1204 fn handle_assistant_configuration_event(
1205 &mut self,
1206 _entity: &Entity<AssistantConfiguration>,
1207 event: &AssistantConfigurationEvent,
1208 window: &mut Window,
1209 cx: &mut Context<Self>,
1210 ) {
1211 match event {
1212 AssistantConfigurationEvent::NewThread(provider) => {
1213 if LanguageModelRegistry::read_global(cx)
1214 .default_model()
1215 .map_or(true, |model| model.provider.id() != provider.id())
1216 {
1217 if let Some(model) = provider.default_model(cx) {
1218 update_settings_file::<AssistantSettings>(
1219 self.fs.clone(),
1220 cx,
1221 move |settings, _| settings.set_model(model),
1222 );
1223 }
1224 }
1225
1226 self.new_thread(&NewThread::default(), window, cx);
1227 }
1228 }
1229 }
1230
1231 pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1232 match &self.active_view {
1233 ActiveView::Thread { thread, .. } => thread.upgrade(),
1234 _ => None,
1235 }
1236 }
1237
1238 pub(crate) fn delete_thread(
1239 &mut self,
1240 thread_id: &ThreadId,
1241 cx: &mut Context<Self>,
1242 ) -> Task<Result<()>> {
1243 self.thread_store
1244 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1245 }
1246
1247 pub(crate) fn has_active_thread(&self) -> bool {
1248 matches!(self.active_view, ActiveView::Thread { .. })
1249 }
1250
1251 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1252 match &self.active_view {
1253 ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1254 _ => None,
1255 }
1256 }
1257
1258 pub(crate) fn delete_context(
1259 &mut self,
1260 path: Arc<Path>,
1261 cx: &mut Context<Self>,
1262 ) -> Task<Result<()>> {
1263 self.context_store
1264 .update(cx, |this, cx| this.delete_local_context(path, cx))
1265 }
1266
1267 fn set_active_view(
1268 &mut self,
1269 new_view: ActiveView,
1270 window: &mut Window,
1271 cx: &mut Context<Self>,
1272 ) {
1273 let current_is_history = matches!(self.active_view, ActiveView::History);
1274 let new_is_history = matches!(new_view, ActiveView::History);
1275
1276 match &self.active_view {
1277 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1278 if let Some(thread) = thread.upgrade() {
1279 if thread.read(cx).is_empty() {
1280 let id = thread.read(cx).id().clone();
1281 store.remove_recently_opened_thread(id, cx);
1282 }
1283 }
1284 }),
1285 _ => {}
1286 }
1287
1288 match &new_view {
1289 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1290 if let Some(thread) = thread.upgrade() {
1291 let id = thread.read(cx).id().clone();
1292 store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1293 }
1294 }),
1295 ActiveView::PromptEditor { context_editor, .. } => {
1296 self.history_store.update(cx, |store, cx| {
1297 let context = context_editor.read(cx).context().clone();
1298 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1299 })
1300 }
1301 _ => {}
1302 }
1303
1304 if current_is_history && !new_is_history {
1305 self.active_view = new_view;
1306 } else if !current_is_history && new_is_history {
1307 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1308 } else {
1309 if !new_is_history {
1310 self.previous_view = None;
1311 }
1312 self.active_view = new_view;
1313 }
1314
1315 self.focus_handle(cx).focus(window);
1316 }
1317}
1318
1319impl Focusable for AssistantPanel {
1320 fn focus_handle(&self, cx: &App) -> FocusHandle {
1321 match &self.active_view {
1322 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1323 ActiveView::History => self.history.focus_handle(cx),
1324 ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1325 ActiveView::Configuration => {
1326 if let Some(configuration) = self.configuration.as_ref() {
1327 configuration.focus_handle(cx)
1328 } else {
1329 cx.focus_handle()
1330 }
1331 }
1332 }
1333 }
1334}
1335
1336fn agent_panel_dock_position(cx: &App) -> DockPosition {
1337 match AssistantSettings::get_global(cx).dock {
1338 AssistantDockPosition::Left => DockPosition::Left,
1339 AssistantDockPosition::Bottom => DockPosition::Bottom,
1340 AssistantDockPosition::Right => DockPosition::Right,
1341 }
1342}
1343
1344impl EventEmitter<PanelEvent> for AssistantPanel {}
1345
1346impl Panel for AssistantPanel {
1347 fn persistent_name() -> &'static str {
1348 "AgentPanel"
1349 }
1350
1351 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1352 agent_panel_dock_position(cx)
1353 }
1354
1355 fn position_is_valid(&self, position: DockPosition) -> bool {
1356 position != DockPosition::Bottom
1357 }
1358
1359 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1360 self.message_editor.update(cx, |message_editor, cx| {
1361 message_editor.set_dock_position(position, cx);
1362 });
1363
1364 settings::update_settings_file::<AssistantSettings>(
1365 self.fs.clone(),
1366 cx,
1367 move |settings, _| {
1368 let dock = match position {
1369 DockPosition::Left => AssistantDockPosition::Left,
1370 DockPosition::Bottom => AssistantDockPosition::Bottom,
1371 DockPosition::Right => AssistantDockPosition::Right,
1372 };
1373 settings.set_dock(dock);
1374 },
1375 );
1376 }
1377
1378 fn size(&self, window: &Window, cx: &App) -> Pixels {
1379 let settings = AssistantSettings::get_global(cx);
1380 match self.position(window, cx) {
1381 DockPosition::Left | DockPosition::Right => {
1382 self.width.unwrap_or(settings.default_width)
1383 }
1384 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1385 }
1386 }
1387
1388 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1389 match self.position(window, cx) {
1390 DockPosition::Left | DockPosition::Right => self.width = size,
1391 DockPosition::Bottom => self.height = size,
1392 }
1393 self.serialize(cx);
1394 cx.notify();
1395 }
1396
1397 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1398
1399 fn remote_id() -> Option<proto::PanelId> {
1400 Some(proto::PanelId::AssistantPanel)
1401 }
1402
1403 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1404 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1405 .then_some(IconName::ZedAssistant)
1406 }
1407
1408 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1409 Some("Agent Panel")
1410 }
1411
1412 fn toggle_action(&self) -> Box<dyn Action> {
1413 Box::new(ToggleFocus)
1414 }
1415
1416 fn activation_priority(&self) -> u32 {
1417 3
1418 }
1419
1420 fn enabled(&self, cx: &App) -> bool {
1421 AssistantSettings::get_global(cx).enabled
1422 }
1423}
1424
1425impl AssistantPanel {
1426 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1427 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1428
1429 let content = match &self.active_view {
1430 ActiveView::Thread {
1431 change_title_editor,
1432 ..
1433 } => {
1434 let active_thread = self.thread.read(cx);
1435 let is_empty = active_thread.is_empty();
1436
1437 let summary = active_thread.summary(cx);
1438
1439 if is_empty {
1440 Label::new(Thread::DEFAULT_SUMMARY.clone())
1441 .truncate()
1442 .into_any_element()
1443 } else if summary.is_none() {
1444 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1445 .truncate()
1446 .into_any_element()
1447 } else {
1448 div()
1449 .w_full()
1450 .child(change_title_editor.clone())
1451 .into_any_element()
1452 }
1453 }
1454 ActiveView::PromptEditor {
1455 title_editor,
1456 context_editor,
1457 ..
1458 } => {
1459 let context_editor = context_editor.read(cx);
1460 let summary = context_editor.context().read(cx).summary();
1461
1462 match summary {
1463 None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1464 .truncate()
1465 .into_any_element(),
1466 Some(summary) => {
1467 if summary.done {
1468 div()
1469 .w_full()
1470 .child(title_editor.clone())
1471 .into_any_element()
1472 } else {
1473 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1474 .truncate()
1475 .into_any_element()
1476 }
1477 }
1478 }
1479 }
1480 ActiveView::History => Label::new("History").truncate().into_any_element(),
1481 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1482 };
1483
1484 h_flex()
1485 .key_context("TitleEditor")
1486 .id("TitleEditor")
1487 .flex_grow()
1488 .w_full()
1489 .max_w_full()
1490 .overflow_x_scroll()
1491 .child(content)
1492 .into_any()
1493 }
1494
1495 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1496 let active_thread = self.thread.read(cx);
1497 let user_store = self.user_store.read(cx);
1498 let thread = active_thread.thread().read(cx);
1499 let thread_id = thread.id().clone();
1500 let is_empty = active_thread.is_empty();
1501 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1502 maybe!({
1503 let amount = user_store.model_request_usage_amount()?;
1504 let limit = user_store.model_request_usage_limit()?.variant?;
1505
1506 Some(RequestUsage {
1507 amount: amount as i32,
1508 limit: match limit {
1509 proto::usage_limit::Variant::Limited(limited) => {
1510 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1511 }
1512 proto::usage_limit::Variant::Unlimited(_) => {
1513 zed_llm_client::UsageLimit::Unlimited
1514 }
1515 },
1516 })
1517 })
1518 });
1519
1520 let account_url = zed_urls::account_url(cx);
1521
1522 let show_token_count = match &self.active_view {
1523 ActiveView::Thread { .. } => !is_empty,
1524 ActiveView::PromptEditor { .. } => true,
1525 _ => false,
1526 };
1527
1528 let focus_handle = self.focus_handle(cx);
1529
1530 let go_back_button = div().child(
1531 IconButton::new("go-back", IconName::ArrowLeft)
1532 .icon_size(IconSize::Small)
1533 .on_click(cx.listener(|this, _, window, cx| {
1534 this.go_back(&workspace::GoBack, window, cx);
1535 }))
1536 .tooltip({
1537 let focus_handle = focus_handle.clone();
1538 move |window, cx| {
1539 Tooltip::for_action_in(
1540 "Go Back",
1541 &workspace::GoBack,
1542 &focus_handle,
1543 window,
1544 cx,
1545 )
1546 }
1547 }),
1548 );
1549
1550 let recent_entries_menu = div().child(
1551 PopoverMenu::new("agent-nav-menu")
1552 .trigger_with_tooltip(
1553 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1554 .icon_size(IconSize::Small)
1555 .style(ui::ButtonStyle::Subtle),
1556 {
1557 let focus_handle = focus_handle.clone();
1558 move |window, cx| {
1559 Tooltip::for_action_in(
1560 "Toggle Panel Menu",
1561 &ToggleNavigationMenu,
1562 &focus_handle,
1563 window,
1564 cx,
1565 )
1566 }
1567 },
1568 )
1569 .anchor(Corner::TopLeft)
1570 .with_handle(self.assistant_navigation_menu_handle.clone())
1571 .menu({
1572 let menu = self.assistant_navigation_menu.clone();
1573 move |window, cx| {
1574 if let Some(menu) = menu.as_ref() {
1575 menu.update(cx, |_, cx| {
1576 cx.defer_in(window, |menu, window, cx| {
1577 menu.rebuild(window, cx);
1578 });
1579 })
1580 }
1581 menu.clone()
1582 }
1583 }),
1584 );
1585
1586 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1587 .trigger_with_tooltip(
1588 IconButton::new("agent-options-menu", IconName::Ellipsis)
1589 .icon_size(IconSize::Small),
1590 {
1591 let focus_handle = focus_handle.clone();
1592 move |window, cx| {
1593 Tooltip::for_action_in(
1594 "Toggle Agent Menu",
1595 &ToggleOptionsMenu,
1596 &focus_handle,
1597 window,
1598 cx,
1599 )
1600 }
1601 },
1602 )
1603 .anchor(Corner::TopRight)
1604 .with_handle(self.assistant_dropdown_menu_handle.clone())
1605 .menu(move |window, cx| {
1606 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1607 menu = menu
1608 .action("New Thread", NewThread::default().boxed_clone())
1609 .action("New Text Thread", NewTextThread.boxed_clone())
1610 .when(!is_empty, |menu| {
1611 menu.action(
1612 "New From Summary",
1613 Box::new(NewThread {
1614 from_thread_id: Some(thread_id.clone()),
1615 }),
1616 )
1617 })
1618 .separator();
1619
1620 menu = menu
1621 .header("MCP Servers")
1622 .action(
1623 "View Server Extensions",
1624 Box::new(zed_actions::Extensions {
1625 category_filter: Some(
1626 zed_actions::ExtensionCategoryFilter::ContextServers,
1627 ),
1628 }),
1629 )
1630 .action("Add Custom Server…", Box::new(AddContextServer))
1631 .separator();
1632
1633 if let Some(usage) = last_usage {
1634 menu = menu
1635 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1636 .custom_entry(
1637 move |_window, cx| {
1638 let used_percentage = match usage.limit {
1639 UsageLimit::Limited(limit) => {
1640 Some((usage.amount as f32 / limit as f32) * 100.)
1641 }
1642 UsageLimit::Unlimited => None,
1643 };
1644
1645 h_flex()
1646 .flex_1()
1647 .gap_1p5()
1648 .children(used_percentage.map(|percent| {
1649 ProgressBar::new("usage", percent, 100., cx)
1650 }))
1651 .child(
1652 Label::new(match usage.limit {
1653 UsageLimit::Limited(limit) => {
1654 format!("{} / {limit}", usage.amount)
1655 }
1656 UsageLimit::Unlimited => {
1657 format!("{} / ∞", usage.amount)
1658 }
1659 })
1660 .size(LabelSize::Small)
1661 .color(Color::Muted),
1662 )
1663 .into_any_element()
1664 },
1665 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1666 )
1667 .separator()
1668 }
1669
1670 menu = menu
1671 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1672 .action("Settings", Box::new(OpenConfiguration));
1673 menu
1674 }))
1675 });
1676
1677 h_flex()
1678 .id("assistant-toolbar")
1679 .h(Tab::container_height(cx))
1680 .max_w_full()
1681 .flex_none()
1682 .justify_between()
1683 .gap_2()
1684 .bg(cx.theme().colors().tab_bar_background)
1685 .border_b_1()
1686 .border_color(cx.theme().colors().border)
1687 .child(
1688 h_flex()
1689 .size_full()
1690 .pl_1()
1691 .gap_1()
1692 .child(match &self.active_view {
1693 ActiveView::History | ActiveView::Configuration => go_back_button,
1694 _ => recent_entries_menu,
1695 })
1696 .child(self.render_title_view(window, cx)),
1697 )
1698 .child(
1699 h_flex()
1700 .h_full()
1701 .gap_2()
1702 .when(show_token_count, |parent| {
1703 parent.children(self.render_token_count(&thread, cx))
1704 })
1705 .child(
1706 h_flex()
1707 .h_full()
1708 .gap(DynamicSpacing::Base02.rems(cx))
1709 .px(DynamicSpacing::Base08.rems(cx))
1710 .border_l_1()
1711 .border_color(cx.theme().colors().border)
1712 .child(
1713 IconButton::new("new", IconName::Plus)
1714 .icon_size(IconSize::Small)
1715 .style(ButtonStyle::Subtle)
1716 .tooltip(move |window, cx| {
1717 Tooltip::for_action_in(
1718 "New Thread",
1719 &NewThread::default(),
1720 &focus_handle,
1721 window,
1722 cx,
1723 )
1724 })
1725 .on_click(move |_event, window, cx| {
1726 window.dispatch_action(
1727 NewThread::default().boxed_clone(),
1728 cx,
1729 );
1730 }),
1731 )
1732 .child(agent_extra_menu),
1733 ),
1734 )
1735 }
1736
1737 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1738 let is_generating = thread.is_generating();
1739 let message_editor = self.message_editor.read(cx);
1740
1741 let conversation_token_usage = thread.total_token_usage()?;
1742
1743 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1744 self.thread.read(cx).editing_message_id()
1745 {
1746 let combined = thread
1747 .token_usage_up_to_message(editing_message_id)
1748 .add(unsent_tokens);
1749
1750 (combined, unsent_tokens > 0)
1751 } else {
1752 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1753 let combined = conversation_token_usage.add(unsent_tokens);
1754
1755 (combined, unsent_tokens > 0)
1756 };
1757
1758 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1759
1760 match &self.active_view {
1761 ActiveView::Thread { .. } => {
1762 if total_token_usage.total == 0 {
1763 return None;
1764 }
1765
1766 let token_color = match total_token_usage.ratio() {
1767 TokenUsageRatio::Normal if is_estimating => Color::Default,
1768 TokenUsageRatio::Normal => Color::Muted,
1769 TokenUsageRatio::Warning => Color::Warning,
1770 TokenUsageRatio::Exceeded => Color::Error,
1771 };
1772
1773 let token_count = h_flex()
1774 .id("token-count")
1775 .flex_shrink_0()
1776 .gap_0p5()
1777 .when(!is_generating && is_estimating, |parent| {
1778 parent
1779 .child(
1780 h_flex()
1781 .mr_1()
1782 .size_2p5()
1783 .justify_center()
1784 .rounded_full()
1785 .bg(cx.theme().colors().text.opacity(0.1))
1786 .child(
1787 div().size_1().rounded_full().bg(cx.theme().colors().text),
1788 ),
1789 )
1790 .tooltip(move |window, cx| {
1791 Tooltip::with_meta(
1792 "Estimated New Token Count",
1793 None,
1794 format!(
1795 "Current Conversation Tokens: {}",
1796 humanize_token_count(conversation_token_usage.total)
1797 ),
1798 window,
1799 cx,
1800 )
1801 })
1802 })
1803 .child(
1804 Label::new(humanize_token_count(total_token_usage.total))
1805 .size(LabelSize::Small)
1806 .color(token_color)
1807 .map(|label| {
1808 if is_generating || is_waiting_to_update_token_count {
1809 label
1810 .with_animation(
1811 "used-tokens-label",
1812 Animation::new(Duration::from_secs(2))
1813 .repeat()
1814 .with_easing(pulsating_between(0.6, 1.)),
1815 |label, delta| label.alpha(delta),
1816 )
1817 .into_any()
1818 } else {
1819 label.into_any_element()
1820 }
1821 }),
1822 )
1823 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1824 .child(
1825 Label::new(humanize_token_count(total_token_usage.max))
1826 .size(LabelSize::Small)
1827 .color(Color::Muted),
1828 )
1829 .into_any();
1830
1831 Some(token_count)
1832 }
1833 ActiveView::PromptEditor { context_editor, .. } => {
1834 let element = render_remaining_tokens(context_editor, cx)?;
1835
1836 Some(element.into_any_element())
1837 }
1838 _ => None,
1839 }
1840 }
1841
1842 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1843 if self.hide_trial_upsell || dismissed_trial_upsell() {
1844 return false;
1845 }
1846
1847 let plan = self.user_store.read(cx).current_plan();
1848 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
1849 return false;
1850 }
1851
1852 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1853 if has_previous_trial {
1854 return false;
1855 }
1856
1857 true
1858 }
1859
1860 fn render_trial_upsell(
1861 &self,
1862 _window: &mut Window,
1863 cx: &mut Context<Self>,
1864 ) -> Option<impl IntoElement> {
1865 if !self.should_render_upsell(cx) {
1866 return None;
1867 }
1868
1869 let checkbox = CheckboxWithLabel::new(
1870 "dont-show-again",
1871 Label::new("Don't show again").color(Color::Muted),
1872 ToggleState::Unselected,
1873 move |toggle_state, _window, cx| {
1874 let toggle_state_bool = toggle_state.selected();
1875
1876 set_trial_upsell_dismissed(toggle_state_bool, cx);
1877 },
1878 );
1879
1880 Some(
1881 div().p_2().child(
1882 v_flex()
1883 .w_full()
1884 .elevation_2(cx)
1885 .rounded(px(8.))
1886 .bg(cx.theme().colors().background.alpha(0.5))
1887 .p(px(3.))
1888
1889 .child(
1890 div()
1891 .gap_2()
1892 .flex()
1893 .flex_col()
1894 .size_full()
1895 .border_1()
1896 .rounded(px(5.))
1897 .border_color(cx.theme().colors().text.alpha(0.1))
1898 .overflow_hidden()
1899 .relative()
1900 .bg(cx.theme().colors().panel_background)
1901 .px_4()
1902 .py_3()
1903 .child(
1904 div()
1905 .absolute()
1906 .top_0()
1907 .right(px(-1.0))
1908 .w(px(441.))
1909 .h(px(167.))
1910 .child(
1911 Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
1912 )
1913 )
1914 .child(
1915 div()
1916 .absolute()
1917 .top(px(-8.0))
1918 .right_0()
1919 .w(px(400.))
1920 .h(px(92.))
1921 .child(
1922 Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
1923 )
1924 )
1925 // .child(
1926 // div()
1927 // .absolute()
1928 // .top_0()
1929 // .right(px(360.))
1930 // .size(px(401.))
1931 // .overflow_hidden()
1932 // .bg(cx.theme().colors().panel_background)
1933 // )
1934 .child(
1935 div()
1936 .absolute()
1937 .top_0()
1938 .right_0()
1939 .w(px(660.))
1940 .h(px(401.))
1941 .overflow_hidden()
1942 .bg(linear_gradient(
1943 75.,
1944 linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
1945 linear_color_stop(cx.theme().colors().panel_background, 0.45),
1946 ))
1947 )
1948 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
1949 .child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
1950 .child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
1951 .child(
1952 h_flex()
1953 .w_full()
1954 .px_neg_1()
1955 .justify_between()
1956 .items_center()
1957 .child(h_flex().items_center().gap_1().child(checkbox))
1958 .child(
1959 h_flex()
1960 .gap_2()
1961 .child(
1962 Button::new("dismiss-button", "Not Now")
1963 .style(ButtonStyle::Transparent)
1964 .color(Color::Muted)
1965 .on_click({
1966 let assistant_panel = cx.entity();
1967 move |_, _, cx| {
1968 assistant_panel.update(
1969 cx,
1970 |this, cx| {
1971 let hidden =
1972 this.hide_trial_upsell;
1973 println!("hidden: {}", hidden);
1974 this.hide_trial_upsell = true;
1975 let new_hidden =
1976 this.hide_trial_upsell;
1977 println!(
1978 "new_hidden: {}",
1979 new_hidden
1980 );
1981
1982 cx.notify();
1983 },
1984 );
1985 }
1986 }),
1987 )
1988 .child(
1989 Button::new("cta-button", "Start Trial")
1990 .style(ButtonStyle::Transparent)
1991 .on_click(|_, _, cx| {
1992 cx.open_url(&zed_urls::account_url(cx))
1993 }),
1994 ),
1995 ),
1996 ),
1997 ),
1998 ),
1999 )
2000 }
2001
2002 fn render_active_thread_or_empty_state(
2003 &self,
2004 window: &mut Window,
2005 cx: &mut Context<Self>,
2006 ) -> AnyElement {
2007 if self.thread.read(cx).is_empty() {
2008 return self
2009 .render_thread_empty_state(window, cx)
2010 .into_any_element();
2011 }
2012
2013 self.thread.clone().into_any_element()
2014 }
2015
2016 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2017 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2018 return Some(ConfigurationError::NoProvider);
2019 };
2020
2021 if !model.provider.is_authenticated(cx) {
2022 return Some(ConfigurationError::ProviderNotAuthenticated);
2023 }
2024
2025 if model.provider.must_accept_terms(cx) {
2026 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2027 model.provider,
2028 ));
2029 }
2030
2031 None
2032 }
2033
2034 fn render_thread_empty_state(
2035 &self,
2036 window: &mut Window,
2037 cx: &mut Context<Self>,
2038 ) -> impl IntoElement {
2039 let recent_history = self
2040 .history_store
2041 .update(cx, |this, cx| this.recent_entries(6, cx));
2042
2043 let configuration_error = self.configuration_error(cx);
2044 let no_error = configuration_error.is_none();
2045 let focus_handle = self.focus_handle(cx);
2046
2047 v_flex()
2048 .size_full()
2049 .when(recent_history.is_empty(), |this| {
2050 let configuration_error_ref = &configuration_error;
2051 this.child(
2052 v_flex()
2053 .size_full()
2054 .max_w_80()
2055 .mx_auto()
2056 .justify_center()
2057 .items_center()
2058 .gap_1()
2059 .child(
2060 h_flex().child(
2061 Headline::new("Welcome to the Agent Panel")
2062 ),
2063 )
2064 .when(no_error, |parent| {
2065 parent
2066 .child(
2067 h_flex().child(
2068 Label::new("Ask and build anything.")
2069 .color(Color::Muted)
2070 .mb_2p5(),
2071 ),
2072 )
2073 .child(
2074 Button::new("new-thread", "Start New Thread")
2075 .icon(IconName::Plus)
2076 .icon_position(IconPosition::Start)
2077 .icon_size(IconSize::Small)
2078 .icon_color(Color::Muted)
2079 .full_width()
2080 .key_binding(KeyBinding::for_action_in(
2081 &NewThread::default(),
2082 &focus_handle,
2083 window,
2084 cx,
2085 ))
2086 .on_click(|_event, window, cx| {
2087 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2088 }),
2089 )
2090 .child(
2091 Button::new("context", "Add Context")
2092 .icon(IconName::FileCode)
2093 .icon_position(IconPosition::Start)
2094 .icon_size(IconSize::Small)
2095 .icon_color(Color::Muted)
2096 .full_width()
2097 .key_binding(KeyBinding::for_action_in(
2098 &ToggleContextPicker,
2099 &focus_handle,
2100 window,
2101 cx,
2102 ))
2103 .on_click(|_event, window, cx| {
2104 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2105 }),
2106 )
2107 .child(
2108 Button::new("mode", "Switch Model")
2109 .icon(IconName::DatabaseZap)
2110 .icon_position(IconPosition::Start)
2111 .icon_size(IconSize::Small)
2112 .icon_color(Color::Muted)
2113 .full_width()
2114 .key_binding(KeyBinding::for_action_in(
2115 &ToggleModelSelector,
2116 &focus_handle,
2117 window,
2118 cx,
2119 ))
2120 .on_click(|_event, window, cx| {
2121 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2122 }),
2123 )
2124 .child(
2125 Button::new("settings", "View Settings")
2126 .icon(IconName::Settings)
2127 .icon_position(IconPosition::Start)
2128 .icon_size(IconSize::Small)
2129 .icon_color(Color::Muted)
2130 .full_width()
2131 .key_binding(KeyBinding::for_action_in(
2132 &OpenConfiguration,
2133 &focus_handle,
2134 window,
2135 cx,
2136 ))
2137 .on_click(|_event, window, cx| {
2138 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2139 }),
2140 )
2141 })
2142 .map(|parent| {
2143 match configuration_error_ref {
2144 Some(ConfigurationError::ProviderNotAuthenticated)
2145 | Some(ConfigurationError::NoProvider) => {
2146 parent
2147 .child(
2148 h_flex().child(
2149 Label::new("To start using the agent, configure at least one LLM provider.")
2150 .color(Color::Muted)
2151 .mb_2p5()
2152 )
2153 )
2154 .child(
2155 Button::new("settings", "Configure a Provider")
2156 .icon(IconName::Settings)
2157 .icon_position(IconPosition::Start)
2158 .icon_size(IconSize::Small)
2159 .icon_color(Color::Muted)
2160 .full_width()
2161 .key_binding(KeyBinding::for_action_in(
2162 &OpenConfiguration,
2163 &focus_handle,
2164 window,
2165 cx,
2166 ))
2167 .on_click(|_event, window, cx| {
2168 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2169 }),
2170 )
2171 }
2172 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2173 parent.children(
2174 provider.render_accept_terms(
2175 LanguageModelProviderTosView::ThreadFreshStart,
2176 cx,
2177 ),
2178 )
2179 }
2180 None => parent,
2181 }
2182 })
2183 )
2184 })
2185 .when(!recent_history.is_empty(), |parent| {
2186 let focus_handle = focus_handle.clone();
2187 let configuration_error_ref = &configuration_error;
2188
2189 parent
2190 .overflow_hidden()
2191 .p_1p5()
2192 .justify_end()
2193 .gap_1()
2194 .child(
2195 h_flex()
2196 .pl_1p5()
2197 .pb_1()
2198 .w_full()
2199 .justify_between()
2200 .border_b_1()
2201 .border_color(cx.theme().colors().border_variant)
2202 .child(
2203 Label::new("Past Interactions")
2204 .size(LabelSize::Small)
2205 .color(Color::Muted),
2206 )
2207 .child(
2208 Button::new("view-history", "View All")
2209 .style(ButtonStyle::Subtle)
2210 .label_size(LabelSize::Small)
2211 .key_binding(
2212 KeyBinding::for_action_in(
2213 &OpenHistory,
2214 &self.focus_handle(cx),
2215 window,
2216 cx,
2217 ).map(|kb| kb.size(rems_from_px(12.))),
2218 )
2219 .on_click(move |_event, window, cx| {
2220 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2221 }),
2222 ),
2223 )
2224 .child(
2225 v_flex()
2226 .gap_1()
2227 .children(
2228 recent_history.into_iter().map(|entry| {
2229 // TODO: Add keyboard navigation.
2230 match entry {
2231 HistoryEntry::Thread(thread) => {
2232 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
2233 .into_any_element()
2234 }
2235 HistoryEntry::Context(context) => {
2236 PastContext::new(context, cx.entity().downgrade(), false, vec![])
2237 .into_any_element()
2238 }
2239 }
2240 }),
2241 )
2242 )
2243 .map(|parent| {
2244 match configuration_error_ref {
2245 Some(ConfigurationError::ProviderNotAuthenticated)
2246 | Some(ConfigurationError::NoProvider) => {
2247 parent
2248 .child(
2249 Banner::new()
2250 .severity(ui::Severity::Warning)
2251 .child(
2252 Label::new(
2253 "Configure at least one LLM provider to start using the panel.",
2254 )
2255 .size(LabelSize::Small),
2256 )
2257 .action_slot(
2258 Button::new("settings", "Configure Provider")
2259 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2260 .label_size(LabelSize::Small)
2261 .key_binding(
2262 KeyBinding::for_action_in(
2263 &OpenConfiguration,
2264 &focus_handle,
2265 window,
2266 cx,
2267 )
2268 .map(|kb| kb.size(rems_from_px(12.))),
2269 )
2270 .on_click(|_event, window, cx| {
2271 window.dispatch_action(
2272 OpenConfiguration.boxed_clone(),
2273 cx,
2274 )
2275 }),
2276 ),
2277 )
2278 }
2279 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2280 parent
2281 .child(
2282 Banner::new()
2283 .severity(ui::Severity::Warning)
2284 .child(
2285 h_flex()
2286 .w_full()
2287 .children(
2288 provider.render_accept_terms(
2289 LanguageModelProviderTosView::ThreadtEmptyState,
2290 cx,
2291 ),
2292 ),
2293 ),
2294 )
2295 }
2296 None => parent,
2297 }
2298 })
2299 })
2300 }
2301
2302 fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2303 let tool_use_limit_reached = self
2304 .thread
2305 .read(cx)
2306 .thread()
2307 .read(cx)
2308 .tool_use_limit_reached();
2309 if !tool_use_limit_reached {
2310 return None;
2311 }
2312
2313 let model = self
2314 .thread
2315 .read(cx)
2316 .thread()
2317 .read(cx)
2318 .configured_model()?
2319 .model;
2320
2321 let max_mode_upsell = if model.supports_max_mode() {
2322 " Enable max mode for unlimited tool use."
2323 } else {
2324 ""
2325 };
2326
2327 Some(
2328 Banner::new()
2329 .severity(ui::Severity::Info)
2330 .child(h_flex().child(Label::new(format!(
2331 "Consecutive tool use limit reached.{max_mode_upsell}"
2332 ))))
2333 .into_any_element(),
2334 )
2335 }
2336
2337 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2338 let last_error = self.thread.read(cx).last_error()?;
2339
2340 Some(
2341 div()
2342 .absolute()
2343 .right_3()
2344 .bottom_12()
2345 .max_w_96()
2346 .py_2()
2347 .px_3()
2348 .elevation_2(cx)
2349 .occlude()
2350 .child(match last_error {
2351 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2352 ThreadError::MaxMonthlySpendReached => {
2353 self.render_max_monthly_spend_reached_error(cx)
2354 }
2355 ThreadError::ModelRequestLimitReached { plan } => {
2356 self.render_model_request_limit_reached_error(plan, cx)
2357 }
2358 ThreadError::Message { header, message } => {
2359 self.render_error_message(header, message, cx)
2360 }
2361 })
2362 .into_any(),
2363 )
2364 }
2365
2366 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2367 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.";
2368
2369 v_flex()
2370 .gap_0p5()
2371 .child(
2372 h_flex()
2373 .gap_1p5()
2374 .items_center()
2375 .child(Icon::new(IconName::XCircle).color(Color::Error))
2376 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2377 )
2378 .child(
2379 div()
2380 .id("error-message")
2381 .max_h_24()
2382 .overflow_y_scroll()
2383 .child(Label::new(ERROR_MESSAGE)),
2384 )
2385 .child(
2386 h_flex()
2387 .justify_end()
2388 .mt_1()
2389 .gap_1()
2390 .child(self.create_copy_button(ERROR_MESSAGE))
2391 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2392 |this, _, _, cx| {
2393 this.thread.update(cx, |this, _cx| {
2394 this.clear_last_error();
2395 });
2396
2397 cx.open_url(&zed_urls::account_url(cx));
2398 cx.notify();
2399 },
2400 )))
2401 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2402 |this, _, _, cx| {
2403 this.thread.update(cx, |this, _cx| {
2404 this.clear_last_error();
2405 });
2406
2407 cx.notify();
2408 },
2409 ))),
2410 )
2411 .into_any()
2412 }
2413
2414 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2415 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2416
2417 v_flex()
2418 .gap_0p5()
2419 .child(
2420 h_flex()
2421 .gap_1p5()
2422 .items_center()
2423 .child(Icon::new(IconName::XCircle).color(Color::Error))
2424 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2425 )
2426 .child(
2427 div()
2428 .id("error-message")
2429 .max_h_24()
2430 .overflow_y_scroll()
2431 .child(Label::new(ERROR_MESSAGE)),
2432 )
2433 .child(
2434 h_flex()
2435 .justify_end()
2436 .mt_1()
2437 .gap_1()
2438 .child(self.create_copy_button(ERROR_MESSAGE))
2439 .child(
2440 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2441 cx.listener(|this, _, _, cx| {
2442 this.thread.update(cx, |this, _cx| {
2443 this.clear_last_error();
2444 });
2445
2446 cx.open_url(&zed_urls::account_url(cx));
2447 cx.notify();
2448 }),
2449 ),
2450 )
2451 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2452 |this, _, _, cx| {
2453 this.thread.update(cx, |this, _cx| {
2454 this.clear_last_error();
2455 });
2456
2457 cx.notify();
2458 },
2459 ))),
2460 )
2461 .into_any()
2462 }
2463
2464 fn render_model_request_limit_reached_error(
2465 &self,
2466 plan: Plan,
2467 cx: &mut Context<Self>,
2468 ) -> AnyElement {
2469 let error_message = match plan {
2470 Plan::ZedPro => {
2471 "Model request limit reached. Upgrade to usage-based billing for more requests."
2472 }
2473 Plan::ZedProTrial => {
2474 "Model request limit reached. Upgrade to Zed Pro for more requests."
2475 }
2476 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2477 };
2478 let call_to_action = match plan {
2479 Plan::ZedPro => "Upgrade to usage-based billing",
2480 Plan::ZedProTrial => "Upgrade to Zed Pro",
2481 Plan::Free => "Upgrade to Zed Pro",
2482 };
2483
2484 v_flex()
2485 .gap_0p5()
2486 .child(
2487 h_flex()
2488 .gap_1p5()
2489 .items_center()
2490 .child(Icon::new(IconName::XCircle).color(Color::Error))
2491 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2492 )
2493 .child(
2494 div()
2495 .id("error-message")
2496 .max_h_24()
2497 .overflow_y_scroll()
2498 .child(Label::new(error_message)),
2499 )
2500 .child(
2501 h_flex()
2502 .justify_end()
2503 .mt_1()
2504 .gap_1()
2505 .child(self.create_copy_button(error_message))
2506 .child(
2507 Button::new("subscribe", call_to_action).on_click(cx.listener(
2508 |this, _, _, cx| {
2509 this.thread.update(cx, |this, _cx| {
2510 this.clear_last_error();
2511 });
2512
2513 cx.open_url(&zed_urls::account_url(cx));
2514 cx.notify();
2515 },
2516 )),
2517 )
2518 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2519 |this, _, _, cx| {
2520 this.thread.update(cx, |this, _cx| {
2521 this.clear_last_error();
2522 });
2523
2524 cx.notify();
2525 },
2526 ))),
2527 )
2528 .into_any()
2529 }
2530
2531 fn render_error_message(
2532 &self,
2533 header: SharedString,
2534 message: SharedString,
2535 cx: &mut Context<Self>,
2536 ) -> AnyElement {
2537 let message_with_header = format!("{}\n{}", header, message);
2538 v_flex()
2539 .gap_0p5()
2540 .child(
2541 h_flex()
2542 .gap_1p5()
2543 .items_center()
2544 .child(Icon::new(IconName::XCircle).color(Color::Error))
2545 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2546 )
2547 .child(
2548 div()
2549 .id("error-message")
2550 .max_h_32()
2551 .overflow_y_scroll()
2552 .child(Label::new(message.clone())),
2553 )
2554 .child(
2555 h_flex()
2556 .justify_end()
2557 .mt_1()
2558 .gap_1()
2559 .child(self.create_copy_button(message_with_header))
2560 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2561 |this, _, _, cx| {
2562 this.thread.update(cx, |this, _cx| {
2563 this.clear_last_error();
2564 });
2565
2566 cx.notify();
2567 },
2568 ))),
2569 )
2570 .into_any()
2571 }
2572
2573 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2574 let is_local = self.project.read(cx).is_local();
2575 div()
2576 .invisible()
2577 .absolute()
2578 .top_0()
2579 .right_0()
2580 .bottom_0()
2581 .left_0()
2582 .bg(cx.theme().colors().drop_target_background)
2583 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2584 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2585 .when(is_local, |this| {
2586 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2587 })
2588 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2589 let item = tab.pane.read(cx).item_for_index(tab.ix);
2590 let project_paths = item
2591 .and_then(|item| item.project_path(cx))
2592 .into_iter()
2593 .collect::<Vec<_>>();
2594 this.handle_drop(project_paths, vec![], window, cx);
2595 }))
2596 .on_drop(
2597 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2598 let project_paths = selection
2599 .items()
2600 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2601 .collect::<Vec<_>>();
2602 this.handle_drop(project_paths, vec![], window, cx);
2603 }),
2604 )
2605 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2606 let tasks = paths
2607 .paths()
2608 .into_iter()
2609 .map(|path| {
2610 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2611 })
2612 .collect::<Vec<_>>();
2613 cx.spawn_in(window, async move |this, cx| {
2614 let mut paths = vec![];
2615 let mut added_worktrees = vec![];
2616 let opened_paths = futures::future::join_all(tasks).await;
2617 for entry in opened_paths {
2618 if let Some((worktree, project_path)) = entry.log_err() {
2619 added_worktrees.push(worktree);
2620 paths.push(project_path);
2621 }
2622 }
2623 this.update_in(cx, |this, window, cx| {
2624 this.handle_drop(paths, added_worktrees, window, cx);
2625 })
2626 .ok();
2627 })
2628 .detach();
2629 }))
2630 }
2631
2632 fn handle_drop(
2633 &mut self,
2634 paths: Vec<ProjectPath>,
2635 added_worktrees: Vec<Entity<Worktree>>,
2636 window: &mut Window,
2637 cx: &mut Context<Self>,
2638 ) {
2639 match &self.active_view {
2640 ActiveView::Thread { .. } => {
2641 let context_store = self.thread.read(cx).context_store().clone();
2642 context_store.update(cx, move |context_store, cx| {
2643 let mut tasks = Vec::new();
2644 for project_path in &paths {
2645 tasks.push(context_store.add_file_from_path(
2646 project_path.clone(),
2647 false,
2648 cx,
2649 ));
2650 }
2651 cx.background_spawn(async move {
2652 futures::future::join_all(tasks).await;
2653 // Need to hold onto the worktrees until they have already been used when
2654 // opening the buffers.
2655 drop(added_worktrees);
2656 })
2657 .detach();
2658 });
2659 }
2660 ActiveView::PromptEditor { context_editor, .. } => {
2661 context_editor.update(cx, |context_editor, cx| {
2662 ContextEditor::insert_dragged_files(
2663 context_editor,
2664 paths,
2665 added_worktrees,
2666 window,
2667 cx,
2668 );
2669 });
2670 }
2671 ActiveView::History | ActiveView::Configuration => {}
2672 }
2673 }
2674
2675 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2676 let message = message.into();
2677 IconButton::new("copy", IconName::Copy)
2678 .on_click(move |_, _, cx| {
2679 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2680 })
2681 .tooltip(Tooltip::text("Copy Error Message"))
2682 }
2683
2684 fn key_context(&self) -> KeyContext {
2685 let mut key_context = KeyContext::new_with_defaults();
2686 key_context.add("AgentPanel");
2687 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2688 key_context.add("prompt_editor");
2689 }
2690 key_context
2691 }
2692}
2693
2694impl Render for AssistantPanel {
2695 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2696 v_flex()
2697 .key_context(self.key_context())
2698 .justify_between()
2699 .size_full()
2700 .on_action(cx.listener(Self::cancel))
2701 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2702 this.new_thread(action, window, cx);
2703 }))
2704 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2705 this.open_history(window, cx);
2706 }))
2707 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2708 this.open_configuration(window, cx);
2709 }))
2710 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2711 .on_action(cx.listener(Self::deploy_rules_library))
2712 .on_action(cx.listener(Self::open_agent_diff))
2713 .on_action(cx.listener(Self::go_back))
2714 .on_action(cx.listener(Self::toggle_navigation_menu))
2715 .on_action(cx.listener(Self::toggle_options_menu))
2716 .on_action(cx.listener(Self::increase_font_size))
2717 .on_action(cx.listener(Self::decrease_font_size))
2718 .on_action(cx.listener(Self::reset_font_size))
2719 .child(self.render_toolbar(window, cx))
2720 .children(self.render_trial_upsell(window, cx))
2721 .map(|parent| match &self.active_view {
2722 ActiveView::Thread { .. } => parent.child(
2723 v_flex()
2724 .relative()
2725 .justify_between()
2726 .size_full()
2727 .child(self.render_active_thread_or_empty_state(window, cx))
2728 .children(self.render_tool_use_limit_reached(cx))
2729 .child(h_flex().child(self.message_editor.clone()))
2730 .children(self.render_last_error(cx))
2731 .child(self.render_drag_target(cx)),
2732 ),
2733 ActiveView::History => parent.child(self.history.clone()),
2734 ActiveView::PromptEditor {
2735 context_editor,
2736 buffer_search_bar,
2737 ..
2738 } => {
2739 let mut registrar = buffer_search::DivRegistrar::new(
2740 |this, _, _cx| match &this.active_view {
2741 ActiveView::PromptEditor {
2742 buffer_search_bar, ..
2743 } => Some(buffer_search_bar.clone()),
2744 _ => None,
2745 },
2746 cx,
2747 );
2748 BufferSearchBar::register(&mut registrar);
2749 parent.child(
2750 registrar
2751 .into_div()
2752 .size_full()
2753 .relative()
2754 .map(|parent| {
2755 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2756 if buffer_search_bar.is_dismissed() {
2757 return parent;
2758 }
2759 parent.child(
2760 div()
2761 .p(DynamicSpacing::Base08.rems(cx))
2762 .border_b_1()
2763 .border_color(cx.theme().colors().border_variant)
2764 .bg(cx.theme().colors().editor_background)
2765 .child(buffer_search_bar.render(window, cx)),
2766 )
2767 })
2768 })
2769 .child(context_editor.clone())
2770 .child(self.render_drag_target(cx)),
2771 )
2772 }
2773 ActiveView::Configuration => parent.children(self.configuration.clone()),
2774 })
2775 }
2776}
2777
2778struct PromptLibraryInlineAssist {
2779 workspace: WeakEntity<Workspace>,
2780}
2781
2782impl PromptLibraryInlineAssist {
2783 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2784 Self { workspace }
2785 }
2786}
2787
2788impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2789 fn assist(
2790 &self,
2791 prompt_editor: &Entity<Editor>,
2792 initial_prompt: Option<String>,
2793 window: &mut Window,
2794 cx: &mut Context<RulesLibrary>,
2795 ) {
2796 InlineAssistant::update_global(cx, |assistant, cx| {
2797 let Some(project) = self
2798 .workspace
2799 .upgrade()
2800 .map(|workspace| workspace.read(cx).project().downgrade())
2801 else {
2802 return;
2803 };
2804 let prompt_store = None;
2805 let thread_store = None;
2806 let text_thread_store = None;
2807 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
2808 assistant.assist(
2809 &prompt_editor,
2810 self.workspace.clone(),
2811 context_store,
2812 project,
2813 prompt_store,
2814 thread_store,
2815 text_thread_store,
2816 initial_prompt,
2817 window,
2818 cx,
2819 )
2820 })
2821 }
2822
2823 fn focus_assistant_panel(
2824 &self,
2825 workspace: &mut Workspace,
2826 window: &mut Window,
2827 cx: &mut Context<Workspace>,
2828 ) -> bool {
2829 workspace
2830 .focus_panel::<AssistantPanel>(window, cx)
2831 .is_some()
2832 }
2833}
2834
2835pub struct ConcreteAssistantPanelDelegate;
2836
2837impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2838 fn active_context_editor(
2839 &self,
2840 workspace: &mut Workspace,
2841 _window: &mut Window,
2842 cx: &mut Context<Workspace>,
2843 ) -> Option<Entity<ContextEditor>> {
2844 let panel = workspace.panel::<AssistantPanel>(cx)?;
2845 panel.read(cx).active_context_editor()
2846 }
2847
2848 fn open_saved_context(
2849 &self,
2850 workspace: &mut Workspace,
2851 path: Arc<Path>,
2852 window: &mut Window,
2853 cx: &mut Context<Workspace>,
2854 ) -> Task<Result<()>> {
2855 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2856 return Task::ready(Err(anyhow!("Agent panel not found")));
2857 };
2858
2859 panel.update(cx, |panel, cx| {
2860 panel.open_saved_prompt_editor(path, window, cx)
2861 })
2862 }
2863
2864 fn open_remote_context(
2865 &self,
2866 _workspace: &mut Workspace,
2867 _context_id: assistant_context_editor::ContextId,
2868 _window: &mut Window,
2869 _cx: &mut Context<Workspace>,
2870 ) -> Task<Result<Entity<ContextEditor>>> {
2871 Task::ready(Err(anyhow!("opening remote context not implemented")))
2872 }
2873
2874 fn quote_selection(
2875 &self,
2876 workspace: &mut Workspace,
2877 selection_ranges: Vec<Range<Anchor>>,
2878 buffer: Entity<MultiBuffer>,
2879 window: &mut Window,
2880 cx: &mut Context<Workspace>,
2881 ) {
2882 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2883 return;
2884 };
2885
2886 if !panel.focus_handle(cx).contains_focused(window, cx) {
2887 workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2888 }
2889
2890 panel.update(cx, |_, cx| {
2891 // Wait to create a new context until the workspace is no longer
2892 // being updated.
2893 cx.defer_in(window, move |panel, window, cx| {
2894 if panel.has_active_thread() {
2895 panel.message_editor.update(cx, |message_editor, cx| {
2896 message_editor.context_store().update(cx, |store, cx| {
2897 let buffer = buffer.read(cx);
2898 let selection_ranges = selection_ranges
2899 .into_iter()
2900 .flat_map(|range| {
2901 let (start_buffer, start) =
2902 buffer.text_anchor_for_position(range.start, cx)?;
2903 let (end_buffer, end) =
2904 buffer.text_anchor_for_position(range.end, cx)?;
2905 if start_buffer != end_buffer {
2906 return None;
2907 }
2908 Some((start_buffer, start..end))
2909 })
2910 .collect::<Vec<_>>();
2911
2912 for (buffer, range) in selection_ranges {
2913 store.add_selection(buffer, range, cx);
2914 }
2915 })
2916 })
2917 } else if let Some(context_editor) = panel.active_context_editor() {
2918 let snapshot = buffer.read(cx).snapshot(cx);
2919 let selection_ranges = selection_ranges
2920 .into_iter()
2921 .map(|range| range.to_point(&snapshot))
2922 .collect::<Vec<_>>();
2923
2924 context_editor.update(cx, |context_editor, cx| {
2925 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2926 });
2927 }
2928 });
2929 });
2930 }
2931}
2932
2933const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
2934
2935fn dismissed_trial_upsell() -> bool {
2936 db::kvp::KEY_VALUE_STORE
2937 .read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
2938 .log_err()
2939 .map_or(false, |s| s.is_some())
2940}
2941
2942fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
2943 db::write_and_log(cx, move || async move {
2944 if is_dismissed {
2945 db::kvp::KEY_VALUE_STORE
2946 .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
2947 .await
2948 } else {
2949 db::kvp::KEY_VALUE_STORE
2950 .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
2951 .await
2952 }
2953 })
2954}