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