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