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