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 {
1403 update_settings_file::<AgentSettings>(
1404 self.fs.clone(),
1405 cx,
1406 move |settings, _| settings.set_model(model),
1407 );
1408 }
1409
1410 self.new_thread(&NewThread::default(), window, cx);
1411 if let Some((thread, model)) =
1412 self.active_thread(cx).zip(provider.default_model(cx))
1413 {
1414 thread.update(cx, |thread, cx| {
1415 thread.set_configured_model(
1416 Some(ConfiguredModel {
1417 provider: provider.clone(),
1418 model,
1419 }),
1420 cx,
1421 );
1422 });
1423 }
1424 }
1425 }
1426 }
1427
1428 pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
1429 match &self.active_view {
1430 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1431 _ => None,
1432 }
1433 }
1434
1435 pub(crate) fn delete_thread(
1436 &mut self,
1437 thread_id: &ThreadId,
1438 cx: &mut Context<Self>,
1439 ) -> Task<Result<()>> {
1440 self.thread_store
1441 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1442 }
1443
1444 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1445 let ActiveView::Thread { thread, .. } = &self.active_view else {
1446 return;
1447 };
1448
1449 let thread_state = thread.read(cx).thread().read(cx);
1450 if !thread_state.tool_use_limit_reached() {
1451 return;
1452 }
1453
1454 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1455 if let Some(model) = model {
1456 thread.update(cx, |active_thread, cx| {
1457 active_thread.thread().update(cx, |thread, cx| {
1458 thread.insert_invisible_continue_message(cx);
1459 thread.advance_prompt_id();
1460 thread.send_to_model(
1461 model,
1462 CompletionIntent::UserPrompt,
1463 Some(window.window_handle()),
1464 cx,
1465 );
1466 });
1467 });
1468 } else {
1469 log::warn!("No configured model available for continuation");
1470 }
1471 }
1472
1473 fn toggle_burn_mode(
1474 &mut self,
1475 _: &ToggleBurnMode,
1476 _window: &mut Window,
1477 cx: &mut Context<Self>,
1478 ) {
1479 let ActiveView::Thread { thread, .. } = &self.active_view else {
1480 return;
1481 };
1482
1483 thread.update(cx, |active_thread, cx| {
1484 active_thread.thread().update(cx, |thread, _cx| {
1485 let current_mode = thread.completion_mode();
1486
1487 thread.set_completion_mode(match current_mode {
1488 CompletionMode::Burn => CompletionMode::Normal,
1489 CompletionMode::Normal => CompletionMode::Burn,
1490 });
1491 });
1492 });
1493 }
1494
1495 pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
1496 match &self.active_view {
1497 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1498 _ => None,
1499 }
1500 }
1501
1502 pub(crate) fn delete_context(
1503 &mut self,
1504 path: Arc<Path>,
1505 cx: &mut Context<Self>,
1506 ) -> Task<Result<()>> {
1507 self.context_store
1508 .update(cx, |this, cx| this.delete_local_context(path, cx))
1509 }
1510
1511 fn set_active_view(
1512 &mut self,
1513 new_view: ActiveView,
1514 window: &mut Window,
1515 cx: &mut Context<Self>,
1516 ) {
1517 let current_is_history = matches!(self.active_view, ActiveView::History);
1518 let new_is_history = matches!(new_view, ActiveView::History);
1519
1520 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1521 let new_is_config = matches!(new_view, ActiveView::Configuration);
1522
1523 let current_is_special = current_is_history || current_is_config;
1524 let new_is_special = new_is_history || new_is_config;
1525
1526 match &self.active_view {
1527 ActiveView::Thread { thread, .. } => {
1528 let thread = thread.read(cx);
1529 if thread.is_empty() {
1530 let id = thread.thread().read(cx).id().clone();
1531 self.history_store.update(cx, |store, cx| {
1532 store.remove_recently_opened_thread(id, cx);
1533 });
1534 }
1535 }
1536 _ => {}
1537 }
1538
1539 match &new_view {
1540 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1541 let id = thread.read(cx).thread().read(cx).id().clone();
1542 store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
1543 }),
1544 ActiveView::TextThread { context_editor, .. } => {
1545 self.history_store.update(cx, |store, cx| {
1546 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1547 store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
1548 }
1549 })
1550 }
1551 ActiveView::ExternalAgentThread { .. } => {}
1552 ActiveView::History | ActiveView::Configuration => {}
1553 }
1554
1555 if current_is_special && !new_is_special {
1556 self.active_view = new_view;
1557 } else if !current_is_special && new_is_special {
1558 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1559 } else {
1560 if !new_is_special {
1561 self.previous_view = None;
1562 }
1563 self.active_view = new_view;
1564 }
1565
1566 self.focus_handle(cx).focus(window);
1567 }
1568
1569 fn populate_recently_opened_menu_section(
1570 mut menu: ContextMenu,
1571 panel: Entity<Self>,
1572 cx: &mut Context<ContextMenu>,
1573 ) -> ContextMenu {
1574 let entries = panel
1575 .read(cx)
1576 .history_store
1577 .read(cx)
1578 .recently_opened_entries(cx);
1579
1580 if entries.is_empty() {
1581 return menu;
1582 }
1583
1584 menu = menu.header("Recently Opened");
1585
1586 for entry in entries {
1587 let title = entry.title().clone();
1588 let id = entry.id();
1589
1590 menu = menu.entry_with_end_slot_on_hover(
1591 title,
1592 None,
1593 {
1594 let panel = panel.downgrade();
1595 let id = id.clone();
1596 move |window, cx| {
1597 let id = id.clone();
1598 panel
1599 .update(cx, move |this, cx| match id {
1600 HistoryEntryId::Thread(id) => this
1601 .open_thread_by_id(&id, window, cx)
1602 .detach_and_log_err(cx),
1603 HistoryEntryId::Context(path) => this
1604 .open_saved_prompt_editor(path.clone(), window, cx)
1605 .detach_and_log_err(cx),
1606 })
1607 .ok();
1608 }
1609 },
1610 IconName::Close,
1611 "Close Entry".into(),
1612 {
1613 let panel = panel.downgrade();
1614 let id = id.clone();
1615 move |_window, cx| {
1616 panel
1617 .update(cx, |this, cx| {
1618 this.history_store.update(cx, |history_store, cx| {
1619 history_store.remove_recently_opened_entry(&id, cx);
1620 });
1621 })
1622 .ok();
1623 }
1624 },
1625 );
1626 }
1627
1628 menu = menu.separator();
1629
1630 menu
1631 }
1632
1633 pub fn set_selected_agent(
1634 &mut self,
1635 agent: AgentType,
1636 window: &mut Window,
1637 cx: &mut Context<Self>,
1638 ) {
1639 if self.selected_agent != agent {
1640 self.selected_agent = agent;
1641 self.serialize(cx);
1642 self.new_agent_thread(agent, window, cx);
1643 }
1644 }
1645
1646 pub fn selected_agent(&self) -> AgentType {
1647 self.selected_agent
1648 }
1649
1650 pub fn new_agent_thread(
1651 &mut self,
1652 agent: AgentType,
1653 window: &mut Window,
1654 cx: &mut Context<Self>,
1655 ) {
1656 match agent {
1657 AgentType::Zed => {
1658 window.dispatch_action(
1659 NewThread {
1660 from_thread_id: None,
1661 }
1662 .boxed_clone(),
1663 cx,
1664 );
1665 }
1666 AgentType::TextThread => {
1667 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1668 }
1669 AgentType::NativeAgent => {
1670 self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx)
1671 }
1672 AgentType::Gemini => {
1673 self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx)
1674 }
1675 AgentType::ClaudeCode => {
1676 self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx)
1677 }
1678 }
1679 }
1680}
1681
1682impl Focusable for AgentPanel {
1683 fn focus_handle(&self, cx: &App) -> FocusHandle {
1684 match &self.active_view {
1685 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1686 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1687 ActiveView::History => self.history.focus_handle(cx),
1688 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1689 ActiveView::Configuration => {
1690 if let Some(configuration) = self.configuration.as_ref() {
1691 configuration.focus_handle(cx)
1692 } else {
1693 cx.focus_handle()
1694 }
1695 }
1696 }
1697 }
1698}
1699
1700fn agent_panel_dock_position(cx: &App) -> DockPosition {
1701 match AgentSettings::get_global(cx).dock {
1702 AgentDockPosition::Left => DockPosition::Left,
1703 AgentDockPosition::Bottom => DockPosition::Bottom,
1704 AgentDockPosition::Right => DockPosition::Right,
1705 }
1706}
1707
1708impl EventEmitter<PanelEvent> for AgentPanel {}
1709
1710impl Panel for AgentPanel {
1711 fn persistent_name() -> &'static str {
1712 "AgentPanel"
1713 }
1714
1715 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1716 agent_panel_dock_position(cx)
1717 }
1718
1719 fn position_is_valid(&self, position: DockPosition) -> bool {
1720 position != DockPosition::Bottom
1721 }
1722
1723 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1724 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1725 let dock = match position {
1726 DockPosition::Left => AgentDockPosition::Left,
1727 DockPosition::Bottom => AgentDockPosition::Bottom,
1728 DockPosition::Right => AgentDockPosition::Right,
1729 };
1730 settings.set_dock(dock);
1731 });
1732 }
1733
1734 fn size(&self, window: &Window, cx: &App) -> Pixels {
1735 let settings = AgentSettings::get_global(cx);
1736 match self.position(window, cx) {
1737 DockPosition::Left | DockPosition::Right => {
1738 self.width.unwrap_or(settings.default_width)
1739 }
1740 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1741 }
1742 }
1743
1744 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1745 match self.position(window, cx) {
1746 DockPosition::Left | DockPosition::Right => self.width = size,
1747 DockPosition::Bottom => self.height = size,
1748 }
1749 self.serialize(cx);
1750 cx.notify();
1751 }
1752
1753 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1754
1755 fn remote_id() -> Option<proto::PanelId> {
1756 Some(proto::PanelId::AssistantPanel)
1757 }
1758
1759 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1760 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1761 }
1762
1763 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1764 Some("Agent Panel")
1765 }
1766
1767 fn toggle_action(&self) -> Box<dyn Action> {
1768 Box::new(ToggleFocus)
1769 }
1770
1771 fn activation_priority(&self) -> u32 {
1772 3
1773 }
1774
1775 fn enabled(&self, cx: &App) -> bool {
1776 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
1777 }
1778
1779 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1780 self.zoomed
1781 }
1782
1783 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1784 self.zoomed = zoomed;
1785 cx.notify();
1786 }
1787}
1788
1789impl AgentPanel {
1790 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1791 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1792
1793 let content = match &self.active_view {
1794 ActiveView::Thread {
1795 thread: active_thread,
1796 change_title_editor,
1797 ..
1798 } => {
1799 let state = {
1800 let active_thread = active_thread.read(cx);
1801 if active_thread.is_empty() {
1802 &ThreadSummary::Pending
1803 } else {
1804 active_thread.summary(cx)
1805 }
1806 };
1807
1808 match state {
1809 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1810 .truncate()
1811 .into_any_element(),
1812 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1813 .truncate()
1814 .into_any_element(),
1815 ThreadSummary::Ready(_) => div()
1816 .w_full()
1817 .child(change_title_editor.clone())
1818 .into_any_element(),
1819 ThreadSummary::Error => h_flex()
1820 .w_full()
1821 .child(change_title_editor.clone())
1822 .child(
1823 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1824 .icon_size(IconSize::Small)
1825 .on_click({
1826 let active_thread = active_thread.clone();
1827 move |_, _window, cx| {
1828 active_thread.update(cx, |thread, cx| {
1829 thread.regenerate_summary(cx);
1830 });
1831 }
1832 })
1833 .tooltip(move |_window, cx| {
1834 cx.new(|_| {
1835 Tooltip::new("Failed to generate title")
1836 .meta("Click to try again")
1837 })
1838 .into()
1839 }),
1840 )
1841 .into_any_element(),
1842 }
1843 }
1844 ActiveView::ExternalAgentThread { thread_view } => {
1845 Label::new(thread_view.read(cx).title(cx))
1846 .truncate()
1847 .into_any_element()
1848 }
1849 ActiveView::TextThread {
1850 title_editor,
1851 context_editor,
1852 ..
1853 } => {
1854 let summary = context_editor.read(cx).context().read(cx).summary();
1855
1856 match summary {
1857 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1858 .truncate()
1859 .into_any_element(),
1860 ContextSummary::Content(summary) => {
1861 if summary.done {
1862 div()
1863 .w_full()
1864 .child(title_editor.clone())
1865 .into_any_element()
1866 } else {
1867 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1868 .truncate()
1869 .into_any_element()
1870 }
1871 }
1872 ContextSummary::Error => h_flex()
1873 .w_full()
1874 .child(title_editor.clone())
1875 .child(
1876 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1877 .icon_size(IconSize::Small)
1878 .on_click({
1879 let context_editor = context_editor.clone();
1880 move |_, _window, cx| {
1881 context_editor.update(cx, |context_editor, cx| {
1882 context_editor.regenerate_summary(cx);
1883 });
1884 }
1885 })
1886 .tooltip(move |_window, cx| {
1887 cx.new(|_| {
1888 Tooltip::new("Failed to generate title")
1889 .meta("Click to try again")
1890 })
1891 .into()
1892 }),
1893 )
1894 .into_any_element(),
1895 }
1896 }
1897 ActiveView::History => Label::new("History").truncate().into_any_element(),
1898 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1899 };
1900
1901 h_flex()
1902 .key_context("TitleEditor")
1903 .id("TitleEditor")
1904 .flex_grow()
1905 .w_full()
1906 .max_w_full()
1907 .overflow_x_scroll()
1908 .child(content)
1909 .into_any()
1910 }
1911
1912 fn render_panel_options_menu(
1913 &self,
1914 window: &mut Window,
1915 cx: &mut Context<Self>,
1916 ) -> impl IntoElement {
1917 let user_store = self.user_store.read(cx);
1918 let usage = user_store.model_request_usage();
1919 let account_url = zed_urls::account_url(cx);
1920
1921 let focus_handle = self.focus_handle(cx);
1922
1923 let full_screen_label = if self.is_zoomed(window, cx) {
1924 "Disable Full Screen"
1925 } else {
1926 "Enable Full Screen"
1927 };
1928
1929 PopoverMenu::new("agent-options-menu")
1930 .trigger_with_tooltip(
1931 IconButton::new("agent-options-menu", IconName::Ellipsis)
1932 .icon_size(IconSize::Small),
1933 {
1934 let focus_handle = focus_handle.clone();
1935 move |window, cx| {
1936 Tooltip::for_action_in(
1937 "Toggle Agent Menu",
1938 &ToggleOptionsMenu,
1939 &focus_handle,
1940 window,
1941 cx,
1942 )
1943 }
1944 },
1945 )
1946 .anchor(Corner::TopRight)
1947 .with_handle(self.agent_panel_menu_handle.clone())
1948 .menu({
1949 let focus_handle = focus_handle.clone();
1950 move |window, cx| {
1951 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
1952 menu = menu.context(focus_handle.clone());
1953 if let Some(usage) = usage {
1954 menu = menu
1955 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1956 .custom_entry(
1957 move |_window, cx| {
1958 let used_percentage = match usage.limit {
1959 UsageLimit::Limited(limit) => {
1960 Some((usage.amount as f32 / limit as f32) * 100.)
1961 }
1962 UsageLimit::Unlimited => None,
1963 };
1964
1965 h_flex()
1966 .flex_1()
1967 .gap_1p5()
1968 .children(used_percentage.map(|percent| {
1969 ProgressBar::new("usage", percent, 100., cx)
1970 }))
1971 .child(
1972 Label::new(match usage.limit {
1973 UsageLimit::Limited(limit) => {
1974 format!("{} / {limit}", usage.amount)
1975 }
1976 UsageLimit::Unlimited => {
1977 format!("{} / ∞", usage.amount)
1978 }
1979 })
1980 .size(LabelSize::Small)
1981 .color(Color::Muted),
1982 )
1983 .into_any_element()
1984 },
1985 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1986 )
1987 .separator()
1988 }
1989
1990 menu = menu
1991 .header("MCP Servers")
1992 .action(
1993 "View Server Extensions",
1994 Box::new(zed_actions::Extensions {
1995 category_filter: Some(
1996 zed_actions::ExtensionCategoryFilter::ContextServers,
1997 ),
1998 id: None,
1999 }),
2000 )
2001 .action("Add Custom Server…", Box::new(AddContextServer))
2002 .separator();
2003
2004 menu = menu
2005 .action("Rules…", Box::new(OpenRulesLibrary::default()))
2006 .action("Settings", Box::new(OpenSettings))
2007 .separator()
2008 .action(full_screen_label, Box::new(ToggleZoom));
2009 menu
2010 }))
2011 }
2012 })
2013 }
2014
2015 fn render_recent_entries_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2016 let focus_handle = self.focus_handle(cx);
2017
2018 PopoverMenu::new("agent-nav-menu")
2019 .trigger_with_tooltip(
2020 IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small),
2021 {
2022 let focus_handle = focus_handle.clone();
2023 move |window, cx| {
2024 Tooltip::for_action_in(
2025 "Toggle Recent Threads",
2026 &ToggleNavigationMenu,
2027 &focus_handle,
2028 window,
2029 cx,
2030 )
2031 }
2032 },
2033 )
2034 .anchor(Corner::TopLeft)
2035 .with_handle(self.assistant_navigation_menu_handle.clone())
2036 .menu({
2037 let menu = self.assistant_navigation_menu.clone();
2038 move |window, cx| {
2039 if let Some(menu) = menu.as_ref() {
2040 menu.update(cx, |_, cx| {
2041 cx.defer_in(window, |menu, window, cx| {
2042 menu.rebuild(window, cx);
2043 });
2044 })
2045 }
2046 menu.clone()
2047 }
2048 })
2049 }
2050
2051 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2052 let focus_handle = self.focus_handle(cx);
2053
2054 IconButton::new("go-back", IconName::ArrowLeft)
2055 .icon_size(IconSize::Small)
2056 .on_click(cx.listener(|this, _, window, cx| {
2057 this.go_back(&workspace::GoBack, window, cx);
2058 }))
2059 .tooltip({
2060 let focus_handle = focus_handle.clone();
2061
2062 move |window, cx| {
2063 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
2064 }
2065 })
2066 }
2067
2068 fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2069 let focus_handle = self.focus_handle(cx);
2070
2071 let active_thread = match &self.active_view {
2072 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2073 ActiveView::ExternalAgentThread { .. }
2074 | ActiveView::TextThread { .. }
2075 | ActiveView::History
2076 | ActiveView::Configuration => None,
2077 };
2078
2079 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2080 .trigger_with_tooltip(
2081 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2082 Tooltip::text("New Thread…"),
2083 )
2084 .anchor(Corner::TopRight)
2085 .with_handle(self.new_thread_menu_handle.clone())
2086 .menu({
2087 let focus_handle = focus_handle.clone();
2088 move |window, cx| {
2089 let active_thread = active_thread.clone();
2090 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2091 menu = menu
2092 .context(focus_handle.clone())
2093 .when_some(active_thread, |this, active_thread| {
2094 let thread = active_thread.read(cx);
2095
2096 if !thread.is_empty() {
2097 let thread_id = thread.id().clone();
2098 this.item(
2099 ContextMenuEntry::new("New From Summary")
2100 .icon(IconName::ThreadFromSummary)
2101 .icon_color(Color::Muted)
2102 .handler(move |window, cx| {
2103 window.dispatch_action(
2104 Box::new(NewThread {
2105 from_thread_id: Some(thread_id.clone()),
2106 }),
2107 cx,
2108 );
2109 }),
2110 )
2111 } else {
2112 this
2113 }
2114 })
2115 .item(
2116 ContextMenuEntry::new("New Thread")
2117 .icon(IconName::Thread)
2118 .icon_color(Color::Muted)
2119 .action(NewThread::default().boxed_clone())
2120 .handler(move |window, cx| {
2121 window.dispatch_action(
2122 NewThread::default().boxed_clone(),
2123 cx,
2124 );
2125 }),
2126 )
2127 .item(
2128 ContextMenuEntry::new("New Text Thread")
2129 .icon(IconName::TextThread)
2130 .icon_color(Color::Muted)
2131 .action(NewTextThread.boxed_clone())
2132 .handler(move |window, cx| {
2133 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2134 }),
2135 );
2136 menu
2137 }))
2138 }
2139 });
2140
2141 h_flex()
2142 .id("assistant-toolbar")
2143 .h(Tab::container_height(cx))
2144 .max_w_full()
2145 .flex_none()
2146 .justify_between()
2147 .gap_2()
2148 .bg(cx.theme().colors().tab_bar_background)
2149 .border_b_1()
2150 .border_color(cx.theme().colors().border)
2151 .child(
2152 h_flex()
2153 .size_full()
2154 .pl_1()
2155 .gap_1()
2156 .child(match &self.active_view {
2157 ActiveView::History | ActiveView::Configuration => div()
2158 .pl(DynamicSpacing::Base04.rems(cx))
2159 .child(self.render_toolbar_back_button(cx))
2160 .into_any_element(),
2161 _ => self.render_recent_entries_menu(cx).into_any_element(),
2162 })
2163 .child(self.render_title_view(window, cx)),
2164 )
2165 .child(
2166 h_flex()
2167 .h_full()
2168 .gap_2()
2169 .children(self.render_token_count(cx))
2170 .child(
2171 h_flex()
2172 .h_full()
2173 .gap(DynamicSpacing::Base02.rems(cx))
2174 .px(DynamicSpacing::Base08.rems(cx))
2175 .border_l_1()
2176 .border_color(cx.theme().colors().border)
2177 .child(new_thread_menu)
2178 .child(self.render_panel_options_menu(window, cx)),
2179 ),
2180 )
2181 }
2182
2183 fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2184 let focus_handle = self.focus_handle(cx);
2185
2186 let active_thread = match &self.active_view {
2187 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2188 ActiveView::ExternalAgentThread { .. }
2189 | ActiveView::TextThread { .. }
2190 | ActiveView::History
2191 | ActiveView::Configuration => None,
2192 };
2193
2194 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2195 .trigger_with_tooltip(
2196 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2197 {
2198 let focus_handle = focus_handle.clone();
2199 move |window, cx| {
2200 Tooltip::for_action_in(
2201 "New…",
2202 &ToggleNewThreadMenu,
2203 &focus_handle,
2204 window,
2205 cx,
2206 )
2207 }
2208 },
2209 )
2210 .anchor(Corner::TopLeft)
2211 .with_handle(self.new_thread_menu_handle.clone())
2212 .menu({
2213 let focus_handle = focus_handle.clone();
2214 let workspace = self.workspace.clone();
2215
2216 move |window, cx| {
2217 let active_thread = active_thread.clone();
2218 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2219 menu = menu
2220 .context(focus_handle.clone())
2221 .header("Zed Agent")
2222 .when_some(active_thread, |this, active_thread| {
2223 let thread = active_thread.read(cx);
2224
2225 if !thread.is_empty() {
2226 let thread_id = thread.id().clone();
2227 this.item(
2228 ContextMenuEntry::new("New From Summary")
2229 .icon(IconName::ThreadFromSummary)
2230 .icon_color(Color::Muted)
2231 .handler(move |window, cx| {
2232 window.dispatch_action(
2233 Box::new(NewThread {
2234 from_thread_id: Some(thread_id.clone()),
2235 }),
2236 cx,
2237 );
2238 }),
2239 )
2240 } else {
2241 this
2242 }
2243 })
2244 .item(
2245 ContextMenuEntry::new("New Thread")
2246 .icon(IconName::Thread)
2247 .icon_color(Color::Muted)
2248 .action(NewThread::default().boxed_clone())
2249 .handler({
2250 let workspace = workspace.clone();
2251 move |window, cx| {
2252 if let Some(workspace) = workspace.upgrade() {
2253 workspace.update(cx, |workspace, cx| {
2254 if let Some(panel) =
2255 workspace.panel::<AgentPanel>(cx)
2256 {
2257 panel.update(cx, |panel, cx| {
2258 panel.set_selected_agent(
2259 AgentType::Zed,
2260 window,
2261 cx,
2262 );
2263 });
2264 }
2265 });
2266 }
2267 }
2268 }),
2269 )
2270 .item(
2271 ContextMenuEntry::new("New Text Thread")
2272 .icon(IconName::TextThread)
2273 .icon_color(Color::Muted)
2274 .action(NewTextThread.boxed_clone())
2275 .handler({
2276 let workspace = workspace.clone();
2277 move |window, cx| {
2278 if let Some(workspace) = workspace.upgrade() {
2279 workspace.update(cx, |workspace, cx| {
2280 if let Some(panel) =
2281 workspace.panel::<AgentPanel>(cx)
2282 {
2283 panel.update(cx, |panel, cx| {
2284 panel.set_selected_agent(
2285 AgentType::TextThread,
2286 window,
2287 cx,
2288 );
2289 });
2290 }
2291 });
2292 }
2293 }
2294 }),
2295 )
2296 .item(
2297 ContextMenuEntry::new("New Native Agent Thread")
2298 .icon(IconName::ZedAssistant)
2299 .icon_color(Color::Muted)
2300 .handler({
2301 let workspace = workspace.clone();
2302 move |window, cx| {
2303 if let Some(workspace) = workspace.upgrade() {
2304 workspace.update(cx, |workspace, cx| {
2305 if let Some(panel) =
2306 workspace.panel::<AgentPanel>(cx)
2307 {
2308 panel.update(cx, |panel, cx| {
2309 panel.set_selected_agent(
2310 AgentType::NativeAgent,
2311 window,
2312 cx,
2313 );
2314 });
2315 }
2316 });
2317 }
2318 }
2319 }),
2320 )
2321 .separator()
2322 .header("External Agents")
2323 .item(
2324 ContextMenuEntry::new("New Gemini Thread")
2325 .icon(IconName::AiGemini)
2326 .icon_color(Color::Muted)
2327 .handler({
2328 let workspace = workspace.clone();
2329 move |window, cx| {
2330 if let Some(workspace) = workspace.upgrade() {
2331 workspace.update(cx, |workspace, cx| {
2332 if let Some(panel) =
2333 workspace.panel::<AgentPanel>(cx)
2334 {
2335 panel.update(cx, |panel, cx| {
2336 panel.set_selected_agent(
2337 AgentType::Gemini,
2338 window,
2339 cx,
2340 );
2341 });
2342 }
2343 });
2344 }
2345 }
2346 }),
2347 )
2348 .item(
2349 ContextMenuEntry::new("New Claude Code Thread")
2350 .icon(IconName::AiClaude)
2351 .icon_color(Color::Muted)
2352 .handler({
2353 let workspace = workspace.clone();
2354 move |window, cx| {
2355 if let Some(workspace) = workspace.upgrade() {
2356 workspace.update(cx, |workspace, cx| {
2357 if let Some(panel) =
2358 workspace.panel::<AgentPanel>(cx)
2359 {
2360 panel.update(cx, |panel, cx| {
2361 panel.set_selected_agent(
2362 AgentType::ClaudeCode,
2363 window,
2364 cx,
2365 );
2366 });
2367 }
2368 });
2369 }
2370 }
2371 }),
2372 );
2373 menu
2374 }))
2375 }
2376 });
2377
2378 let selected_agent_label = self.selected_agent.label().into();
2379 let selected_agent = div()
2380 .id("selected_agent_icon")
2381 .px(DynamicSpacing::Base02.rems(cx))
2382 .child(Icon::new(self.selected_agent.icon()).color(Color::Muted))
2383 .tooltip(move |window, cx| {
2384 Tooltip::with_meta(
2385 selected_agent_label.clone(),
2386 None,
2387 "Selected Agent",
2388 window,
2389 cx,
2390 )
2391 })
2392 .into_any_element();
2393
2394 h_flex()
2395 .id("agent-panel-toolbar")
2396 .h(Tab::container_height(cx))
2397 .max_w_full()
2398 .flex_none()
2399 .justify_between()
2400 .gap_2()
2401 .bg(cx.theme().colors().tab_bar_background)
2402 .border_b_1()
2403 .border_color(cx.theme().colors().border)
2404 .child(
2405 h_flex()
2406 .size_full()
2407 .gap(DynamicSpacing::Base04.rems(cx))
2408 .pl(DynamicSpacing::Base04.rems(cx))
2409 .child(match &self.active_view {
2410 ActiveView::History | ActiveView::Configuration => {
2411 self.render_toolbar_back_button(cx).into_any_element()
2412 }
2413 _ => h_flex()
2414 .gap_1()
2415 .child(self.render_recent_entries_menu(cx))
2416 .child(Divider::vertical())
2417 .child(selected_agent)
2418 .into_any_element(),
2419 })
2420 .child(self.render_title_view(window, cx)),
2421 )
2422 .child(
2423 h_flex()
2424 .h_full()
2425 .gap_2()
2426 .children(self.render_token_count(cx))
2427 .child(
2428 h_flex()
2429 .h_full()
2430 .gap(DynamicSpacing::Base02.rems(cx))
2431 .pl(DynamicSpacing::Base04.rems(cx))
2432 .pr(DynamicSpacing::Base06.rems(cx))
2433 .border_l_1()
2434 .border_color(cx.theme().colors().border)
2435 .child(new_thread_menu)
2436 .child(self.render_panel_options_menu(window, cx)),
2437 ),
2438 )
2439 }
2440
2441 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2442 if cx.has_flag::<feature_flags::AcpFeatureFlag>() {
2443 self.render_toolbar_new(window, cx).into_any_element()
2444 } else {
2445 self.render_toolbar_old(window, cx).into_any_element()
2446 }
2447 }
2448
2449 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
2450 match &self.active_view {
2451 ActiveView::Thread {
2452 thread,
2453 message_editor,
2454 ..
2455 } => {
2456 let active_thread = thread.read(cx);
2457 let message_editor = message_editor.read(cx);
2458
2459 let editor_empty = message_editor.is_editor_fully_empty(cx);
2460
2461 if active_thread.is_empty() && editor_empty {
2462 return None;
2463 }
2464
2465 let thread = active_thread.thread().read(cx);
2466 let is_generating = thread.is_generating();
2467 let conversation_token_usage = thread.total_token_usage()?;
2468
2469 let (total_token_usage, is_estimating) =
2470 if let Some((editing_message_id, unsent_tokens)) =
2471 active_thread.editing_message_id()
2472 {
2473 let combined = thread
2474 .token_usage_up_to_message(editing_message_id)
2475 .add(unsent_tokens);
2476
2477 (combined, unsent_tokens > 0)
2478 } else {
2479 let unsent_tokens =
2480 message_editor.last_estimated_token_count().unwrap_or(0);
2481 let combined = conversation_token_usage.add(unsent_tokens);
2482
2483 (combined, unsent_tokens > 0)
2484 };
2485
2486 let is_waiting_to_update_token_count =
2487 message_editor.is_waiting_to_update_token_count();
2488
2489 if total_token_usage.total == 0 {
2490 return None;
2491 }
2492
2493 let token_color = match total_token_usage.ratio() {
2494 TokenUsageRatio::Normal if is_estimating => Color::Default,
2495 TokenUsageRatio::Normal => Color::Muted,
2496 TokenUsageRatio::Warning => Color::Warning,
2497 TokenUsageRatio::Exceeded => Color::Error,
2498 };
2499
2500 let token_count = h_flex()
2501 .id("token-count")
2502 .flex_shrink_0()
2503 .gap_0p5()
2504 .when(!is_generating && is_estimating, |parent| {
2505 parent
2506 .child(
2507 h_flex()
2508 .mr_1()
2509 .size_2p5()
2510 .justify_center()
2511 .rounded_full()
2512 .bg(cx.theme().colors().text.opacity(0.1))
2513 .child(
2514 div().size_1().rounded_full().bg(cx.theme().colors().text),
2515 ),
2516 )
2517 .tooltip(move |window, cx| {
2518 Tooltip::with_meta(
2519 "Estimated New Token Count",
2520 None,
2521 format!(
2522 "Current Conversation Tokens: {}",
2523 humanize_token_count(conversation_token_usage.total)
2524 ),
2525 window,
2526 cx,
2527 )
2528 })
2529 })
2530 .child(
2531 Label::new(humanize_token_count(total_token_usage.total))
2532 .size(LabelSize::Small)
2533 .color(token_color)
2534 .map(|label| {
2535 if is_generating || is_waiting_to_update_token_count {
2536 label
2537 .with_animation(
2538 "used-tokens-label",
2539 Animation::new(Duration::from_secs(2))
2540 .repeat()
2541 .with_easing(pulsating_between(0.6, 1.)),
2542 |label, delta| label.alpha(delta),
2543 )
2544 .into_any()
2545 } else {
2546 label.into_any_element()
2547 }
2548 }),
2549 )
2550 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2551 .child(
2552 Label::new(humanize_token_count(total_token_usage.max))
2553 .size(LabelSize::Small)
2554 .color(Color::Muted),
2555 )
2556 .into_any();
2557
2558 Some(token_count)
2559 }
2560 ActiveView::TextThread { context_editor, .. } => {
2561 let element = render_remaining_tokens(context_editor, cx)?;
2562
2563 Some(element.into_any_element())
2564 }
2565 ActiveView::ExternalAgentThread { .. }
2566 | ActiveView::History
2567 | ActiveView::Configuration => {
2568 return None;
2569 }
2570 }
2571 }
2572
2573 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2574 if TrialEndUpsell::dismissed() {
2575 return false;
2576 }
2577
2578 match &self.active_view {
2579 ActiveView::Thread { thread, .. } => {
2580 if thread
2581 .read(cx)
2582 .thread()
2583 .read(cx)
2584 .configured_model()
2585 .map_or(false, |model| {
2586 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2587 })
2588 {
2589 return false;
2590 }
2591 }
2592 ActiveView::TextThread { .. } => {
2593 if LanguageModelRegistry::global(cx)
2594 .read(cx)
2595 .default_model()
2596 .map_or(false, |model| {
2597 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2598 })
2599 {
2600 return false;
2601 }
2602 }
2603 ActiveView::ExternalAgentThread { .. }
2604 | ActiveView::History
2605 | ActiveView::Configuration => return false,
2606 }
2607
2608 let plan = self.user_store.read(cx).plan();
2609 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2610
2611 matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
2612 }
2613
2614 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2615 if OnboardingUpsell::dismissed() {
2616 return false;
2617 }
2618
2619 match &self.active_view {
2620 ActiveView::History | ActiveView::Configuration => false,
2621 ActiveView::ExternalAgentThread { thread_view, .. }
2622 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2623 {
2624 false
2625 }
2626 _ => {
2627 let history_is_empty = self
2628 .history_store
2629 .update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
2630
2631 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2632 .providers()
2633 .iter()
2634 .any(|provider| {
2635 provider.is_authenticated(cx)
2636 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2637 });
2638
2639 history_is_empty || !has_configured_non_zed_providers
2640 }
2641 }
2642 }
2643
2644 fn render_onboarding(
2645 &self,
2646 _window: &mut Window,
2647 cx: &mut Context<Self>,
2648 ) -> Option<impl IntoElement> {
2649 if !self.should_render_onboarding(cx) {
2650 return None;
2651 }
2652
2653 let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
2654 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2655
2656 Some(
2657 div()
2658 .when(thread_view, |this| {
2659 this.size_full().bg(cx.theme().colors().panel_background)
2660 })
2661 .when(text_thread_view, |this| {
2662 this.bg(cx.theme().colors().editor_background)
2663 })
2664 .child(self.onboarding.clone()),
2665 )
2666 }
2667
2668 fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
2669 div()
2670 .size_full()
2671 .absolute()
2672 .inset_0()
2673 .bg(cx.theme().colors().panel_background)
2674 .opacity(0.8)
2675 .block_mouse_except_scroll()
2676 }
2677
2678 fn render_trial_end_upsell(
2679 &self,
2680 _window: &mut Window,
2681 cx: &mut Context<Self>,
2682 ) -> Option<impl IntoElement> {
2683 if !self.should_render_trial_end_upsell(cx) {
2684 return None;
2685 }
2686
2687 Some(
2688 v_flex()
2689 .absolute()
2690 .inset_0()
2691 .size_full()
2692 .bg(cx.theme().colors().panel_background)
2693 .opacity(0.85)
2694 .block_mouse_except_scroll()
2695 .child(EndTrialUpsell::new(Arc::new({
2696 let this = cx.entity();
2697 move |_, cx| {
2698 this.update(cx, |_this, cx| {
2699 TrialEndUpsell::set_dismissed(true, cx);
2700 cx.notify();
2701 });
2702 }
2703 }))),
2704 )
2705 }
2706
2707 fn render_empty_state_section_header(
2708 &self,
2709 label: impl Into<SharedString>,
2710 action_slot: Option<AnyElement>,
2711 cx: &mut Context<Self>,
2712 ) -> impl IntoElement {
2713 div().pl_1().pr_1p5().child(
2714 h_flex()
2715 .mt_2()
2716 .pl_1p5()
2717 .pb_1()
2718 .w_full()
2719 .justify_between()
2720 .border_b_1()
2721 .border_color(cx.theme().colors().border_variant)
2722 .child(
2723 Label::new(label.into())
2724 .size(LabelSize::Small)
2725 .color(Color::Muted),
2726 )
2727 .children(action_slot),
2728 )
2729 }
2730
2731 fn render_thread_empty_state(
2732 &self,
2733 window: &mut Window,
2734 cx: &mut Context<Self>,
2735 ) -> impl IntoElement {
2736 let recent_history = self
2737 .history_store
2738 .update(cx, |this, cx| this.recent_entries(6, cx));
2739
2740 let model_registry = LanguageModelRegistry::read_global(cx);
2741
2742 let configuration_error =
2743 model_registry.configuration_error(model_registry.default_model(), cx);
2744
2745 let no_error = configuration_error.is_none();
2746 let focus_handle = self.focus_handle(cx);
2747
2748 v_flex()
2749 .size_full()
2750 .bg(cx.theme().colors().panel_background)
2751 .when(recent_history.is_empty(), |this| {
2752 this.child(
2753 v_flex()
2754 .size_full()
2755 .mx_auto()
2756 .justify_center()
2757 .items_center()
2758 .gap_1()
2759 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
2760 .when(no_error, |parent| {
2761 parent
2762 .child(h_flex().child(
2763 Label::new("Ask and build anything.").color(Color::Muted),
2764 ))
2765 .child(
2766 v_flex()
2767 .mt_2()
2768 .gap_1()
2769 .max_w_48()
2770 .child(
2771 Button::new("context", "Add Context")
2772 .label_size(LabelSize::Small)
2773 .icon(IconName::FileCode)
2774 .icon_position(IconPosition::Start)
2775 .icon_size(IconSize::Small)
2776 .icon_color(Color::Muted)
2777 .full_width()
2778 .key_binding(KeyBinding::for_action_in(
2779 &ToggleContextPicker,
2780 &focus_handle,
2781 window,
2782 cx,
2783 ))
2784 .on_click(|_event, window, cx| {
2785 window.dispatch_action(
2786 ToggleContextPicker.boxed_clone(),
2787 cx,
2788 )
2789 }),
2790 )
2791 .child(
2792 Button::new("mode", "Switch Model")
2793 .label_size(LabelSize::Small)
2794 .icon(IconName::DatabaseZap)
2795 .icon_position(IconPosition::Start)
2796 .icon_size(IconSize::Small)
2797 .icon_color(Color::Muted)
2798 .full_width()
2799 .key_binding(KeyBinding::for_action_in(
2800 &ToggleModelSelector,
2801 &focus_handle,
2802 window,
2803 cx,
2804 ))
2805 .on_click(|_event, window, cx| {
2806 window.dispatch_action(
2807 ToggleModelSelector.boxed_clone(),
2808 cx,
2809 )
2810 }),
2811 )
2812 .child(
2813 Button::new("settings", "View Settings")
2814 .label_size(LabelSize::Small)
2815 .icon(IconName::Settings)
2816 .icon_position(IconPosition::Start)
2817 .icon_size(IconSize::Small)
2818 .icon_color(Color::Muted)
2819 .full_width()
2820 .key_binding(KeyBinding::for_action_in(
2821 &OpenSettings,
2822 &focus_handle,
2823 window,
2824 cx,
2825 ))
2826 .on_click(|_event, window, cx| {
2827 window.dispatch_action(
2828 OpenSettings.boxed_clone(),
2829 cx,
2830 )
2831 }),
2832 ),
2833 )
2834 }),
2835 )
2836 })
2837 .when(!recent_history.is_empty(), |parent| {
2838 parent
2839 .overflow_hidden()
2840 .justify_end()
2841 .gap_1()
2842 .child(
2843 self.render_empty_state_section_header(
2844 "Recent",
2845 Some(
2846 Button::new("view-history", "View All")
2847 .style(ButtonStyle::Subtle)
2848 .label_size(LabelSize::Small)
2849 .key_binding(
2850 KeyBinding::for_action_in(
2851 &OpenHistory,
2852 &self.focus_handle(cx),
2853 window,
2854 cx,
2855 )
2856 .map(|kb| kb.size(rems_from_px(12.))),
2857 )
2858 .on_click(move |_event, window, cx| {
2859 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2860 })
2861 .into_any_element(),
2862 ),
2863 cx,
2864 ),
2865 )
2866 .child(
2867 v_flex().p_1().pr_1p5().gap_1().children(
2868 recent_history
2869 .into_iter()
2870 .enumerate()
2871 .map(|(index, entry)| {
2872 // TODO: Add keyboard navigation.
2873 let is_hovered =
2874 self.hovered_recent_history_item == Some(index);
2875 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2876 .hovered(is_hovered)
2877 .on_hover(cx.listener(
2878 move |this, is_hovered, _window, cx| {
2879 if *is_hovered {
2880 this.hovered_recent_history_item = Some(index);
2881 } else if this.hovered_recent_history_item
2882 == Some(index)
2883 {
2884 this.hovered_recent_history_item = None;
2885 }
2886 cx.notify();
2887 },
2888 ))
2889 .into_any_element()
2890 }),
2891 ),
2892 )
2893 })
2894 .when_some(configuration_error.as_ref(), |this, err| {
2895 this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
2896 })
2897 }
2898
2899 fn render_configuration_error(
2900 &self,
2901 border_bottom: bool,
2902 configuration_error: &ConfigurationError,
2903 focus_handle: &FocusHandle,
2904 window: &mut Window,
2905 cx: &mut App,
2906 ) -> impl IntoElement {
2907 let zed_provider_configured = AgentSettings::get_global(cx)
2908 .default_model
2909 .as_ref()
2910 .map_or(false, |selection| {
2911 selection.provider.0.as_str() == "zed.dev"
2912 });
2913
2914 let callout = if zed_provider_configured {
2915 Callout::new()
2916 .icon(IconName::Warning)
2917 .severity(Severity::Warning)
2918 .when(border_bottom, |this| {
2919 this.border_position(ui::BorderPosition::Bottom)
2920 })
2921 .title("Sign in to continue using Zed as your LLM provider.")
2922 .actions_slot(
2923 Button::new("sign_in", "Sign In")
2924 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2925 .label_size(LabelSize::Small)
2926 .on_click({
2927 let workspace = self.workspace.clone();
2928 move |_, _, cx| {
2929 let Ok(client) =
2930 workspace.update(cx, |workspace, _| workspace.client().clone())
2931 else {
2932 return;
2933 };
2934
2935 cx.spawn(async move |cx| {
2936 client.sign_in_with_optional_connect(true, cx).await
2937 })
2938 .detach_and_log_err(cx);
2939 }
2940 }),
2941 )
2942 } else {
2943 Callout::new()
2944 .icon(IconName::Warning)
2945 .severity(Severity::Warning)
2946 .when(border_bottom, |this| {
2947 this.border_position(ui::BorderPosition::Bottom)
2948 })
2949 .title(configuration_error.to_string())
2950 .actions_slot(
2951 Button::new("settings", "Configure")
2952 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2953 .label_size(LabelSize::Small)
2954 .key_binding(
2955 KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
2956 .map(|kb| kb.size(rems_from_px(12.))),
2957 )
2958 .on_click(|_event, window, cx| {
2959 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2960 }),
2961 )
2962 };
2963
2964 match configuration_error {
2965 ConfigurationError::ModelNotFound
2966 | ConfigurationError::ProviderNotAuthenticated(_)
2967 | ConfigurationError::NoProvider => callout.into_any_element(),
2968 ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
2969 Banner::new()
2970 .severity(Severity::Warning)
2971 .child(h_flex().w_full().children(
2972 provider.render_accept_terms(
2973 LanguageModelProviderTosView::ThreadEmptyState,
2974 cx,
2975 ),
2976 ))
2977 .into_any_element()
2978 }
2979 }
2980 }
2981
2982 fn render_tool_use_limit_reached(
2983 &self,
2984 window: &mut Window,
2985 cx: &mut Context<Self>,
2986 ) -> Option<AnyElement> {
2987 let active_thread = match &self.active_view {
2988 ActiveView::Thread { thread, .. } => thread,
2989 ActiveView::ExternalAgentThread { .. } => {
2990 return None;
2991 }
2992 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2993 return None;
2994 }
2995 };
2996
2997 let thread = active_thread.read(cx).thread().read(cx);
2998
2999 let tool_use_limit_reached = thread.tool_use_limit_reached();
3000 if !tool_use_limit_reached {
3001 return None;
3002 }
3003
3004 let model = thread.configured_model()?.model;
3005
3006 let focus_handle = self.focus_handle(cx);
3007
3008 let banner = Banner::new()
3009 .severity(Severity::Info)
3010 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
3011 .action_slot(
3012 h_flex()
3013 .gap_1()
3014 .child(
3015 Button::new("continue-conversation", "Continue")
3016 .layer(ElevationIndex::ModalSurface)
3017 .label_size(LabelSize::Small)
3018 .key_binding(
3019 KeyBinding::for_action_in(
3020 &ContinueThread,
3021 &focus_handle,
3022 window,
3023 cx,
3024 )
3025 .map(|kb| kb.size(rems_from_px(10.))),
3026 )
3027 .on_click(cx.listener(|this, _, window, cx| {
3028 this.continue_conversation(window, cx);
3029 })),
3030 )
3031 .when(model.supports_burn_mode(), |this| {
3032 this.child(
3033 Button::new("continue-burn-mode", "Continue with Burn Mode")
3034 .style(ButtonStyle::Filled)
3035 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3036 .layer(ElevationIndex::ModalSurface)
3037 .label_size(LabelSize::Small)
3038 .key_binding(
3039 KeyBinding::for_action_in(
3040 &ContinueWithBurnMode,
3041 &focus_handle,
3042 window,
3043 cx,
3044 )
3045 .map(|kb| kb.size(rems_from_px(10.))),
3046 )
3047 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
3048 .on_click({
3049 let active_thread = active_thread.clone();
3050 cx.listener(move |this, _, window, cx| {
3051 active_thread.update(cx, |active_thread, cx| {
3052 active_thread.thread().update(cx, |thread, _cx| {
3053 thread.set_completion_mode(CompletionMode::Burn);
3054 });
3055 });
3056 this.continue_conversation(window, cx);
3057 })
3058 }),
3059 )
3060 }),
3061 );
3062
3063 Some(div().px_2().pb_2().child(banner).into_any_element())
3064 }
3065
3066 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3067 let message = message.into();
3068
3069 IconButton::new("copy", IconName::Copy)
3070 .icon_size(IconSize::Small)
3071 .icon_color(Color::Muted)
3072 .tooltip(Tooltip::text("Copy Error Message"))
3073 .on_click(move |_, _, cx| {
3074 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3075 })
3076 }
3077
3078 fn dismiss_error_button(
3079 &self,
3080 thread: &Entity<ActiveThread>,
3081 cx: &mut Context<Self>,
3082 ) -> impl IntoElement {
3083 IconButton::new("dismiss", IconName::Close)
3084 .icon_size(IconSize::Small)
3085 .icon_color(Color::Muted)
3086 .tooltip(Tooltip::text("Dismiss Error"))
3087 .on_click(cx.listener({
3088 let thread = thread.clone();
3089 move |_, _, _, cx| {
3090 thread.update(cx, |this, _cx| {
3091 this.clear_last_error();
3092 });
3093
3094 cx.notify();
3095 }
3096 }))
3097 }
3098
3099 fn upgrade_button(
3100 &self,
3101 thread: &Entity<ActiveThread>,
3102 cx: &mut Context<Self>,
3103 ) -> impl IntoElement {
3104 Button::new("upgrade", "Upgrade")
3105 .label_size(LabelSize::Small)
3106 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3107 .on_click(cx.listener({
3108 let thread = thread.clone();
3109 move |_, _, _, cx| {
3110 thread.update(cx, |this, _cx| {
3111 this.clear_last_error();
3112 });
3113
3114 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
3115 cx.notify();
3116 }
3117 }))
3118 }
3119
3120 fn render_payment_required_error(
3121 &self,
3122 thread: &Entity<ActiveThread>,
3123 cx: &mut Context<Self>,
3124 ) -> AnyElement {
3125 const ERROR_MESSAGE: &str =
3126 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
3127
3128 Callout::new()
3129 .severity(Severity::Error)
3130 .icon(IconName::XCircle)
3131 .title("Free Usage Exceeded")
3132 .description(ERROR_MESSAGE)
3133 .actions_slot(
3134 h_flex()
3135 .gap_0p5()
3136 .child(self.upgrade_button(thread, cx))
3137 .child(self.create_copy_button(ERROR_MESSAGE)),
3138 )
3139 .dismiss_action(self.dismiss_error_button(thread, cx))
3140 .into_any_element()
3141 }
3142
3143 fn render_model_request_limit_reached_error(
3144 &self,
3145 plan: Plan,
3146 thread: &Entity<ActiveThread>,
3147 cx: &mut Context<Self>,
3148 ) -> AnyElement {
3149 let error_message = match plan {
3150 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
3151 Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
3152 };
3153
3154 Callout::new()
3155 .severity(Severity::Error)
3156 .title("Model Prompt Limit Reached")
3157 .description(error_message)
3158 .actions_slot(
3159 h_flex()
3160 .gap_0p5()
3161 .child(self.upgrade_button(thread, cx))
3162 .child(self.create_copy_button(error_message)),
3163 )
3164 .dismiss_action(self.dismiss_error_button(thread, cx))
3165 .into_any_element()
3166 }
3167
3168 fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
3169 Button::new("retry", "Retry")
3170 .icon(IconName::RotateCw)
3171 .icon_position(IconPosition::Start)
3172 .icon_size(IconSize::Small)
3173 .label_size(LabelSize::Small)
3174 .on_click({
3175 let thread = thread.clone();
3176 move |_, window, cx| {
3177 thread.update(cx, |thread, cx| {
3178 thread.clear_last_error();
3179 thread.thread().update(cx, |thread, cx| {
3180 thread.retry_last_completion(Some(window.window_handle()), cx);
3181 });
3182 });
3183 }
3184 })
3185 .into_any_element()
3186 }
3187
3188 fn render_error_message(
3189 &self,
3190 header: SharedString,
3191 message: SharedString,
3192 thread: &Entity<ActiveThread>,
3193 cx: &mut Context<Self>,
3194 ) -> AnyElement {
3195 let message_with_header = format!("{}\n{}", header, message);
3196
3197 Callout::new()
3198 .severity(Severity::Error)
3199 .icon(IconName::XCircle)
3200 .title(header)
3201 .description(message.clone())
3202 .actions_slot(
3203 h_flex()
3204 .gap_0p5()
3205 .child(self.render_retry_button(thread))
3206 .child(self.create_copy_button(message_with_header)),
3207 )
3208 .dismiss_action(self.dismiss_error_button(thread, cx))
3209 .into_any_element()
3210 }
3211
3212 fn render_retryable_error(
3213 &self,
3214 message: SharedString,
3215 can_enable_burn_mode: bool,
3216 thread: &Entity<ActiveThread>,
3217 ) -> AnyElement {
3218 Callout::new()
3219 .severity(Severity::Error)
3220 .title("Error")
3221 .description(message.clone())
3222 .actions_slot(
3223 h_flex()
3224 .gap_0p5()
3225 .when(can_enable_burn_mode, |this| {
3226 this.child(
3227 Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
3228 .icon(IconName::ZedBurnMode)
3229 .icon_position(IconPosition::Start)
3230 .icon_size(IconSize::Small)
3231 .label_size(LabelSize::Small)
3232 .on_click({
3233 let thread = thread.clone();
3234 move |_, window, cx| {
3235 thread.update(cx, |thread, cx| {
3236 thread.clear_last_error();
3237 thread.thread().update(cx, |thread, cx| {
3238 thread.enable_burn_mode_and_retry(
3239 Some(window.window_handle()),
3240 cx,
3241 );
3242 });
3243 });
3244 }
3245 }),
3246 )
3247 })
3248 .child(self.render_retry_button(thread)),
3249 )
3250 .into_any_element()
3251 }
3252
3253 fn render_prompt_editor(
3254 &self,
3255 context_editor: &Entity<TextThreadEditor>,
3256 buffer_search_bar: &Entity<BufferSearchBar>,
3257 window: &mut Window,
3258 cx: &mut Context<Self>,
3259 ) -> Div {
3260 let mut registrar = buffer_search::DivRegistrar::new(
3261 |this, _, _cx| match &this.active_view {
3262 ActiveView::TextThread {
3263 buffer_search_bar, ..
3264 } => Some(buffer_search_bar.clone()),
3265 _ => None,
3266 },
3267 cx,
3268 );
3269 BufferSearchBar::register(&mut registrar);
3270 registrar
3271 .into_div()
3272 .size_full()
3273 .relative()
3274 .map(|parent| {
3275 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3276 if buffer_search_bar.is_dismissed() {
3277 return parent;
3278 }
3279 parent.child(
3280 div()
3281 .p(DynamicSpacing::Base08.rems(cx))
3282 .border_b_1()
3283 .border_color(cx.theme().colors().border_variant)
3284 .bg(cx.theme().colors().editor_background)
3285 .child(buffer_search_bar.render(window, cx)),
3286 )
3287 })
3288 })
3289 .child(context_editor.clone())
3290 .child(self.render_drag_target(cx))
3291 }
3292
3293 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3294 let is_local = self.project.read(cx).is_local();
3295 div()
3296 .invisible()
3297 .absolute()
3298 .top_0()
3299 .right_0()
3300 .bottom_0()
3301 .left_0()
3302 .bg(cx.theme().colors().drop_target_background)
3303 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3304 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3305 .when(is_local, |this| {
3306 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3307 })
3308 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3309 let item = tab.pane.read(cx).item_for_index(tab.ix);
3310 let project_paths = item
3311 .and_then(|item| item.project_path(cx))
3312 .into_iter()
3313 .collect::<Vec<_>>();
3314 this.handle_drop(project_paths, vec![], window, cx);
3315 }))
3316 .on_drop(
3317 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3318 let project_paths = selection
3319 .items()
3320 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3321 .collect::<Vec<_>>();
3322 this.handle_drop(project_paths, vec![], window, cx);
3323 }),
3324 )
3325 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3326 let tasks = paths
3327 .paths()
3328 .into_iter()
3329 .map(|path| {
3330 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3331 })
3332 .collect::<Vec<_>>();
3333 cx.spawn_in(window, async move |this, cx| {
3334 let mut paths = vec![];
3335 let mut added_worktrees = vec![];
3336 let opened_paths = futures::future::join_all(tasks).await;
3337 for entry in opened_paths {
3338 if let Some((worktree, project_path)) = entry.log_err() {
3339 added_worktrees.push(worktree);
3340 paths.push(project_path);
3341 }
3342 }
3343 this.update_in(cx, |this, window, cx| {
3344 this.handle_drop(paths, added_worktrees, window, cx);
3345 })
3346 .ok();
3347 })
3348 .detach();
3349 }))
3350 }
3351
3352 fn handle_drop(
3353 &mut self,
3354 paths: Vec<ProjectPath>,
3355 added_worktrees: Vec<Entity<Worktree>>,
3356 window: &mut Window,
3357 cx: &mut Context<Self>,
3358 ) {
3359 match &self.active_view {
3360 ActiveView::Thread { thread, .. } => {
3361 let context_store = thread.read(cx).context_store().clone();
3362 context_store.update(cx, move |context_store, cx| {
3363 let mut tasks = Vec::new();
3364 for project_path in &paths {
3365 tasks.push(context_store.add_file_from_path(
3366 project_path.clone(),
3367 false,
3368 cx,
3369 ));
3370 }
3371 cx.background_spawn(async move {
3372 futures::future::join_all(tasks).await;
3373 // Need to hold onto the worktrees until they have already been used when
3374 // opening the buffers.
3375 drop(added_worktrees);
3376 })
3377 .detach();
3378 });
3379 }
3380 ActiveView::ExternalAgentThread { thread_view } => {
3381 thread_view.update(cx, |thread_view, cx| {
3382 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3383 });
3384 }
3385 ActiveView::TextThread { context_editor, .. } => {
3386 context_editor.update(cx, |context_editor, cx| {
3387 TextThreadEditor::insert_dragged_files(
3388 context_editor,
3389 paths,
3390 added_worktrees,
3391 window,
3392 cx,
3393 );
3394 });
3395 }
3396 ActiveView::History | ActiveView::Configuration => {}
3397 }
3398 }
3399
3400 fn key_context(&self) -> KeyContext {
3401 let mut key_context = KeyContext::new_with_defaults();
3402 key_context.add("AgentPanel");
3403 match &self.active_view {
3404 ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
3405 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3406 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3407 }
3408 key_context
3409 }
3410}
3411
3412impl Render for AgentPanel {
3413 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3414 // WARNING: Changes to this element hierarchy can have
3415 // non-obvious implications to the layout of children.
3416 //
3417 // If you need to change it, please confirm:
3418 // - The message editor expands (cmd-option-esc) correctly
3419 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3420 // - Font size works as expected and can be changed with cmd-+/cmd-
3421 // - Scrolling in all views works as expected
3422 // - Files can be dropped into the panel
3423 let content = v_flex()
3424 .relative()
3425 .size_full()
3426 .justify_between()
3427 .key_context(self.key_context())
3428 .on_action(cx.listener(Self::cancel))
3429 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3430 this.new_thread(action, window, cx);
3431 }))
3432 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3433 this.open_history(window, cx);
3434 }))
3435 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3436 this.open_configuration(window, cx);
3437 }))
3438 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3439 .on_action(cx.listener(Self::deploy_rules_library))
3440 .on_action(cx.listener(Self::open_agent_diff))
3441 .on_action(cx.listener(Self::go_back))
3442 .on_action(cx.listener(Self::toggle_navigation_menu))
3443 .on_action(cx.listener(Self::toggle_options_menu))
3444 .on_action(cx.listener(Self::increase_font_size))
3445 .on_action(cx.listener(Self::decrease_font_size))
3446 .on_action(cx.listener(Self::reset_font_size))
3447 .on_action(cx.listener(Self::toggle_zoom))
3448 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3449 this.continue_conversation(window, cx);
3450 }))
3451 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3452 match &this.active_view {
3453 ActiveView::Thread { thread, .. } => {
3454 thread.update(cx, |active_thread, cx| {
3455 active_thread.thread().update(cx, |thread, _cx| {
3456 thread.set_completion_mode(CompletionMode::Burn);
3457 });
3458 });
3459 this.continue_conversation(window, cx);
3460 }
3461 ActiveView::ExternalAgentThread { .. } => {}
3462 ActiveView::TextThread { .. }
3463 | ActiveView::History
3464 | ActiveView::Configuration => {}
3465 }
3466 }))
3467 .on_action(cx.listener(Self::toggle_burn_mode))
3468 .child(self.render_toolbar(window, cx))
3469 .children(self.render_onboarding(window, cx))
3470 .map(|parent| match &self.active_view {
3471 ActiveView::Thread {
3472 thread,
3473 message_editor,
3474 ..
3475 } => parent
3476 .child(
3477 if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
3478 self.render_thread_empty_state(window, cx)
3479 .into_any_element()
3480 } else {
3481 thread.clone().into_any_element()
3482 },
3483 )
3484 .children(self.render_tool_use_limit_reached(window, cx))
3485 .when_some(thread.read(cx).last_error(), |this, last_error| {
3486 this.child(
3487 div()
3488 .child(match last_error {
3489 ThreadError::PaymentRequired => {
3490 self.render_payment_required_error(thread, cx)
3491 }
3492 ThreadError::ModelRequestLimitReached { plan } => self
3493 .render_model_request_limit_reached_error(plan, thread, cx),
3494 ThreadError::Message { header, message } => {
3495 self.render_error_message(header, message, thread, cx)
3496 }
3497 ThreadError::RetryableError {
3498 message,
3499 can_enable_burn_mode,
3500 } => self.render_retryable_error(
3501 message,
3502 can_enable_burn_mode,
3503 thread,
3504 ),
3505 })
3506 .into_any(),
3507 )
3508 })
3509 .child(h_flex().relative().child(message_editor.clone()).when(
3510 !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
3511 |this| this.child(self.render_backdrop(cx)),
3512 ))
3513 .child(self.render_drag_target(cx)),
3514 ActiveView::ExternalAgentThread { thread_view, .. } => parent
3515 .child(thread_view.clone())
3516 .child(self.render_drag_target(cx)),
3517 ActiveView::History => parent.child(self.history.clone()),
3518 ActiveView::TextThread {
3519 context_editor,
3520 buffer_search_bar,
3521 ..
3522 } => {
3523 let model_registry = LanguageModelRegistry::read_global(cx);
3524 let configuration_error =
3525 model_registry.configuration_error(model_registry.default_model(), cx);
3526 parent
3527 .map(|this| {
3528 if !self.should_render_onboarding(cx)
3529 && let Some(err) = configuration_error.as_ref()
3530 {
3531 this.child(self.render_configuration_error(
3532 true,
3533 err,
3534 &self.focus_handle(cx),
3535 window,
3536 cx,
3537 ))
3538 } else {
3539 this
3540 }
3541 })
3542 .child(self.render_prompt_editor(
3543 context_editor,
3544 buffer_search_bar,
3545 window,
3546 cx,
3547 ))
3548 }
3549 ActiveView::Configuration => parent.children(self.configuration.clone()),
3550 })
3551 .children(self.render_trial_end_upsell(window, cx));
3552
3553 match self.active_view.which_font_size_used() {
3554 WhichFontSize::AgentFont => {
3555 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3556 .size_full()
3557 .child(content)
3558 .into_any()
3559 }
3560 _ => content.into_any(),
3561 }
3562 }
3563}
3564
3565struct PromptLibraryInlineAssist {
3566 workspace: WeakEntity<Workspace>,
3567}
3568
3569impl PromptLibraryInlineAssist {
3570 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3571 Self { workspace }
3572 }
3573}
3574
3575impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3576 fn assist(
3577 &self,
3578 prompt_editor: &Entity<Editor>,
3579 initial_prompt: Option<String>,
3580 window: &mut Window,
3581 cx: &mut Context<RulesLibrary>,
3582 ) {
3583 InlineAssistant::update_global(cx, |assistant, cx| {
3584 let Some(project) = self
3585 .workspace
3586 .upgrade()
3587 .map(|workspace| workspace.read(cx).project().downgrade())
3588 else {
3589 return;
3590 };
3591 let prompt_store = None;
3592 let thread_store = None;
3593 let text_thread_store = None;
3594 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3595 assistant.assist(
3596 prompt_editor,
3597 self.workspace.clone(),
3598 context_store,
3599 project,
3600 prompt_store,
3601 thread_store,
3602 text_thread_store,
3603 initial_prompt,
3604 window,
3605 cx,
3606 )
3607 })
3608 }
3609
3610 fn focus_agent_panel(
3611 &self,
3612 workspace: &mut Workspace,
3613 window: &mut Window,
3614 cx: &mut Context<Workspace>,
3615 ) -> bool {
3616 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3617 }
3618}
3619
3620pub struct ConcreteAssistantPanelDelegate;
3621
3622impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3623 fn active_context_editor(
3624 &self,
3625 workspace: &mut Workspace,
3626 _window: &mut Window,
3627 cx: &mut Context<Workspace>,
3628 ) -> Option<Entity<TextThreadEditor>> {
3629 let panel = workspace.panel::<AgentPanel>(cx)?;
3630 panel.read(cx).active_context_editor()
3631 }
3632
3633 fn open_saved_context(
3634 &self,
3635 workspace: &mut Workspace,
3636 path: Arc<Path>,
3637 window: &mut Window,
3638 cx: &mut Context<Workspace>,
3639 ) -> Task<Result<()>> {
3640 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3641 return Task::ready(Err(anyhow!("Agent panel not found")));
3642 };
3643
3644 panel.update(cx, |panel, cx| {
3645 panel.open_saved_prompt_editor(path, window, cx)
3646 })
3647 }
3648
3649 fn open_remote_context(
3650 &self,
3651 _workspace: &mut Workspace,
3652 _context_id: assistant_context::ContextId,
3653 _window: &mut Window,
3654 _cx: &mut Context<Workspace>,
3655 ) -> Task<Result<Entity<TextThreadEditor>>> {
3656 Task::ready(Err(anyhow!("opening remote context not implemented")))
3657 }
3658
3659 fn quote_selection(
3660 &self,
3661 workspace: &mut Workspace,
3662 selection_ranges: Vec<Range<Anchor>>,
3663 buffer: Entity<MultiBuffer>,
3664 window: &mut Window,
3665 cx: &mut Context<Workspace>,
3666 ) {
3667 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3668 return;
3669 };
3670
3671 if !panel.focus_handle(cx).contains_focused(window, cx) {
3672 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3673 }
3674
3675 panel.update(cx, |_, cx| {
3676 // Wait to create a new context until the workspace is no longer
3677 // being updated.
3678 cx.defer_in(window, move |panel, window, cx| {
3679 if let Some(message_editor) = panel.active_message_editor() {
3680 message_editor.update(cx, |message_editor, cx| {
3681 message_editor.context_store().update(cx, |store, cx| {
3682 let buffer = buffer.read(cx);
3683 let selection_ranges = selection_ranges
3684 .into_iter()
3685 .flat_map(|range| {
3686 let (start_buffer, start) =
3687 buffer.text_anchor_for_position(range.start, cx)?;
3688 let (end_buffer, end) =
3689 buffer.text_anchor_for_position(range.end, cx)?;
3690 if start_buffer != end_buffer {
3691 return None;
3692 }
3693 Some((start_buffer, start..end))
3694 })
3695 .collect::<Vec<_>>();
3696
3697 for (buffer, range) in selection_ranges {
3698 store.add_selection(buffer, range, cx);
3699 }
3700 })
3701 })
3702 } else if let Some(context_editor) = panel.active_context_editor() {
3703 let snapshot = buffer.read(cx).snapshot(cx);
3704 let selection_ranges = selection_ranges
3705 .into_iter()
3706 .map(|range| range.to_point(&snapshot))
3707 .collect::<Vec<_>>();
3708
3709 context_editor.update(cx, |context_editor, cx| {
3710 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3711 });
3712 }
3713 });
3714 });
3715 }
3716}
3717
3718struct OnboardingUpsell;
3719
3720impl Dismissable for OnboardingUpsell {
3721 const KEY: &'static str = "dismissed-trial-upsell";
3722}
3723
3724struct TrialEndUpsell;
3725
3726impl Dismissable for TrialEndUpsell {
3727 const KEY: &'static str = "dismissed-trial-end-upsell";
3728}