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