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