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