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