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