1use std::ops::Range;
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Duration;
6
7use db::kvp::{Dismissable, KEY_VALUE_STORE};
8use serde::{Deserialize, Serialize};
9
10use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
11use anyhow::{Result, anyhow};
12use assistant_context_editor::{
13 AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
14 ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
15 make_lsp_adapter_delegate, render_remaining_tokens,
16};
17use assistant_slash_command::SlashCommandWorkingSet;
18use assistant_tool::ToolWorkingSet;
19
20use assistant_context_editor::language_model_selector::ToggleModelSelector;
21use client::{UserStore, zed_urls};
22use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
23use fs::Fs;
24use gpui::{
25 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
26 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
27 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
28 linear_gradient, prelude::*, pulsating_between,
29};
30use language::LanguageRegistry;
31use language_model::{
32 LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
33};
34use project::{Project, ProjectPath, Worktree};
35use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
36use proto::Plan;
37use rules_library::{RulesLibrary, open_rules_library};
38use search::{BufferSearchBar, buffer_search};
39use settings::{Settings, update_settings_file};
40use theme::ThemeSettings;
41use time::UtcOffset;
42use ui::utils::WithRemSize;
43use ui::{
44 Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
45 PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
46};
47use util::{ResultExt as _, maybe};
48use workspace::dock::{DockPosition, Panel, PanelEvent};
49use workspace::{
50 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
51};
52use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
53use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
54use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
55use zed_llm_client::{CompletionIntent, UsageLimit};
56
57use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
58use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
59use crate::agent_diff::AgentDiff;
60use crate::history_store::{HistoryStore, RecentEntry};
61use crate::message_editor::{MessageEditor, MessageEditorEvent};
62use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
63use crate::thread_history::{HistoryEntryElement, ThreadHistory};
64use crate::thread_store::ThreadStore;
65use crate::ui::AgentOnboardingModal;
66use crate::{
67 AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode,
68 DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
69 NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
70 ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu,
71 ToggleOptionsMenu,
72};
73
74const AGENT_PANEL_KEY: &str = "agent_panel";
75
76#[derive(Serialize, Deserialize)]
77struct SerializedAgentPanel {
78 width: Option<Pixels>,
79}
80
81pub fn init(cx: &mut App) {
82 cx.observe_new(
83 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
84 workspace
85 .register_action(|workspace, action: &NewThread, window, cx| {
86 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
87 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
88 workspace.focus_panel::<AgentPanel>(window, cx);
89 }
90 })
91 .register_action(|workspace, _: &OpenHistory, window, cx| {
92 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
93 workspace.focus_panel::<AgentPanel>(window, cx);
94 panel.update(cx, |panel, cx| panel.open_history(window, cx));
95 }
96 })
97 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
98 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
99 workspace.focus_panel::<AgentPanel>(window, cx);
100 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
101 }
102 })
103 .register_action(|workspace, _: &NewTextThread, window, cx| {
104 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
105 workspace.focus_panel::<AgentPanel>(window, cx);
106 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
107 }
108 })
109 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
110 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
111 workspace.focus_panel::<AgentPanel>(window, cx);
112 panel.update(cx, |panel, cx| {
113 panel.deploy_rules_library(action, window, cx)
114 });
115 }
116 })
117 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
118 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
119 workspace.focus_panel::<AgentPanel>(window, cx);
120 let thread = panel.read(cx).thread.read(cx).thread().clone();
121 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
122 }
123 })
124 .register_action(|workspace, _: &Follow, window, cx| {
125 workspace.follow(CollaboratorId::Agent, window, cx);
126 })
127 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
128 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
129 workspace.focus_panel::<AgentPanel>(window, cx);
130 panel.update(cx, |panel, cx| {
131 panel.message_editor.update(cx, |editor, cx| {
132 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
133 });
134 });
135 }
136 })
137 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
138 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
139 workspace.focus_panel::<AgentPanel>(window, cx);
140 panel.update(cx, |panel, cx| {
141 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
142 });
143 }
144 })
145 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
146 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
147 workspace.focus_panel::<AgentPanel>(window, cx);
148 panel.update(cx, |panel, cx| {
149 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
150 });
151 }
152 })
153 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
154 AgentOnboardingModal::toggle(workspace, window, cx)
155 })
156 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
157 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
158 window.refresh();
159 })
160 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
161 Upsell::set_dismissed(false, cx);
162 })
163 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
164 TrialEndUpsell::set_dismissed(false, cx);
165 });
166 },
167 )
168 .detach();
169}
170
171enum ActiveView {
172 Thread {
173 change_title_editor: Entity<Editor>,
174 thread: WeakEntity<Thread>,
175 _subscriptions: Vec<gpui::Subscription>,
176 },
177 PromptEditor {
178 context_editor: Entity<ContextEditor>,
179 title_editor: Entity<Editor>,
180 buffer_search_bar: Entity<BufferSearchBar>,
181 _subscriptions: Vec<gpui::Subscription>,
182 },
183 History,
184 Configuration,
185}
186
187enum WhichFontSize {
188 AgentFont,
189 BufferFont,
190 None,
191}
192
193impl ActiveView {
194 pub fn which_font_size_used(&self) -> WhichFontSize {
195 match self {
196 ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
197 ActiveView::PromptEditor { .. } => WhichFontSize::BufferFont,
198 ActiveView::Configuration => WhichFontSize::None,
199 }
200 }
201
202 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
203 let summary = thread.read(cx).summary().or_default();
204
205 let editor = cx.new(|cx| {
206 let mut editor = Editor::single_line(window, cx);
207 editor.set_text(summary.clone(), window, cx);
208 editor
209 });
210
211 let subscriptions = vec![
212 window.subscribe(&editor, cx, {
213 {
214 let thread = thread.clone();
215 move |editor, event, window, cx| match event {
216 EditorEvent::BufferEdited => {
217 let new_summary = editor.read(cx).text(cx);
218
219 thread.update(cx, |thread, cx| {
220 thread.set_summary(new_summary, cx);
221 })
222 }
223 EditorEvent::Blurred => {
224 if editor.read(cx).text(cx).is_empty() {
225 let summary = thread.read(cx).summary().or_default();
226
227 editor.update(cx, |editor, cx| {
228 editor.set_text(summary, window, cx);
229 });
230 }
231 }
232 _ => {}
233 }
234 }
235 }),
236 window.subscribe(&thread, cx, {
237 let editor = editor.clone();
238 move |thread, event, window, cx| match event {
239 ThreadEvent::SummaryGenerated => {
240 let summary = thread.read(cx).summary().or_default();
241
242 editor.update(cx, |editor, cx| {
243 editor.set_text(summary, window, cx);
244 })
245 }
246 _ => {}
247 }
248 }),
249 ];
250
251 Self::Thread {
252 change_title_editor: editor,
253 thread: thread.downgrade(),
254 _subscriptions: subscriptions,
255 }
256 }
257
258 pub fn prompt_editor(
259 context_editor: Entity<ContextEditor>,
260 language_registry: Arc<LanguageRegistry>,
261 window: &mut Window,
262 cx: &mut App,
263 ) -> Self {
264 let title = context_editor.read(cx).title(cx).to_string();
265
266 let editor = cx.new(|cx| {
267 let mut editor = Editor::single_line(window, cx);
268 editor.set_text(title, window, cx);
269 editor
270 });
271
272 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
273 // cause a custom summary to be set. The presence of this custom summary would cause
274 // summarization to not happen.
275 let mut suppress_first_edit = true;
276
277 let subscriptions = vec![
278 window.subscribe(&editor, cx, {
279 {
280 let context_editor = context_editor.clone();
281 move |editor, event, window, cx| match event {
282 EditorEvent::BufferEdited => {
283 if suppress_first_edit {
284 suppress_first_edit = false;
285 return;
286 }
287 let new_summary = editor.read(cx).text(cx);
288
289 context_editor.update(cx, |context_editor, cx| {
290 context_editor
291 .context()
292 .update(cx, |assistant_context, cx| {
293 assistant_context.set_custom_summary(new_summary, cx);
294 })
295 })
296 }
297 EditorEvent::Blurred => {
298 if editor.read(cx).text(cx).is_empty() {
299 let summary = context_editor
300 .read(cx)
301 .context()
302 .read(cx)
303 .summary()
304 .or_default();
305
306 editor.update(cx, |editor, cx| {
307 editor.set_text(summary, window, cx);
308 });
309 }
310 }
311 _ => {}
312 }
313 }
314 }),
315 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
316 let editor = editor.clone();
317 move |assistant_context, event, window, cx| match event {
318 ContextEvent::SummaryGenerated => {
319 let summary = assistant_context.read(cx).summary().or_default();
320
321 editor.update(cx, |editor, cx| {
322 editor.set_text(summary, window, cx);
323 })
324 }
325 _ => {}
326 }
327 }),
328 ];
329
330 let buffer_search_bar =
331 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
332 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
333 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
334 });
335
336 Self::PromptEditor {
337 context_editor,
338 title_editor: editor,
339 buffer_search_bar,
340 _subscriptions: subscriptions,
341 }
342 }
343}
344
345pub struct AgentPanel {
346 workspace: WeakEntity<Workspace>,
347 user_store: Entity<UserStore>,
348 project: Entity<Project>,
349 fs: Arc<dyn Fs>,
350 language_registry: Arc<LanguageRegistry>,
351 thread_store: Entity<ThreadStore>,
352 thread: Entity<ActiveThread>,
353 message_editor: Entity<MessageEditor>,
354 _active_thread_subscriptions: Vec<Subscription>,
355 _default_model_subscription: Subscription,
356 context_store: Entity<TextThreadStore>,
357 prompt_store: Option<Entity<PromptStore>>,
358 inline_assist_context_store: Entity<crate::context_store::ContextStore>,
359 configuration: Option<Entity<AgentConfiguration>>,
360 configuration_subscription: Option<Subscription>,
361 local_timezone: UtcOffset,
362 active_view: ActiveView,
363 previous_view: Option<ActiveView>,
364 history_store: Entity<HistoryStore>,
365 history: Entity<ThreadHistory>,
366 hovered_recent_history_item: Option<usize>,
367 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
368 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
369 assistant_navigation_menu: Option<Entity<ContextMenu>>,
370 width: Option<Pixels>,
371 height: Option<Pixels>,
372 zoomed: bool,
373 pending_serialization: Option<Task<Result<()>>>,
374 hide_upsell: bool,
375}
376
377impl AgentPanel {
378 fn serialize(&mut self, cx: &mut Context<Self>) {
379 let width = self.width;
380 self.pending_serialization = Some(cx.background_spawn(async move {
381 KEY_VALUE_STORE
382 .write_kvp(
383 AGENT_PANEL_KEY.into(),
384 serde_json::to_string(&SerializedAgentPanel { width })?,
385 )
386 .await?;
387 anyhow::Ok(())
388 }));
389 }
390 pub fn load(
391 workspace: WeakEntity<Workspace>,
392 prompt_builder: Arc<PromptBuilder>,
393 mut cx: AsyncWindowContext,
394 ) -> Task<Result<Entity<Self>>> {
395 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
396 cx.spawn(async move |cx| {
397 let prompt_store = match prompt_store {
398 Ok(prompt_store) => prompt_store.await.ok(),
399 Err(_) => None,
400 };
401 let tools = cx.new(|_| ToolWorkingSet::default())?;
402 let thread_store = workspace
403 .update(cx, |workspace, cx| {
404 let project = workspace.project().clone();
405 ThreadStore::load(
406 project,
407 tools.clone(),
408 prompt_store.clone(),
409 prompt_builder.clone(),
410 cx,
411 )
412 })?
413 .await?;
414
415 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
416 let context_store = workspace
417 .update(cx, |workspace, cx| {
418 let project = workspace.project().clone();
419 assistant_context_editor::ContextStore::new(
420 project,
421 prompt_builder.clone(),
422 slash_commands,
423 cx,
424 )
425 })?
426 .await?;
427
428 let serialized_panel = if let Some(panel) = cx
429 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
430 .await
431 .log_err()
432 .flatten()
433 {
434 Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
435 } else {
436 None
437 };
438
439 let panel = workspace.update_in(cx, |workspace, window, cx| {
440 let panel = cx.new(|cx| {
441 Self::new(
442 workspace,
443 thread_store,
444 context_store,
445 prompt_store,
446 window,
447 cx,
448 )
449 });
450 if let Some(serialized_panel) = serialized_panel {
451 panel.update(cx, |panel, cx| {
452 panel.width = serialized_panel.width.map(|w| w.round());
453 cx.notify();
454 });
455 }
456 panel
457 })?;
458
459 Ok(panel)
460 })
461 }
462
463 fn new(
464 workspace: &Workspace,
465 thread_store: Entity<ThreadStore>,
466 context_store: Entity<TextThreadStore>,
467 prompt_store: Option<Entity<PromptStore>>,
468 window: &mut Window,
469 cx: &mut Context<Self>,
470 ) -> Self {
471 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
472 let fs = workspace.app_state().fs.clone();
473 let user_store = workspace.app_state().user_store.clone();
474 let project = workspace.project();
475 let language_registry = project.read(cx).languages().clone();
476 let workspace = workspace.weak_handle();
477 let weak_self = cx.entity().downgrade();
478
479 let message_editor_context_store = cx.new(|_cx| {
480 crate::context_store::ContextStore::new(
481 project.downgrade(),
482 Some(thread_store.downgrade()),
483 )
484 });
485 let inline_assist_context_store = cx.new(|_cx| {
486 crate::context_store::ContextStore::new(
487 project.downgrade(),
488 Some(thread_store.downgrade()),
489 )
490 });
491
492 let message_editor = cx.new(|cx| {
493 MessageEditor::new(
494 fs.clone(),
495 workspace.clone(),
496 user_store.clone(),
497 message_editor_context_store.clone(),
498 prompt_store.clone(),
499 thread_store.downgrade(),
500 context_store.downgrade(),
501 thread.clone(),
502 window,
503 cx,
504 )
505 });
506
507 let message_editor_subscription =
508 cx.subscribe(&message_editor, |_, _, event, cx| match event {
509 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
510 cx.notify();
511 }
512 });
513
514 let thread_id = thread.read(cx).id().clone();
515 let history_store = cx.new(|cx| {
516 HistoryStore::new(
517 thread_store.clone(),
518 context_store.clone(),
519 [RecentEntry::Thread(thread_id, thread.clone())],
520 window,
521 cx,
522 )
523 });
524
525 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
526
527 let panel_type = AgentSettings::get_global(cx).default_view;
528 let active_view = match panel_type {
529 DefaultView::Thread => ActiveView::thread(thread.clone(), window, cx),
530 DefaultView::TextThread => {
531 let context =
532 context_store.update(cx, |context_store, cx| context_store.create(cx));
533 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
534 let context_editor = cx.new(|cx| {
535 let mut editor = ContextEditor::for_context(
536 context,
537 fs.clone(),
538 workspace.clone(),
539 project.clone(),
540 lsp_adapter_delegate,
541 window,
542 cx,
543 );
544 editor.insert_default_prompt(window, cx);
545 editor
546 });
547 ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
548 }
549 };
550
551 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
552 if let ThreadEvent::MessageAdded(_) = &event {
553 // needed to leave empty state
554 cx.notify();
555 }
556 });
557 let active_thread = cx.new(|cx| {
558 ActiveThread::new(
559 thread.clone(),
560 thread_store.clone(),
561 context_store.clone(),
562 message_editor_context_store.clone(),
563 language_registry.clone(),
564 workspace.clone(),
565 window,
566 cx,
567 )
568 });
569 AgentDiff::set_active_thread(&workspace, &thread, window, cx);
570
571 let active_thread_subscription =
572 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
573 ActiveThreadEvent::EditingMessageTokenCountChanged => {
574 cx.notify();
575 }
576 });
577
578 let weak_panel = weak_self.clone();
579
580 window.defer(cx, move |window, cx| {
581 let panel = weak_panel.clone();
582 let assistant_navigation_menu =
583 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
584 let recently_opened = panel
585 .update(cx, |this, cx| {
586 this.history_store.update(cx, |history_store, cx| {
587 history_store.recently_opened_entries(cx)
588 })
589 })
590 .unwrap_or_default();
591
592 if !recently_opened.is_empty() {
593 menu = menu.header("Recently Opened");
594
595 for entry in recently_opened.iter() {
596 if let RecentEntry::Context(context) = entry {
597 if context.read(cx).path().is_none() {
598 log::error!(
599 "bug: text thread in recent history list was never saved"
600 );
601 continue;
602 }
603 }
604
605 let summary = entry.summary(cx);
606
607 menu = menu.entry_with_end_slot_on_hover(
608 summary,
609 None,
610 {
611 let panel = panel.clone();
612 let entry = entry.clone();
613 move |window, cx| {
614 panel
615 .update(cx, {
616 let entry = entry.clone();
617 move |this, cx| match entry {
618 RecentEntry::Thread(_, thread) => {
619 this.open_thread(thread, window, cx)
620 }
621 RecentEntry::Context(context) => {
622 let Some(path) = context.read(cx).path()
623 else {
624 return;
625 };
626 this.open_saved_prompt_editor(
627 path.clone(),
628 window,
629 cx,
630 )
631 .detach_and_log_err(cx)
632 }
633 }
634 })
635 .ok();
636 }
637 },
638 IconName::Close,
639 "Close Entry".into(),
640 {
641 let panel = panel.clone();
642 let entry = entry.clone();
643 move |_window, cx| {
644 panel
645 .update(cx, |this, cx| {
646 this.history_store.update(
647 cx,
648 |history_store, cx| {
649 history_store.remove_recently_opened_entry(
650 &entry, cx,
651 );
652 },
653 );
654 })
655 .ok();
656 }
657 },
658 );
659 }
660
661 menu = menu.separator();
662 }
663
664 menu.action("View All", Box::new(OpenHistory))
665 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
666 .fixed_width(px(320.).into())
667 .keep_open_on_confirm(false)
668 .key_context("NavigationMenu")
669 });
670 weak_panel
671 .update(cx, |panel, cx| {
672 cx.subscribe_in(
673 &assistant_navigation_menu,
674 window,
675 |_, menu, _: &DismissEvent, window, cx| {
676 menu.update(cx, |menu, _| {
677 menu.clear_selected();
678 });
679 cx.focus_self(window);
680 },
681 )
682 .detach();
683 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
684 })
685 .ok();
686 });
687
688 let _default_model_subscription = cx.subscribe(
689 &LanguageModelRegistry::global(cx),
690 |this, _, event: &language_model::Event, cx| match event {
691 language_model::Event::DefaultModelChanged => {
692 this.thread
693 .read(cx)
694 .thread()
695 .clone()
696 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
697 }
698 _ => {}
699 },
700 );
701
702 Self {
703 active_view,
704 workspace,
705 user_store,
706 project: project.clone(),
707 fs: fs.clone(),
708 language_registry,
709 thread_store: thread_store.clone(),
710 thread: active_thread,
711 message_editor,
712 _active_thread_subscriptions: vec![
713 thread_subscription,
714 active_thread_subscription,
715 message_editor_subscription,
716 ],
717 _default_model_subscription,
718 context_store,
719 prompt_store,
720 configuration: None,
721 configuration_subscription: None,
722 local_timezone: UtcOffset::from_whole_seconds(
723 chrono::Local::now().offset().local_minus_utc(),
724 )
725 .unwrap(),
726 inline_assist_context_store,
727 previous_view: None,
728 history_store: history_store.clone(),
729 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
730 hovered_recent_history_item: None,
731 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
732 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
733 assistant_navigation_menu: None,
734 width: None,
735 height: None,
736 zoomed: false,
737 pending_serialization: None,
738 hide_upsell: false,
739 }
740 }
741
742 pub fn toggle_focus(
743 workspace: &mut Workspace,
744 _: &ToggleFocus,
745 window: &mut Window,
746 cx: &mut Context<Workspace>,
747 ) {
748 if workspace
749 .panel::<Self>(cx)
750 .is_some_and(|panel| panel.read(cx).enabled(cx))
751 {
752 workspace.toggle_panel_focus::<Self>(window, cx);
753 }
754 }
755
756 pub(crate) fn local_timezone(&self) -> UtcOffset {
757 self.local_timezone
758 }
759
760 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
761 &self.prompt_store
762 }
763
764 pub(crate) fn inline_assist_context_store(
765 &self,
766 ) -> &Entity<crate::context_store::ContextStore> {
767 &self.inline_assist_context_store
768 }
769
770 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
771 &self.thread_store
772 }
773
774 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
775 &self.context_store
776 }
777
778 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
779 self.thread
780 .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
781 }
782
783 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
784 let thread = self
785 .thread_store
786 .update(cx, |this, cx| this.create_thread(cx));
787
788 let thread_view = ActiveView::thread(thread.clone(), window, cx);
789 self.set_active_view(thread_view, window, cx);
790
791 let context_store = cx.new(|_cx| {
792 crate::context_store::ContextStore::new(
793 self.project.downgrade(),
794 Some(self.thread_store.downgrade()),
795 )
796 });
797
798 if let Some(other_thread_id) = action.from_thread_id.clone() {
799 let other_thread_task = self.thread_store.update(cx, |this, cx| {
800 this.open_thread(&other_thread_id, window, cx)
801 });
802
803 cx.spawn({
804 let context_store = context_store.clone();
805
806 async move |_panel, cx| {
807 let other_thread = other_thread_task.await?;
808
809 context_store.update(cx, |this, cx| {
810 this.add_thread(other_thread, false, cx);
811 })?;
812 anyhow::Ok(())
813 }
814 })
815 .detach_and_log_err(cx);
816 }
817
818 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
819 if let ThreadEvent::MessageAdded(_) = &event {
820 // needed to leave empty state
821 cx.notify();
822 }
823 });
824
825 self.thread = cx.new(|cx| {
826 ActiveThread::new(
827 thread.clone(),
828 self.thread_store.clone(),
829 self.context_store.clone(),
830 context_store.clone(),
831 self.language_registry.clone(),
832 self.workspace.clone(),
833 window,
834 cx,
835 )
836 });
837 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
838
839 let active_thread_subscription =
840 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
841 ActiveThreadEvent::EditingMessageTokenCountChanged => {
842 cx.notify();
843 }
844 });
845
846 self.message_editor = cx.new(|cx| {
847 MessageEditor::new(
848 self.fs.clone(),
849 self.workspace.clone(),
850 self.user_store.clone(),
851 context_store,
852 self.prompt_store.clone(),
853 self.thread_store.downgrade(),
854 self.context_store.downgrade(),
855 thread,
856 window,
857 cx,
858 )
859 });
860 self.message_editor.focus_handle(cx).focus(window);
861
862 let message_editor_subscription =
863 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
864 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
865 cx.notify();
866 }
867 });
868
869 self._active_thread_subscriptions = vec![
870 thread_subscription,
871 active_thread_subscription,
872 message_editor_subscription,
873 ];
874 }
875
876 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
877 let context = self
878 .context_store
879 .update(cx, |context_store, cx| context_store.create(cx));
880 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
881 .log_err()
882 .flatten();
883
884 let context_editor = cx.new(|cx| {
885 let mut editor = ContextEditor::for_context(
886 context,
887 self.fs.clone(),
888 self.workspace.clone(),
889 self.project.clone(),
890 lsp_adapter_delegate,
891 window,
892 cx,
893 );
894 editor.insert_default_prompt(window, cx);
895 editor
896 });
897
898 self.set_active_view(
899 ActiveView::prompt_editor(
900 context_editor.clone(),
901 self.language_registry.clone(),
902 window,
903 cx,
904 ),
905 window,
906 cx,
907 );
908 context_editor.focus_handle(cx).focus(window);
909 }
910
911 fn deploy_rules_library(
912 &mut self,
913 action: &OpenRulesLibrary,
914 _window: &mut Window,
915 cx: &mut Context<Self>,
916 ) {
917 open_rules_library(
918 self.language_registry.clone(),
919 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
920 Rc::new(|| {
921 Rc::new(SlashCommandCompletionProvider::new(
922 Arc::new(SlashCommandWorkingSet::default()),
923 None,
924 None,
925 ))
926 }),
927 action
928 .prompt_to_select
929 .map(|uuid| UserPromptId(uuid).into()),
930 cx,
931 )
932 .detach_and_log_err(cx);
933 }
934
935 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
936 if matches!(self.active_view, ActiveView::History) {
937 if let Some(previous_view) = self.previous_view.take() {
938 self.set_active_view(previous_view, window, cx);
939 }
940 } else {
941 self.thread_store
942 .update(cx, |thread_store, cx| thread_store.reload(cx))
943 .detach_and_log_err(cx);
944 self.set_active_view(ActiveView::History, window, cx);
945 }
946 cx.notify();
947 }
948
949 pub(crate) fn open_saved_prompt_editor(
950 &mut self,
951 path: Arc<Path>,
952 window: &mut Window,
953 cx: &mut Context<Self>,
954 ) -> Task<Result<()>> {
955 let context = self
956 .context_store
957 .update(cx, |store, cx| store.open_local_context(path, cx));
958 cx.spawn_in(window, async move |this, cx| {
959 let context = context.await?;
960 this.update_in(cx, |this, window, cx| {
961 this.open_prompt_editor(context, window, cx);
962 })
963 })
964 }
965
966 pub(crate) fn open_prompt_editor(
967 &mut self,
968 context: Entity<AssistantContext>,
969 window: &mut Window,
970 cx: &mut Context<Self>,
971 ) {
972 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
973 .log_err()
974 .flatten();
975 let editor = cx.new(|cx| {
976 ContextEditor::for_context(
977 context,
978 self.fs.clone(),
979 self.workspace.clone(),
980 self.project.clone(),
981 lsp_adapter_delegate,
982 window,
983 cx,
984 )
985 });
986 self.set_active_view(
987 ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
988 window,
989 cx,
990 );
991 }
992
993 pub(crate) fn open_thread_by_id(
994 &mut self,
995 thread_id: &ThreadId,
996 window: &mut Window,
997 cx: &mut Context<Self>,
998 ) -> Task<Result<()>> {
999 let open_thread_task = self
1000 .thread_store
1001 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
1002 cx.spawn_in(window, async move |this, cx| {
1003 let thread = open_thread_task.await?;
1004 this.update_in(cx, |this, window, cx| {
1005 this.open_thread(thread, window, cx);
1006 anyhow::Ok(())
1007 })??;
1008 Ok(())
1009 })
1010 }
1011
1012 pub(crate) fn open_thread(
1013 &mut self,
1014 thread: Entity<Thread>,
1015 window: &mut Window,
1016 cx: &mut Context<Self>,
1017 ) {
1018 let thread_view = ActiveView::thread(thread.clone(), window, cx);
1019 self.set_active_view(thread_view, window, cx);
1020 let context_store = cx.new(|_cx| {
1021 crate::context_store::ContextStore::new(
1022 self.project.downgrade(),
1023 Some(self.thread_store.downgrade()),
1024 )
1025 });
1026 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
1027 if let ThreadEvent::MessageAdded(_) = &event {
1028 // needed to leave empty state
1029 cx.notify();
1030 }
1031 });
1032
1033 self.thread = cx.new(|cx| {
1034 ActiveThread::new(
1035 thread.clone(),
1036 self.thread_store.clone(),
1037 self.context_store.clone(),
1038 context_store.clone(),
1039 self.language_registry.clone(),
1040 self.workspace.clone(),
1041 window,
1042 cx,
1043 )
1044 });
1045 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
1046
1047 let active_thread_subscription =
1048 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
1049 ActiveThreadEvent::EditingMessageTokenCountChanged => {
1050 cx.notify();
1051 }
1052 });
1053
1054 self.message_editor = cx.new(|cx| {
1055 MessageEditor::new(
1056 self.fs.clone(),
1057 self.workspace.clone(),
1058 self.user_store.clone(),
1059 context_store,
1060 self.prompt_store.clone(),
1061 self.thread_store.downgrade(),
1062 self.context_store.downgrade(),
1063 thread,
1064 window,
1065 cx,
1066 )
1067 });
1068 self.message_editor.focus_handle(cx).focus(window);
1069
1070 let message_editor_subscription =
1071 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
1072 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
1073 cx.notify();
1074 }
1075 });
1076
1077 self._active_thread_subscriptions = vec![
1078 thread_subscription,
1079 active_thread_subscription,
1080 message_editor_subscription,
1081 ];
1082 }
1083
1084 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1085 match self.active_view {
1086 ActiveView::Configuration | ActiveView::History => {
1087 self.active_view =
1088 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
1089 self.message_editor.focus_handle(cx).focus(window);
1090 cx.notify();
1091 }
1092 _ => {}
1093 }
1094 }
1095
1096 pub fn toggle_navigation_menu(
1097 &mut self,
1098 _: &ToggleNavigationMenu,
1099 window: &mut Window,
1100 cx: &mut Context<Self>,
1101 ) {
1102 self.assistant_navigation_menu_handle.toggle(window, cx);
1103 }
1104
1105 pub fn toggle_options_menu(
1106 &mut self,
1107 _: &ToggleOptionsMenu,
1108 window: &mut Window,
1109 cx: &mut Context<Self>,
1110 ) {
1111 self.assistant_dropdown_menu_handle.toggle(window, cx);
1112 }
1113
1114 pub fn increase_font_size(
1115 &mut self,
1116 action: &IncreaseBufferFontSize,
1117 _: &mut Window,
1118 cx: &mut Context<Self>,
1119 ) {
1120 self.handle_font_size_action(action.persist, px(1.0), cx);
1121 }
1122
1123 pub fn decrease_font_size(
1124 &mut self,
1125 action: &DecreaseBufferFontSize,
1126 _: &mut Window,
1127 cx: &mut Context<Self>,
1128 ) {
1129 self.handle_font_size_action(action.persist, px(-1.0), cx);
1130 }
1131
1132 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1133 match self.active_view.which_font_size_used() {
1134 WhichFontSize::AgentFont => {
1135 if persist {
1136 update_settings_file::<ThemeSettings>(
1137 self.fs.clone(),
1138 cx,
1139 move |settings, cx| {
1140 let agent_font_size =
1141 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1142 let _ = settings
1143 .agent_font_size
1144 .insert(theme::clamp_font_size(agent_font_size).0);
1145 },
1146 );
1147 } else {
1148 theme::adjust_agent_font_size(cx, |size| {
1149 *size += delta;
1150 });
1151 }
1152 }
1153 WhichFontSize::BufferFont => {
1154 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1155 // default handler that changes that font size.
1156 cx.propagate();
1157 }
1158 WhichFontSize::None => {}
1159 }
1160 }
1161
1162 pub fn reset_font_size(
1163 &mut self,
1164 action: &ResetBufferFontSize,
1165 _: &mut Window,
1166 cx: &mut Context<Self>,
1167 ) {
1168 if action.persist {
1169 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1170 settings.agent_font_size = None;
1171 });
1172 } else {
1173 theme::reset_agent_font_size(cx);
1174 }
1175 }
1176
1177 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1178 if self.zoomed {
1179 cx.emit(PanelEvent::ZoomOut);
1180 } else {
1181 if !self.focus_handle(cx).contains_focused(window, cx) {
1182 cx.focus_self(window);
1183 }
1184 cx.emit(PanelEvent::ZoomIn);
1185 }
1186 }
1187
1188 pub fn open_agent_diff(
1189 &mut self,
1190 _: &OpenAgentDiff,
1191 window: &mut Window,
1192 cx: &mut Context<Self>,
1193 ) {
1194 let thread = self.thread.read(cx).thread().clone();
1195 self.workspace
1196 .update(cx, |workspace, cx| {
1197 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1198 })
1199 .log_err();
1200 }
1201
1202 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1203 let context_server_store = self.project.read(cx).context_server_store();
1204 let tools = self.thread_store.read(cx).tools();
1205 let fs = self.fs.clone();
1206
1207 self.set_active_view(ActiveView::Configuration, window, cx);
1208 self.configuration =
1209 Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
1210
1211 if let Some(configuration) = self.configuration.as_ref() {
1212 self.configuration_subscription = Some(cx.subscribe_in(
1213 configuration,
1214 window,
1215 Self::handle_agent_configuration_event,
1216 ));
1217
1218 configuration.focus_handle(cx).focus(window);
1219 }
1220 }
1221
1222 pub(crate) fn open_active_thread_as_markdown(
1223 &mut self,
1224 _: &OpenActiveThreadAsMarkdown,
1225 window: &mut Window,
1226 cx: &mut Context<Self>,
1227 ) {
1228 let Some(workspace) = self.workspace.upgrade() else {
1229 return;
1230 };
1231
1232 let Some(thread) = self.active_thread() else {
1233 return;
1234 };
1235
1236 active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
1237 .detach_and_log_err(cx);
1238 }
1239
1240 fn handle_agent_configuration_event(
1241 &mut self,
1242 _entity: &Entity<AgentConfiguration>,
1243 event: &AssistantConfigurationEvent,
1244 window: &mut Window,
1245 cx: &mut Context<Self>,
1246 ) {
1247 match event {
1248 AssistantConfigurationEvent::NewThread(provider) => {
1249 if LanguageModelRegistry::read_global(cx)
1250 .default_model()
1251 .map_or(true, |model| model.provider.id() != provider.id())
1252 {
1253 if let Some(model) = provider.default_model(cx) {
1254 update_settings_file::<AgentSettings>(
1255 self.fs.clone(),
1256 cx,
1257 move |settings, _| settings.set_model(model),
1258 );
1259 }
1260 }
1261
1262 self.new_thread(&NewThread::default(), window, cx);
1263 }
1264 }
1265 }
1266
1267 pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1268 match &self.active_view {
1269 ActiveView::Thread { thread, .. } => thread.upgrade(),
1270 _ => None,
1271 }
1272 }
1273
1274 pub(crate) fn delete_thread(
1275 &mut self,
1276 thread_id: &ThreadId,
1277 cx: &mut Context<Self>,
1278 ) -> Task<Result<()>> {
1279 self.thread_store
1280 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1281 }
1282
1283 pub(crate) fn has_active_thread(&self) -> bool {
1284 matches!(self.active_view, ActiveView::Thread { .. })
1285 }
1286
1287 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1288 let thread_state = self.thread.read(cx).thread().read(cx);
1289 if !thread_state.tool_use_limit_reached() {
1290 return;
1291 }
1292
1293 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1294 if let Some(model) = model {
1295 self.thread.update(cx, |active_thread, cx| {
1296 active_thread.thread().update(cx, |thread, cx| {
1297 thread.insert_invisible_continue_message(cx);
1298 thread.advance_prompt_id();
1299 thread.send_to_model(
1300 model,
1301 CompletionIntent::UserPrompt,
1302 Some(window.window_handle()),
1303 cx,
1304 );
1305 });
1306 });
1307 } else {
1308 log::warn!("No configured model available for continuation");
1309 }
1310 }
1311
1312 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1313 match &self.active_view {
1314 ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1315 _ => None,
1316 }
1317 }
1318
1319 pub(crate) fn delete_context(
1320 &mut self,
1321 path: Arc<Path>,
1322 cx: &mut Context<Self>,
1323 ) -> Task<Result<()>> {
1324 self.context_store
1325 .update(cx, |this, cx| this.delete_local_context(path, cx))
1326 }
1327
1328 fn set_active_view(
1329 &mut self,
1330 new_view: ActiveView,
1331 window: &mut Window,
1332 cx: &mut Context<Self>,
1333 ) {
1334 let current_is_history = matches!(self.active_view, ActiveView::History);
1335 let new_is_history = matches!(new_view, ActiveView::History);
1336
1337 match &self.active_view {
1338 ActiveView::Thread { thread, .. } => {
1339 if let Some(thread) = thread.upgrade() {
1340 if thread.read(cx).is_empty() {
1341 let id = thread.read(cx).id().clone();
1342 self.history_store.update(cx, |store, cx| {
1343 store.remove_recently_opened_thread(id, cx);
1344 });
1345 }
1346 }
1347 }
1348 ActiveView::PromptEditor { context_editor, .. } => {
1349 let context = context_editor.read(cx).context();
1350 // When switching away from an unsaved text thread, delete its entry.
1351 if context.read(cx).path().is_none() {
1352 let context = context.clone();
1353 self.history_store.update(cx, |store, cx| {
1354 store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
1355 });
1356 }
1357 }
1358 _ => {}
1359 }
1360
1361 match &new_view {
1362 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1363 if let Some(thread) = thread.upgrade() {
1364 let id = thread.read(cx).id().clone();
1365 store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1366 }
1367 }),
1368 ActiveView::PromptEditor { context_editor, .. } => {
1369 self.history_store.update(cx, |store, cx| {
1370 let context = context_editor.read(cx).context().clone();
1371 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1372 })
1373 }
1374 _ => {}
1375 }
1376
1377 if current_is_history && !new_is_history {
1378 self.active_view = new_view;
1379 } else if !current_is_history && new_is_history {
1380 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1381 } else {
1382 if !new_is_history {
1383 self.previous_view = None;
1384 }
1385 self.active_view = new_view;
1386 }
1387
1388 self.focus_handle(cx).focus(window);
1389 }
1390}
1391
1392impl Focusable for AgentPanel {
1393 fn focus_handle(&self, cx: &App) -> FocusHandle {
1394 match &self.active_view {
1395 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1396 ActiveView::History => self.history.focus_handle(cx),
1397 ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1398 ActiveView::Configuration => {
1399 if let Some(configuration) = self.configuration.as_ref() {
1400 configuration.focus_handle(cx)
1401 } else {
1402 cx.focus_handle()
1403 }
1404 }
1405 }
1406 }
1407}
1408
1409fn agent_panel_dock_position(cx: &App) -> DockPosition {
1410 match AgentSettings::get_global(cx).dock {
1411 AgentDockPosition::Left => DockPosition::Left,
1412 AgentDockPosition::Bottom => DockPosition::Bottom,
1413 AgentDockPosition::Right => DockPosition::Right,
1414 }
1415}
1416
1417impl EventEmitter<PanelEvent> for AgentPanel {}
1418
1419impl Panel for AgentPanel {
1420 fn persistent_name() -> &'static str {
1421 "AgentPanel"
1422 }
1423
1424 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1425 agent_panel_dock_position(cx)
1426 }
1427
1428 fn position_is_valid(&self, position: DockPosition) -> bool {
1429 position != DockPosition::Bottom
1430 }
1431
1432 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1433 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1434 let dock = match position {
1435 DockPosition::Left => AgentDockPosition::Left,
1436 DockPosition::Bottom => AgentDockPosition::Bottom,
1437 DockPosition::Right => AgentDockPosition::Right,
1438 };
1439 settings.set_dock(dock);
1440 });
1441 }
1442
1443 fn size(&self, window: &Window, cx: &App) -> Pixels {
1444 let settings = AgentSettings::get_global(cx);
1445 match self.position(window, cx) {
1446 DockPosition::Left | DockPosition::Right => {
1447 self.width.unwrap_or(settings.default_width)
1448 }
1449 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1450 }
1451 }
1452
1453 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1454 match self.position(window, cx) {
1455 DockPosition::Left | DockPosition::Right => self.width = size,
1456 DockPosition::Bottom => self.height = size,
1457 }
1458 self.serialize(cx);
1459 cx.notify();
1460 }
1461
1462 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1463
1464 fn remote_id() -> Option<proto::PanelId> {
1465 Some(proto::PanelId::AssistantPanel)
1466 }
1467
1468 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1469 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1470 }
1471
1472 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1473 Some("Agent Panel")
1474 }
1475
1476 fn toggle_action(&self) -> Box<dyn Action> {
1477 Box::new(ToggleFocus)
1478 }
1479
1480 fn activation_priority(&self) -> u32 {
1481 3
1482 }
1483
1484 fn enabled(&self, cx: &App) -> bool {
1485 AgentSettings::get_global(cx).enabled
1486 }
1487
1488 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1489 self.zoomed
1490 }
1491
1492 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1493 self.zoomed = zoomed;
1494 cx.notify();
1495 }
1496}
1497
1498impl AgentPanel {
1499 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1500 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1501
1502 let content = match &self.active_view {
1503 ActiveView::Thread {
1504 change_title_editor,
1505 ..
1506 } => {
1507 let active_thread = self.thread.read(cx);
1508 let state = if active_thread.is_empty() {
1509 &ThreadSummary::Pending
1510 } else {
1511 active_thread.summary(cx)
1512 };
1513
1514 match state {
1515 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1516 .truncate()
1517 .into_any_element(),
1518 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1519 .truncate()
1520 .into_any_element(),
1521 ThreadSummary::Ready(_) => div()
1522 .w_full()
1523 .child(change_title_editor.clone())
1524 .into_any_element(),
1525 ThreadSummary::Error => h_flex()
1526 .w_full()
1527 .child(change_title_editor.clone())
1528 .child(
1529 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1530 .on_click({
1531 let active_thread = self.thread.clone();
1532 move |_, _window, cx| {
1533 active_thread.update(cx, |thread, cx| {
1534 thread.regenerate_summary(cx);
1535 });
1536 }
1537 })
1538 .tooltip(move |_window, cx| {
1539 cx.new(|_| {
1540 Tooltip::new("Failed to generate title")
1541 .meta("Click to try again")
1542 })
1543 .into()
1544 }),
1545 )
1546 .into_any_element(),
1547 }
1548 }
1549 ActiveView::PromptEditor {
1550 title_editor,
1551 context_editor,
1552 ..
1553 } => {
1554 let summary = context_editor.read(cx).context().read(cx).summary();
1555
1556 match summary {
1557 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1558 .truncate()
1559 .into_any_element(),
1560 ContextSummary::Content(summary) => {
1561 if summary.done {
1562 div()
1563 .w_full()
1564 .child(title_editor.clone())
1565 .into_any_element()
1566 } else {
1567 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1568 .truncate()
1569 .into_any_element()
1570 }
1571 }
1572 ContextSummary::Error => h_flex()
1573 .w_full()
1574 .child(title_editor.clone())
1575 .child(
1576 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1577 .on_click({
1578 let context_editor = context_editor.clone();
1579 move |_, _window, cx| {
1580 context_editor.update(cx, |context_editor, cx| {
1581 context_editor.regenerate_summary(cx);
1582 });
1583 }
1584 })
1585 .tooltip(move |_window, cx| {
1586 cx.new(|_| {
1587 Tooltip::new("Failed to generate title")
1588 .meta("Click to try again")
1589 })
1590 .into()
1591 }),
1592 )
1593 .into_any_element(),
1594 }
1595 }
1596 ActiveView::History => Label::new("History").truncate().into_any_element(),
1597 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1598 };
1599
1600 h_flex()
1601 .key_context("TitleEditor")
1602 .id("TitleEditor")
1603 .flex_grow()
1604 .w_full()
1605 .max_w_full()
1606 .overflow_x_scroll()
1607 .child(content)
1608 .into_any()
1609 }
1610
1611 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1612 let active_thread = self.thread.read(cx);
1613 let user_store = self.user_store.read(cx);
1614 let thread = active_thread.thread().read(cx);
1615 let thread_id = thread.id().clone();
1616 let is_empty = active_thread.is_empty();
1617 let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
1618 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1619 maybe!({
1620 let amount = user_store.model_request_usage_amount()?;
1621 let limit = user_store.model_request_usage_limit()?.variant?;
1622
1623 Some(RequestUsage {
1624 amount: amount as i32,
1625 limit: match limit {
1626 proto::usage_limit::Variant::Limited(limited) => {
1627 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1628 }
1629 proto::usage_limit::Variant::Unlimited(_) => {
1630 zed_llm_client::UsageLimit::Unlimited
1631 }
1632 },
1633 })
1634 })
1635 });
1636
1637 let account_url = zed_urls::account_url(cx);
1638
1639 let show_token_count = match &self.active_view {
1640 ActiveView::Thread { .. } => !is_empty || !editor_empty,
1641 ActiveView::PromptEditor { .. } => true,
1642 _ => false,
1643 };
1644
1645 let focus_handle = self.focus_handle(cx);
1646
1647 let go_back_button = div().child(
1648 IconButton::new("go-back", IconName::ArrowLeft)
1649 .icon_size(IconSize::Small)
1650 .on_click(cx.listener(|this, _, window, cx| {
1651 this.go_back(&workspace::GoBack, window, cx);
1652 }))
1653 .tooltip({
1654 let focus_handle = focus_handle.clone();
1655 move |window, cx| {
1656 Tooltip::for_action_in(
1657 "Go Back",
1658 &workspace::GoBack,
1659 &focus_handle,
1660 window,
1661 cx,
1662 )
1663 }
1664 }),
1665 );
1666
1667 let recent_entries_menu = div().child(
1668 PopoverMenu::new("agent-nav-menu")
1669 .trigger_with_tooltip(
1670 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1671 .icon_size(IconSize::Small)
1672 .style(ui::ButtonStyle::Subtle),
1673 {
1674 let focus_handle = focus_handle.clone();
1675 move |window, cx| {
1676 Tooltip::for_action_in(
1677 "Toggle Panel Menu",
1678 &ToggleNavigationMenu,
1679 &focus_handle,
1680 window,
1681 cx,
1682 )
1683 }
1684 },
1685 )
1686 .anchor(Corner::TopLeft)
1687 .with_handle(self.assistant_navigation_menu_handle.clone())
1688 .menu({
1689 let menu = self.assistant_navigation_menu.clone();
1690 move |window, cx| {
1691 if let Some(menu) = menu.as_ref() {
1692 menu.update(cx, |_, cx| {
1693 cx.defer_in(window, |menu, window, cx| {
1694 menu.rebuild(window, cx);
1695 });
1696 })
1697 }
1698 menu.clone()
1699 }
1700 }),
1701 );
1702
1703 let zoom_in_label = if self.is_zoomed(window, cx) {
1704 "Zoom Out"
1705 } else {
1706 "Zoom In"
1707 };
1708
1709 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1710 .trigger_with_tooltip(
1711 IconButton::new("agent-options-menu", IconName::Ellipsis)
1712 .icon_size(IconSize::Small),
1713 {
1714 let focus_handle = focus_handle.clone();
1715 move |window, cx| {
1716 Tooltip::for_action_in(
1717 "Toggle Agent Menu",
1718 &ToggleOptionsMenu,
1719 &focus_handle,
1720 window,
1721 cx,
1722 )
1723 }
1724 },
1725 )
1726 .anchor(Corner::TopRight)
1727 .with_handle(self.assistant_dropdown_menu_handle.clone())
1728 .menu(move |window, cx| {
1729 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1730 menu = menu
1731 .action("New Thread", NewThread::default().boxed_clone())
1732 .action("New Text Thread", NewTextThread.boxed_clone())
1733 .when(!is_empty, |menu| {
1734 menu.action(
1735 "New From Summary",
1736 Box::new(NewThread {
1737 from_thread_id: Some(thread_id.clone()),
1738 }),
1739 )
1740 })
1741 .separator();
1742
1743 menu = menu
1744 .header("MCP Servers")
1745 .action(
1746 "View Server Extensions",
1747 Box::new(zed_actions::Extensions {
1748 category_filter: Some(
1749 zed_actions::ExtensionCategoryFilter::ContextServers,
1750 ),
1751 }),
1752 )
1753 .action("Add Custom Server…", Box::new(AddContextServer))
1754 .separator();
1755
1756 if let Some(usage) = last_usage {
1757 menu = menu
1758 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1759 .custom_entry(
1760 move |_window, cx| {
1761 let used_percentage = match usage.limit {
1762 UsageLimit::Limited(limit) => {
1763 Some((usage.amount as f32 / limit as f32) * 100.)
1764 }
1765 UsageLimit::Unlimited => None,
1766 };
1767
1768 h_flex()
1769 .flex_1()
1770 .gap_1p5()
1771 .children(used_percentage.map(|percent| {
1772 ProgressBar::new("usage", percent, 100., cx)
1773 }))
1774 .child(
1775 Label::new(match usage.limit {
1776 UsageLimit::Limited(limit) => {
1777 format!("{} / {limit}", usage.amount)
1778 }
1779 UsageLimit::Unlimited => {
1780 format!("{} / ∞", usage.amount)
1781 }
1782 })
1783 .size(LabelSize::Small)
1784 .color(Color::Muted),
1785 )
1786 .into_any_element()
1787 },
1788 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1789 )
1790 .separator()
1791 }
1792
1793 menu = menu
1794 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1795 .action("Settings", Box::new(OpenConfiguration))
1796 .action(zoom_in_label, Box::new(ToggleZoom));
1797 menu
1798 }))
1799 });
1800
1801 h_flex()
1802 .id("assistant-toolbar")
1803 .h(Tab::container_height(cx))
1804 .max_w_full()
1805 .flex_none()
1806 .justify_between()
1807 .gap_2()
1808 .bg(cx.theme().colors().tab_bar_background)
1809 .border_b_1()
1810 .border_color(cx.theme().colors().border)
1811 .child(
1812 h_flex()
1813 .size_full()
1814 .pl_1()
1815 .gap_1()
1816 .child(match &self.active_view {
1817 ActiveView::History | ActiveView::Configuration => go_back_button,
1818 _ => recent_entries_menu,
1819 })
1820 .child(self.render_title_view(window, cx)),
1821 )
1822 .child(
1823 h_flex()
1824 .h_full()
1825 .gap_2()
1826 .when(show_token_count, |parent| {
1827 parent.children(self.render_token_count(&thread, cx))
1828 })
1829 .child(
1830 h_flex()
1831 .h_full()
1832 .gap(DynamicSpacing::Base02.rems(cx))
1833 .px(DynamicSpacing::Base08.rems(cx))
1834 .border_l_1()
1835 .border_color(cx.theme().colors().border)
1836 .child(
1837 IconButton::new("new", IconName::Plus)
1838 .icon_size(IconSize::Small)
1839 .style(ButtonStyle::Subtle)
1840 .tooltip(move |window, cx| {
1841 Tooltip::for_action_in(
1842 "New Thread",
1843 &NewThread::default(),
1844 &focus_handle,
1845 window,
1846 cx,
1847 )
1848 })
1849 .on_click(move |_event, window, cx| {
1850 window.dispatch_action(
1851 NewThread::default().boxed_clone(),
1852 cx,
1853 );
1854 }),
1855 )
1856 .child(agent_extra_menu),
1857 ),
1858 )
1859 }
1860
1861 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1862 let is_generating = thread.is_generating();
1863 let message_editor = self.message_editor.read(cx);
1864
1865 let conversation_token_usage = thread.total_token_usage()?;
1866
1867 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1868 self.thread.read(cx).editing_message_id()
1869 {
1870 let combined = thread
1871 .token_usage_up_to_message(editing_message_id)
1872 .add(unsent_tokens);
1873
1874 (combined, unsent_tokens > 0)
1875 } else {
1876 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1877 let combined = conversation_token_usage.add(unsent_tokens);
1878
1879 (combined, unsent_tokens > 0)
1880 };
1881
1882 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1883
1884 match &self.active_view {
1885 ActiveView::Thread { .. } => {
1886 if total_token_usage.total == 0 {
1887 return None;
1888 }
1889
1890 let token_color = match total_token_usage.ratio() {
1891 TokenUsageRatio::Normal if is_estimating => Color::Default,
1892 TokenUsageRatio::Normal => Color::Muted,
1893 TokenUsageRatio::Warning => Color::Warning,
1894 TokenUsageRatio::Exceeded => Color::Error,
1895 };
1896
1897 let token_count = h_flex()
1898 .id("token-count")
1899 .flex_shrink_0()
1900 .gap_0p5()
1901 .when(!is_generating && is_estimating, |parent| {
1902 parent
1903 .child(
1904 h_flex()
1905 .mr_1()
1906 .size_2p5()
1907 .justify_center()
1908 .rounded_full()
1909 .bg(cx.theme().colors().text.opacity(0.1))
1910 .child(
1911 div().size_1().rounded_full().bg(cx.theme().colors().text),
1912 ),
1913 )
1914 .tooltip(move |window, cx| {
1915 Tooltip::with_meta(
1916 "Estimated New Token Count",
1917 None,
1918 format!(
1919 "Current Conversation Tokens: {}",
1920 humanize_token_count(conversation_token_usage.total)
1921 ),
1922 window,
1923 cx,
1924 )
1925 })
1926 })
1927 .child(
1928 Label::new(humanize_token_count(total_token_usage.total))
1929 .size(LabelSize::Small)
1930 .color(token_color)
1931 .map(|label| {
1932 if is_generating || is_waiting_to_update_token_count {
1933 label
1934 .with_animation(
1935 "used-tokens-label",
1936 Animation::new(Duration::from_secs(2))
1937 .repeat()
1938 .with_easing(pulsating_between(0.6, 1.)),
1939 |label, delta| label.alpha(delta),
1940 )
1941 .into_any()
1942 } else {
1943 label.into_any_element()
1944 }
1945 }),
1946 )
1947 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1948 .child(
1949 Label::new(humanize_token_count(total_token_usage.max))
1950 .size(LabelSize::Small)
1951 .color(Color::Muted),
1952 )
1953 .into_any();
1954
1955 Some(token_count)
1956 }
1957 ActiveView::PromptEditor { context_editor, .. } => {
1958 let element = render_remaining_tokens(context_editor, cx)?;
1959
1960 Some(element.into_any_element())
1961 }
1962 _ => None,
1963 }
1964 }
1965
1966 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
1967 if TrialEndUpsell::dismissed() {
1968 return false;
1969 }
1970
1971 let plan = self.user_store.read(cx).current_plan();
1972 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1973
1974 matches!(plan, Some(Plan::Free)) && has_previous_trial
1975 }
1976
1977 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1978 if !matches!(self.active_view, ActiveView::Thread { .. }) {
1979 return false;
1980 }
1981
1982 if self.hide_upsell || Upsell::dismissed() {
1983 return false;
1984 }
1985
1986 let is_using_zed_provider = self
1987 .thread
1988 .read(cx)
1989 .thread()
1990 .read(cx)
1991 .configured_model()
1992 .map_or(false, |model| {
1993 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1994 });
1995 if !is_using_zed_provider {
1996 return false;
1997 }
1998
1999 let plan = self.user_store.read(cx).current_plan();
2000 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
2001 return false;
2002 }
2003
2004 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2005 if has_previous_trial {
2006 return false;
2007 }
2008
2009 true
2010 }
2011
2012 fn render_upsell(
2013 &self,
2014 _window: &mut Window,
2015 cx: &mut Context<Self>,
2016 ) -> Option<impl IntoElement> {
2017 if !self.should_render_upsell(cx) {
2018 return None;
2019 }
2020
2021 if self.user_store.read(cx).account_too_young() {
2022 Some(self.render_young_account_upsell(cx).into_any_element())
2023 } else {
2024 Some(self.render_trial_upsell(cx).into_any_element())
2025 }
2026 }
2027
2028 fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2029 let checkbox = CheckboxWithLabel::new(
2030 "dont-show-again",
2031 Label::new("Don't show again").color(Color::Muted),
2032 ToggleState::Unselected,
2033 move |toggle_state, _window, cx| {
2034 let toggle_state_bool = toggle_state.selected();
2035
2036 Upsell::set_dismissed(toggle_state_bool, cx);
2037 },
2038 );
2039
2040 let contents = div()
2041 .size_full()
2042 .gap_2()
2043 .flex()
2044 .flex_col()
2045 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2046 .child(
2047 Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
2048 .size(LabelSize::Small),
2049 )
2050 .child(
2051 Label::new(
2052 "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
2053 )
2054 .color(Color::Muted),
2055 )
2056 .child(
2057 h_flex()
2058 .w_full()
2059 .px_neg_1()
2060 .justify_between()
2061 .items_center()
2062 .child(h_flex().items_center().gap_1().child(checkbox))
2063 .child(
2064 h_flex()
2065 .gap_2()
2066 .child(
2067 Button::new("dismiss-button", "Not Now")
2068 .style(ButtonStyle::Transparent)
2069 .color(Color::Muted)
2070 .on_click({
2071 let agent_panel = cx.entity();
2072 move |_, _, cx| {
2073 agent_panel.update(cx, |this, cx| {
2074 this.hide_upsell = true;
2075 cx.notify();
2076 });
2077 }
2078 }),
2079 )
2080 .child(
2081 Button::new("cta-button", "Upgrade to Zed Pro")
2082 .style(ButtonStyle::Transparent)
2083 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2084 ),
2085 ),
2086 );
2087
2088 self.render_upsell_container(cx, contents)
2089 }
2090
2091 fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2092 let checkbox = CheckboxWithLabel::new(
2093 "dont-show-again",
2094 Label::new("Don't show again").color(Color::Muted),
2095 ToggleState::Unselected,
2096 move |toggle_state, _window, cx| {
2097 let toggle_state_bool = toggle_state.selected();
2098
2099 Upsell::set_dismissed(toggle_state_bool, cx);
2100 },
2101 );
2102
2103 let contents = div()
2104 .size_full()
2105 .gap_2()
2106 .flex()
2107 .flex_col()
2108 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2109 .child(
2110 Label::new("Try Zed Pro for free for 14 days - no credit card required.")
2111 .size(LabelSize::Small),
2112 )
2113 .child(
2114 Label::new(
2115 "Use your own API keys or enable usage-based billing once you hit the cap.",
2116 )
2117 .color(Color::Muted),
2118 )
2119 .child(
2120 h_flex()
2121 .w_full()
2122 .px_neg_1()
2123 .justify_between()
2124 .items_center()
2125 .child(h_flex().items_center().gap_1().child(checkbox))
2126 .child(
2127 h_flex()
2128 .gap_2()
2129 .child(
2130 Button::new("dismiss-button", "Not Now")
2131 .style(ButtonStyle::Transparent)
2132 .color(Color::Muted)
2133 .on_click({
2134 let agent_panel = cx.entity();
2135 move |_, _, cx| {
2136 agent_panel.update(cx, |this, cx| {
2137 this.hide_upsell = true;
2138 cx.notify();
2139 });
2140 }
2141 }),
2142 )
2143 .child(
2144 Button::new("cta-button", "Start Trial")
2145 .style(ButtonStyle::Transparent)
2146 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2147 ),
2148 ),
2149 );
2150
2151 self.render_upsell_container(cx, contents)
2152 }
2153
2154 fn render_trial_end_upsell(
2155 &self,
2156 _window: &mut Window,
2157 cx: &mut Context<Self>,
2158 ) -> Option<impl IntoElement> {
2159 if !self.should_render_trial_end_upsell(cx) {
2160 return None;
2161 }
2162
2163 Some(
2164 self.render_upsell_container(
2165 cx,
2166 div()
2167 .size_full()
2168 .gap_2()
2169 .flex()
2170 .flex_col()
2171 .child(
2172 Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
2173 )
2174 .child(
2175 Label::new("You've been automatically reset to the free plan.")
2176 .size(LabelSize::Small),
2177 )
2178 .child(
2179 h_flex()
2180 .w_full()
2181 .px_neg_1()
2182 .justify_between()
2183 .items_center()
2184 .child(div())
2185 .child(
2186 h_flex()
2187 .gap_2()
2188 .child(
2189 Button::new("dismiss-button", "Stay on Free")
2190 .style(ButtonStyle::Transparent)
2191 .color(Color::Muted)
2192 .on_click({
2193 let agent_panel = cx.entity();
2194 move |_, _, cx| {
2195 agent_panel.update(cx, |_this, cx| {
2196 TrialEndUpsell::set_dismissed(true, cx);
2197 cx.notify();
2198 });
2199 }
2200 }),
2201 )
2202 .child(
2203 Button::new("cta-button", "Upgrade to Zed Pro")
2204 .style(ButtonStyle::Transparent)
2205 .on_click(|_, _, cx| {
2206 cx.open_url(&zed_urls::account_url(cx))
2207 }),
2208 ),
2209 ),
2210 ),
2211 ),
2212 )
2213 }
2214
2215 fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
2216 div().p_2().child(
2217 v_flex()
2218 .w_full()
2219 .elevation_2(cx)
2220 .rounded(px(8.))
2221 .bg(cx.theme().colors().background.alpha(0.5))
2222 .p(px(3.))
2223 .child(
2224 div()
2225 .gap_2()
2226 .flex()
2227 .flex_col()
2228 .size_full()
2229 .border_1()
2230 .rounded(px(5.))
2231 .border_color(cx.theme().colors().text.alpha(0.1))
2232 .overflow_hidden()
2233 .relative()
2234 .bg(cx.theme().colors().panel_background)
2235 .px_4()
2236 .py_3()
2237 .child(
2238 div()
2239 .absolute()
2240 .top_0()
2241 .right(px(-1.0))
2242 .w(px(441.))
2243 .h(px(167.))
2244 .child(
2245 Vector::new(
2246 VectorName::Grid,
2247 rems_from_px(441.),
2248 rems_from_px(167.),
2249 )
2250 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
2251 ),
2252 )
2253 .child(
2254 div()
2255 .absolute()
2256 .top(px(-8.0))
2257 .right_0()
2258 .w(px(400.))
2259 .h(px(92.))
2260 .child(
2261 Vector::new(
2262 VectorName::AiGrid,
2263 rems_from_px(400.),
2264 rems_from_px(92.),
2265 )
2266 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
2267 ),
2268 )
2269 // .child(
2270 // div()
2271 // .absolute()
2272 // .top_0()
2273 // .right(px(360.))
2274 // .size(px(401.))
2275 // .overflow_hidden()
2276 // .bg(cx.theme().colors().panel_background)
2277 // )
2278 .child(
2279 div()
2280 .absolute()
2281 .top_0()
2282 .right_0()
2283 .w(px(660.))
2284 .h(px(401.))
2285 .overflow_hidden()
2286 .bg(linear_gradient(
2287 75.,
2288 linear_color_stop(
2289 cx.theme().colors().panel_background.alpha(0.01),
2290 1.0,
2291 ),
2292 linear_color_stop(cx.theme().colors().panel_background, 0.45),
2293 )),
2294 )
2295 .child(content),
2296 ),
2297 )
2298 }
2299
2300 fn render_active_thread_or_empty_state(
2301 &self,
2302 window: &mut Window,
2303 cx: &mut Context<Self>,
2304 ) -> AnyElement {
2305 if self.thread.read(cx).is_empty() {
2306 return self
2307 .render_thread_empty_state(window, cx)
2308 .into_any_element();
2309 }
2310
2311 self.thread.clone().into_any_element()
2312 }
2313
2314 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2315 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2316 return Some(ConfigurationError::NoProvider);
2317 };
2318
2319 if !model.provider.is_authenticated(cx) {
2320 return Some(ConfigurationError::ProviderNotAuthenticated);
2321 }
2322
2323 if model.provider.must_accept_terms(cx) {
2324 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2325 model.provider,
2326 ));
2327 }
2328
2329 None
2330 }
2331
2332 fn render_thread_empty_state(
2333 &self,
2334 window: &mut Window,
2335 cx: &mut Context<Self>,
2336 ) -> impl IntoElement {
2337 let recent_history = self
2338 .history_store
2339 .update(cx, |this, cx| this.recent_entries(6, cx));
2340
2341 let configuration_error = self.configuration_error(cx);
2342 let no_error = configuration_error.is_none();
2343 let focus_handle = self.focus_handle(cx);
2344
2345 v_flex()
2346 .size_full()
2347 .bg(cx.theme().colors().panel_background)
2348 .when(recent_history.is_empty(), |this| {
2349 let configuration_error_ref = &configuration_error;
2350 this.child(
2351 v_flex()
2352 .size_full()
2353 .max_w_80()
2354 .mx_auto()
2355 .justify_center()
2356 .items_center()
2357 .gap_1()
2358 .child(
2359 h_flex().child(
2360 Headline::new("Welcome to the Agent Panel")
2361 ),
2362 )
2363 .when(no_error, |parent| {
2364 parent
2365 .child(
2366 h_flex().child(
2367 Label::new("Ask and build anything.")
2368 .color(Color::Muted)
2369 .mb_2p5(),
2370 ),
2371 )
2372 .child(
2373 Button::new("new-thread", "Start New Thread")
2374 .icon(IconName::Plus)
2375 .icon_position(IconPosition::Start)
2376 .icon_size(IconSize::Small)
2377 .icon_color(Color::Muted)
2378 .full_width()
2379 .key_binding(KeyBinding::for_action_in(
2380 &NewThread::default(),
2381 &focus_handle,
2382 window,
2383 cx,
2384 ))
2385 .on_click(|_event, window, cx| {
2386 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2387 }),
2388 )
2389 .child(
2390 Button::new("context", "Add Context")
2391 .icon(IconName::FileCode)
2392 .icon_position(IconPosition::Start)
2393 .icon_size(IconSize::Small)
2394 .icon_color(Color::Muted)
2395 .full_width()
2396 .key_binding(KeyBinding::for_action_in(
2397 &ToggleContextPicker,
2398 &focus_handle,
2399 window,
2400 cx,
2401 ))
2402 .on_click(|_event, window, cx| {
2403 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2404 }),
2405 )
2406 .child(
2407 Button::new("mode", "Switch Model")
2408 .icon(IconName::DatabaseZap)
2409 .icon_position(IconPosition::Start)
2410 .icon_size(IconSize::Small)
2411 .icon_color(Color::Muted)
2412 .full_width()
2413 .key_binding(KeyBinding::for_action_in(
2414 &ToggleModelSelector,
2415 &focus_handle,
2416 window,
2417 cx,
2418 ))
2419 .on_click(|_event, window, cx| {
2420 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2421 }),
2422 )
2423 .child(
2424 Button::new("settings", "View Settings")
2425 .icon(IconName::Settings)
2426 .icon_position(IconPosition::Start)
2427 .icon_size(IconSize::Small)
2428 .icon_color(Color::Muted)
2429 .full_width()
2430 .key_binding(KeyBinding::for_action_in(
2431 &OpenConfiguration,
2432 &focus_handle,
2433 window,
2434 cx,
2435 ))
2436 .on_click(|_event, window, cx| {
2437 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2438 }),
2439 )
2440 })
2441 .map(|parent| {
2442 match configuration_error_ref {
2443 Some(ConfigurationError::ProviderNotAuthenticated)
2444 | Some(ConfigurationError::NoProvider) => {
2445 parent
2446 .child(
2447 h_flex().child(
2448 Label::new("To start using the agent, configure at least one LLM provider.")
2449 .color(Color::Muted)
2450 .mb_2p5()
2451 )
2452 )
2453 .child(
2454 Button::new("settings", "Configure a Provider")
2455 .icon(IconName::Settings)
2456 .icon_position(IconPosition::Start)
2457 .icon_size(IconSize::Small)
2458 .icon_color(Color::Muted)
2459 .full_width()
2460 .key_binding(KeyBinding::for_action_in(
2461 &OpenConfiguration,
2462 &focus_handle,
2463 window,
2464 cx,
2465 ))
2466 .on_click(|_event, window, cx| {
2467 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2468 }),
2469 )
2470 }
2471 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2472 parent.children(
2473 provider.render_accept_terms(
2474 LanguageModelProviderTosView::ThreadFreshStart,
2475 cx,
2476 ),
2477 )
2478 }
2479 None => parent,
2480 }
2481 })
2482 )
2483 })
2484 .when(!recent_history.is_empty(), |parent| {
2485 let focus_handle = focus_handle.clone();
2486 let configuration_error_ref = &configuration_error;
2487
2488 parent
2489 .overflow_hidden()
2490 .p_1p5()
2491 .justify_end()
2492 .gap_1()
2493 .child(
2494 h_flex()
2495 .pl_1p5()
2496 .pb_1()
2497 .w_full()
2498 .justify_between()
2499 .border_b_1()
2500 .border_color(cx.theme().colors().border_variant)
2501 .child(
2502 Label::new("Recent")
2503 .size(LabelSize::Small)
2504 .color(Color::Muted),
2505 )
2506 .child(
2507 Button::new("view-history", "View All")
2508 .style(ButtonStyle::Subtle)
2509 .label_size(LabelSize::Small)
2510 .key_binding(
2511 KeyBinding::for_action_in(
2512 &OpenHistory,
2513 &self.focus_handle(cx),
2514 window,
2515 cx,
2516 ).map(|kb| kb.size(rems_from_px(12.))),
2517 )
2518 .on_click(move |_event, window, cx| {
2519 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2520 }),
2521 ),
2522 )
2523 .child(
2524 v_flex()
2525 .gap_1()
2526 .children(
2527 recent_history.into_iter().enumerate().map(|(index, entry)| {
2528 // TODO: Add keyboard navigation.
2529 let is_hovered = self.hovered_recent_history_item == Some(index);
2530 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2531 .hovered(is_hovered)
2532 .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2533 if *is_hovered {
2534 this.hovered_recent_history_item = Some(index);
2535 } else if this.hovered_recent_history_item == Some(index) {
2536 this.hovered_recent_history_item = None;
2537 }
2538 cx.notify();
2539 }))
2540 .into_any_element()
2541 }),
2542 )
2543 )
2544 .map(|parent| {
2545 match configuration_error_ref {
2546 Some(ConfigurationError::ProviderNotAuthenticated)
2547 | Some(ConfigurationError::NoProvider) => {
2548 parent
2549 .child(
2550 Banner::new()
2551 .severity(ui::Severity::Warning)
2552 .child(
2553 Label::new(
2554 "Configure at least one LLM provider to start using the panel.",
2555 )
2556 .size(LabelSize::Small),
2557 )
2558 .action_slot(
2559 Button::new("settings", "Configure Provider")
2560 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2561 .label_size(LabelSize::Small)
2562 .key_binding(
2563 KeyBinding::for_action_in(
2564 &OpenConfiguration,
2565 &focus_handle,
2566 window,
2567 cx,
2568 )
2569 .map(|kb| kb.size(rems_from_px(12.))),
2570 )
2571 .on_click(|_event, window, cx| {
2572 window.dispatch_action(
2573 OpenConfiguration.boxed_clone(),
2574 cx,
2575 )
2576 }),
2577 ),
2578 )
2579 }
2580 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2581 parent
2582 .child(
2583 Banner::new()
2584 .severity(ui::Severity::Warning)
2585 .child(
2586 h_flex()
2587 .w_full()
2588 .children(
2589 provider.render_accept_terms(
2590 LanguageModelProviderTosView::ThreadtEmptyState,
2591 cx,
2592 ),
2593 ),
2594 ),
2595 )
2596 }
2597 None => parent,
2598 }
2599 })
2600 })
2601 }
2602
2603 fn render_tool_use_limit_reached(
2604 &self,
2605 window: &mut Window,
2606 cx: &mut Context<Self>,
2607 ) -> Option<AnyElement> {
2608 let tool_use_limit_reached = self
2609 .thread
2610 .read(cx)
2611 .thread()
2612 .read(cx)
2613 .tool_use_limit_reached();
2614 if !tool_use_limit_reached {
2615 return None;
2616 }
2617
2618 let model = self
2619 .thread
2620 .read(cx)
2621 .thread()
2622 .read(cx)
2623 .configured_model()?
2624 .model;
2625
2626 let focus_handle = self.focus_handle(cx);
2627
2628 let banner = Banner::new()
2629 .severity(ui::Severity::Info)
2630 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
2631 .action_slot(
2632 h_flex()
2633 .gap_1()
2634 .child(
2635 Button::new("continue-conversation", "Continue")
2636 .layer(ElevationIndex::ModalSurface)
2637 .label_size(LabelSize::Small)
2638 .key_binding(
2639 KeyBinding::for_action_in(
2640 &ContinueThread,
2641 &focus_handle,
2642 window,
2643 cx,
2644 )
2645 .map(|kb| kb.size(rems_from_px(10.))),
2646 )
2647 .on_click(cx.listener(|this, _, window, cx| {
2648 this.continue_conversation(window, cx);
2649 })),
2650 )
2651 .when(model.supports_max_mode(), |this| {
2652 this.child(
2653 Button::new("continue-burn-mode", "Continue with Burn Mode")
2654 .style(ButtonStyle::Filled)
2655 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2656 .layer(ElevationIndex::ModalSurface)
2657 .label_size(LabelSize::Small)
2658 .key_binding(
2659 KeyBinding::for_action_in(
2660 &ContinueWithBurnMode,
2661 &focus_handle,
2662 window,
2663 cx,
2664 )
2665 .map(|kb| kb.size(rems_from_px(10.))),
2666 )
2667 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
2668 .on_click(cx.listener(|this, _, window, cx| {
2669 this.thread.update(cx, |active_thread, cx| {
2670 active_thread.thread().update(cx, |thread, _cx| {
2671 thread.set_completion_mode(CompletionMode::Max);
2672 });
2673 });
2674 this.continue_conversation(window, cx);
2675 })),
2676 )
2677 }),
2678 );
2679
2680 Some(div().px_2().pb_2().child(banner).into_any_element())
2681 }
2682
2683 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2684 let last_error = self.thread.read(cx).last_error()?;
2685
2686 Some(
2687 div()
2688 .absolute()
2689 .right_3()
2690 .bottom_12()
2691 .max_w_96()
2692 .py_2()
2693 .px_3()
2694 .elevation_2(cx)
2695 .occlude()
2696 .child(match last_error {
2697 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2698 ThreadError::ModelRequestLimitReached { plan } => {
2699 self.render_model_request_limit_reached_error(plan, cx)
2700 }
2701 ThreadError::Message { header, message } => {
2702 self.render_error_message(header, message, cx)
2703 }
2704 })
2705 .into_any(),
2706 )
2707 }
2708
2709 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2710 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.";
2711
2712 v_flex()
2713 .gap_0p5()
2714 .child(
2715 h_flex()
2716 .gap_1p5()
2717 .items_center()
2718 .child(Icon::new(IconName::XCircle).color(Color::Error))
2719 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2720 )
2721 .child(
2722 div()
2723 .id("error-message")
2724 .max_h_24()
2725 .overflow_y_scroll()
2726 .child(Label::new(ERROR_MESSAGE)),
2727 )
2728 .child(
2729 h_flex()
2730 .justify_end()
2731 .mt_1()
2732 .gap_1()
2733 .child(self.create_copy_button(ERROR_MESSAGE))
2734 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2735 |this, _, _, cx| {
2736 this.thread.update(cx, |this, _cx| {
2737 this.clear_last_error();
2738 });
2739
2740 cx.open_url(&zed_urls::account_url(cx));
2741 cx.notify();
2742 },
2743 )))
2744 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2745 |this, _, _, cx| {
2746 this.thread.update(cx, |this, _cx| {
2747 this.clear_last_error();
2748 });
2749
2750 cx.notify();
2751 },
2752 ))),
2753 )
2754 .into_any()
2755 }
2756
2757 fn render_model_request_limit_reached_error(
2758 &self,
2759 plan: Plan,
2760 cx: &mut Context<Self>,
2761 ) -> AnyElement {
2762 let error_message = match plan {
2763 Plan::ZedPro => {
2764 "Model request limit reached. Upgrade to usage-based billing for more requests."
2765 }
2766 Plan::ZedProTrial => {
2767 "Model request limit reached. Upgrade to Zed Pro for more requests."
2768 }
2769 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2770 };
2771 let call_to_action = match plan {
2772 Plan::ZedPro => "Upgrade to usage-based billing",
2773 Plan::ZedProTrial => "Upgrade to Zed Pro",
2774 Plan::Free => "Upgrade to Zed Pro",
2775 };
2776
2777 v_flex()
2778 .gap_0p5()
2779 .child(
2780 h_flex()
2781 .gap_1p5()
2782 .items_center()
2783 .child(Icon::new(IconName::XCircle).color(Color::Error))
2784 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2785 )
2786 .child(
2787 div()
2788 .id("error-message")
2789 .max_h_24()
2790 .overflow_y_scroll()
2791 .child(Label::new(error_message)),
2792 )
2793 .child(
2794 h_flex()
2795 .justify_end()
2796 .mt_1()
2797 .gap_1()
2798 .child(self.create_copy_button(error_message))
2799 .child(
2800 Button::new("subscribe", call_to_action).on_click(cx.listener(
2801 |this, _, _, cx| {
2802 this.thread.update(cx, |this, _cx| {
2803 this.clear_last_error();
2804 });
2805
2806 cx.open_url(&zed_urls::account_url(cx));
2807 cx.notify();
2808 },
2809 )),
2810 )
2811 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2812 |this, _, _, cx| {
2813 this.thread.update(cx, |this, _cx| {
2814 this.clear_last_error();
2815 });
2816
2817 cx.notify();
2818 },
2819 ))),
2820 )
2821 .into_any()
2822 }
2823
2824 fn render_error_message(
2825 &self,
2826 header: SharedString,
2827 message: SharedString,
2828 cx: &mut Context<Self>,
2829 ) -> AnyElement {
2830 let message_with_header = format!("{}\n{}", header, message);
2831 v_flex()
2832 .gap_0p5()
2833 .child(
2834 h_flex()
2835 .gap_1p5()
2836 .items_center()
2837 .child(Icon::new(IconName::XCircle).color(Color::Error))
2838 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2839 )
2840 .child(
2841 div()
2842 .id("error-message")
2843 .max_h_32()
2844 .overflow_y_scroll()
2845 .child(Label::new(message.clone())),
2846 )
2847 .child(
2848 h_flex()
2849 .justify_end()
2850 .mt_1()
2851 .gap_1()
2852 .child(self.create_copy_button(message_with_header))
2853 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2854 |this, _, _, cx| {
2855 this.thread.update(cx, |this, _cx| {
2856 this.clear_last_error();
2857 });
2858
2859 cx.notify();
2860 },
2861 ))),
2862 )
2863 .into_any()
2864 }
2865
2866 fn render_prompt_editor(
2867 &self,
2868 context_editor: &Entity<ContextEditor>,
2869 buffer_search_bar: &Entity<BufferSearchBar>,
2870 window: &mut Window,
2871 cx: &mut Context<Self>,
2872 ) -> Div {
2873 let mut registrar = buffer_search::DivRegistrar::new(
2874 |this, _, _cx| match &this.active_view {
2875 ActiveView::PromptEditor {
2876 buffer_search_bar, ..
2877 } => Some(buffer_search_bar.clone()),
2878 _ => None,
2879 },
2880 cx,
2881 );
2882 BufferSearchBar::register(&mut registrar);
2883 registrar
2884 .into_div()
2885 .size_full()
2886 .relative()
2887 .map(|parent| {
2888 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2889 if buffer_search_bar.is_dismissed() {
2890 return parent;
2891 }
2892 parent.child(
2893 div()
2894 .p(DynamicSpacing::Base08.rems(cx))
2895 .border_b_1()
2896 .border_color(cx.theme().colors().border_variant)
2897 .bg(cx.theme().colors().editor_background)
2898 .child(buffer_search_bar.render(window, cx)),
2899 )
2900 })
2901 })
2902 .child(context_editor.clone())
2903 .child(self.render_drag_target(cx))
2904 }
2905
2906 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2907 let is_local = self.project.read(cx).is_local();
2908 div()
2909 .invisible()
2910 .absolute()
2911 .top_0()
2912 .right_0()
2913 .bottom_0()
2914 .left_0()
2915 .bg(cx.theme().colors().drop_target_background)
2916 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2917 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2918 .when(is_local, |this| {
2919 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2920 })
2921 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2922 let item = tab.pane.read(cx).item_for_index(tab.ix);
2923 let project_paths = item
2924 .and_then(|item| item.project_path(cx))
2925 .into_iter()
2926 .collect::<Vec<_>>();
2927 this.handle_drop(project_paths, vec![], window, cx);
2928 }))
2929 .on_drop(
2930 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2931 let project_paths = selection
2932 .items()
2933 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2934 .collect::<Vec<_>>();
2935 this.handle_drop(project_paths, vec![], window, cx);
2936 }),
2937 )
2938 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2939 let tasks = paths
2940 .paths()
2941 .into_iter()
2942 .map(|path| {
2943 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2944 })
2945 .collect::<Vec<_>>();
2946 cx.spawn_in(window, async move |this, cx| {
2947 let mut paths = vec![];
2948 let mut added_worktrees = vec![];
2949 let opened_paths = futures::future::join_all(tasks).await;
2950 for entry in opened_paths {
2951 if let Some((worktree, project_path)) = entry.log_err() {
2952 added_worktrees.push(worktree);
2953 paths.push(project_path);
2954 }
2955 }
2956 this.update_in(cx, |this, window, cx| {
2957 this.handle_drop(paths, added_worktrees, window, cx);
2958 })
2959 .ok();
2960 })
2961 .detach();
2962 }))
2963 }
2964
2965 fn handle_drop(
2966 &mut self,
2967 paths: Vec<ProjectPath>,
2968 added_worktrees: Vec<Entity<Worktree>>,
2969 window: &mut Window,
2970 cx: &mut Context<Self>,
2971 ) {
2972 match &self.active_view {
2973 ActiveView::Thread { .. } => {
2974 let context_store = self.thread.read(cx).context_store().clone();
2975 context_store.update(cx, move |context_store, cx| {
2976 let mut tasks = Vec::new();
2977 for project_path in &paths {
2978 tasks.push(context_store.add_file_from_path(
2979 project_path.clone(),
2980 false,
2981 cx,
2982 ));
2983 }
2984 cx.background_spawn(async move {
2985 futures::future::join_all(tasks).await;
2986 // Need to hold onto the worktrees until they have already been used when
2987 // opening the buffers.
2988 drop(added_worktrees);
2989 })
2990 .detach();
2991 });
2992 }
2993 ActiveView::PromptEditor { context_editor, .. } => {
2994 context_editor.update(cx, |context_editor, cx| {
2995 ContextEditor::insert_dragged_files(
2996 context_editor,
2997 paths,
2998 added_worktrees,
2999 window,
3000 cx,
3001 );
3002 });
3003 }
3004 ActiveView::History | ActiveView::Configuration => {}
3005 }
3006 }
3007
3008 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3009 let message = message.into();
3010 IconButton::new("copy", IconName::Copy)
3011 .on_click(move |_, _, cx| {
3012 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3013 })
3014 .tooltip(Tooltip::text("Copy Error Message"))
3015 }
3016
3017 fn key_context(&self) -> KeyContext {
3018 let mut key_context = KeyContext::new_with_defaults();
3019 key_context.add("AgentPanel");
3020 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
3021 key_context.add("prompt_editor");
3022 }
3023 key_context
3024 }
3025}
3026
3027impl Render for AgentPanel {
3028 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3029 // WARNING: Changes to this element hierarchy can have
3030 // non-obvious implications to the layout of children.
3031 //
3032 // If you need to change it, please confirm:
3033 // - The message editor expands (cmd-option-esc) correctly
3034 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3035 // - Font size works as expected and can be changed with cmd-+/cmd-
3036 // - Scrolling in all views works as expected
3037 // - Files can be dropped into the panel
3038 let content = v_flex()
3039 .key_context(self.key_context())
3040 .justify_between()
3041 .size_full()
3042 .on_action(cx.listener(Self::cancel))
3043 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3044 this.new_thread(action, window, cx);
3045 }))
3046 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3047 this.open_history(window, cx);
3048 }))
3049 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
3050 this.open_configuration(window, cx);
3051 }))
3052 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3053 .on_action(cx.listener(Self::deploy_rules_library))
3054 .on_action(cx.listener(Self::open_agent_diff))
3055 .on_action(cx.listener(Self::go_back))
3056 .on_action(cx.listener(Self::toggle_navigation_menu))
3057 .on_action(cx.listener(Self::toggle_options_menu))
3058 .on_action(cx.listener(Self::increase_font_size))
3059 .on_action(cx.listener(Self::decrease_font_size))
3060 .on_action(cx.listener(Self::reset_font_size))
3061 .on_action(cx.listener(Self::toggle_zoom))
3062 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3063 this.continue_conversation(window, cx);
3064 }))
3065 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3066 this.thread.update(cx, |active_thread, cx| {
3067 active_thread.thread().update(cx, |thread, _cx| {
3068 thread.set_completion_mode(CompletionMode::Max);
3069 });
3070 });
3071 this.continue_conversation(window, cx);
3072 }))
3073 .child(self.render_toolbar(window, cx))
3074 .children(self.render_upsell(window, cx))
3075 .children(self.render_trial_end_upsell(window, cx))
3076 .map(|parent| match &self.active_view {
3077 ActiveView::Thread { .. } => parent
3078 .relative()
3079 .child(self.render_active_thread_or_empty_state(window, cx))
3080 .children(self.render_tool_use_limit_reached(window, cx))
3081 .child(h_flex().child(self.message_editor.clone()))
3082 .children(self.render_last_error(cx))
3083 .child(self.render_drag_target(cx)),
3084 ActiveView::History => parent.child(self.history.clone()),
3085 ActiveView::PromptEditor {
3086 context_editor,
3087 buffer_search_bar,
3088 ..
3089 } => parent.child(self.render_prompt_editor(
3090 context_editor,
3091 buffer_search_bar,
3092 window,
3093 cx,
3094 )),
3095 ActiveView::Configuration => parent.children(self.configuration.clone()),
3096 });
3097
3098 match self.active_view.which_font_size_used() {
3099 WhichFontSize::AgentFont => {
3100 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3101 .size_full()
3102 .child(content)
3103 .into_any()
3104 }
3105 _ => content.into_any(),
3106 }
3107 }
3108}
3109
3110struct PromptLibraryInlineAssist {
3111 workspace: WeakEntity<Workspace>,
3112}
3113
3114impl PromptLibraryInlineAssist {
3115 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3116 Self { workspace }
3117 }
3118}
3119
3120impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3121 fn assist(
3122 &self,
3123 prompt_editor: &Entity<Editor>,
3124 initial_prompt: Option<String>,
3125 window: &mut Window,
3126 cx: &mut Context<RulesLibrary>,
3127 ) {
3128 InlineAssistant::update_global(cx, |assistant, cx| {
3129 let Some(project) = self
3130 .workspace
3131 .upgrade()
3132 .map(|workspace| workspace.read(cx).project().downgrade())
3133 else {
3134 return;
3135 };
3136 let prompt_store = None;
3137 let thread_store = None;
3138 let text_thread_store = None;
3139 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3140 assistant.assist(
3141 &prompt_editor,
3142 self.workspace.clone(),
3143 context_store,
3144 project,
3145 prompt_store,
3146 thread_store,
3147 text_thread_store,
3148 initial_prompt,
3149 window,
3150 cx,
3151 )
3152 })
3153 }
3154
3155 fn focus_agent_panel(
3156 &self,
3157 workspace: &mut Workspace,
3158 window: &mut Window,
3159 cx: &mut Context<Workspace>,
3160 ) -> bool {
3161 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3162 }
3163}
3164
3165pub struct ConcreteAssistantPanelDelegate;
3166
3167impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3168 fn active_context_editor(
3169 &self,
3170 workspace: &mut Workspace,
3171 _window: &mut Window,
3172 cx: &mut Context<Workspace>,
3173 ) -> Option<Entity<ContextEditor>> {
3174 let panel = workspace.panel::<AgentPanel>(cx)?;
3175 panel.read(cx).active_context_editor()
3176 }
3177
3178 fn open_saved_context(
3179 &self,
3180 workspace: &mut Workspace,
3181 path: Arc<Path>,
3182 window: &mut Window,
3183 cx: &mut Context<Workspace>,
3184 ) -> Task<Result<()>> {
3185 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3186 return Task::ready(Err(anyhow!("Agent panel not found")));
3187 };
3188
3189 panel.update(cx, |panel, cx| {
3190 panel.open_saved_prompt_editor(path, window, cx)
3191 })
3192 }
3193
3194 fn open_remote_context(
3195 &self,
3196 _workspace: &mut Workspace,
3197 _context_id: assistant_context_editor::ContextId,
3198 _window: &mut Window,
3199 _cx: &mut Context<Workspace>,
3200 ) -> Task<Result<Entity<ContextEditor>>> {
3201 Task::ready(Err(anyhow!("opening remote context not implemented")))
3202 }
3203
3204 fn quote_selection(
3205 &self,
3206 workspace: &mut Workspace,
3207 selection_ranges: Vec<Range<Anchor>>,
3208 buffer: Entity<MultiBuffer>,
3209 window: &mut Window,
3210 cx: &mut Context<Workspace>,
3211 ) {
3212 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3213 return;
3214 };
3215
3216 if !panel.focus_handle(cx).contains_focused(window, cx) {
3217 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3218 }
3219
3220 panel.update(cx, |_, cx| {
3221 // Wait to create a new context until the workspace is no longer
3222 // being updated.
3223 cx.defer_in(window, move |panel, window, cx| {
3224 if panel.has_active_thread() {
3225 panel.message_editor.update(cx, |message_editor, cx| {
3226 message_editor.context_store().update(cx, |store, cx| {
3227 let buffer = buffer.read(cx);
3228 let selection_ranges = selection_ranges
3229 .into_iter()
3230 .flat_map(|range| {
3231 let (start_buffer, start) =
3232 buffer.text_anchor_for_position(range.start, cx)?;
3233 let (end_buffer, end) =
3234 buffer.text_anchor_for_position(range.end, cx)?;
3235 if start_buffer != end_buffer {
3236 return None;
3237 }
3238 Some((start_buffer, start..end))
3239 })
3240 .collect::<Vec<_>>();
3241
3242 for (buffer, range) in selection_ranges {
3243 store.add_selection(buffer, range, cx);
3244 }
3245 })
3246 })
3247 } else if let Some(context_editor) = panel.active_context_editor() {
3248 let snapshot = buffer.read(cx).snapshot(cx);
3249 let selection_ranges = selection_ranges
3250 .into_iter()
3251 .map(|range| range.to_point(&snapshot))
3252 .collect::<Vec<_>>();
3253
3254 context_editor.update(cx, |context_editor, cx| {
3255 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3256 });
3257 }
3258 });
3259 });
3260 }
3261}
3262
3263struct Upsell;
3264
3265impl Dismissable for Upsell {
3266 const KEY: &'static str = "dismissed-trial-upsell";
3267}
3268
3269struct TrialEndUpsell;
3270
3271impl Dismissable for TrialEndUpsell {
3272 const KEY: &'static str = "dismissed-trial-end-upsell";
3273}