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