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