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