1use std::ops::{Not, Range};
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Duration;
6
7use agent_servers::AgentServer;
8use db::kvp::{Dismissable, KEY_VALUE_STORE};
9use serde::{Deserialize, Serialize};
10
11use crate::NewExternalAgentThread;
12use crate::agent_diff::AgentDiffThread;
13use crate::{
14 AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
15 DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
16 NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
17 ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu,
18 ToggleNewThreadMenu, ToggleOptionsMenu,
19 acp::AcpThreadView,
20 active_thread::{self, ActiveThread, ActiveThreadEvent},
21 agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
22 agent_diff::AgentDiff,
23 message_editor::{MessageEditor, MessageEditorEvent},
24 slash_command::SlashCommandCompletionProvider,
25 text_thread_editor::{
26 AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
27 render_remaining_tokens,
28 },
29 thread_history::{HistoryEntryElement, ThreadHistory},
30 ui::{AgentOnboardingModal, EndTrialUpsell},
31};
32use agent::{
33 Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
34 context_store::ContextStore,
35 history_store::{HistoryEntryId, HistoryStore},
36 thread_store::{TextThreadStore, ThreadStore},
37};
38use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
39use ai_onboarding::AgentPanelOnboarding;
40use anyhow::{Result, anyhow};
41use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
42use assistant_slash_command::SlashCommandWorkingSet;
43use assistant_tool::ToolWorkingSet;
44use client::{UserStore, zed_urls};
45use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
46use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
47use feature_flags::{self, FeatureFlagAppExt};
48use fs::Fs;
49use gpui::{
50 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
51 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
52 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
53 pulsating_between,
54};
55use language::LanguageRegistry;
56use language_model::{
57 ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
58};
59use project::{DisableAiSettings, Project, ProjectPath, Worktree};
60use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
61use rules_library::{RulesLibrary, open_rules_library};
62use search::{BufferSearchBar, buffer_search};
63use settings::{Settings, update_settings_file};
64use theme::ThemeSettings;
65use time::UtcOffset;
66use ui::utils::WithRemSize;
67use ui::{
68 Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding,
69 PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
70};
71use util::ResultExt as _;
72use workspace::{
73 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
74 dock::{DockPosition, Panel, PanelEvent},
75};
76use zed_actions::{
77 DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
78 agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
79 assistant::{OpenRulesLibrary, ToggleFocus},
80};
81
82const AGENT_PANEL_KEY: &str = "agent_panel";
83
84#[derive(Serialize, Deserialize)]
85struct SerializedAgentPanel {
86 width: Option<Pixels>,
87 selected_agent: Option<AgentType>,
88}
89
90pub fn init(cx: &mut App) {
91 cx.observe_new(
92 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
93 workspace
94 .register_action(|workspace, action: &NewThread, window, cx| {
95 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
96 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
97 workspace.focus_panel::<AgentPanel>(window, cx);
98 }
99 })
100 .register_action(|workspace, _: &OpenHistory, window, cx| {
101 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
102 workspace.focus_panel::<AgentPanel>(window, cx);
103 panel.update(cx, |panel, cx| panel.open_history(window, cx));
104 }
105 })
106 .register_action(|workspace, _: &OpenSettings, window, cx| {
107 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
108 workspace.focus_panel::<AgentPanel>(window, cx);
109 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
110 }
111 })
112 .register_action(|workspace, _: &NewTextThread, window, cx| {
113 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
114 workspace.focus_panel::<AgentPanel>(window, cx);
115 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
116 }
117 })
118 .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
119 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
120 workspace.focus_panel::<AgentPanel>(window, cx);
121 panel.update(cx, |panel, cx| {
122 panel.new_external_thread(action.agent, window, cx)
123 });
124 }
125 })
126 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
127 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
128 workspace.focus_panel::<AgentPanel>(window, cx);
129 panel.update(cx, |panel, cx| {
130 panel.deploy_rules_library(action, window, cx)
131 });
132 }
133 })
134 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
135 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
136 workspace.focus_panel::<AgentPanel>(window, cx);
137 match &panel.read(cx).active_view {
138 ActiveView::Thread { thread, .. } => {
139 let thread = thread.read(cx).thread().clone();
140 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
141 }
142 ActiveView::ExternalAgentThread { .. }
143 | ActiveView::TextThread { .. }
144 | ActiveView::History
145 | ActiveView::Configuration => {}
146 }
147 }
148 })
149 .register_action(|workspace, _: &Follow, window, cx| {
150 workspace.follow(CollaboratorId::Agent, window, cx);
151 })
152 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
153 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
154 return;
155 };
156 workspace.focus_panel::<AgentPanel>(window, cx);
157 panel.update(cx, |panel, cx| {
158 if let Some(message_editor) = panel.active_message_editor() {
159 message_editor.update(cx, |editor, cx| {
160 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
161 });
162 }
163 });
164 })
165 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
166 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
167 workspace.focus_panel::<AgentPanel>(window, cx);
168 panel.update(cx, |panel, cx| {
169 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
170 });
171 }
172 })
173 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
174 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
175 workspace.focus_panel::<AgentPanel>(window, cx);
176 panel.update(cx, |panel, cx| {
177 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
178 });
179 }
180 })
181 .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
182 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
183 workspace.focus_panel::<AgentPanel>(window, cx);
184 panel.update(cx, |panel, cx| {
185 panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
186 });
187 }
188 })
189 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
190 AgentOnboardingModal::toggle(workspace, window, cx)
191 })
192 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
193 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
194 window.refresh();
195 })
196 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
197 OnboardingUpsell::set_dismissed(false, cx);
198 })
199 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
200 TrialEndUpsell::set_dismissed(false, cx);
201 });
202 },
203 )
204 .detach();
205}
206
207enum ActiveView {
208 Thread {
209 thread: Entity<ActiveThread>,
210 change_title_editor: Entity<Editor>,
211 message_editor: Entity<MessageEditor>,
212 _subscriptions: Vec<gpui::Subscription>,
213 },
214 ExternalAgentThread {
215 thread_view: Entity<AcpThreadView>,
216 },
217 TextThread {
218 context_editor: Entity<TextThreadEditor>,
219 title_editor: Entity<Editor>,
220 buffer_search_bar: Entity<BufferSearchBar>,
221 _subscriptions: Vec<gpui::Subscription>,
222 },
223 History,
224 Configuration,
225}
226
227enum WhichFontSize {
228 AgentFont,
229 BufferFont,
230 None,
231}
232
233#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234pub enum AgentType {
235 #[default]
236 Zed,
237 TextThread,
238 Gemini,
239 ClaudeCode,
240 NativeAgent,
241}
242
243impl AgentType {
244 fn label(self) -> impl Into<SharedString> {
245 match self {
246 Self::Zed | Self::TextThread => "Zed Agent",
247 Self::NativeAgent => "Agent 2",
248 Self::Gemini => "Google Gemini",
249 Self::ClaudeCode => "Claude Code",
250 }
251 }
252
253 fn icon(self) -> IconName {
254 match self {
255 Self::Zed | Self::TextThread => IconName::AiZed,
256 Self::NativeAgent => IconName::ZedAssistant,
257 Self::Gemini => IconName::AiGemini,
258 Self::ClaudeCode => IconName::AiClaude,
259 }
260 }
261}
262
263impl ActiveView {
264 pub fn which_font_size_used(&self) -> WhichFontSize {
265 match self {
266 ActiveView::Thread { .. }
267 | ActiveView::ExternalAgentThread { .. }
268 | ActiveView::History => WhichFontSize::AgentFont,
269 ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
270 ActiveView::Configuration => WhichFontSize::None,
271 }
272 }
273
274 pub fn thread(
275 active_thread: Entity<ActiveThread>,
276 message_editor: Entity<MessageEditor>,
277 window: &mut Window,
278 cx: &mut Context<AgentPanel>,
279 ) -> Self {
280 let summary = active_thread.read(cx).summary(cx).or_default();
281
282 let editor = cx.new(|cx| {
283 let mut editor = Editor::single_line(window, cx);
284 editor.set_text(summary.clone(), window, cx);
285 editor
286 });
287
288 let subscriptions = vec![
289 cx.subscribe(&message_editor, |this, _, event, cx| match event {
290 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
291 cx.notify();
292 }
293 MessageEditorEvent::ScrollThreadToBottom => match &this.active_view {
294 ActiveView::Thread { thread, .. } => {
295 thread.update(cx, |thread, cx| {
296 thread.scroll_to_bottom(cx);
297 });
298 }
299 ActiveView::ExternalAgentThread { .. } => {}
300 ActiveView::TextThread { .. }
301 | ActiveView::History
302 | ActiveView::Configuration => {}
303 },
304 }),
305 window.subscribe(&editor, cx, {
306 {
307 let thread = active_thread.clone();
308 move |editor, event, window, cx| match event {
309 EditorEvent::BufferEdited => {
310 let new_summary = editor.read(cx).text(cx);
311
312 thread.update(cx, |thread, cx| {
313 thread.thread().update(cx, |thread, cx| {
314 thread.set_summary(new_summary, cx);
315 });
316 })
317 }
318 EditorEvent::Blurred => {
319 if editor.read(cx).text(cx).is_empty() {
320 let summary = thread.read(cx).summary(cx).or_default();
321
322 editor.update(cx, |editor, cx| {
323 editor.set_text(summary, window, cx);
324 });
325 }
326 }
327 _ => {}
328 }
329 }
330 }),
331 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
332 ActiveThreadEvent::EditingMessageTokenCountChanged => {
333 cx.notify();
334 }
335 }),
336 cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
337 let editor = editor.clone();
338 move |_, thread, event, window, cx| match event {
339 ThreadEvent::SummaryGenerated => {
340 let summary = thread.read(cx).summary().or_default();
341
342 editor.update(cx, |editor, cx| {
343 editor.set_text(summary, window, cx);
344 })
345 }
346 ThreadEvent::MessageAdded(_) => {
347 cx.notify();
348 }
349 _ => {}
350 }
351 }),
352 ];
353
354 Self::Thread {
355 change_title_editor: editor,
356 thread: active_thread,
357 message_editor: message_editor,
358 _subscriptions: subscriptions,
359 }
360 }
361
362 pub fn prompt_editor(
363 context_editor: Entity<TextThreadEditor>,
364 history_store: Entity<HistoryStore>,
365 language_registry: Arc<LanguageRegistry>,
366 window: &mut Window,
367 cx: &mut App,
368 ) -> Self {
369 let title = context_editor.read(cx).title(cx).to_string();
370
371 let editor = cx.new(|cx| {
372 let mut editor = Editor::single_line(window, cx);
373 editor.set_text(title, window, cx);
374 editor
375 });
376
377 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
378 // cause a custom summary to be set. The presence of this custom summary would cause
379 // summarization to not happen.
380 let mut suppress_first_edit = true;
381
382 let subscriptions = vec![
383 window.subscribe(&editor, cx, {
384 {
385 let context_editor = context_editor.clone();
386 move |editor, event, window, cx| match event {
387 EditorEvent::BufferEdited => {
388 if suppress_first_edit {
389 suppress_first_edit = false;
390 return;
391 }
392 let new_summary = editor.read(cx).text(cx);
393
394 context_editor.update(cx, |context_editor, cx| {
395 context_editor
396 .context()
397 .update(cx, |assistant_context, cx| {
398 assistant_context.set_custom_summary(new_summary, cx);
399 })
400 })
401 }
402 EditorEvent::Blurred => {
403 if editor.read(cx).text(cx).is_empty() {
404 let summary = context_editor
405 .read(cx)
406 .context()
407 .read(cx)
408 .summary()
409 .or_default();
410
411 editor.update(cx, |editor, cx| {
412 editor.set_text(summary, window, cx);
413 });
414 }
415 }
416 _ => {}
417 }
418 }
419 }),
420 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
421 let editor = editor.clone();
422 move |assistant_context, event, window, cx| match event {
423 ContextEvent::SummaryGenerated => {
424 let summary = assistant_context.read(cx).summary().or_default();
425
426 editor.update(cx, |editor, cx| {
427 editor.set_text(summary, window, cx);
428 })
429 }
430 ContextEvent::PathChanged { old_path, new_path } => {
431 history_store.update(cx, |history_store, cx| {
432 if let Some(old_path) = old_path {
433 history_store
434 .replace_recently_opened_text_thread(old_path, new_path, cx);
435 } else {
436 history_store.push_recently_opened_entry(
437 HistoryEntryId::Context(new_path.clone()),
438 cx,
439 );
440 }
441 });
442 }
443 _ => {}
444 }
445 }),
446 ];
447
448 let buffer_search_bar =
449 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
450 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
451 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
452 });
453
454 Self::TextThread {
455 context_editor,
456 title_editor: editor,
457 buffer_search_bar,
458 _subscriptions: subscriptions,
459 }
460 }
461}
462
463pub struct AgentPanel {
464 workspace: WeakEntity<Workspace>,
465 user_store: Entity<UserStore>,
466 project: Entity<Project>,
467 fs: Arc<dyn Fs>,
468 language_registry: Arc<LanguageRegistry>,
469 thread_store: Entity<ThreadStore>,
470 _default_model_subscription: Subscription,
471 context_store: Entity<TextThreadStore>,
472 prompt_store: Option<Entity<PromptStore>>,
473 inline_assist_context_store: Entity<ContextStore>,
474 configuration: Option<Entity<AgentConfiguration>>,
475 configuration_subscription: Option<Subscription>,
476 local_timezone: UtcOffset,
477 active_view: ActiveView,
478 previous_view: Option<ActiveView>,
479 history_store: Entity<HistoryStore>,
480 history: Entity<ThreadHistory>,
481 hovered_recent_history_item: Option<usize>,
482 new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
483 agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
484 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
485 assistant_navigation_menu: Option<Entity<ContextMenu>>,
486 width: Option<Pixels>,
487 height: Option<Pixels>,
488 zoomed: bool,
489 pending_serialization: Option<Task<Result<()>>>,
490 onboarding: Entity<AgentPanelOnboarding>,
491 selected_agent: AgentType,
492}
493
494impl AgentPanel {
495 fn serialize(&mut self, cx: &mut Context<Self>) {
496 let width = self.width;
497 let selected_agent = self.selected_agent;
498 self.pending_serialization = Some(cx.background_spawn(async move {
499 KEY_VALUE_STORE
500 .write_kvp(
501 AGENT_PANEL_KEY.into(),
502 serde_json::to_string(&SerializedAgentPanel {
503 width,
504 selected_agent: Some(selected_agent),
505 })?,
506 )
507 .await?;
508 anyhow::Ok(())
509 }));
510 }
511 pub fn load(
512 workspace: WeakEntity<Workspace>,
513 prompt_builder: Arc<PromptBuilder>,
514 mut cx: AsyncWindowContext,
515 ) -> Task<Result<Entity<Self>>> {
516 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
517 cx.spawn(async move |cx| {
518 let prompt_store = match prompt_store {
519 Ok(prompt_store) => prompt_store.await.ok(),
520 Err(_) => None,
521 };
522 let tools = cx.new(|_| ToolWorkingSet::default())?;
523 let thread_store = workspace
524 .update(cx, |workspace, cx| {
525 let project = workspace.project().clone();
526 ThreadStore::load(
527 project,
528 tools.clone(),
529 prompt_store.clone(),
530 prompt_builder.clone(),
531 cx,
532 )
533 })?
534 .await?;
535
536 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
537 let context_store = workspace
538 .update(cx, |workspace, cx| {
539 let project = workspace.project().clone();
540 assistant_context::ContextStore::new(
541 project,
542 prompt_builder.clone(),
543 slash_commands,
544 cx,
545 )
546 })?
547 .await?;
548
549 let serialized_panel = if let Some(panel) = cx
550 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
551 .await
552 .log_err()
553 .flatten()
554 {
555 Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
556 } else {
557 None
558 };
559
560 let panel = workspace.update_in(cx, |workspace, window, cx| {
561 let panel = cx.new(|cx| {
562 Self::new(
563 workspace,
564 thread_store,
565 context_store,
566 prompt_store,
567 window,
568 cx,
569 )
570 });
571 if let Some(serialized_panel) = serialized_panel {
572 panel.update(cx, |panel, cx| {
573 panel.width = serialized_panel.width.map(|w| w.round());
574 if let Some(selected_agent) = serialized_panel.selected_agent {
575 panel.selected_agent = selected_agent;
576 }
577 cx.notify();
578 });
579 }
580 panel
581 })?;
582
583 Ok(panel)
584 })
585 }
586
587 fn new(
588 workspace: &Workspace,
589 thread_store: Entity<ThreadStore>,
590 context_store: Entity<TextThreadStore>,
591 prompt_store: Option<Entity<PromptStore>>,
592 window: &mut Window,
593 cx: &mut Context<Self>,
594 ) -> Self {
595 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
596 let fs = workspace.app_state().fs.clone();
597 let user_store = workspace.app_state().user_store.clone();
598 let project = workspace.project();
599 let language_registry = project.read(cx).languages().clone();
600 let client = workspace.client().clone();
601 let workspace = workspace.weak_handle();
602 let weak_self = cx.entity().downgrade();
603
604 let message_editor_context_store =
605 cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
606 let inline_assist_context_store =
607 cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
608
609 let thread_id = thread.read(cx).id().clone();
610
611 let history_store = cx.new(|cx| {
612 HistoryStore::new(
613 thread_store.clone(),
614 context_store.clone(),
615 [HistoryEntryId::Thread(thread_id)],
616 cx,
617 )
618 });
619
620 let message_editor = cx.new(|cx| {
621 MessageEditor::new(
622 fs.clone(),
623 workspace.clone(),
624 message_editor_context_store.clone(),
625 prompt_store.clone(),
626 thread_store.downgrade(),
627 context_store.downgrade(),
628 Some(history_store.downgrade()),
629 thread.clone(),
630 window,
631 cx,
632 )
633 });
634
635 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
636
637 let active_thread = cx.new(|cx| {
638 ActiveThread::new(
639 thread.clone(),
640 thread_store.clone(),
641 context_store.clone(),
642 message_editor_context_store.clone(),
643 language_registry.clone(),
644 workspace.clone(),
645 window,
646 cx,
647 )
648 });
649
650 let panel_type = AgentSettings::get_global(cx).default_view;
651 let active_view = match panel_type {
652 DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx),
653 DefaultView::TextThread => {
654 let context =
655 context_store.update(cx, |context_store, cx| context_store.create(cx));
656 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
657 let context_editor = cx.new(|cx| {
658 let mut editor = TextThreadEditor::for_context(
659 context,
660 fs.clone(),
661 workspace.clone(),
662 project.clone(),
663 lsp_adapter_delegate,
664 window,
665 cx,
666 );
667 editor.insert_default_prompt(window, cx);
668 editor
669 });
670 ActiveView::prompt_editor(
671 context_editor,
672 history_store.clone(),
673 language_registry.clone(),
674 window,
675 cx,
676 )
677 }
678 };
679
680 AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
681
682 let weak_panel = weak_self.clone();
683
684 window.defer(cx, move |window, cx| {
685 let panel = weak_panel.clone();
686 let assistant_navigation_menu =
687 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
688 if let Some(panel) = panel.upgrade() {
689 menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
690 }
691 menu.action("View All", Box::new(OpenHistory))
692 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
693 .fixed_width(px(320.).into())
694 .keep_open_on_confirm(false)
695 .key_context("NavigationMenu")
696 });
697 weak_panel
698 .update(cx, |panel, cx| {
699 cx.subscribe_in(
700 &assistant_navigation_menu,
701 window,
702 |_, menu, _: &DismissEvent, window, cx| {
703 menu.update(cx, |menu, _| {
704 menu.clear_selected();
705 });
706 cx.focus_self(window);
707 },
708 )
709 .detach();
710 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
711 })
712 .ok();
713 });
714
715 let _default_model_subscription = cx.subscribe(
716 &LanguageModelRegistry::global(cx),
717 |this, _, event: &language_model::Event, cx| match event {
718 language_model::Event::DefaultModelChanged => match &this.active_view {
719 ActiveView::Thread { thread, .. } => {
720 thread
721 .read(cx)
722 .thread()
723 .clone()
724 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
725 }
726 ActiveView::ExternalAgentThread { .. }
727 | ActiveView::TextThread { .. }
728 | ActiveView::History
729 | ActiveView::Configuration => {}
730 },
731 _ => {}
732 },
733 );
734
735 let onboarding = cx.new(|cx| {
736 AgentPanelOnboarding::new(
737 user_store.clone(),
738 client,
739 |_window, cx| {
740 OnboardingUpsell::set_dismissed(true, cx);
741 },
742 cx,
743 )
744 });
745
746 Self {
747 active_view,
748 workspace,
749 user_store,
750 project: project.clone(),
751 fs: fs.clone(),
752 language_registry,
753 thread_store: thread_store.clone(),
754 _default_model_subscription,
755 context_store,
756 prompt_store,
757 configuration: None,
758 configuration_subscription: None,
759 local_timezone: UtcOffset::from_whole_seconds(
760 chrono::Local::now().offset().local_minus_utc(),
761 )
762 .unwrap(),
763 inline_assist_context_store,
764 previous_view: None,
765 history_store: history_store.clone(),
766 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
767 hovered_recent_history_item: None,
768 new_thread_menu_handle: PopoverMenuHandle::default(),
769 agent_panel_menu_handle: PopoverMenuHandle::default(),
770 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
771 assistant_navigation_menu: None,
772 width: None,
773 height: None,
774 zoomed: false,
775 pending_serialization: None,
776 onboarding,
777 selected_agent: AgentType::default(),
778 }
779 }
780
781 pub fn toggle_focus(
782 workspace: &mut Workspace,
783 _: &ToggleFocus,
784 window: &mut Window,
785 cx: &mut Context<Workspace>,
786 ) {
787 if workspace
788 .panel::<Self>(cx)
789 .is_some_and(|panel| panel.read(cx).enabled(cx))
790 && !DisableAiSettings::get_global(cx).disable_ai
791 {
792 workspace.toggle_panel_focus::<Self>(window, cx);
793 }
794 }
795
796 pub(crate) fn local_timezone(&self) -> UtcOffset {
797 self.local_timezone
798 }
799
800 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
801 &self.prompt_store
802 }
803
804 pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
805 &self.inline_assist_context_store
806 }
807
808 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
809 &self.thread_store
810 }
811
812 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
813 &self.context_store
814 }
815
816 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
817 match &self.active_view {
818 ActiveView::Thread { thread, .. } => {
819 thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
820 }
821 ActiveView::ExternalAgentThread { .. }
822 | ActiveView::TextThread { .. }
823 | ActiveView::History
824 | ActiveView::Configuration => {}
825 }
826 }
827
828 fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
829 match &self.active_view {
830 ActiveView::Thread { message_editor, .. } => Some(message_editor),
831 ActiveView::ExternalAgentThread { .. }
832 | ActiveView::TextThread { .. }
833 | ActiveView::History
834 | ActiveView::Configuration => None,
835 }
836 }
837
838 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
839 // Preserve chat box text when using creating new thread
840 let preserved_text = self
841 .active_message_editor()
842 .map(|editor| editor.read(cx).get_text(cx).trim().to_string());
843
844 let thread = self
845 .thread_store
846 .update(cx, |this, cx| this.create_thread(cx));
847
848 let context_store = cx.new(|_cx| {
849 ContextStore::new(
850 self.project.downgrade(),
851 Some(self.thread_store.downgrade()),
852 )
853 });
854
855 if let Some(other_thread_id) = action.from_thread_id.clone() {
856 let other_thread_task = self.thread_store.update(cx, |this, cx| {
857 this.open_thread(&other_thread_id, window, cx)
858 });
859
860 cx.spawn({
861 let context_store = context_store.clone();
862
863 async move |_panel, cx| {
864 let other_thread = other_thread_task.await?;
865
866 context_store.update(cx, |this, cx| {
867 this.add_thread(other_thread, false, cx);
868 })?;
869 anyhow::Ok(())
870 }
871 })
872 .detach_and_log_err(cx);
873 }
874
875 let active_thread = cx.new(|cx| {
876 ActiveThread::new(
877 thread.clone(),
878 self.thread_store.clone(),
879 self.context_store.clone(),
880 context_store.clone(),
881 self.language_registry.clone(),
882 self.workspace.clone(),
883 window,
884 cx,
885 )
886 });
887
888 let message_editor = cx.new(|cx| {
889 MessageEditor::new(
890 self.fs.clone(),
891 self.workspace.clone(),
892 context_store.clone(),
893 self.prompt_store.clone(),
894 self.thread_store.downgrade(),
895 self.context_store.downgrade(),
896 Some(self.history_store.downgrade()),
897 thread.clone(),
898 window,
899 cx,
900 )
901 });
902
903 if let Some(text) = preserved_text {
904 message_editor.update(cx, |editor, cx| {
905 editor.set_text(text, window, cx);
906 });
907 }
908
909 message_editor.focus_handle(cx).focus(window);
910
911 let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
912 self.set_active_view(thread_view, window, cx);
913
914 AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
915 }
916
917 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
918 let context = self
919 .context_store
920 .update(cx, |context_store, cx| context_store.create(cx));
921 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
922 .log_err()
923 .flatten();
924
925 let context_editor = cx.new(|cx| {
926 let mut editor = TextThreadEditor::for_context(
927 context,
928 self.fs.clone(),
929 self.workspace.clone(),
930 self.project.clone(),
931 lsp_adapter_delegate,
932 window,
933 cx,
934 );
935 editor.insert_default_prompt(window, cx);
936 editor
937 });
938
939 self.set_active_view(
940 ActiveView::prompt_editor(
941 context_editor.clone(),
942 self.history_store.clone(),
943 self.language_registry.clone(),
944 window,
945 cx,
946 ),
947 window,
948 cx,
949 );
950 context_editor.focus_handle(cx).focus(window);
951 }
952
953 fn new_external_thread(
954 &mut self,
955 agent_choice: Option<crate::ExternalAgent>,
956 window: &mut Window,
957 cx: &mut Context<Self>,
958 ) {
959 let workspace = self.workspace.clone();
960 let project = self.project.clone();
961 let fs = self.fs.clone();
962
963 const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
964
965 #[derive(Default, Serialize, Deserialize)]
966 struct LastUsedExternalAgent {
967 agent: crate::ExternalAgent,
968 }
969
970 let thread_store = self.thread_store.clone();
971 let text_thread_store = self.context_store.clone();
972
973 cx.spawn_in(window, async move |this, cx| {
974 let server: Rc<dyn AgentServer> = match agent_choice {
975 Some(agent) => {
976 cx.background_spawn(async move {
977 if let Some(serialized) =
978 serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
979 {
980 KEY_VALUE_STORE
981 .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
982 .await
983 .log_err();
984 }
985 })
986 .detach();
987
988 agent.server(fs)
989 }
990 None => cx
991 .background_spawn(async move {
992 KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
993 })
994 .await
995 .log_err()
996 .flatten()
997 .and_then(|value| {
998 serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
999 })
1000 .unwrap_or_default()
1001 .agent
1002 .server(fs),
1003 };
1004
1005 this.update_in(cx, |this, window, cx| {
1006 let thread_view = cx.new(|cx| {
1007 crate::acp::AcpThreadView::new(
1008 server,
1009 workspace.clone(),
1010 project,
1011 thread_store.clone(),
1012 text_thread_store.clone(),
1013 window,
1014 cx,
1015 )
1016 });
1017
1018 this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
1019 })
1020 })
1021 .detach_and_log_err(cx);
1022 }
1023
1024 fn deploy_rules_library(
1025 &mut self,
1026 action: &OpenRulesLibrary,
1027 _window: &mut Window,
1028 cx: &mut Context<Self>,
1029 ) {
1030 open_rules_library(
1031 self.language_registry.clone(),
1032 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1033 Rc::new(|| {
1034 Rc::new(SlashCommandCompletionProvider::new(
1035 Arc::new(SlashCommandWorkingSet::default()),
1036 None,
1037 None,
1038 ))
1039 }),
1040 action
1041 .prompt_to_select
1042 .map(|uuid| UserPromptId(uuid).into()),
1043 cx,
1044 )
1045 .detach_and_log_err(cx);
1046 }
1047
1048 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1049 if matches!(self.active_view, ActiveView::History) {
1050 if let Some(previous_view) = self.previous_view.take() {
1051 self.set_active_view(previous_view, window, cx);
1052 }
1053 } else {
1054 self.thread_store
1055 .update(cx, |thread_store, cx| thread_store.reload(cx))
1056 .detach_and_log_err(cx);
1057 self.set_active_view(ActiveView::History, window, cx);
1058 }
1059 cx.notify();
1060 }
1061
1062 pub(crate) fn open_saved_prompt_editor(
1063 &mut self,
1064 path: Arc<Path>,
1065 window: &mut Window,
1066 cx: &mut Context<Self>,
1067 ) -> Task<Result<()>> {
1068 let context = self
1069 .context_store
1070 .update(cx, |store, cx| store.open_local_context(path, cx));
1071 cx.spawn_in(window, async move |this, cx| {
1072 let context = context.await?;
1073 this.update_in(cx, |this, window, cx| {
1074 this.open_prompt_editor(context, window, cx);
1075 })
1076 })
1077 }
1078
1079 pub(crate) fn open_prompt_editor(
1080 &mut self,
1081 context: Entity<AssistantContext>,
1082 window: &mut Window,
1083 cx: &mut Context<Self>,
1084 ) {
1085 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1086 .log_err()
1087 .flatten();
1088 let editor = cx.new(|cx| {
1089 TextThreadEditor::for_context(
1090 context,
1091 self.fs.clone(),
1092 self.workspace.clone(),
1093 self.project.clone(),
1094 lsp_adapter_delegate,
1095 window,
1096 cx,
1097 )
1098 });
1099 self.set_active_view(
1100 ActiveView::prompt_editor(
1101 editor.clone(),
1102 self.history_store.clone(),
1103 self.language_registry.clone(),
1104 window,
1105 cx,
1106 ),
1107 window,
1108 cx,
1109 );
1110 }
1111
1112 pub(crate) fn open_thread_by_id(
1113 &mut self,
1114 thread_id: &ThreadId,
1115 window: &mut Window,
1116 cx: &mut Context<Self>,
1117 ) -> Task<Result<()>> {
1118 let open_thread_task = self
1119 .thread_store
1120 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
1121 cx.spawn_in(window, async move |this, cx| {
1122 let thread = open_thread_task.await?;
1123 this.update_in(cx, |this, window, cx| {
1124 this.open_thread(thread, window, cx);
1125 anyhow::Ok(())
1126 })??;
1127 Ok(())
1128 })
1129 }
1130
1131 pub(crate) fn open_thread(
1132 &mut self,
1133 thread: Entity<Thread>,
1134 window: &mut Window,
1135 cx: &mut Context<Self>,
1136 ) {
1137 let context_store = cx.new(|_cx| {
1138 ContextStore::new(
1139 self.project.downgrade(),
1140 Some(self.thread_store.downgrade()),
1141 )
1142 });
1143
1144 let active_thread = cx.new(|cx| {
1145 ActiveThread::new(
1146 thread.clone(),
1147 self.thread_store.clone(),
1148 self.context_store.clone(),
1149 context_store.clone(),
1150 self.language_registry.clone(),
1151 self.workspace.clone(),
1152 window,
1153 cx,
1154 )
1155 });
1156
1157 let message_editor = cx.new(|cx| {
1158 MessageEditor::new(
1159 self.fs.clone(),
1160 self.workspace.clone(),
1161 context_store,
1162 self.prompt_store.clone(),
1163 self.thread_store.downgrade(),
1164 self.context_store.downgrade(),
1165 Some(self.history_store.downgrade()),
1166 thread.clone(),
1167 window,
1168 cx,
1169 )
1170 });
1171 message_editor.focus_handle(cx).focus(window);
1172
1173 let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
1174 self.set_active_view(thread_view, window, cx);
1175 AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
1176 }
1177
1178 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1179 match self.active_view {
1180 ActiveView::Configuration | ActiveView::History => {
1181 if let Some(previous_view) = self.previous_view.take() {
1182 self.active_view = previous_view;
1183
1184 match &self.active_view {
1185 ActiveView::Thread { message_editor, .. } => {
1186 message_editor.focus_handle(cx).focus(window);
1187 }
1188 ActiveView::ExternalAgentThread { thread_view } => {
1189 thread_view.focus_handle(cx).focus(window);
1190 }
1191 ActiveView::TextThread { context_editor, .. } => {
1192 context_editor.focus_handle(cx).focus(window);
1193 }
1194 ActiveView::History | ActiveView::Configuration => {}
1195 }
1196 }
1197 cx.notify();
1198 }
1199 _ => {}
1200 }
1201 }
1202
1203 pub fn toggle_navigation_menu(
1204 &mut self,
1205 _: &ToggleNavigationMenu,
1206 window: &mut Window,
1207 cx: &mut Context<Self>,
1208 ) {
1209 self.assistant_navigation_menu_handle.toggle(window, cx);
1210 }
1211
1212 pub fn toggle_options_menu(
1213 &mut self,
1214 _: &ToggleOptionsMenu,
1215 window: &mut Window,
1216 cx: &mut Context<Self>,
1217 ) {
1218 self.agent_panel_menu_handle.toggle(window, cx);
1219 }
1220
1221 pub fn toggle_new_thread_menu(
1222 &mut self,
1223 _: &ToggleNewThreadMenu,
1224 window: &mut Window,
1225 cx: &mut Context<Self>,
1226 ) {
1227 self.new_thread_menu_handle.toggle(window, cx);
1228 }
1229
1230 pub fn increase_font_size(
1231 &mut self,
1232 action: &IncreaseBufferFontSize,
1233 _: &mut Window,
1234 cx: &mut Context<Self>,
1235 ) {
1236 self.handle_font_size_action(action.persist, px(1.0), cx);
1237 }
1238
1239 pub fn decrease_font_size(
1240 &mut self,
1241 action: &DecreaseBufferFontSize,
1242 _: &mut Window,
1243 cx: &mut Context<Self>,
1244 ) {
1245 self.handle_font_size_action(action.persist, px(-1.0), cx);
1246 }
1247
1248 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1249 match self.active_view.which_font_size_used() {
1250 WhichFontSize::AgentFont => {
1251 if persist {
1252 update_settings_file::<ThemeSettings>(
1253 self.fs.clone(),
1254 cx,
1255 move |settings, cx| {
1256 let agent_font_size =
1257 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1258 let _ = settings
1259 .agent_font_size
1260 .insert(Some(theme::clamp_font_size(agent_font_size).into()));
1261 },
1262 );
1263 } else {
1264 theme::adjust_agent_font_size(cx, |size| size + delta);
1265 }
1266 }
1267 WhichFontSize::BufferFont => {
1268 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1269 // default handler that changes that font size.
1270 cx.propagate();
1271 }
1272 WhichFontSize::None => {}
1273 }
1274 }
1275
1276 pub fn reset_font_size(
1277 &mut self,
1278 action: &ResetBufferFontSize,
1279 _: &mut Window,
1280 cx: &mut Context<Self>,
1281 ) {
1282 if action.persist {
1283 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1284 settings.agent_font_size = None;
1285 });
1286 } else {
1287 theme::reset_agent_font_size(cx);
1288 }
1289 }
1290
1291 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1292 if self.zoomed {
1293 cx.emit(PanelEvent::ZoomOut);
1294 } else {
1295 if !self.focus_handle(cx).contains_focused(window, cx) {
1296 cx.focus_self(window);
1297 }
1298 cx.emit(PanelEvent::ZoomIn);
1299 }
1300 }
1301
1302 pub fn open_agent_diff(
1303 &mut self,
1304 _: &OpenAgentDiff,
1305 window: &mut Window,
1306 cx: &mut Context<Self>,
1307 ) {
1308 match &self.active_view {
1309 ActiveView::Thread { thread, .. } => {
1310 let thread = thread.read(cx).thread().clone();
1311 self.workspace
1312 .update(cx, |workspace, cx| {
1313 AgentDiffPane::deploy_in_workspace(
1314 AgentDiffThread::Native(thread),
1315 workspace,
1316 window,
1317 cx,
1318 )
1319 })
1320 .log_err();
1321 }
1322 ActiveView::ExternalAgentThread { .. }
1323 | ActiveView::TextThread { .. }
1324 | ActiveView::History
1325 | ActiveView::Configuration => {}
1326 }
1327 }
1328
1329 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1330 let context_server_store = self.project.read(cx).context_server_store();
1331 let tools = self.thread_store.read(cx).tools();
1332 let fs = self.fs.clone();
1333
1334 self.set_active_view(ActiveView::Configuration, window, cx);
1335 self.configuration = Some(cx.new(|cx| {
1336 AgentConfiguration::new(
1337 fs,
1338 context_server_store,
1339 tools,
1340 self.language_registry.clone(),
1341 self.workspace.clone(),
1342 window,
1343 cx,
1344 )
1345 }));
1346
1347 if let Some(configuration) = self.configuration.as_ref() {
1348 self.configuration_subscription = Some(cx.subscribe_in(
1349 configuration,
1350 window,
1351 Self::handle_agent_configuration_event,
1352 ));
1353
1354 configuration.focus_handle(cx).focus(window);
1355 }
1356 }
1357
1358 pub(crate) fn open_active_thread_as_markdown(
1359 &mut self,
1360 _: &OpenActiveThreadAsMarkdown,
1361 window: &mut Window,
1362 cx: &mut Context<Self>,
1363 ) {
1364 let Some(workspace) = self.workspace.upgrade() else {
1365 return;
1366 };
1367
1368 match &self.active_view {
1369 ActiveView::Thread { thread, .. } => {
1370 active_thread::open_active_thread_as_markdown(
1371 thread.read(cx).thread().clone(),
1372 workspace,
1373 window,
1374 cx,
1375 )
1376 .detach_and_log_err(cx);
1377 }
1378 ActiveView::ExternalAgentThread { thread_view } => {
1379 thread_view
1380 .update(cx, |thread_view, cx| {
1381 thread_view.open_thread_as_markdown(workspace, window, cx)
1382 })
1383 .detach_and_log_err(cx);
1384 }
1385 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1386 }
1387 }
1388
1389 fn handle_agent_configuration_event(
1390 &mut self,
1391 _entity: &Entity<AgentConfiguration>,
1392 event: &AssistantConfigurationEvent,
1393 window: &mut Window,
1394 cx: &mut Context<Self>,
1395 ) {
1396 match event {
1397 AssistantConfigurationEvent::NewThread(provider) => {
1398 if LanguageModelRegistry::read_global(cx)
1399 .default_model()
1400 .map_or(true, |model| model.provider.id() != provider.id())
1401 {
1402 if let Some(model) = provider.default_model(cx) {
1403 update_settings_file::<AgentSettings>(
1404 self.fs.clone(),
1405 cx,
1406 move |settings, _| settings.set_model(model),
1407 );
1408 }
1409 }
1410
1411 self.new_thread(&NewThread::default(), window, cx);
1412 if let Some((thread, model)) =
1413 self.active_thread(cx).zip(provider.default_model(cx))
1414 {
1415 thread.update(cx, |thread, cx| {
1416 thread.set_configured_model(
1417 Some(ConfiguredModel {
1418 provider: provider.clone(),
1419 model,
1420 }),
1421 cx,
1422 );
1423 });
1424 }
1425 }
1426 }
1427 }
1428
1429 pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
1430 match &self.active_view {
1431 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1432 _ => None,
1433 }
1434 }
1435
1436 pub(crate) fn delete_thread(
1437 &mut self,
1438 thread_id: &ThreadId,
1439 cx: &mut Context<Self>,
1440 ) -> Task<Result<()>> {
1441 self.thread_store
1442 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1443 }
1444
1445 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1446 let ActiveView::Thread { thread, .. } = &self.active_view else {
1447 return;
1448 };
1449
1450 let thread_state = thread.read(cx).thread().read(cx);
1451 if !thread_state.tool_use_limit_reached() {
1452 return;
1453 }
1454
1455 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1456 if let Some(model) = model {
1457 thread.update(cx, |active_thread, cx| {
1458 active_thread.thread().update(cx, |thread, cx| {
1459 thread.insert_invisible_continue_message(cx);
1460 thread.advance_prompt_id();
1461 thread.send_to_model(
1462 model,
1463 CompletionIntent::UserPrompt,
1464 Some(window.window_handle()),
1465 cx,
1466 );
1467 });
1468 });
1469 } else {
1470 log::warn!("No configured model available for continuation");
1471 }
1472 }
1473
1474 fn toggle_burn_mode(
1475 &mut self,
1476 _: &ToggleBurnMode,
1477 _window: &mut Window,
1478 cx: &mut Context<Self>,
1479 ) {
1480 let ActiveView::Thread { thread, .. } = &self.active_view else {
1481 return;
1482 };
1483
1484 thread.update(cx, |active_thread, cx| {
1485 active_thread.thread().update(cx, |thread, _cx| {
1486 let current_mode = thread.completion_mode();
1487
1488 thread.set_completion_mode(match current_mode {
1489 CompletionMode::Burn => CompletionMode::Normal,
1490 CompletionMode::Normal => CompletionMode::Burn,
1491 });
1492 });
1493 });
1494 }
1495
1496 pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
1497 match &self.active_view {
1498 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1499 _ => None,
1500 }
1501 }
1502
1503 pub(crate) fn delete_context(
1504 &mut self,
1505 path: Arc<Path>,
1506 cx: &mut Context<Self>,
1507 ) -> Task<Result<()>> {
1508 self.context_store
1509 .update(cx, |this, cx| this.delete_local_context(path, cx))
1510 }
1511
1512 fn set_active_view(
1513 &mut self,
1514 new_view: ActiveView,
1515 window: &mut Window,
1516 cx: &mut Context<Self>,
1517 ) {
1518 let current_is_history = matches!(self.active_view, ActiveView::History);
1519 let new_is_history = matches!(new_view, ActiveView::History);
1520
1521 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1522 let new_is_config = matches!(new_view, ActiveView::Configuration);
1523
1524 let current_is_special = current_is_history || current_is_config;
1525 let new_is_special = new_is_history || new_is_config;
1526
1527 match &self.active_view {
1528 ActiveView::Thread { thread, .. } => {
1529 let thread = thread.read(cx);
1530 if thread.is_empty() {
1531 let id = thread.thread().read(cx).id().clone();
1532 self.history_store.update(cx, |store, cx| {
1533 store.remove_recently_opened_thread(id, cx);
1534 });
1535 }
1536 }
1537 _ => {}
1538 }
1539
1540 match &new_view {
1541 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1542 let id = thread.read(cx).thread().read(cx).id().clone();
1543 store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
1544 }),
1545 ActiveView::TextThread { context_editor, .. } => {
1546 self.history_store.update(cx, |store, cx| {
1547 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1548 store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
1549 }
1550 })
1551 }
1552 ActiveView::ExternalAgentThread { .. } => {}
1553 ActiveView::History | ActiveView::Configuration => {}
1554 }
1555
1556 if current_is_special && !new_is_special {
1557 self.active_view = new_view;
1558 } else if !current_is_special && new_is_special {
1559 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1560 } else {
1561 if !new_is_special {
1562 self.previous_view = None;
1563 }
1564 self.active_view = new_view;
1565 }
1566
1567 self.focus_handle(cx).focus(window);
1568 }
1569
1570 fn populate_recently_opened_menu_section(
1571 mut menu: ContextMenu,
1572 panel: Entity<Self>,
1573 cx: &mut Context<ContextMenu>,
1574 ) -> ContextMenu {
1575 let entries = panel
1576 .read(cx)
1577 .history_store
1578 .read(cx)
1579 .recently_opened_entries(cx);
1580
1581 if entries.is_empty() {
1582 return menu;
1583 }
1584
1585 menu = menu.header("Recently Opened");
1586
1587 for entry in entries {
1588 let title = entry.title().clone();
1589 let id = entry.id();
1590
1591 menu = menu.entry_with_end_slot_on_hover(
1592 title,
1593 None,
1594 {
1595 let panel = panel.downgrade();
1596 let id = id.clone();
1597 move |window, cx| {
1598 let id = id.clone();
1599 panel
1600 .update(cx, move |this, cx| match id {
1601 HistoryEntryId::Thread(id) => this
1602 .open_thread_by_id(&id, window, cx)
1603 .detach_and_log_err(cx),
1604 HistoryEntryId::Context(path) => this
1605 .open_saved_prompt_editor(path.clone(), window, cx)
1606 .detach_and_log_err(cx),
1607 })
1608 .ok();
1609 }
1610 },
1611 IconName::Close,
1612 "Close Entry".into(),
1613 {
1614 let panel = panel.downgrade();
1615 let id = id.clone();
1616 move |_window, cx| {
1617 panel
1618 .update(cx, |this, cx| {
1619 this.history_store.update(cx, |history_store, cx| {
1620 history_store.remove_recently_opened_entry(&id, cx);
1621 });
1622 })
1623 .ok();
1624 }
1625 },
1626 );
1627 }
1628
1629 menu = menu.separator();
1630
1631 menu
1632 }
1633
1634 pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context<Self>) {
1635 if self.selected_agent != agent {
1636 self.selected_agent = agent;
1637 self.serialize(cx);
1638 }
1639 }
1640
1641 pub fn selected_agent(&self) -> AgentType {
1642 self.selected_agent
1643 }
1644}
1645
1646impl Focusable for AgentPanel {
1647 fn focus_handle(&self, cx: &App) -> FocusHandle {
1648 match &self.active_view {
1649 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1650 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1651 ActiveView::History => self.history.focus_handle(cx),
1652 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1653 ActiveView::Configuration => {
1654 if let Some(configuration) = self.configuration.as_ref() {
1655 configuration.focus_handle(cx)
1656 } else {
1657 cx.focus_handle()
1658 }
1659 }
1660 }
1661 }
1662}
1663
1664fn agent_panel_dock_position(cx: &App) -> DockPosition {
1665 match AgentSettings::get_global(cx).dock {
1666 AgentDockPosition::Left => DockPosition::Left,
1667 AgentDockPosition::Bottom => DockPosition::Bottom,
1668 AgentDockPosition::Right => DockPosition::Right,
1669 }
1670}
1671
1672impl EventEmitter<PanelEvent> for AgentPanel {}
1673
1674impl Panel for AgentPanel {
1675 fn persistent_name() -> &'static str {
1676 "AgentPanel"
1677 }
1678
1679 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1680 agent_panel_dock_position(cx)
1681 }
1682
1683 fn position_is_valid(&self, position: DockPosition) -> bool {
1684 position != DockPosition::Bottom
1685 }
1686
1687 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1688 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1689 let dock = match position {
1690 DockPosition::Left => AgentDockPosition::Left,
1691 DockPosition::Bottom => AgentDockPosition::Bottom,
1692 DockPosition::Right => AgentDockPosition::Right,
1693 };
1694 settings.set_dock(dock);
1695 });
1696 }
1697
1698 fn size(&self, window: &Window, cx: &App) -> Pixels {
1699 let settings = AgentSettings::get_global(cx);
1700 match self.position(window, cx) {
1701 DockPosition::Left | DockPosition::Right => {
1702 self.width.unwrap_or(settings.default_width)
1703 }
1704 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1705 }
1706 }
1707
1708 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1709 match self.position(window, cx) {
1710 DockPosition::Left | DockPosition::Right => self.width = size,
1711 DockPosition::Bottom => self.height = size,
1712 }
1713 self.serialize(cx);
1714 cx.notify();
1715 }
1716
1717 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1718
1719 fn remote_id() -> Option<proto::PanelId> {
1720 Some(proto::PanelId::AssistantPanel)
1721 }
1722
1723 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1724 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1725 }
1726
1727 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1728 Some("Agent Panel")
1729 }
1730
1731 fn toggle_action(&self) -> Box<dyn Action> {
1732 Box::new(ToggleFocus)
1733 }
1734
1735 fn activation_priority(&self) -> u32 {
1736 3
1737 }
1738
1739 fn enabled(&self, cx: &App) -> bool {
1740 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
1741 }
1742
1743 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1744 self.zoomed
1745 }
1746
1747 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1748 self.zoomed = zoomed;
1749 cx.notify();
1750 }
1751}
1752
1753impl AgentPanel {
1754 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1755 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1756
1757 let content = match &self.active_view {
1758 ActiveView::Thread {
1759 thread: active_thread,
1760 change_title_editor,
1761 ..
1762 } => {
1763 let state = {
1764 let active_thread = active_thread.read(cx);
1765 if active_thread.is_empty() {
1766 &ThreadSummary::Pending
1767 } else {
1768 active_thread.summary(cx)
1769 }
1770 };
1771
1772 match state {
1773 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1774 .truncate()
1775 .into_any_element(),
1776 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1777 .truncate()
1778 .into_any_element(),
1779 ThreadSummary::Ready(_) => div()
1780 .w_full()
1781 .child(change_title_editor.clone())
1782 .into_any_element(),
1783 ThreadSummary::Error => h_flex()
1784 .w_full()
1785 .child(change_title_editor.clone())
1786 .child(
1787 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1788 .icon_size(IconSize::Small)
1789 .on_click({
1790 let active_thread = active_thread.clone();
1791 move |_, _window, cx| {
1792 active_thread.update(cx, |thread, cx| {
1793 thread.regenerate_summary(cx);
1794 });
1795 }
1796 })
1797 .tooltip(move |_window, cx| {
1798 cx.new(|_| {
1799 Tooltip::new("Failed to generate title")
1800 .meta("Click to try again")
1801 })
1802 .into()
1803 }),
1804 )
1805 .into_any_element(),
1806 }
1807 }
1808 ActiveView::ExternalAgentThread { thread_view } => {
1809 Label::new(thread_view.read(cx).title(cx))
1810 .truncate()
1811 .into_any_element()
1812 }
1813 ActiveView::TextThread {
1814 title_editor,
1815 context_editor,
1816 ..
1817 } => {
1818 let summary = context_editor.read(cx).context().read(cx).summary();
1819
1820 match summary {
1821 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1822 .truncate()
1823 .into_any_element(),
1824 ContextSummary::Content(summary) => {
1825 if summary.done {
1826 div()
1827 .w_full()
1828 .child(title_editor.clone())
1829 .into_any_element()
1830 } else {
1831 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1832 .truncate()
1833 .into_any_element()
1834 }
1835 }
1836 ContextSummary::Error => h_flex()
1837 .w_full()
1838 .child(title_editor.clone())
1839 .child(
1840 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1841 .icon_size(IconSize::Small)
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(&self, cx: &mut Context<Self>) -> impl IntoElement {
1980 let focus_handle = self.focus_handle(cx);
1981
1982 PopoverMenu::new("agent-nav-menu")
1983 .trigger_with_tooltip(
1984 IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small),
1985 {
1986 let focus_handle = focus_handle.clone();
1987 move |window, cx| {
1988 Tooltip::for_action_in(
1989 "Toggle Recent Threads",
1990 &ToggleNavigationMenu,
1991 &focus_handle,
1992 window,
1993 cx,
1994 )
1995 }
1996 },
1997 )
1998 .anchor(Corner::TopLeft)
1999 .with_handle(self.assistant_navigation_menu_handle.clone())
2000 .menu({
2001 let menu = self.assistant_navigation_menu.clone();
2002 move |window, cx| {
2003 if let Some(menu) = menu.as_ref() {
2004 menu.update(cx, |_, cx| {
2005 cx.defer_in(window, |menu, window, cx| {
2006 menu.rebuild(window, cx);
2007 });
2008 })
2009 }
2010 menu.clone()
2011 }
2012 })
2013 }
2014
2015 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2016 let focus_handle = self.focus_handle(cx);
2017
2018 IconButton::new("go-back", IconName::ArrowLeft)
2019 .icon_size(IconSize::Small)
2020 .on_click(cx.listener(|this, _, window, cx| {
2021 this.go_back(&workspace::GoBack, window, cx);
2022 }))
2023 .tooltip({
2024 let focus_handle = focus_handle.clone();
2025
2026 move |window, cx| {
2027 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
2028 }
2029 })
2030 }
2031
2032 fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2033 let focus_handle = self.focus_handle(cx);
2034
2035 let active_thread = match &self.active_view {
2036 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2037 ActiveView::ExternalAgentThread { .. }
2038 | ActiveView::TextThread { .. }
2039 | ActiveView::History
2040 | ActiveView::Configuration => None,
2041 };
2042
2043 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2044 .trigger_with_tooltip(
2045 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2046 Tooltip::text("New Thread…"),
2047 )
2048 .anchor(Corner::TopRight)
2049 .with_handle(self.new_thread_menu_handle.clone())
2050 .menu({
2051 let focus_handle = focus_handle.clone();
2052 move |window, cx| {
2053 let active_thread = active_thread.clone();
2054 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2055 menu = menu
2056 .context(focus_handle.clone())
2057 .when_some(active_thread, |this, active_thread| {
2058 let thread = active_thread.read(cx);
2059
2060 if !thread.is_empty() {
2061 let thread_id = thread.id().clone();
2062 this.item(
2063 ContextMenuEntry::new("New From Summary")
2064 .icon(IconName::ThreadFromSummary)
2065 .icon_color(Color::Muted)
2066 .handler(move |window, cx| {
2067 window.dispatch_action(
2068 Box::new(NewThread {
2069 from_thread_id: Some(thread_id.clone()),
2070 }),
2071 cx,
2072 );
2073 }),
2074 )
2075 } else {
2076 this
2077 }
2078 })
2079 .item(
2080 ContextMenuEntry::new("New Thread")
2081 .icon(IconName::Thread)
2082 .icon_color(Color::Muted)
2083 .action(NewThread::default().boxed_clone())
2084 .handler(move |window, cx| {
2085 window.dispatch_action(
2086 NewThread::default().boxed_clone(),
2087 cx,
2088 );
2089 }),
2090 )
2091 .item(
2092 ContextMenuEntry::new("New Text Thread")
2093 .icon(IconName::TextThread)
2094 .icon_color(Color::Muted)
2095 .action(NewTextThread.boxed_clone())
2096 .handler(move |window, cx| {
2097 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2098 }),
2099 );
2100 menu
2101 }))
2102 }
2103 });
2104
2105 h_flex()
2106 .id("assistant-toolbar")
2107 .h(Tab::container_height(cx))
2108 .max_w_full()
2109 .flex_none()
2110 .justify_between()
2111 .gap_2()
2112 .bg(cx.theme().colors().tab_bar_background)
2113 .border_b_1()
2114 .border_color(cx.theme().colors().border)
2115 .child(
2116 h_flex()
2117 .size_full()
2118 .pl_1()
2119 .gap_1()
2120 .child(match &self.active_view {
2121 ActiveView::History | ActiveView::Configuration => div()
2122 .pl(DynamicSpacing::Base04.rems(cx))
2123 .child(self.render_toolbar_back_button(cx))
2124 .into_any_element(),
2125 _ => self.render_recent_entries_menu(cx).into_any_element(),
2126 })
2127 .child(self.render_title_view(window, cx)),
2128 )
2129 .child(
2130 h_flex()
2131 .h_full()
2132 .gap_2()
2133 .children(self.render_token_count(cx))
2134 .child(
2135 h_flex()
2136 .h_full()
2137 .gap(DynamicSpacing::Base02.rems(cx))
2138 .px(DynamicSpacing::Base08.rems(cx))
2139 .border_l_1()
2140 .border_color(cx.theme().colors().border)
2141 .child(new_thread_menu)
2142 .child(self.render_panel_options_menu(window, cx)),
2143 ),
2144 )
2145 }
2146
2147 fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2148 let focus_handle = self.focus_handle(cx);
2149
2150 let active_thread = match &self.active_view {
2151 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2152 ActiveView::ExternalAgentThread { .. }
2153 | ActiveView::TextThread { .. }
2154 | ActiveView::History
2155 | ActiveView::Configuration => None,
2156 };
2157
2158 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2159 .trigger_with_tooltip(
2160 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2161 {
2162 let focus_handle = focus_handle.clone();
2163 move |window, cx| {
2164 Tooltip::for_action_in(
2165 "New…",
2166 &ToggleNewThreadMenu,
2167 &focus_handle,
2168 window,
2169 cx,
2170 )
2171 }
2172 },
2173 )
2174 .anchor(Corner::TopLeft)
2175 .with_handle(self.new_thread_menu_handle.clone())
2176 .menu({
2177 let focus_handle = focus_handle.clone();
2178 let workspace = self.workspace.clone();
2179
2180 move |window, cx| {
2181 let active_thread = active_thread.clone();
2182 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2183 menu = menu
2184 .context(focus_handle.clone())
2185 .header("Zed Agent")
2186 .when_some(active_thread, |this, active_thread| {
2187 let thread = active_thread.read(cx);
2188
2189 if !thread.is_empty() {
2190 let thread_id = thread.id().clone();
2191 this.item(
2192 ContextMenuEntry::new("New From Summary")
2193 .icon(IconName::ThreadFromSummary)
2194 .icon_color(Color::Muted)
2195 .handler(move |window, cx| {
2196 window.dispatch_action(
2197 Box::new(NewThread {
2198 from_thread_id: Some(thread_id.clone()),
2199 }),
2200 cx,
2201 );
2202 }),
2203 )
2204 } else {
2205 this
2206 }
2207 })
2208 .item(
2209 ContextMenuEntry::new("New Thread")
2210 .icon(IconName::Thread)
2211 .icon_color(Color::Muted)
2212 .action(NewThread::default().boxed_clone())
2213 .handler({
2214 let workspace = workspace.clone();
2215 move |window, cx| {
2216 if let Some(workspace) = workspace.upgrade() {
2217 workspace.update(cx, |workspace, cx| {
2218 if let Some(panel) =
2219 workspace.panel::<AgentPanel>(cx)
2220 {
2221 panel.update(cx, |panel, cx| {
2222 panel.set_selected_agent(
2223 AgentType::Zed,
2224 cx,
2225 );
2226 });
2227 }
2228 });
2229 }
2230 window.dispatch_action(
2231 NewThread::default().boxed_clone(),
2232 cx,
2233 );
2234 }
2235 }),
2236 )
2237 .item(
2238 ContextMenuEntry::new("New Text Thread")
2239 .icon(IconName::TextThread)
2240 .icon_color(Color::Muted)
2241 .action(NewTextThread.boxed_clone())
2242 .handler({
2243 let workspace = workspace.clone();
2244 move |window, cx| {
2245 if let Some(workspace) = workspace.upgrade() {
2246 workspace.update(cx, |workspace, cx| {
2247 if let Some(panel) =
2248 workspace.panel::<AgentPanel>(cx)
2249 {
2250 panel.update(cx, |panel, cx| {
2251 panel.set_selected_agent(
2252 AgentType::TextThread,
2253 cx,
2254 );
2255 });
2256 }
2257 });
2258 }
2259 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2260 }
2261 }),
2262 )
2263 .item(
2264 ContextMenuEntry::new("New Native Agent Thread")
2265 .icon(IconName::ZedAssistant)
2266 .icon_color(Color::Muted)
2267 .handler({
2268 let workspace = workspace.clone();
2269 move |window, cx| {
2270 if let Some(workspace) = workspace.upgrade() {
2271 workspace.update(cx, |workspace, cx| {
2272 if let Some(panel) =
2273 workspace.panel::<AgentPanel>(cx)
2274 {
2275 panel.update(cx, |panel, cx| {
2276 panel.set_selected_agent(
2277 AgentType::NativeAgent,
2278 cx,
2279 );
2280 });
2281 }
2282 });
2283 }
2284 window.dispatch_action(
2285 NewExternalAgentThread {
2286 agent: Some(crate::ExternalAgent::NativeAgent),
2287 }
2288 .boxed_clone(),
2289 cx,
2290 );
2291 }
2292 }),
2293 )
2294 .separator()
2295 .header("External Agents")
2296 .item(
2297 ContextMenuEntry::new("New Gemini Thread")
2298 .icon(IconName::AiGemini)
2299 .icon_color(Color::Muted)
2300 .handler({
2301 let workspace = workspace.clone();
2302 move |window, cx| {
2303 if let Some(workspace) = workspace.upgrade() {
2304 workspace.update(cx, |workspace, cx| {
2305 if let Some(panel) =
2306 workspace.panel::<AgentPanel>(cx)
2307 {
2308 panel.update(cx, |panel, cx| {
2309 panel.set_selected_agent(
2310 AgentType::Gemini,
2311 cx,
2312 );
2313 });
2314 }
2315 });
2316 }
2317 window.dispatch_action(
2318 NewExternalAgentThread {
2319 agent: Some(crate::ExternalAgent::Gemini),
2320 }
2321 .boxed_clone(),
2322 cx,
2323 );
2324 }
2325 }),
2326 )
2327 .item(
2328 ContextMenuEntry::new("New Claude Code Thread")
2329 .icon(IconName::AiClaude)
2330 .icon_color(Color::Muted)
2331 .handler({
2332 let workspace = workspace.clone();
2333 move |window, cx| {
2334 if let Some(workspace) = workspace.upgrade() {
2335 workspace.update(cx, |workspace, cx| {
2336 if let Some(panel) =
2337 workspace.panel::<AgentPanel>(cx)
2338 {
2339 panel.update(cx, |panel, cx| {
2340 panel.set_selected_agent(
2341 AgentType::ClaudeCode,
2342 cx,
2343 );
2344 });
2345 }
2346 });
2347 }
2348 window.dispatch_action(
2349 NewExternalAgentThread {
2350 agent: Some(crate::ExternalAgent::ClaudeCode),
2351 }
2352 .boxed_clone(),
2353 cx,
2354 );
2355 }
2356 }),
2357 );
2358 menu
2359 }))
2360 }
2361 });
2362
2363 let selected_agent_label = self.selected_agent.label().into();
2364 let selected_agent = div()
2365 .id("selected_agent_icon")
2366 .px(DynamicSpacing::Base02.rems(cx))
2367 .child(Icon::new(self.selected_agent.icon()).color(Color::Muted))
2368 .tooltip(move |window, cx| {
2369 Tooltip::with_meta(
2370 selected_agent_label.clone(),
2371 None,
2372 "Selected Agent",
2373 window,
2374 cx,
2375 )
2376 })
2377 .into_any_element();
2378
2379 h_flex()
2380 .id("agent-panel-toolbar")
2381 .h(Tab::container_height(cx))
2382 .max_w_full()
2383 .flex_none()
2384 .justify_between()
2385 .gap_2()
2386 .bg(cx.theme().colors().tab_bar_background)
2387 .border_b_1()
2388 .border_color(cx.theme().colors().border)
2389 .child(
2390 h_flex()
2391 .size_full()
2392 .gap(DynamicSpacing::Base04.rems(cx))
2393 .pl(DynamicSpacing::Base04.rems(cx))
2394 .child(match &self.active_view {
2395 ActiveView::History | ActiveView::Configuration => {
2396 self.render_toolbar_back_button(cx).into_any_element()
2397 }
2398 _ => h_flex()
2399 .gap_1()
2400 .child(self.render_recent_entries_menu(cx))
2401 .child(Divider::vertical())
2402 .child(selected_agent)
2403 .into_any_element(),
2404 })
2405 .child(self.render_title_view(window, cx)),
2406 )
2407 .child(
2408 h_flex()
2409 .h_full()
2410 .gap_2()
2411 .children(self.render_token_count(cx))
2412 .child(
2413 h_flex()
2414 .h_full()
2415 .gap(DynamicSpacing::Base02.rems(cx))
2416 .pl(DynamicSpacing::Base04.rems(cx))
2417 .pr(DynamicSpacing::Base06.rems(cx))
2418 .border_l_1()
2419 .border_color(cx.theme().colors().border)
2420 .child(new_thread_menu)
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}