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};
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 _;
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_manager = self.thread_store.read(cx).context_server_manager();
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_manager, 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 thread = active_thread.thread().read(cx);
1392 let thread_id = thread.id().clone();
1393 let is_empty = active_thread.is_empty();
1394 let last_usage = active_thread.thread().read(cx).last_usage();
1395 let account_url = zed_urls::account_url(cx);
1396
1397 let show_token_count = match &self.active_view {
1398 ActiveView::Thread { .. } => !is_empty,
1399 ActiveView::PromptEditor { .. } => true,
1400 _ => false,
1401 };
1402
1403 let focus_handle = self.focus_handle(cx);
1404
1405 let go_back_button = div().child(
1406 IconButton::new("go-back", IconName::ArrowLeft)
1407 .icon_size(IconSize::Small)
1408 .on_click(cx.listener(|this, _, window, cx| {
1409 this.go_back(&workspace::GoBack, window, cx);
1410 }))
1411 .tooltip({
1412 let focus_handle = focus_handle.clone();
1413 move |window, cx| {
1414 Tooltip::for_action_in(
1415 "Go Back",
1416 &workspace::GoBack,
1417 &focus_handle,
1418 window,
1419 cx,
1420 )
1421 }
1422 }),
1423 );
1424
1425 let recent_entries_menu = div().child(
1426 PopoverMenu::new("agent-nav-menu")
1427 .trigger_with_tooltip(
1428 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1429 .icon_size(IconSize::Small)
1430 .style(ui::ButtonStyle::Subtle),
1431 {
1432 let focus_handle = focus_handle.clone();
1433 move |window, cx| {
1434 Tooltip::for_action_in(
1435 "Toggle Panel Menu",
1436 &ToggleNavigationMenu,
1437 &focus_handle,
1438 window,
1439 cx,
1440 )
1441 }
1442 },
1443 )
1444 .anchor(Corner::TopLeft)
1445 .with_handle(self.assistant_navigation_menu_handle.clone())
1446 .menu({
1447 let menu = self.assistant_navigation_menu.clone();
1448 move |window, cx| {
1449 if let Some(menu) = menu.as_ref() {
1450 menu.update(cx, |_, cx| {
1451 cx.defer_in(window, |menu, window, cx| {
1452 menu.rebuild(window, cx);
1453 });
1454 })
1455 }
1456 menu.clone()
1457 }
1458 }),
1459 );
1460
1461 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1462 .trigger_with_tooltip(
1463 IconButton::new("agent-options-menu", IconName::Ellipsis)
1464 .icon_size(IconSize::Small),
1465 {
1466 let focus_handle = focus_handle.clone();
1467 move |window, cx| {
1468 Tooltip::for_action_in(
1469 "Toggle Agent Menu",
1470 &ToggleOptionsMenu,
1471 &focus_handle,
1472 window,
1473 cx,
1474 )
1475 }
1476 },
1477 )
1478 .anchor(Corner::TopRight)
1479 .with_handle(self.assistant_dropdown_menu_handle.clone())
1480 .menu(move |window, cx| {
1481 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1482 menu = menu
1483 .action("New Thread", NewThread::default().boxed_clone())
1484 .action("New Text Thread", NewTextThread.boxed_clone())
1485 .when(!is_empty, |menu| {
1486 menu.action(
1487 "New From Summary",
1488 Box::new(NewThread {
1489 from_thread_id: Some(thread_id.clone()),
1490 }),
1491 )
1492 })
1493 .separator();
1494
1495 menu = menu
1496 .header("MCP Servers")
1497 .action(
1498 "View Server Extensions",
1499 Box::new(zed_actions::Extensions {
1500 category_filter: Some(
1501 zed_actions::ExtensionCategoryFilter::ContextServers,
1502 ),
1503 }),
1504 )
1505 .action("Add Custom Server…", Box::new(AddContextServer))
1506 .separator();
1507
1508 if let Some(usage) = last_usage {
1509 menu = menu
1510 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1511 .custom_entry(
1512 move |_window, cx| {
1513 let used_percentage = match usage.limit {
1514 UsageLimit::Limited(limit) => {
1515 Some((usage.amount as f32 / limit as f32) * 100.)
1516 }
1517 UsageLimit::Unlimited => None,
1518 };
1519
1520 h_flex()
1521 .flex_1()
1522 .gap_1p5()
1523 .children(used_percentage.map(|percent| {
1524 ProgressBar::new("usage", percent, 100., cx)
1525 }))
1526 .child(
1527 Label::new(match usage.limit {
1528 UsageLimit::Limited(limit) => {
1529 format!("{} / {limit}", usage.amount)
1530 }
1531 UsageLimit::Unlimited => {
1532 format!("{} / ∞", usage.amount)
1533 }
1534 })
1535 .size(LabelSize::Small)
1536 .color(Color::Muted),
1537 )
1538 .into_any_element()
1539 },
1540 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1541 )
1542 .separator()
1543 }
1544
1545 menu = menu
1546 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1547 .action("Settings", Box::new(OpenConfiguration));
1548 menu
1549 }))
1550 });
1551
1552 h_flex()
1553 .id("assistant-toolbar")
1554 .h(Tab::container_height(cx))
1555 .max_w_full()
1556 .flex_none()
1557 .justify_between()
1558 .gap_2()
1559 .bg(cx.theme().colors().tab_bar_background)
1560 .border_b_1()
1561 .border_color(cx.theme().colors().border)
1562 .child(
1563 h_flex()
1564 .size_full()
1565 .pl_1()
1566 .gap_1()
1567 .child(match &self.active_view {
1568 ActiveView::History | ActiveView::Configuration => go_back_button,
1569 _ => recent_entries_menu,
1570 })
1571 .child(self.render_title_view(window, cx)),
1572 )
1573 .child(
1574 h_flex()
1575 .h_full()
1576 .gap_2()
1577 .when(show_token_count, |parent| {
1578 parent.children(self.render_token_count(&thread, cx))
1579 })
1580 .child(
1581 h_flex()
1582 .h_full()
1583 .gap(DynamicSpacing::Base02.rems(cx))
1584 .px(DynamicSpacing::Base08.rems(cx))
1585 .border_l_1()
1586 .border_color(cx.theme().colors().border)
1587 .child(
1588 IconButton::new("new", IconName::Plus)
1589 .icon_size(IconSize::Small)
1590 .style(ButtonStyle::Subtle)
1591 .tooltip(move |window, cx| {
1592 Tooltip::for_action_in(
1593 "New Thread",
1594 &NewThread::default(),
1595 &focus_handle,
1596 window,
1597 cx,
1598 )
1599 })
1600 .on_click(move |_event, window, cx| {
1601 window.dispatch_action(
1602 NewThread::default().boxed_clone(),
1603 cx,
1604 );
1605 }),
1606 )
1607 .child(agent_extra_menu),
1608 ),
1609 )
1610 }
1611
1612 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1613 let is_generating = thread.is_generating();
1614 let message_editor = self.message_editor.read(cx);
1615
1616 let conversation_token_usage = thread.total_token_usage()?;
1617
1618 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1619 self.thread.read(cx).editing_message_id()
1620 {
1621 let combined = thread
1622 .token_usage_up_to_message(editing_message_id)
1623 .add(unsent_tokens);
1624
1625 (combined, unsent_tokens > 0)
1626 } else {
1627 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1628 let combined = conversation_token_usage.add(unsent_tokens);
1629
1630 (combined, unsent_tokens > 0)
1631 };
1632
1633 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1634
1635 match &self.active_view {
1636 ActiveView::Thread { .. } => {
1637 if total_token_usage.total == 0 {
1638 return None;
1639 }
1640
1641 let token_color = match total_token_usage.ratio() {
1642 TokenUsageRatio::Normal if is_estimating => Color::Default,
1643 TokenUsageRatio::Normal => Color::Muted,
1644 TokenUsageRatio::Warning => Color::Warning,
1645 TokenUsageRatio::Exceeded => Color::Error,
1646 };
1647
1648 let token_count = h_flex()
1649 .id("token-count")
1650 .flex_shrink_0()
1651 .gap_0p5()
1652 .when(!is_generating && is_estimating, |parent| {
1653 parent
1654 .child(
1655 h_flex()
1656 .mr_1()
1657 .size_2p5()
1658 .justify_center()
1659 .rounded_full()
1660 .bg(cx.theme().colors().text.opacity(0.1))
1661 .child(
1662 div().size_1().rounded_full().bg(cx.theme().colors().text),
1663 ),
1664 )
1665 .tooltip(move |window, cx| {
1666 Tooltip::with_meta(
1667 "Estimated New Token Count",
1668 None,
1669 format!(
1670 "Current Conversation Tokens: {}",
1671 humanize_token_count(conversation_token_usage.total)
1672 ),
1673 window,
1674 cx,
1675 )
1676 })
1677 })
1678 .child(
1679 Label::new(humanize_token_count(total_token_usage.total))
1680 .size(LabelSize::Small)
1681 .color(token_color)
1682 .map(|label| {
1683 if is_generating || is_waiting_to_update_token_count {
1684 label
1685 .with_animation(
1686 "used-tokens-label",
1687 Animation::new(Duration::from_secs(2))
1688 .repeat()
1689 .with_easing(pulsating_between(0.6, 1.)),
1690 |label, delta| label.alpha(delta),
1691 )
1692 .into_any()
1693 } else {
1694 label.into_any_element()
1695 }
1696 }),
1697 )
1698 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1699 .child(
1700 Label::new(humanize_token_count(total_token_usage.max))
1701 .size(LabelSize::Small)
1702 .color(Color::Muted),
1703 )
1704 .into_any();
1705
1706 Some(token_count)
1707 }
1708 ActiveView::PromptEditor { context_editor, .. } => {
1709 let element = render_remaining_tokens(context_editor, cx)?;
1710
1711 Some(element.into_any_element())
1712 }
1713 _ => None,
1714 }
1715 }
1716
1717 fn render_active_thread_or_empty_state(
1718 &self,
1719 window: &mut Window,
1720 cx: &mut Context<Self>,
1721 ) -> AnyElement {
1722 if self.thread.read(cx).is_empty() {
1723 return self
1724 .render_thread_empty_state(window, cx)
1725 .into_any_element();
1726 }
1727
1728 self.thread.clone().into_any_element()
1729 }
1730
1731 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1732 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1733 return Some(ConfigurationError::NoProvider);
1734 };
1735
1736 if !model.provider.is_authenticated(cx) {
1737 return Some(ConfigurationError::ProviderNotAuthenticated);
1738 }
1739
1740 if model.provider.must_accept_terms(cx) {
1741 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1742 model.provider,
1743 ));
1744 }
1745
1746 None
1747 }
1748
1749 fn render_thread_empty_state(
1750 &self,
1751 window: &mut Window,
1752 cx: &mut Context<Self>,
1753 ) -> impl IntoElement {
1754 let recent_history = self
1755 .history_store
1756 .update(cx, |this, cx| this.recent_entries(6, cx));
1757
1758 let configuration_error = self.configuration_error(cx);
1759 let no_error = configuration_error.is_none();
1760 let focus_handle = self.focus_handle(cx);
1761
1762 v_flex()
1763 .size_full()
1764 .when(recent_history.is_empty(), |this| {
1765 let configuration_error_ref = &configuration_error;
1766 this.child(
1767 v_flex()
1768 .size_full()
1769 .max_w_80()
1770 .mx_auto()
1771 .justify_center()
1772 .items_center()
1773 .gap_1()
1774 .child(
1775 h_flex().child(
1776 Headline::new("Welcome to the Agent Panel")
1777 ),
1778 )
1779 .when(no_error, |parent| {
1780 parent
1781 .child(
1782 h_flex().child(
1783 Label::new("Ask and build anything.")
1784 .color(Color::Muted)
1785 .mb_2p5(),
1786 ),
1787 )
1788 .child(
1789 Button::new("new-thread", "Start New Thread")
1790 .icon(IconName::Plus)
1791 .icon_position(IconPosition::Start)
1792 .icon_size(IconSize::Small)
1793 .icon_color(Color::Muted)
1794 .full_width()
1795 .key_binding(KeyBinding::for_action_in(
1796 &NewThread::default(),
1797 &focus_handle,
1798 window,
1799 cx,
1800 ))
1801 .on_click(|_event, window, cx| {
1802 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1803 }),
1804 )
1805 .child(
1806 Button::new("context", "Add Context")
1807 .icon(IconName::FileCode)
1808 .icon_position(IconPosition::Start)
1809 .icon_size(IconSize::Small)
1810 .icon_color(Color::Muted)
1811 .full_width()
1812 .key_binding(KeyBinding::for_action_in(
1813 &ToggleContextPicker,
1814 &focus_handle,
1815 window,
1816 cx,
1817 ))
1818 .on_click(|_event, window, cx| {
1819 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1820 }),
1821 )
1822 .child(
1823 Button::new("mode", "Switch Model")
1824 .icon(IconName::DatabaseZap)
1825 .icon_position(IconPosition::Start)
1826 .icon_size(IconSize::Small)
1827 .icon_color(Color::Muted)
1828 .full_width()
1829 .key_binding(KeyBinding::for_action_in(
1830 &ToggleModelSelector,
1831 &focus_handle,
1832 window,
1833 cx,
1834 ))
1835 .on_click(|_event, window, cx| {
1836 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1837 }),
1838 )
1839 .child(
1840 Button::new("settings", "View Settings")
1841 .icon(IconName::Settings)
1842 .icon_position(IconPosition::Start)
1843 .icon_size(IconSize::Small)
1844 .icon_color(Color::Muted)
1845 .full_width()
1846 .key_binding(KeyBinding::for_action_in(
1847 &OpenConfiguration,
1848 &focus_handle,
1849 window,
1850 cx,
1851 ))
1852 .on_click(|_event, window, cx| {
1853 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1854 }),
1855 )
1856 })
1857 .map(|parent| {
1858 match configuration_error_ref {
1859 Some(ConfigurationError::ProviderNotAuthenticated)
1860 | Some(ConfigurationError::NoProvider) => {
1861 parent
1862 .child(
1863 h_flex().child(
1864 Label::new("To start using the agent, configure at least one LLM provider.")
1865 .color(Color::Muted)
1866 .mb_2p5()
1867 )
1868 )
1869 .child(
1870 Button::new("settings", "Configure a Provider")
1871 .icon(IconName::Settings)
1872 .icon_position(IconPosition::Start)
1873 .icon_size(IconSize::Small)
1874 .icon_color(Color::Muted)
1875 .full_width()
1876 .key_binding(KeyBinding::for_action_in(
1877 &OpenConfiguration,
1878 &focus_handle,
1879 window,
1880 cx,
1881 ))
1882 .on_click(|_event, window, cx| {
1883 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1884 }),
1885 )
1886 }
1887 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1888 parent.children(
1889 provider.render_accept_terms(
1890 LanguageModelProviderTosView::ThreadFreshStart,
1891 cx,
1892 ),
1893 )
1894 }
1895 None => parent,
1896 }
1897 })
1898 )
1899 })
1900 .when(!recent_history.is_empty(), |parent| {
1901 let focus_handle = focus_handle.clone();
1902 let configuration_error_ref = &configuration_error;
1903
1904 parent
1905 .overflow_hidden()
1906 .p_1p5()
1907 .justify_end()
1908 .gap_1()
1909 .child(
1910 h_flex()
1911 .pl_1p5()
1912 .pb_1()
1913 .w_full()
1914 .justify_between()
1915 .border_b_1()
1916 .border_color(cx.theme().colors().border_variant)
1917 .child(
1918 Label::new("Past Interactions")
1919 .size(LabelSize::Small)
1920 .color(Color::Muted),
1921 )
1922 .child(
1923 Button::new("view-history", "View All")
1924 .style(ButtonStyle::Subtle)
1925 .label_size(LabelSize::Small)
1926 .key_binding(
1927 KeyBinding::for_action_in(
1928 &OpenHistory,
1929 &self.focus_handle(cx),
1930 window,
1931 cx,
1932 ).map(|kb| kb.size(rems_from_px(12.))),
1933 )
1934 .on_click(move |_event, window, cx| {
1935 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1936 }),
1937 ),
1938 )
1939 .child(
1940 v_flex()
1941 .gap_1()
1942 .children(
1943 recent_history.into_iter().map(|entry| {
1944 // TODO: Add keyboard navigation.
1945 match entry {
1946 HistoryEntry::Thread(thread) => {
1947 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1948 .into_any_element()
1949 }
1950 HistoryEntry::Context(context) => {
1951 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1952 .into_any_element()
1953 }
1954 }
1955 }),
1956 )
1957 )
1958 .map(|parent| {
1959 match configuration_error_ref {
1960 Some(ConfigurationError::ProviderNotAuthenticated)
1961 | Some(ConfigurationError::NoProvider) => {
1962 parent
1963 .child(
1964 Banner::new()
1965 .severity(ui::Severity::Warning)
1966 .child(
1967 Label::new(
1968 "Configure at least one LLM provider to start using the panel.",
1969 )
1970 .size(LabelSize::Small),
1971 )
1972 .action_slot(
1973 Button::new("settings", "Configure Provider")
1974 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1975 .label_size(LabelSize::Small)
1976 .key_binding(
1977 KeyBinding::for_action_in(
1978 &OpenConfiguration,
1979 &focus_handle,
1980 window,
1981 cx,
1982 )
1983 .map(|kb| kb.size(rems_from_px(12.))),
1984 )
1985 .on_click(|_event, window, cx| {
1986 window.dispatch_action(
1987 OpenConfiguration.boxed_clone(),
1988 cx,
1989 )
1990 }),
1991 ),
1992 )
1993 }
1994 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1995 parent
1996 .child(
1997 Banner::new()
1998 .severity(ui::Severity::Warning)
1999 .child(
2000 h_flex()
2001 .w_full()
2002 .children(
2003 provider.render_accept_terms(
2004 LanguageModelProviderTosView::ThreadtEmptyState,
2005 cx,
2006 ),
2007 ),
2008 ),
2009 )
2010 }
2011 None => parent,
2012 }
2013 })
2014 })
2015 }
2016
2017 fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2018 let tool_use_limit_reached = self
2019 .thread
2020 .read(cx)
2021 .thread()
2022 .read(cx)
2023 .tool_use_limit_reached();
2024 if !tool_use_limit_reached {
2025 return None;
2026 }
2027
2028 let model = self
2029 .thread
2030 .read(cx)
2031 .thread()
2032 .read(cx)
2033 .configured_model()?
2034 .model;
2035
2036 let max_mode_upsell = if model.supports_max_mode() {
2037 " Enable max mode for unlimited tool use."
2038 } else {
2039 ""
2040 };
2041
2042 Some(
2043 Banner::new()
2044 .severity(ui::Severity::Info)
2045 .child(h_flex().child(Label::new(format!(
2046 "Consecutive tool use limit reached.{max_mode_upsell}"
2047 ))))
2048 .into_any_element(),
2049 )
2050 }
2051
2052 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2053 let last_error = self.thread.read(cx).last_error()?;
2054
2055 Some(
2056 div()
2057 .absolute()
2058 .right_3()
2059 .bottom_12()
2060 .max_w_96()
2061 .py_2()
2062 .px_3()
2063 .elevation_2(cx)
2064 .occlude()
2065 .child(match last_error {
2066 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2067 ThreadError::MaxMonthlySpendReached => {
2068 self.render_max_monthly_spend_reached_error(cx)
2069 }
2070 ThreadError::ModelRequestLimitReached { plan } => {
2071 self.render_model_request_limit_reached_error(plan, cx)
2072 }
2073 ThreadError::Message { header, message } => {
2074 self.render_error_message(header, message, cx)
2075 }
2076 })
2077 .into_any(),
2078 )
2079 }
2080
2081 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2082 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.";
2083
2084 v_flex()
2085 .gap_0p5()
2086 .child(
2087 h_flex()
2088 .gap_1p5()
2089 .items_center()
2090 .child(Icon::new(IconName::XCircle).color(Color::Error))
2091 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2092 )
2093 .child(
2094 div()
2095 .id("error-message")
2096 .max_h_24()
2097 .overflow_y_scroll()
2098 .child(Label::new(ERROR_MESSAGE)),
2099 )
2100 .child(
2101 h_flex()
2102 .justify_end()
2103 .mt_1()
2104 .gap_1()
2105 .child(self.create_copy_button(ERROR_MESSAGE))
2106 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2107 |this, _, _, cx| {
2108 this.thread.update(cx, |this, _cx| {
2109 this.clear_last_error();
2110 });
2111
2112 cx.open_url(&zed_urls::account_url(cx));
2113 cx.notify();
2114 },
2115 )))
2116 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2117 |this, _, _, cx| {
2118 this.thread.update(cx, |this, _cx| {
2119 this.clear_last_error();
2120 });
2121
2122 cx.notify();
2123 },
2124 ))),
2125 )
2126 .into_any()
2127 }
2128
2129 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2130 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2131
2132 v_flex()
2133 .gap_0p5()
2134 .child(
2135 h_flex()
2136 .gap_1p5()
2137 .items_center()
2138 .child(Icon::new(IconName::XCircle).color(Color::Error))
2139 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2140 )
2141 .child(
2142 div()
2143 .id("error-message")
2144 .max_h_24()
2145 .overflow_y_scroll()
2146 .child(Label::new(ERROR_MESSAGE)),
2147 )
2148 .child(
2149 h_flex()
2150 .justify_end()
2151 .mt_1()
2152 .gap_1()
2153 .child(self.create_copy_button(ERROR_MESSAGE))
2154 .child(
2155 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2156 cx.listener(|this, _, _, cx| {
2157 this.thread.update(cx, |this, _cx| {
2158 this.clear_last_error();
2159 });
2160
2161 cx.open_url(&zed_urls::account_url(cx));
2162 cx.notify();
2163 }),
2164 ),
2165 )
2166 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2167 |this, _, _, cx| {
2168 this.thread.update(cx, |this, _cx| {
2169 this.clear_last_error();
2170 });
2171
2172 cx.notify();
2173 },
2174 ))),
2175 )
2176 .into_any()
2177 }
2178
2179 fn render_model_request_limit_reached_error(
2180 &self,
2181 plan: Plan,
2182 cx: &mut Context<Self>,
2183 ) -> AnyElement {
2184 let error_message = match plan {
2185 Plan::ZedPro => {
2186 "Model request limit reached. Upgrade to usage-based billing for more requests."
2187 }
2188 Plan::ZedProTrial => {
2189 "Model request limit reached. Upgrade to Zed Pro for more requests."
2190 }
2191 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2192 };
2193 let call_to_action = match plan {
2194 Plan::ZedPro => "Upgrade to usage-based billing",
2195 Plan::ZedProTrial => "Upgrade to Zed Pro",
2196 Plan::Free => "Upgrade to Zed Pro",
2197 };
2198
2199 v_flex()
2200 .gap_0p5()
2201 .child(
2202 h_flex()
2203 .gap_1p5()
2204 .items_center()
2205 .child(Icon::new(IconName::XCircle).color(Color::Error))
2206 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2207 )
2208 .child(
2209 div()
2210 .id("error-message")
2211 .max_h_24()
2212 .overflow_y_scroll()
2213 .child(Label::new(error_message)),
2214 )
2215 .child(
2216 h_flex()
2217 .justify_end()
2218 .mt_1()
2219 .gap_1()
2220 .child(self.create_copy_button(error_message))
2221 .child(
2222 Button::new("subscribe", call_to_action).on_click(cx.listener(
2223 |this, _, _, cx| {
2224 this.thread.update(cx, |this, _cx| {
2225 this.clear_last_error();
2226 });
2227
2228 cx.open_url(&zed_urls::account_url(cx));
2229 cx.notify();
2230 },
2231 )),
2232 )
2233 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2234 |this, _, _, cx| {
2235 this.thread.update(cx, |this, _cx| {
2236 this.clear_last_error();
2237 });
2238
2239 cx.notify();
2240 },
2241 ))),
2242 )
2243 .into_any()
2244 }
2245
2246 fn render_error_message(
2247 &self,
2248 header: SharedString,
2249 message: SharedString,
2250 cx: &mut Context<Self>,
2251 ) -> AnyElement {
2252 let message_with_header = format!("{}\n{}", header, message);
2253 v_flex()
2254 .gap_0p5()
2255 .child(
2256 h_flex()
2257 .gap_1p5()
2258 .items_center()
2259 .child(Icon::new(IconName::XCircle).color(Color::Error))
2260 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2261 )
2262 .child(
2263 div()
2264 .id("error-message")
2265 .max_h_32()
2266 .overflow_y_scroll()
2267 .child(Label::new(message.clone())),
2268 )
2269 .child(
2270 h_flex()
2271 .justify_end()
2272 .mt_1()
2273 .gap_1()
2274 .child(self.create_copy_button(message_with_header))
2275 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2276 |this, _, _, cx| {
2277 this.thread.update(cx, |this, _cx| {
2278 this.clear_last_error();
2279 });
2280
2281 cx.notify();
2282 },
2283 ))),
2284 )
2285 .into_any()
2286 }
2287
2288 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2289 let message = message.into();
2290 IconButton::new("copy", IconName::Copy)
2291 .on_click(move |_, _, cx| {
2292 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2293 })
2294 .tooltip(Tooltip::text("Copy Error Message"))
2295 }
2296
2297 fn key_context(&self) -> KeyContext {
2298 let mut key_context = KeyContext::new_with_defaults();
2299 key_context.add("AgentPanel");
2300 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2301 key_context.add("prompt_editor");
2302 }
2303 key_context
2304 }
2305}
2306
2307impl Render for AssistantPanel {
2308 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2309 v_flex()
2310 .key_context(self.key_context())
2311 .justify_between()
2312 .size_full()
2313 .on_action(cx.listener(Self::cancel))
2314 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2315 this.new_thread(action, window, cx);
2316 }))
2317 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2318 this.open_history(window, cx);
2319 }))
2320 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2321 this.open_configuration(window, cx);
2322 }))
2323 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2324 .on_action(cx.listener(Self::deploy_rules_library))
2325 .on_action(cx.listener(Self::open_agent_diff))
2326 .on_action(cx.listener(Self::go_back))
2327 .on_action(cx.listener(Self::toggle_navigation_menu))
2328 .on_action(cx.listener(Self::toggle_options_menu))
2329 .child(self.render_toolbar(window, cx))
2330 .map(|parent| match &self.active_view {
2331 ActiveView::Thread { .. } => parent
2332 .child(self.render_active_thread_or_empty_state(window, cx))
2333 .children(self.render_tool_use_limit_reached(cx))
2334 .child(h_flex().child(self.message_editor.clone()))
2335 .children(self.render_last_error(cx)),
2336 ActiveView::History => parent.child(self.history.clone()),
2337 ActiveView::PromptEditor {
2338 context_editor,
2339 buffer_search_bar,
2340 ..
2341 } => {
2342 let mut registrar = DivRegistrar::new(
2343 |this, _, _cx| match &this.active_view {
2344 ActiveView::PromptEditor {
2345 buffer_search_bar, ..
2346 } => Some(buffer_search_bar.clone()),
2347 _ => None,
2348 },
2349 cx,
2350 );
2351 BufferSearchBar::register(&mut registrar);
2352 parent.child(
2353 registrar
2354 .into_div()
2355 .size_full()
2356 .map(|parent| {
2357 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2358 if buffer_search_bar.is_dismissed() {
2359 return parent;
2360 }
2361 parent.child(
2362 div()
2363 .p(DynamicSpacing::Base08.rems(cx))
2364 .border_b_1()
2365 .border_color(cx.theme().colors().border_variant)
2366 .bg(cx.theme().colors().editor_background)
2367 .child(buffer_search_bar.render(window, cx)),
2368 )
2369 })
2370 })
2371 .child(context_editor.clone()),
2372 )
2373 }
2374 ActiveView::Configuration => parent.children(self.configuration.clone()),
2375 })
2376 }
2377}
2378
2379struct PromptLibraryInlineAssist {
2380 workspace: WeakEntity<Workspace>,
2381}
2382
2383impl PromptLibraryInlineAssist {
2384 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2385 Self { workspace }
2386 }
2387}
2388
2389impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2390 fn assist(
2391 &self,
2392 prompt_editor: &Entity<Editor>,
2393 _initial_prompt: Option<String>,
2394 window: &mut Window,
2395 cx: &mut Context<RulesLibrary>,
2396 ) {
2397 InlineAssistant::update_global(cx, |assistant, cx| {
2398 let Some(project) = self
2399 .workspace
2400 .upgrade()
2401 .map(|workspace| workspace.read(cx).project().downgrade())
2402 else {
2403 return;
2404 };
2405 let prompt_store = None;
2406 let thread_store = None;
2407 assistant.assist(
2408 &prompt_editor,
2409 self.workspace.clone(),
2410 project,
2411 prompt_store,
2412 thread_store,
2413 window,
2414 cx,
2415 )
2416 })
2417 }
2418
2419 fn focus_assistant_panel(
2420 &self,
2421 workspace: &mut Workspace,
2422 window: &mut Window,
2423 cx: &mut Context<Workspace>,
2424 ) -> bool {
2425 workspace
2426 .focus_panel::<AssistantPanel>(window, cx)
2427 .is_some()
2428 }
2429}
2430
2431pub struct ConcreteAssistantPanelDelegate;
2432
2433impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2434 fn active_context_editor(
2435 &self,
2436 workspace: &mut Workspace,
2437 _window: &mut Window,
2438 cx: &mut Context<Workspace>,
2439 ) -> Option<Entity<ContextEditor>> {
2440 let panel = workspace.panel::<AssistantPanel>(cx)?;
2441 panel.read(cx).active_context_editor()
2442 }
2443
2444 fn open_saved_context(
2445 &self,
2446 workspace: &mut Workspace,
2447 path: Arc<Path>,
2448 window: &mut Window,
2449 cx: &mut Context<Workspace>,
2450 ) -> Task<Result<()>> {
2451 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2452 return Task::ready(Err(anyhow!("Agent panel not found")));
2453 };
2454
2455 panel.update(cx, |panel, cx| {
2456 panel.open_saved_prompt_editor(path, window, cx)
2457 })
2458 }
2459
2460 fn open_remote_context(
2461 &self,
2462 _workspace: &mut Workspace,
2463 _context_id: assistant_context_editor::ContextId,
2464 _window: &mut Window,
2465 _cx: &mut Context<Workspace>,
2466 ) -> Task<Result<Entity<ContextEditor>>> {
2467 Task::ready(Err(anyhow!("opening remote context not implemented")))
2468 }
2469
2470 fn quote_selection(
2471 &self,
2472 workspace: &mut Workspace,
2473 selection_ranges: Vec<Range<Anchor>>,
2474 buffer: Entity<MultiBuffer>,
2475 window: &mut Window,
2476 cx: &mut Context<Workspace>,
2477 ) {
2478 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2479 return;
2480 };
2481
2482 if !panel.focus_handle(cx).contains_focused(window, cx) {
2483 workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2484 }
2485
2486 panel.update(cx, |_, cx| {
2487 // Wait to create a new context until the workspace is no longer
2488 // being updated.
2489 cx.defer_in(window, move |panel, window, cx| {
2490 if panel.has_active_thread() {
2491 panel.message_editor.update(cx, |message_editor, cx| {
2492 message_editor.context_store().update(cx, |store, cx| {
2493 let buffer = buffer.read(cx);
2494 let selection_ranges = selection_ranges
2495 .into_iter()
2496 .flat_map(|range| {
2497 let (start_buffer, start) =
2498 buffer.text_anchor_for_position(range.start, cx)?;
2499 let (end_buffer, end) =
2500 buffer.text_anchor_for_position(range.end, cx)?;
2501 if start_buffer != end_buffer {
2502 return None;
2503 }
2504 Some((start_buffer, start..end))
2505 })
2506 .collect::<Vec<_>>();
2507
2508 for (buffer, range) in selection_ranges {
2509 store.add_selection(buffer, range, cx);
2510 }
2511 })
2512 })
2513 } else if let Some(context_editor) = panel.active_context_editor() {
2514 let snapshot = buffer.read(cx).snapshot(cx);
2515 let selection_ranges = selection_ranges
2516 .into_iter()
2517 .map(|range| range.to_point(&snapshot))
2518 .collect::<Vec<_>>();
2519
2520 context_editor.update(cx, |context_editor, cx| {
2521 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2522 });
2523 }
2524 });
2525 });
2526 }
2527}