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