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