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