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