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