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