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