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