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