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