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