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