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