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