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