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