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