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::AgentServerCommand;
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 command: AgentServerCommand,
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 window,
1483 cx,
1484 )
1485 }));
1486
1487 if let Some(configuration) = self.configuration.as_ref() {
1488 self.configuration_subscription = Some(cx.subscribe_in(
1489 configuration,
1490 window,
1491 Self::handle_agent_configuration_event,
1492 ));
1493
1494 configuration.focus_handle(cx).focus(window);
1495 }
1496 }
1497
1498 pub(crate) fn open_active_thread_as_markdown(
1499 &mut self,
1500 _: &OpenActiveThreadAsMarkdown,
1501 window: &mut Window,
1502 cx: &mut Context<Self>,
1503 ) {
1504 let Some(workspace) = self.workspace.upgrade() else {
1505 return;
1506 };
1507
1508 match &self.active_view {
1509 ActiveView::Thread { thread, .. } => {
1510 active_thread::open_active_thread_as_markdown(
1511 thread.read(cx).thread().clone(),
1512 workspace,
1513 window,
1514 cx,
1515 )
1516 .detach_and_log_err(cx);
1517 }
1518 ActiveView::ExternalAgentThread { thread_view } => {
1519 thread_view
1520 .update(cx, |thread_view, cx| {
1521 thread_view.open_thread_as_markdown(workspace, window, cx)
1522 })
1523 .detach_and_log_err(cx);
1524 }
1525 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1526 }
1527 }
1528
1529 fn handle_agent_configuration_event(
1530 &mut self,
1531 _entity: &Entity<AgentConfiguration>,
1532 event: &AssistantConfigurationEvent,
1533 window: &mut Window,
1534 cx: &mut Context<Self>,
1535 ) {
1536 match event {
1537 AssistantConfigurationEvent::NewThread(provider) => {
1538 if LanguageModelRegistry::read_global(cx)
1539 .default_model()
1540 .is_none_or(|model| model.provider.id() != provider.id())
1541 && let Some(model) = provider.default_model(cx)
1542 {
1543 update_settings_file::<AgentSettings>(
1544 self.fs.clone(),
1545 cx,
1546 move |settings, _| settings.set_model(model),
1547 );
1548 }
1549
1550 self.new_thread(&NewThread::default(), window, cx);
1551 if let Some((thread, model)) =
1552 self.active_thread(cx).zip(provider.default_model(cx))
1553 {
1554 thread.update(cx, |thread, cx| {
1555 thread.set_configured_model(
1556 Some(ConfiguredModel {
1557 provider: provider.clone(),
1558 model,
1559 }),
1560 cx,
1561 );
1562 });
1563 }
1564 }
1565 }
1566 }
1567
1568 pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
1569 match &self.active_view {
1570 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1571 _ => None,
1572 }
1573 }
1574 pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1575 match &self.active_view {
1576 ActiveView::ExternalAgentThread { thread_view, .. } => {
1577 thread_view.read(cx).thread().cloned()
1578 }
1579 _ => None,
1580 }
1581 }
1582
1583 pub(crate) fn delete_thread(
1584 &mut self,
1585 thread_id: &ThreadId,
1586 cx: &mut Context<Self>,
1587 ) -> Task<Result<()>> {
1588 self.thread_store
1589 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1590 }
1591
1592 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1593 let ActiveView::Thread { thread, .. } = &self.active_view else {
1594 return;
1595 };
1596
1597 let thread_state = thread.read(cx).thread().read(cx);
1598 if !thread_state.tool_use_limit_reached() {
1599 return;
1600 }
1601
1602 let model = thread_state.configured_model().map(|cm| cm.model);
1603 if let Some(model) = model {
1604 thread.update(cx, |active_thread, cx| {
1605 active_thread.thread().update(cx, |thread, cx| {
1606 thread.insert_invisible_continue_message(cx);
1607 thread.advance_prompt_id();
1608 thread.send_to_model(
1609 model,
1610 CompletionIntent::UserPrompt,
1611 Some(window.window_handle()),
1612 cx,
1613 );
1614 });
1615 });
1616 } else {
1617 log::warn!("No configured model available for continuation");
1618 }
1619 }
1620
1621 fn toggle_burn_mode(
1622 &mut self,
1623 _: &ToggleBurnMode,
1624 _window: &mut Window,
1625 cx: &mut Context<Self>,
1626 ) {
1627 let ActiveView::Thread { thread, .. } = &self.active_view else {
1628 return;
1629 };
1630
1631 thread.update(cx, |active_thread, cx| {
1632 active_thread.thread().update(cx, |thread, _cx| {
1633 let current_mode = thread.completion_mode();
1634
1635 thread.set_completion_mode(match current_mode {
1636 CompletionMode::Burn => CompletionMode::Normal,
1637 CompletionMode::Normal => CompletionMode::Burn,
1638 });
1639 });
1640 });
1641 }
1642
1643 pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
1644 match &self.active_view {
1645 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1646 _ => None,
1647 }
1648 }
1649
1650 pub(crate) fn delete_context(
1651 &mut self,
1652 path: Arc<Path>,
1653 cx: &mut Context<Self>,
1654 ) -> Task<Result<()>> {
1655 self.context_store
1656 .update(cx, |this, cx| this.delete_local_context(path, cx))
1657 }
1658
1659 fn set_active_view(
1660 &mut self,
1661 new_view: ActiveView,
1662 window: &mut Window,
1663 cx: &mut Context<Self>,
1664 ) {
1665 let current_is_history = matches!(self.active_view, ActiveView::History);
1666 let new_is_history = matches!(new_view, ActiveView::History);
1667
1668 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1669 let new_is_config = matches!(new_view, ActiveView::Configuration);
1670
1671 let current_is_special = current_is_history || current_is_config;
1672 let new_is_special = new_is_history || new_is_config;
1673
1674 if let ActiveView::Thread { thread, .. } = &self.active_view {
1675 let thread = thread.read(cx);
1676 if thread.is_empty() {
1677 let id = thread.thread().read(cx).id().clone();
1678 self.history_store.update(cx, |store, cx| {
1679 store.remove_recently_opened_thread(id, cx);
1680 });
1681 }
1682 }
1683
1684 match &new_view {
1685 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1686 let id = thread.read(cx).thread().read(cx).id().clone();
1687 store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
1688 }),
1689 ActiveView::TextThread { context_editor, .. } => {
1690 self.history_store.update(cx, |store, cx| {
1691 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1692 store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
1693 }
1694 });
1695 self.acp_history_store.update(cx, |store, cx| {
1696 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1697 store.push_recently_opened_entry(
1698 agent2::HistoryEntryId::TextThread(path.clone()),
1699 cx,
1700 )
1701 }
1702 })
1703 }
1704 ActiveView::ExternalAgentThread { .. } => {}
1705 ActiveView::History | ActiveView::Configuration => {}
1706 }
1707
1708 if current_is_special && !new_is_special {
1709 self.active_view = new_view;
1710 } else if !current_is_special && new_is_special {
1711 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1712 } else {
1713 if !new_is_special {
1714 self.previous_view = None;
1715 }
1716 self.active_view = new_view;
1717 }
1718
1719 self.focus_handle(cx).focus(window);
1720 }
1721
1722 fn populate_recently_opened_menu_section_old(
1723 mut menu: ContextMenu,
1724 panel: Entity<Self>,
1725 cx: &mut Context<ContextMenu>,
1726 ) -> ContextMenu {
1727 let entries = panel
1728 .read(cx)
1729 .history_store
1730 .read(cx)
1731 .recently_opened_entries(cx);
1732
1733 if entries.is_empty() {
1734 return menu;
1735 }
1736
1737 menu = menu.header("Recently Opened");
1738
1739 for entry in entries {
1740 let title = entry.title().clone();
1741 let id = entry.id();
1742
1743 menu = menu.entry_with_end_slot_on_hover(
1744 title,
1745 None,
1746 {
1747 let panel = panel.downgrade();
1748 let id = id.clone();
1749 move |window, cx| {
1750 let id = id.clone();
1751 panel
1752 .update(cx, move |this, cx| match id {
1753 HistoryEntryId::Thread(id) => this
1754 .open_thread_by_id(&id, window, cx)
1755 .detach_and_log_err(cx),
1756 HistoryEntryId::Context(path) => this
1757 .open_saved_prompt_editor(path, window, cx)
1758 .detach_and_log_err(cx),
1759 })
1760 .ok();
1761 }
1762 },
1763 IconName::Close,
1764 "Close Entry".into(),
1765 {
1766 let panel = panel.downgrade();
1767 let id = id.clone();
1768 move |_window, cx| {
1769 panel
1770 .update(cx, |this, cx| {
1771 this.history_store.update(cx, |history_store, cx| {
1772 history_store.remove_recently_opened_entry(&id, cx);
1773 });
1774 })
1775 .ok();
1776 }
1777 },
1778 );
1779 }
1780
1781 menu = menu.separator();
1782
1783 menu
1784 }
1785
1786 fn populate_recently_opened_menu_section_new(
1787 mut menu: ContextMenu,
1788 panel: Entity<Self>,
1789 cx: &mut Context<ContextMenu>,
1790 ) -> ContextMenu {
1791 let entries = panel
1792 .read(cx)
1793 .acp_history_store
1794 .read(cx)
1795 .recently_opened_entries(cx);
1796
1797 if entries.is_empty() {
1798 return menu;
1799 }
1800
1801 menu = menu.header("Recently Opened");
1802
1803 for entry in entries {
1804 let title = entry.title().clone();
1805
1806 menu = menu.entry_with_end_slot_on_hover(
1807 title,
1808 None,
1809 {
1810 let panel = panel.downgrade();
1811 let entry = entry.clone();
1812 move |window, cx| {
1813 let entry = entry.clone();
1814 panel
1815 .update(cx, move |this, cx| match &entry {
1816 agent2::HistoryEntry::AcpThread(entry) => this.external_thread(
1817 Some(ExternalAgent::NativeAgent),
1818 Some(entry.clone()),
1819 None,
1820 window,
1821 cx,
1822 ),
1823 agent2::HistoryEntry::TextThread(entry) => this
1824 .open_saved_prompt_editor(entry.path.clone(), window, cx)
1825 .detach_and_log_err(cx),
1826 })
1827 .ok();
1828 }
1829 },
1830 IconName::Close,
1831 "Close Entry".into(),
1832 {
1833 let panel = panel.downgrade();
1834 let id = entry.id();
1835 move |_window, cx| {
1836 panel
1837 .update(cx, |this, cx| {
1838 this.acp_history_store.update(cx, |history_store, cx| {
1839 history_store.remove_recently_opened_entry(&id, cx);
1840 });
1841 })
1842 .ok();
1843 }
1844 },
1845 );
1846 }
1847
1848 menu = menu.separator();
1849
1850 menu
1851 }
1852
1853 pub fn selected_agent(&self) -> AgentType {
1854 self.selected_agent.clone()
1855 }
1856
1857 pub fn new_agent_thread(
1858 &mut self,
1859 agent: AgentType,
1860 window: &mut Window,
1861 cx: &mut Context<Self>,
1862 ) {
1863 if self.selected_agent != agent {
1864 self.selected_agent = agent.clone();
1865 self.serialize(cx);
1866 }
1867
1868 match agent {
1869 AgentType::Zed => {
1870 window.dispatch_action(
1871 NewThread {
1872 from_thread_id: None,
1873 }
1874 .boxed_clone(),
1875 cx,
1876 );
1877 }
1878 AgentType::TextThread => {
1879 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1880 }
1881 AgentType::NativeAgent => self.external_thread(
1882 Some(crate::ExternalAgent::NativeAgent),
1883 None,
1884 None,
1885 window,
1886 cx,
1887 ),
1888 AgentType::Gemini => {
1889 self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1890 }
1891 AgentType::ClaudeCode => self.external_thread(
1892 Some(crate::ExternalAgent::ClaudeCode),
1893 None,
1894 None,
1895 window,
1896 cx,
1897 ),
1898 AgentType::Custom { name, command } => self.external_thread(
1899 Some(crate::ExternalAgent::Custom { name, command }),
1900 None,
1901 None,
1902 window,
1903 cx,
1904 ),
1905 }
1906 }
1907
1908 pub fn load_agent_thread(
1909 &mut self,
1910 thread: DbThreadMetadata,
1911 window: &mut Window,
1912 cx: &mut Context<Self>,
1913 ) {
1914 self.external_thread(
1915 Some(ExternalAgent::NativeAgent),
1916 Some(thread),
1917 None,
1918 window,
1919 cx,
1920 );
1921 }
1922}
1923
1924impl Focusable for AgentPanel {
1925 fn focus_handle(&self, cx: &App) -> FocusHandle {
1926 match &self.active_view {
1927 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1928 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1929 ActiveView::History => {
1930 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
1931 self.acp_history.focus_handle(cx)
1932 } else {
1933 self.history.focus_handle(cx)
1934 }
1935 }
1936 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1937 ActiveView::Configuration => {
1938 if let Some(configuration) = self.configuration.as_ref() {
1939 configuration.focus_handle(cx)
1940 } else {
1941 cx.focus_handle()
1942 }
1943 }
1944 }
1945 }
1946}
1947
1948fn agent_panel_dock_position(cx: &App) -> DockPosition {
1949 match AgentSettings::get_global(cx).dock {
1950 AgentDockPosition::Left => DockPosition::Left,
1951 AgentDockPosition::Bottom => DockPosition::Bottom,
1952 AgentDockPosition::Right => DockPosition::Right,
1953 }
1954}
1955
1956impl EventEmitter<PanelEvent> for AgentPanel {}
1957
1958impl Panel for AgentPanel {
1959 fn persistent_name() -> &'static str {
1960 "AgentPanel"
1961 }
1962
1963 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1964 agent_panel_dock_position(cx)
1965 }
1966
1967 fn position_is_valid(&self, position: DockPosition) -> bool {
1968 position != DockPosition::Bottom
1969 }
1970
1971 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1972 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1973 let dock = match position {
1974 DockPosition::Left => AgentDockPosition::Left,
1975 DockPosition::Bottom => AgentDockPosition::Bottom,
1976 DockPosition::Right => AgentDockPosition::Right,
1977 };
1978 settings.set_dock(dock);
1979 });
1980 }
1981
1982 fn size(&self, window: &Window, cx: &App) -> Pixels {
1983 let settings = AgentSettings::get_global(cx);
1984 match self.position(window, cx) {
1985 DockPosition::Left | DockPosition::Right => {
1986 self.width.unwrap_or(settings.default_width)
1987 }
1988 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1989 }
1990 }
1991
1992 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1993 match self.position(window, cx) {
1994 DockPosition::Left | DockPosition::Right => self.width = size,
1995 DockPosition::Bottom => self.height = size,
1996 }
1997 self.serialize(cx);
1998 cx.notify();
1999 }
2000
2001 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
2002
2003 fn remote_id() -> Option<proto::PanelId> {
2004 Some(proto::PanelId::AssistantPanel)
2005 }
2006
2007 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2008 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2009 }
2010
2011 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2012 Some("Agent Panel")
2013 }
2014
2015 fn toggle_action(&self) -> Box<dyn Action> {
2016 Box::new(ToggleFocus)
2017 }
2018
2019 fn activation_priority(&self) -> u32 {
2020 3
2021 }
2022
2023 fn enabled(&self, cx: &App) -> bool {
2024 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
2025 }
2026
2027 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2028 self.zoomed
2029 }
2030
2031 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2032 self.zoomed = zoomed;
2033 cx.notify();
2034 }
2035}
2036
2037impl AgentPanel {
2038 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2039 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2040
2041 let content = match &self.active_view {
2042 ActiveView::Thread {
2043 thread: active_thread,
2044 change_title_editor,
2045 ..
2046 } => {
2047 let state = {
2048 let active_thread = active_thread.read(cx);
2049 if active_thread.is_empty() {
2050 &ThreadSummary::Pending
2051 } else {
2052 active_thread.summary(cx)
2053 }
2054 };
2055
2056 match state {
2057 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
2058 .truncate()
2059 .color(Color::Muted)
2060 .into_any_element(),
2061 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
2062 .truncate()
2063 .color(Color::Muted)
2064 .into_any_element(),
2065 ThreadSummary::Ready(_) => div()
2066 .w_full()
2067 .child(change_title_editor.clone())
2068 .into_any_element(),
2069 ThreadSummary::Error => h_flex()
2070 .w_full()
2071 .child(change_title_editor.clone())
2072 .child(
2073 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2074 .icon_size(IconSize::Small)
2075 .on_click({
2076 let active_thread = active_thread.clone();
2077 move |_, _window, cx| {
2078 active_thread.update(cx, |thread, cx| {
2079 thread.regenerate_summary(cx);
2080 });
2081 }
2082 })
2083 .tooltip(move |_window, cx| {
2084 cx.new(|_| {
2085 Tooltip::new("Failed to generate title")
2086 .meta("Click to try again")
2087 })
2088 .into()
2089 }),
2090 )
2091 .into_any_element(),
2092 }
2093 }
2094 ActiveView::ExternalAgentThread { thread_view } => {
2095 if let Some(title_editor) = thread_view.read(cx).title_editor() {
2096 div()
2097 .w_full()
2098 .on_action({
2099 let thread_view = thread_view.downgrade();
2100 move |_: &menu::Confirm, window, cx| {
2101 if let Some(thread_view) = thread_view.upgrade() {
2102 thread_view.focus_handle(cx).focus(window);
2103 }
2104 }
2105 })
2106 .on_action({
2107 let thread_view = thread_view.downgrade();
2108 move |_: &editor::actions::Cancel, window, cx| {
2109 if let Some(thread_view) = thread_view.upgrade() {
2110 thread_view.focus_handle(cx).focus(window);
2111 }
2112 }
2113 })
2114 .child(title_editor)
2115 .into_any_element()
2116 } else {
2117 Label::new(thread_view.read(cx).title(cx))
2118 .color(Color::Muted)
2119 .truncate()
2120 .into_any_element()
2121 }
2122 }
2123 ActiveView::TextThread {
2124 title_editor,
2125 context_editor,
2126 ..
2127 } => {
2128 let summary = context_editor.read(cx).context().read(cx).summary();
2129
2130 match summary {
2131 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
2132 .color(Color::Muted)
2133 .truncate()
2134 .into_any_element(),
2135 ContextSummary::Content(summary) => {
2136 if summary.done {
2137 div()
2138 .w_full()
2139 .child(title_editor.clone())
2140 .into_any_element()
2141 } else {
2142 Label::new(LOADING_SUMMARY_PLACEHOLDER)
2143 .truncate()
2144 .color(Color::Muted)
2145 .into_any_element()
2146 }
2147 }
2148 ContextSummary::Error => h_flex()
2149 .w_full()
2150 .child(title_editor.clone())
2151 .child(
2152 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2153 .icon_size(IconSize::Small)
2154 .on_click({
2155 let context_editor = context_editor.clone();
2156 move |_, _window, cx| {
2157 context_editor.update(cx, |context_editor, cx| {
2158 context_editor.regenerate_summary(cx);
2159 });
2160 }
2161 })
2162 .tooltip(move |_window, cx| {
2163 cx.new(|_| {
2164 Tooltip::new("Failed to generate title")
2165 .meta("Click to try again")
2166 })
2167 .into()
2168 }),
2169 )
2170 .into_any_element(),
2171 }
2172 }
2173 ActiveView::History => Label::new("History").truncate().into_any_element(),
2174 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2175 };
2176
2177 h_flex()
2178 .key_context("TitleEditor")
2179 .id("TitleEditor")
2180 .flex_grow()
2181 .w_full()
2182 .max_w_full()
2183 .overflow_x_scroll()
2184 .child(content)
2185 .into_any()
2186 }
2187
2188 fn render_panel_options_menu(
2189 &self,
2190 window: &mut Window,
2191 cx: &mut Context<Self>,
2192 ) -> impl IntoElement {
2193 let user_store = self.user_store.read(cx);
2194 let usage = user_store.model_request_usage();
2195 let account_url = zed_urls::account_url(cx);
2196
2197 let focus_handle = self.focus_handle(cx);
2198
2199 let full_screen_label = if self.is_zoomed(window, cx) {
2200 "Disable Full Screen"
2201 } else {
2202 "Enable Full Screen"
2203 };
2204
2205 let selected_agent = self.selected_agent.clone();
2206
2207 PopoverMenu::new("agent-options-menu")
2208 .trigger_with_tooltip(
2209 IconButton::new("agent-options-menu", IconName::Ellipsis)
2210 .icon_size(IconSize::Small),
2211 {
2212 let focus_handle = focus_handle.clone();
2213 move |window, cx| {
2214 Tooltip::for_action_in(
2215 "Toggle Agent Menu",
2216 &ToggleOptionsMenu,
2217 &focus_handle,
2218 window,
2219 cx,
2220 )
2221 }
2222 },
2223 )
2224 .anchor(Corner::TopRight)
2225 .with_handle(self.agent_panel_menu_handle.clone())
2226 .menu({
2227 move |window, cx| {
2228 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2229 menu = menu.context(focus_handle.clone());
2230 if let Some(usage) = usage {
2231 menu = menu
2232 .header_with_link("Prompt Usage", "Manage", account_url.clone())
2233 .custom_entry(
2234 move |_window, cx| {
2235 let used_percentage = match usage.limit {
2236 UsageLimit::Limited(limit) => {
2237 Some((usage.amount as f32 / limit as f32) * 100.)
2238 }
2239 UsageLimit::Unlimited => None,
2240 };
2241
2242 h_flex()
2243 .flex_1()
2244 .gap_1p5()
2245 .children(used_percentage.map(|percent| {
2246 ProgressBar::new("usage", percent, 100., cx)
2247 }))
2248 .child(
2249 Label::new(match usage.limit {
2250 UsageLimit::Limited(limit) => {
2251 format!("{} / {limit}", usage.amount)
2252 }
2253 UsageLimit::Unlimited => {
2254 format!("{} / ∞", usage.amount)
2255 }
2256 })
2257 .size(LabelSize::Small)
2258 .color(Color::Muted),
2259 )
2260 .into_any_element()
2261 },
2262 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
2263 )
2264 .separator()
2265 }
2266
2267 menu = menu
2268 .header("MCP Servers")
2269 .action(
2270 "View Server Extensions",
2271 Box::new(zed_actions::Extensions {
2272 category_filter: Some(
2273 zed_actions::ExtensionCategoryFilter::ContextServers,
2274 ),
2275 id: None,
2276 }),
2277 )
2278 .action("Add Custom Server…", Box::new(AddContextServer))
2279 .separator();
2280
2281 menu = menu
2282 .action("Rules…", Box::new(OpenRulesLibrary::default()))
2283 .action("Settings", Box::new(OpenSettings))
2284 .separator()
2285 .action(full_screen_label, Box::new(ToggleZoom));
2286
2287 if selected_agent == AgentType::Gemini {
2288 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2289 }
2290
2291 menu
2292 }))
2293 }
2294 })
2295 }
2296
2297 fn render_recent_entries_menu(
2298 &self,
2299 icon: IconName,
2300 corner: Corner,
2301 cx: &mut Context<Self>,
2302 ) -> impl IntoElement {
2303 let focus_handle = self.focus_handle(cx);
2304
2305 PopoverMenu::new("agent-nav-menu")
2306 .trigger_with_tooltip(
2307 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2308 {
2309 move |window, cx| {
2310 Tooltip::for_action_in(
2311 "Toggle Recent Threads",
2312 &ToggleNavigationMenu,
2313 &focus_handle,
2314 window,
2315 cx,
2316 )
2317 }
2318 },
2319 )
2320 .anchor(corner)
2321 .with_handle(self.assistant_navigation_menu_handle.clone())
2322 .menu({
2323 let menu = self.assistant_navigation_menu.clone();
2324 move |window, cx| {
2325 telemetry::event!("View Thread History Clicked");
2326
2327 if let Some(menu) = menu.as_ref() {
2328 menu.update(cx, |_, cx| {
2329 cx.defer_in(window, |menu, window, cx| {
2330 menu.rebuild(window, cx);
2331 });
2332 })
2333 }
2334 menu.clone()
2335 }
2336 })
2337 }
2338
2339 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2340 let focus_handle = self.focus_handle(cx);
2341
2342 IconButton::new("go-back", IconName::ArrowLeft)
2343 .icon_size(IconSize::Small)
2344 .on_click(cx.listener(|this, _, window, cx| {
2345 this.go_back(&workspace::GoBack, window, cx);
2346 }))
2347 .tooltip({
2348 move |window, cx| {
2349 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
2350 }
2351 })
2352 }
2353
2354 fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2355 let focus_handle = self.focus_handle(cx);
2356
2357 let active_thread = match &self.active_view {
2358 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2359 ActiveView::ExternalAgentThread { .. }
2360 | ActiveView::TextThread { .. }
2361 | ActiveView::History
2362 | ActiveView::Configuration => None,
2363 };
2364
2365 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2366 .trigger_with_tooltip(
2367 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2368 Tooltip::text("New Thread…"),
2369 )
2370 .anchor(Corner::TopRight)
2371 .with_handle(self.new_thread_menu_handle.clone())
2372 .menu({
2373 move |window, cx| {
2374 let active_thread = active_thread.clone();
2375 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2376 menu = menu
2377 .context(focus_handle.clone())
2378 .when_some(active_thread, |this, active_thread| {
2379 let thread = active_thread.read(cx);
2380
2381 if !thread.is_empty() {
2382 let thread_id = thread.id().clone();
2383 this.item(
2384 ContextMenuEntry::new("New From Summary")
2385 .icon(IconName::ThreadFromSummary)
2386 .icon_color(Color::Muted)
2387 .handler(move |window, cx| {
2388 window.dispatch_action(
2389 Box::new(NewThread {
2390 from_thread_id: Some(thread_id.clone()),
2391 }),
2392 cx,
2393 );
2394 }),
2395 )
2396 } else {
2397 this
2398 }
2399 })
2400 .item(
2401 ContextMenuEntry::new("New Thread")
2402 .icon(IconName::Thread)
2403 .icon_color(Color::Muted)
2404 .action(NewThread::default().boxed_clone())
2405 .handler(move |window, cx| {
2406 window.dispatch_action(
2407 NewThread::default().boxed_clone(),
2408 cx,
2409 );
2410 }),
2411 )
2412 .item(
2413 ContextMenuEntry::new("New Text Thread")
2414 .icon(IconName::TextThread)
2415 .icon_color(Color::Muted)
2416 .action(NewTextThread.boxed_clone())
2417 .handler(move |window, cx| {
2418 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2419 }),
2420 );
2421 menu
2422 }))
2423 }
2424 });
2425
2426 h_flex()
2427 .id("assistant-toolbar")
2428 .h(Tab::container_height(cx))
2429 .max_w_full()
2430 .flex_none()
2431 .justify_between()
2432 .gap_2()
2433 .bg(cx.theme().colors().tab_bar_background)
2434 .border_b_1()
2435 .border_color(cx.theme().colors().border)
2436 .child(
2437 h_flex()
2438 .size_full()
2439 .pl_1()
2440 .gap_1()
2441 .child(match &self.active_view {
2442 ActiveView::History | ActiveView::Configuration => div()
2443 .pl(DynamicSpacing::Base04.rems(cx))
2444 .child(self.render_toolbar_back_button(cx))
2445 .into_any_element(),
2446 _ => self
2447 .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx)
2448 .into_any_element(),
2449 })
2450 .child(self.render_title_view(window, cx)),
2451 )
2452 .child(
2453 h_flex()
2454 .h_full()
2455 .gap_2()
2456 .children(self.render_token_count(cx))
2457 .child(
2458 h_flex()
2459 .h_full()
2460 .gap(DynamicSpacing::Base02.rems(cx))
2461 .px(DynamicSpacing::Base08.rems(cx))
2462 .border_l_1()
2463 .border_color(cx.theme().colors().border)
2464 .child(new_thread_menu)
2465 .child(self.render_panel_options_menu(window, cx)),
2466 ),
2467 )
2468 }
2469
2470 fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2471 let focus_handle = self.focus_handle(cx);
2472
2473 let active_thread = match &self.active_view {
2474 ActiveView::ExternalAgentThread { thread_view } => {
2475 thread_view.read(cx).as_native_thread(cx)
2476 }
2477 ActiveView::Thread { .. }
2478 | ActiveView::TextThread { .. }
2479 | ActiveView::History
2480 | ActiveView::Configuration => None,
2481 };
2482
2483 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2484 .trigger_with_tooltip(
2485 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2486 {
2487 let focus_handle = focus_handle.clone();
2488 move |window, cx| {
2489 Tooltip::for_action_in(
2490 "New…",
2491 &ToggleNewThreadMenu,
2492 &focus_handle,
2493 window,
2494 cx,
2495 )
2496 }
2497 },
2498 )
2499 .anchor(Corner::TopLeft)
2500 .with_handle(self.new_thread_menu_handle.clone())
2501 .menu({
2502 let workspace = self.workspace.clone();
2503
2504 move |window, cx| {
2505 telemetry::event!("New Thread Clicked");
2506
2507 let active_thread = active_thread.clone();
2508 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2509 menu = menu
2510 .context(focus_handle.clone())
2511 .header("Zed Agent")
2512 .when_some(active_thread, |this, active_thread| {
2513 let thread = active_thread.read(cx);
2514
2515 if !thread.is_empty() {
2516 let session_id = thread.id().clone();
2517 this.item(
2518 ContextMenuEntry::new("New From Summary")
2519 .icon(IconName::ThreadFromSummary)
2520 .icon_color(Color::Muted)
2521 .handler(move |window, cx| {
2522 window.dispatch_action(
2523 Box::new(NewNativeAgentThreadFromSummary {
2524 from_session_id: session_id.clone(),
2525 }),
2526 cx,
2527 );
2528 }),
2529 )
2530 } else {
2531 this
2532 }
2533 })
2534 .item(
2535 ContextMenuEntry::new("New Thread")
2536 .action(NewThread::default().boxed_clone())
2537 .icon(IconName::Thread)
2538 .icon_color(Color::Muted)
2539 .handler({
2540 let workspace = workspace.clone();
2541 move |window, cx| {
2542 if let Some(workspace) = workspace.upgrade() {
2543 workspace.update(cx, |workspace, cx| {
2544 if let Some(panel) =
2545 workspace.panel::<AgentPanel>(cx)
2546 {
2547 panel.update(cx, |panel, cx| {
2548 panel.new_agent_thread(
2549 AgentType::NativeAgent,
2550 window,
2551 cx,
2552 );
2553 });
2554 }
2555 });
2556 }
2557 }
2558 }),
2559 )
2560 .item(
2561 ContextMenuEntry::new("New Text Thread")
2562 .icon(IconName::TextThread)
2563 .icon_color(Color::Muted)
2564 .action(NewTextThread.boxed_clone())
2565 .handler({
2566 let workspace = workspace.clone();
2567 move |window, cx| {
2568 if let Some(workspace) = workspace.upgrade() {
2569 workspace.update(cx, |workspace, cx| {
2570 if let Some(panel) =
2571 workspace.panel::<AgentPanel>(cx)
2572 {
2573 panel.update(cx, |panel, cx| {
2574 panel.new_agent_thread(
2575 AgentType::TextThread,
2576 window,
2577 cx,
2578 );
2579 });
2580 }
2581 });
2582 }
2583 }
2584 }),
2585 )
2586 .separator()
2587 .header("External Agents")
2588 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2589 menu.item(
2590 ContextMenuEntry::new("New Gemini CLI Thread")
2591 .icon(IconName::AiGemini)
2592 .icon_color(Color::Muted)
2593 .handler({
2594 let workspace = workspace.clone();
2595 move |window, cx| {
2596 if let Some(workspace) = workspace.upgrade() {
2597 workspace.update(cx, |workspace, cx| {
2598 if let Some(panel) =
2599 workspace.panel::<AgentPanel>(cx)
2600 {
2601 panel.update(cx, |panel, cx| {
2602 panel.new_agent_thread(
2603 AgentType::Gemini,
2604 window,
2605 cx,
2606 );
2607 });
2608 }
2609 });
2610 }
2611 }
2612 }),
2613 )
2614 })
2615 .when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| {
2616 menu.item(
2617 ContextMenuEntry::new("New Claude Code Thread")
2618 .icon(IconName::AiClaude)
2619 .icon_color(Color::Muted)
2620 .handler({
2621 let workspace = workspace.clone();
2622 move |window, cx| {
2623 if let Some(workspace) = workspace.upgrade() {
2624 workspace.update(cx, |workspace, cx| {
2625 if let Some(panel) =
2626 workspace.panel::<AgentPanel>(cx)
2627 {
2628 panel.update(cx, |panel, cx| {
2629 panel.new_agent_thread(
2630 AgentType::ClaudeCode,
2631 window,
2632 cx,
2633 );
2634 });
2635 }
2636 });
2637 }
2638 }
2639 }),
2640 )
2641 })
2642 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
2643 // Add custom agents from settings
2644 let settings =
2645 agent_servers::AllAgentServersSettings::get_global(cx);
2646 for (agent_name, agent_settings) in &settings.custom {
2647 menu = menu.item(
2648 ContextMenuEntry::new(format!("New {} Thread", agent_name))
2649 .icon(IconName::Terminal)
2650 .icon_color(Color::Muted)
2651 .handler({
2652 let workspace = workspace.clone();
2653 let agent_name = agent_name.clone();
2654 let agent_settings = agent_settings.clone();
2655 move |window, cx| {
2656 if let Some(workspace) = workspace.upgrade() {
2657 workspace.update(cx, |workspace, cx| {
2658 if let Some(panel) =
2659 workspace.panel::<AgentPanel>(cx)
2660 {
2661 panel.update(cx, |panel, cx| {
2662 panel.new_agent_thread(
2663 AgentType::Custom {
2664 name: agent_name
2665 .clone(),
2666 command: agent_settings
2667 .command
2668 .clone(),
2669 },
2670 window,
2671 cx,
2672 );
2673 });
2674 }
2675 });
2676 }
2677 }
2678 }),
2679 );
2680 }
2681
2682 menu
2683 })
2684 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2685 menu.separator().link(
2686 "Add Other Agents",
2687 OpenBrowser {
2688 url: zed_urls::external_agents_docs(cx),
2689 }
2690 .boxed_clone(),
2691 )
2692 });
2693 menu
2694 }))
2695 }
2696 });
2697
2698 let selected_agent_label = self.selected_agent.label();
2699 let selected_agent = div()
2700 .id("selected_agent_icon")
2701 .when_some(self.selected_agent.icon(), |this, icon| {
2702 this.px(DynamicSpacing::Base02.rems(cx))
2703 .child(Icon::new(icon).color(Color::Muted))
2704 .tooltip(move |window, cx| {
2705 Tooltip::with_meta(
2706 selected_agent_label.clone(),
2707 None,
2708 "Selected Agent",
2709 window,
2710 cx,
2711 )
2712 })
2713 })
2714 .into_any_element();
2715
2716 h_flex()
2717 .id("agent-panel-toolbar")
2718 .h(Tab::container_height(cx))
2719 .max_w_full()
2720 .flex_none()
2721 .justify_between()
2722 .gap_2()
2723 .bg(cx.theme().colors().tab_bar_background)
2724 .border_b_1()
2725 .border_color(cx.theme().colors().border)
2726 .child(
2727 h_flex()
2728 .size_full()
2729 .gap(DynamicSpacing::Base04.rems(cx))
2730 .pl(DynamicSpacing::Base04.rems(cx))
2731 .child(match &self.active_view {
2732 ActiveView::History | ActiveView::Configuration => {
2733 self.render_toolbar_back_button(cx).into_any_element()
2734 }
2735 _ => selected_agent.into_any_element(),
2736 })
2737 .child(self.render_title_view(window, cx)),
2738 )
2739 .child(
2740 h_flex()
2741 .flex_none()
2742 .gap(DynamicSpacing::Base02.rems(cx))
2743 .pl(DynamicSpacing::Base04.rems(cx))
2744 .pr(DynamicSpacing::Base06.rems(cx))
2745 .child(new_thread_menu)
2746 .child(self.render_recent_entries_menu(
2747 IconName::MenuAltTemp,
2748 Corner::TopRight,
2749 cx,
2750 ))
2751 .child(self.render_panel_options_menu(window, cx)),
2752 )
2753 }
2754
2755 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2756 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>()
2757 || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>()
2758 {
2759 self.render_toolbar_new(window, cx).into_any_element()
2760 } else {
2761 self.render_toolbar_old(window, cx).into_any_element()
2762 }
2763 }
2764
2765 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
2766 match &self.active_view {
2767 ActiveView::Thread {
2768 thread,
2769 message_editor,
2770 ..
2771 } => {
2772 let active_thread = thread.read(cx);
2773 let message_editor = message_editor.read(cx);
2774
2775 let editor_empty = message_editor.is_editor_fully_empty(cx);
2776
2777 if active_thread.is_empty() && editor_empty {
2778 return None;
2779 }
2780
2781 let thread = active_thread.thread().read(cx);
2782 let is_generating = thread.is_generating();
2783 let conversation_token_usage = thread.total_token_usage()?;
2784
2785 let (total_token_usage, is_estimating) =
2786 if let Some((editing_message_id, unsent_tokens)) =
2787 active_thread.editing_message_id()
2788 {
2789 let combined = thread
2790 .token_usage_up_to_message(editing_message_id)
2791 .add(unsent_tokens);
2792
2793 (combined, unsent_tokens > 0)
2794 } else {
2795 let unsent_tokens =
2796 message_editor.last_estimated_token_count().unwrap_or(0);
2797 let combined = conversation_token_usage.add(unsent_tokens);
2798
2799 (combined, unsent_tokens > 0)
2800 };
2801
2802 let is_waiting_to_update_token_count =
2803 message_editor.is_waiting_to_update_token_count();
2804
2805 if total_token_usage.total == 0 {
2806 return None;
2807 }
2808
2809 let token_color = match total_token_usage.ratio() {
2810 TokenUsageRatio::Normal if is_estimating => Color::Default,
2811 TokenUsageRatio::Normal => Color::Muted,
2812 TokenUsageRatio::Warning => Color::Warning,
2813 TokenUsageRatio::Exceeded => Color::Error,
2814 };
2815
2816 let token_count = h_flex()
2817 .id("token-count")
2818 .flex_shrink_0()
2819 .gap_0p5()
2820 .when(!is_generating && is_estimating, |parent| {
2821 parent
2822 .child(
2823 h_flex()
2824 .mr_1()
2825 .size_2p5()
2826 .justify_center()
2827 .rounded_full()
2828 .bg(cx.theme().colors().text.opacity(0.1))
2829 .child(
2830 div().size_1().rounded_full().bg(cx.theme().colors().text),
2831 ),
2832 )
2833 .tooltip(move |window, cx| {
2834 Tooltip::with_meta(
2835 "Estimated New Token Count",
2836 None,
2837 format!(
2838 "Current Conversation Tokens: {}",
2839 humanize_token_count(conversation_token_usage.total)
2840 ),
2841 window,
2842 cx,
2843 )
2844 })
2845 })
2846 .child(
2847 Label::new(humanize_token_count(total_token_usage.total))
2848 .size(LabelSize::Small)
2849 .color(token_color)
2850 .map(|label| {
2851 if is_generating || is_waiting_to_update_token_count {
2852 label
2853 .with_animation(
2854 "used-tokens-label",
2855 Animation::new(Duration::from_secs(2))
2856 .repeat()
2857 .with_easing(pulsating_between(0.6, 1.)),
2858 |label, delta| label.alpha(delta),
2859 )
2860 .into_any()
2861 } else {
2862 label.into_any_element()
2863 }
2864 }),
2865 )
2866 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2867 .child(
2868 Label::new(humanize_token_count(total_token_usage.max))
2869 .size(LabelSize::Small)
2870 .color(Color::Muted),
2871 )
2872 .into_any();
2873
2874 Some(token_count)
2875 }
2876 ActiveView::ExternalAgentThread { .. }
2877 | ActiveView::TextThread { .. }
2878 | ActiveView::History
2879 | ActiveView::Configuration => None,
2880 }
2881 }
2882
2883 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2884 if TrialEndUpsell::dismissed() {
2885 return false;
2886 }
2887
2888 match &self.active_view {
2889 ActiveView::Thread { thread, .. } => {
2890 if thread
2891 .read(cx)
2892 .thread()
2893 .read(cx)
2894 .configured_model()
2895 .is_some_and(|model| {
2896 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2897 })
2898 {
2899 return false;
2900 }
2901 }
2902 ActiveView::TextThread { .. } => {
2903 if LanguageModelRegistry::global(cx)
2904 .read(cx)
2905 .default_model()
2906 .is_some_and(|model| {
2907 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2908 })
2909 {
2910 return false;
2911 }
2912 }
2913 ActiveView::ExternalAgentThread { .. }
2914 | ActiveView::History
2915 | ActiveView::Configuration => return false,
2916 }
2917
2918 let plan = self.user_store.read(cx).plan();
2919 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2920
2921 matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
2922 }
2923
2924 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2925 if OnboardingUpsell::dismissed() {
2926 return false;
2927 }
2928
2929 match &self.active_view {
2930 ActiveView::History | ActiveView::Configuration => false,
2931 ActiveView::ExternalAgentThread { thread_view, .. }
2932 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2933 {
2934 false
2935 }
2936 _ => {
2937 let history_is_empty = if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
2938 self.acp_history_store.read(cx).is_empty(cx)
2939 } else {
2940 self.history_store
2941 .update(cx, |store, cx| store.recent_entries(1, cx).is_empty())
2942 };
2943
2944 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2945 .providers()
2946 .iter()
2947 .any(|provider| {
2948 provider.is_authenticated(cx)
2949 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2950 });
2951
2952 history_is_empty || !has_configured_non_zed_providers
2953 }
2954 }
2955 }
2956
2957 fn render_onboarding(
2958 &self,
2959 _window: &mut Window,
2960 cx: &mut Context<Self>,
2961 ) -> Option<impl IntoElement> {
2962 if !self.should_render_onboarding(cx) {
2963 return None;
2964 }
2965
2966 let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
2967 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2968
2969 Some(
2970 div()
2971 .when(thread_view, |this| {
2972 this.size_full().bg(cx.theme().colors().panel_background)
2973 })
2974 .when(text_thread_view, |this| {
2975 this.bg(cx.theme().colors().editor_background)
2976 })
2977 .child(self.onboarding.clone()),
2978 )
2979 }
2980
2981 fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
2982 div()
2983 .size_full()
2984 .absolute()
2985 .inset_0()
2986 .bg(cx.theme().colors().panel_background)
2987 .opacity(0.8)
2988 .block_mouse_except_scroll()
2989 }
2990
2991 fn render_trial_end_upsell(
2992 &self,
2993 _window: &mut Window,
2994 cx: &mut Context<Self>,
2995 ) -> Option<impl IntoElement> {
2996 if !self.should_render_trial_end_upsell(cx) {
2997 return None;
2998 }
2999
3000 Some(
3001 v_flex()
3002 .absolute()
3003 .inset_0()
3004 .size_full()
3005 .bg(cx.theme().colors().panel_background)
3006 .opacity(0.85)
3007 .block_mouse_except_scroll()
3008 .child(EndTrialUpsell::new(Arc::new({
3009 let this = cx.entity();
3010 move |_, cx| {
3011 this.update(cx, |_this, cx| {
3012 TrialEndUpsell::set_dismissed(true, cx);
3013 cx.notify();
3014 });
3015 }
3016 }))),
3017 )
3018 }
3019
3020 fn render_empty_state_section_header(
3021 &self,
3022 label: impl Into<SharedString>,
3023 action_slot: Option<AnyElement>,
3024 cx: &mut Context<Self>,
3025 ) -> impl IntoElement {
3026 div().pl_1().pr_1p5().child(
3027 h_flex()
3028 .mt_2()
3029 .pl_1p5()
3030 .pb_1()
3031 .w_full()
3032 .justify_between()
3033 .border_b_1()
3034 .border_color(cx.theme().colors().border_variant)
3035 .child(
3036 Label::new(label.into())
3037 .size(LabelSize::Small)
3038 .color(Color::Muted),
3039 )
3040 .children(action_slot),
3041 )
3042 }
3043
3044 fn render_thread_empty_state(
3045 &self,
3046 window: &mut Window,
3047 cx: &mut Context<Self>,
3048 ) -> impl IntoElement {
3049 let recent_history = self
3050 .history_store
3051 .update(cx, |this, cx| this.recent_entries(6, cx));
3052
3053 let model_registry = LanguageModelRegistry::read_global(cx);
3054
3055 let configuration_error =
3056 model_registry.configuration_error(model_registry.default_model(), cx);
3057
3058 let no_error = configuration_error.is_none();
3059 let focus_handle = self.focus_handle(cx);
3060
3061 v_flex()
3062 .size_full()
3063 .bg(cx.theme().colors().panel_background)
3064 .when(recent_history.is_empty(), |this| {
3065 this.child(
3066 v_flex()
3067 .size_full()
3068 .mx_auto()
3069 .justify_center()
3070 .items_center()
3071 .gap_1()
3072 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
3073 .when(no_error, |parent| {
3074 parent
3075 .child(h_flex().child(
3076 Label::new("Ask and build anything.").color(Color::Muted),
3077 ))
3078 .child(
3079 v_flex()
3080 .mt_2()
3081 .gap_1()
3082 .max_w_48()
3083 .child(
3084 Button::new("context", "Add Context")
3085 .label_size(LabelSize::Small)
3086 .icon(IconName::FileCode)
3087 .icon_position(IconPosition::Start)
3088 .icon_size(IconSize::Small)
3089 .icon_color(Color::Muted)
3090 .full_width()
3091 .key_binding(KeyBinding::for_action_in(
3092 &ToggleContextPicker,
3093 &focus_handle,
3094 window,
3095 cx,
3096 ))
3097 .on_click(|_event, window, cx| {
3098 window.dispatch_action(
3099 ToggleContextPicker.boxed_clone(),
3100 cx,
3101 )
3102 }),
3103 )
3104 .child(
3105 Button::new("mode", "Switch Model")
3106 .label_size(LabelSize::Small)
3107 .icon(IconName::DatabaseZap)
3108 .icon_position(IconPosition::Start)
3109 .icon_size(IconSize::Small)
3110 .icon_color(Color::Muted)
3111 .full_width()
3112 .key_binding(KeyBinding::for_action_in(
3113 &ToggleModelSelector,
3114 &focus_handle,
3115 window,
3116 cx,
3117 ))
3118 .on_click(|_event, window, cx| {
3119 window.dispatch_action(
3120 ToggleModelSelector.boxed_clone(),
3121 cx,
3122 )
3123 }),
3124 )
3125 .child(
3126 Button::new("settings", "View Settings")
3127 .label_size(LabelSize::Small)
3128 .icon(IconName::Settings)
3129 .icon_position(IconPosition::Start)
3130 .icon_size(IconSize::Small)
3131 .icon_color(Color::Muted)
3132 .full_width()
3133 .key_binding(KeyBinding::for_action_in(
3134 &OpenSettings,
3135 &focus_handle,
3136 window,
3137 cx,
3138 ))
3139 .on_click(|_event, window, cx| {
3140 window.dispatch_action(
3141 OpenSettings.boxed_clone(),
3142 cx,
3143 )
3144 }),
3145 ),
3146 )
3147 }),
3148 )
3149 })
3150 .when(!recent_history.is_empty(), |parent| {
3151 parent
3152 .overflow_hidden()
3153 .justify_end()
3154 .gap_1()
3155 .child(
3156 self.render_empty_state_section_header(
3157 "Recent",
3158 Some(
3159 Button::new("view-history", "View All")
3160 .style(ButtonStyle::Subtle)
3161 .label_size(LabelSize::Small)
3162 .key_binding(
3163 KeyBinding::for_action_in(
3164 &OpenHistory,
3165 &self.focus_handle(cx),
3166 window,
3167 cx,
3168 )
3169 .map(|kb| kb.size(rems_from_px(12.))),
3170 )
3171 .on_click(move |_event, window, cx| {
3172 window.dispatch_action(OpenHistory.boxed_clone(), cx);
3173 })
3174 .into_any_element(),
3175 ),
3176 cx,
3177 ),
3178 )
3179 .child(
3180 v_flex().p_1().pr_1p5().gap_1().children(
3181 recent_history
3182 .into_iter()
3183 .enumerate()
3184 .map(|(index, entry)| {
3185 // TODO: Add keyboard navigation.
3186 let is_hovered =
3187 self.hovered_recent_history_item == Some(index);
3188 HistoryEntryElement::new(entry, cx.entity().downgrade())
3189 .hovered(is_hovered)
3190 .on_hover(cx.listener(
3191 move |this, is_hovered, _window, cx| {
3192 if *is_hovered {
3193 this.hovered_recent_history_item = Some(index);
3194 } else if this.hovered_recent_history_item
3195 == Some(index)
3196 {
3197 this.hovered_recent_history_item = None;
3198 }
3199 cx.notify();
3200 },
3201 ))
3202 .into_any_element()
3203 }),
3204 ),
3205 )
3206 })
3207 .when_some(configuration_error.as_ref(), |this, err| {
3208 this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
3209 })
3210 }
3211
3212 fn render_configuration_error(
3213 &self,
3214 border_bottom: bool,
3215 configuration_error: &ConfigurationError,
3216 focus_handle: &FocusHandle,
3217 window: &mut Window,
3218 cx: &mut App,
3219 ) -> impl IntoElement {
3220 let zed_provider_configured = AgentSettings::get_global(cx)
3221 .default_model
3222 .as_ref()
3223 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
3224
3225 let callout = if zed_provider_configured {
3226 Callout::new()
3227 .icon(IconName::Warning)
3228 .severity(Severity::Warning)
3229 .when(border_bottom, |this| {
3230 this.border_position(ui::BorderPosition::Bottom)
3231 })
3232 .title("Sign in to continue using Zed as your LLM provider.")
3233 .actions_slot(
3234 Button::new("sign_in", "Sign In")
3235 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3236 .label_size(LabelSize::Small)
3237 .on_click({
3238 let workspace = self.workspace.clone();
3239 move |_, _, cx| {
3240 let Ok(client) =
3241 workspace.update(cx, |workspace, _| workspace.client().clone())
3242 else {
3243 return;
3244 };
3245
3246 cx.spawn(async move |cx| {
3247 client.sign_in_with_optional_connect(true, cx).await
3248 })
3249 .detach_and_log_err(cx);
3250 }
3251 }),
3252 )
3253 } else {
3254 Callout::new()
3255 .icon(IconName::Warning)
3256 .severity(Severity::Warning)
3257 .when(border_bottom, |this| {
3258 this.border_position(ui::BorderPosition::Bottom)
3259 })
3260 .title(configuration_error.to_string())
3261 .actions_slot(
3262 Button::new("settings", "Configure")
3263 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3264 .label_size(LabelSize::Small)
3265 .key_binding(
3266 KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
3267 .map(|kb| kb.size(rems_from_px(12.))),
3268 )
3269 .on_click(|_event, window, cx| {
3270 window.dispatch_action(OpenSettings.boxed_clone(), cx)
3271 }),
3272 )
3273 };
3274
3275 match configuration_error {
3276 ConfigurationError::ModelNotFound
3277 | ConfigurationError::ProviderNotAuthenticated(_)
3278 | ConfigurationError::NoProvider => callout.into_any_element(),
3279 }
3280 }
3281
3282 fn render_tool_use_limit_reached(
3283 &self,
3284 window: &mut Window,
3285 cx: &mut Context<Self>,
3286 ) -> Option<AnyElement> {
3287 let active_thread = match &self.active_view {
3288 ActiveView::Thread { thread, .. } => thread,
3289 ActiveView::ExternalAgentThread { .. } => {
3290 return None;
3291 }
3292 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
3293 return None;
3294 }
3295 };
3296
3297 let thread = active_thread.read(cx).thread().read(cx);
3298
3299 let tool_use_limit_reached = thread.tool_use_limit_reached();
3300 if !tool_use_limit_reached {
3301 return None;
3302 }
3303
3304 let model = thread.configured_model()?.model;
3305
3306 let focus_handle = self.focus_handle(cx);
3307
3308 let banner = Banner::new()
3309 .severity(Severity::Info)
3310 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
3311 .action_slot(
3312 h_flex()
3313 .gap_1()
3314 .child(
3315 Button::new("continue-conversation", "Continue")
3316 .layer(ElevationIndex::ModalSurface)
3317 .label_size(LabelSize::Small)
3318 .key_binding(
3319 KeyBinding::for_action_in(
3320 &ContinueThread,
3321 &focus_handle,
3322 window,
3323 cx,
3324 )
3325 .map(|kb| kb.size(rems_from_px(10.))),
3326 )
3327 .on_click(cx.listener(|this, _, window, cx| {
3328 this.continue_conversation(window, cx);
3329 })),
3330 )
3331 .when(model.supports_burn_mode(), |this| {
3332 this.child(
3333 Button::new("continue-burn-mode", "Continue with Burn Mode")
3334 .style(ButtonStyle::Filled)
3335 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3336 .layer(ElevationIndex::ModalSurface)
3337 .label_size(LabelSize::Small)
3338 .key_binding(
3339 KeyBinding::for_action_in(
3340 &ContinueWithBurnMode,
3341 &focus_handle,
3342 window,
3343 cx,
3344 )
3345 .map(|kb| kb.size(rems_from_px(10.))),
3346 )
3347 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
3348 .on_click({
3349 let active_thread = active_thread.clone();
3350 cx.listener(move |this, _, window, cx| {
3351 active_thread.update(cx, |active_thread, cx| {
3352 active_thread.thread().update(cx, |thread, _cx| {
3353 thread.set_completion_mode(CompletionMode::Burn);
3354 });
3355 });
3356 this.continue_conversation(window, cx);
3357 })
3358 }),
3359 )
3360 }),
3361 );
3362
3363 Some(div().px_2().pb_2().child(banner).into_any_element())
3364 }
3365
3366 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3367 let message = message.into();
3368
3369 IconButton::new("copy", IconName::Copy)
3370 .icon_size(IconSize::Small)
3371 .icon_color(Color::Muted)
3372 .tooltip(Tooltip::text("Copy Error Message"))
3373 .on_click(move |_, _, cx| {
3374 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3375 })
3376 }
3377
3378 fn dismiss_error_button(
3379 &self,
3380 thread: &Entity<ActiveThread>,
3381 cx: &mut Context<Self>,
3382 ) -> impl IntoElement {
3383 IconButton::new("dismiss", IconName::Close)
3384 .icon_size(IconSize::Small)
3385 .icon_color(Color::Muted)
3386 .tooltip(Tooltip::text("Dismiss Error"))
3387 .on_click(cx.listener({
3388 let thread = thread.clone();
3389 move |_, _, _, cx| {
3390 thread.update(cx, |this, _cx| {
3391 this.clear_last_error();
3392 });
3393
3394 cx.notify();
3395 }
3396 }))
3397 }
3398
3399 fn upgrade_button(
3400 &self,
3401 thread: &Entity<ActiveThread>,
3402 cx: &mut Context<Self>,
3403 ) -> impl IntoElement {
3404 Button::new("upgrade", "Upgrade")
3405 .label_size(LabelSize::Small)
3406 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3407 .on_click(cx.listener({
3408 let thread = thread.clone();
3409 move |_, _, _, cx| {
3410 thread.update(cx, |this, _cx| {
3411 this.clear_last_error();
3412 });
3413
3414 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
3415 cx.notify();
3416 }
3417 }))
3418 }
3419
3420 fn render_payment_required_error(
3421 &self,
3422 thread: &Entity<ActiveThread>,
3423 cx: &mut Context<Self>,
3424 ) -> AnyElement {
3425 const ERROR_MESSAGE: &str =
3426 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
3427
3428 Callout::new()
3429 .severity(Severity::Error)
3430 .icon(IconName::XCircle)
3431 .title("Free Usage Exceeded")
3432 .description(ERROR_MESSAGE)
3433 .actions_slot(
3434 h_flex()
3435 .gap_0p5()
3436 .child(self.upgrade_button(thread, cx))
3437 .child(self.create_copy_button(ERROR_MESSAGE)),
3438 )
3439 .dismiss_action(self.dismiss_error_button(thread, cx))
3440 .into_any_element()
3441 }
3442
3443 fn render_model_request_limit_reached_error(
3444 &self,
3445 plan: Plan,
3446 thread: &Entity<ActiveThread>,
3447 cx: &mut Context<Self>,
3448 ) -> AnyElement {
3449 let error_message = match plan {
3450 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
3451 Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
3452 };
3453
3454 Callout::new()
3455 .severity(Severity::Error)
3456 .title("Model Prompt Limit Reached")
3457 .description(error_message)
3458 .actions_slot(
3459 h_flex()
3460 .gap_0p5()
3461 .child(self.upgrade_button(thread, cx))
3462 .child(self.create_copy_button(error_message)),
3463 )
3464 .dismiss_action(self.dismiss_error_button(thread, cx))
3465 .into_any_element()
3466 }
3467
3468 fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
3469 Button::new("retry", "Retry")
3470 .icon(IconName::RotateCw)
3471 .icon_position(IconPosition::Start)
3472 .icon_size(IconSize::Small)
3473 .label_size(LabelSize::Small)
3474 .on_click({
3475 let thread = thread.clone();
3476 move |_, window, cx| {
3477 thread.update(cx, |thread, cx| {
3478 thread.clear_last_error();
3479 thread.thread().update(cx, |thread, cx| {
3480 thread.retry_last_completion(Some(window.window_handle()), cx);
3481 });
3482 });
3483 }
3484 })
3485 .into_any_element()
3486 }
3487
3488 fn render_error_message(
3489 &self,
3490 header: SharedString,
3491 message: SharedString,
3492 thread: &Entity<ActiveThread>,
3493 cx: &mut Context<Self>,
3494 ) -> AnyElement {
3495 let message_with_header = format!("{}\n{}", header, message);
3496
3497 Callout::new()
3498 .severity(Severity::Error)
3499 .icon(IconName::XCircle)
3500 .title(header)
3501 .description(message)
3502 .actions_slot(
3503 h_flex()
3504 .gap_0p5()
3505 .child(self.render_retry_button(thread))
3506 .child(self.create_copy_button(message_with_header)),
3507 )
3508 .dismiss_action(self.dismiss_error_button(thread, cx))
3509 .into_any_element()
3510 }
3511
3512 fn render_retryable_error(
3513 &self,
3514 message: SharedString,
3515 can_enable_burn_mode: bool,
3516 thread: &Entity<ActiveThread>,
3517 ) -> AnyElement {
3518 Callout::new()
3519 .severity(Severity::Error)
3520 .title("Error")
3521 .description(message)
3522 .actions_slot(
3523 h_flex()
3524 .gap_0p5()
3525 .when(can_enable_burn_mode, |this| {
3526 this.child(
3527 Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
3528 .icon(IconName::ZedBurnMode)
3529 .icon_position(IconPosition::Start)
3530 .icon_size(IconSize::Small)
3531 .label_size(LabelSize::Small)
3532 .on_click({
3533 let thread = thread.clone();
3534 move |_, window, cx| {
3535 thread.update(cx, |thread, cx| {
3536 thread.clear_last_error();
3537 thread.thread().update(cx, |thread, cx| {
3538 thread.enable_burn_mode_and_retry(
3539 Some(window.window_handle()),
3540 cx,
3541 );
3542 });
3543 });
3544 }
3545 }),
3546 )
3547 })
3548 .child(self.render_retry_button(thread)),
3549 )
3550 .into_any_element()
3551 }
3552
3553 fn render_prompt_editor(
3554 &self,
3555 context_editor: &Entity<TextThreadEditor>,
3556 buffer_search_bar: &Entity<BufferSearchBar>,
3557 window: &mut Window,
3558 cx: &mut Context<Self>,
3559 ) -> Div {
3560 let mut registrar = buffer_search::DivRegistrar::new(
3561 |this, _, _cx| match &this.active_view {
3562 ActiveView::TextThread {
3563 buffer_search_bar, ..
3564 } => Some(buffer_search_bar.clone()),
3565 _ => None,
3566 },
3567 cx,
3568 );
3569 BufferSearchBar::register(&mut registrar);
3570 registrar
3571 .into_div()
3572 .size_full()
3573 .relative()
3574 .map(|parent| {
3575 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3576 if buffer_search_bar.is_dismissed() {
3577 return parent;
3578 }
3579 parent.child(
3580 div()
3581 .p(DynamicSpacing::Base08.rems(cx))
3582 .border_b_1()
3583 .border_color(cx.theme().colors().border_variant)
3584 .bg(cx.theme().colors().editor_background)
3585 .child(buffer_search_bar.render(window, cx)),
3586 )
3587 })
3588 })
3589 .child(context_editor.clone())
3590 .child(self.render_drag_target(cx))
3591 }
3592
3593 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3594 let is_local = self.project.read(cx).is_local();
3595 div()
3596 .invisible()
3597 .absolute()
3598 .top_0()
3599 .right_0()
3600 .bottom_0()
3601 .left_0()
3602 .bg(cx.theme().colors().drop_target_background)
3603 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3604 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3605 .when(is_local, |this| {
3606 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3607 })
3608 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3609 let item = tab.pane.read(cx).item_for_index(tab.ix);
3610 let project_paths = item
3611 .and_then(|item| item.project_path(cx))
3612 .into_iter()
3613 .collect::<Vec<_>>();
3614 this.handle_drop(project_paths, vec![], window, cx);
3615 }))
3616 .on_drop(
3617 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3618 let project_paths = selection
3619 .items()
3620 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3621 .collect::<Vec<_>>();
3622 this.handle_drop(project_paths, vec![], window, cx);
3623 }),
3624 )
3625 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3626 let tasks = paths
3627 .paths()
3628 .iter()
3629 .map(|path| {
3630 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3631 })
3632 .collect::<Vec<_>>();
3633 cx.spawn_in(window, async move |this, cx| {
3634 let mut paths = vec![];
3635 let mut added_worktrees = vec![];
3636 let opened_paths = futures::future::join_all(tasks).await;
3637 for entry in opened_paths {
3638 if let Some((worktree, project_path)) = entry.log_err() {
3639 added_worktrees.push(worktree);
3640 paths.push(project_path);
3641 }
3642 }
3643 this.update_in(cx, |this, window, cx| {
3644 this.handle_drop(paths, added_worktrees, window, cx);
3645 })
3646 .ok();
3647 })
3648 .detach();
3649 }))
3650 }
3651
3652 fn handle_drop(
3653 &mut self,
3654 paths: Vec<ProjectPath>,
3655 added_worktrees: Vec<Entity<Worktree>>,
3656 window: &mut Window,
3657 cx: &mut Context<Self>,
3658 ) {
3659 match &self.active_view {
3660 ActiveView::Thread { thread, .. } => {
3661 let context_store = thread.read(cx).context_store().clone();
3662 context_store.update(cx, move |context_store, cx| {
3663 let mut tasks = Vec::new();
3664 for project_path in &paths {
3665 tasks.push(context_store.add_file_from_path(
3666 project_path.clone(),
3667 false,
3668 cx,
3669 ));
3670 }
3671 cx.background_spawn(async move {
3672 futures::future::join_all(tasks).await;
3673 // Need to hold onto the worktrees until they have already been used when
3674 // opening the buffers.
3675 drop(added_worktrees);
3676 })
3677 .detach();
3678 });
3679 }
3680 ActiveView::ExternalAgentThread { thread_view } => {
3681 thread_view.update(cx, |thread_view, cx| {
3682 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3683 });
3684 }
3685 ActiveView::TextThread { context_editor, .. } => {
3686 context_editor.update(cx, |context_editor, cx| {
3687 TextThreadEditor::insert_dragged_files(
3688 context_editor,
3689 paths,
3690 added_worktrees,
3691 window,
3692 cx,
3693 );
3694 });
3695 }
3696 ActiveView::History | ActiveView::Configuration => {}
3697 }
3698 }
3699
3700 fn key_context(&self) -> KeyContext {
3701 let mut key_context = KeyContext::new_with_defaults();
3702 key_context.add("AgentPanel");
3703 match &self.active_view {
3704 ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
3705 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3706 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3707 }
3708 key_context
3709 }
3710}
3711
3712impl Render for AgentPanel {
3713 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3714 // WARNING: Changes to this element hierarchy can have
3715 // non-obvious implications to the layout of children.
3716 //
3717 // If you need to change it, please confirm:
3718 // - The message editor expands (cmd-option-esc) correctly
3719 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3720 // - Font size works as expected and can be changed with cmd-+/cmd-
3721 // - Scrolling in all views works as expected
3722 // - Files can be dropped into the panel
3723 let content = v_flex()
3724 .relative()
3725 .size_full()
3726 .justify_between()
3727 .key_context(self.key_context())
3728 .on_action(cx.listener(Self::cancel))
3729 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3730 this.new_thread(action, window, cx);
3731 }))
3732 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3733 this.open_history(window, cx);
3734 }))
3735 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3736 this.open_configuration(window, cx);
3737 }))
3738 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3739 .on_action(cx.listener(Self::deploy_rules_library))
3740 .on_action(cx.listener(Self::open_agent_diff))
3741 .on_action(cx.listener(Self::go_back))
3742 .on_action(cx.listener(Self::toggle_navigation_menu))
3743 .on_action(cx.listener(Self::toggle_options_menu))
3744 .on_action(cx.listener(Self::increase_font_size))
3745 .on_action(cx.listener(Self::decrease_font_size))
3746 .on_action(cx.listener(Self::reset_font_size))
3747 .on_action(cx.listener(Self::toggle_zoom))
3748 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3749 this.continue_conversation(window, cx);
3750 }))
3751 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3752 match &this.active_view {
3753 ActiveView::Thread { thread, .. } => {
3754 thread.update(cx, |active_thread, cx| {
3755 active_thread.thread().update(cx, |thread, _cx| {
3756 thread.set_completion_mode(CompletionMode::Burn);
3757 });
3758 });
3759 this.continue_conversation(window, cx);
3760 }
3761 ActiveView::ExternalAgentThread { .. } => {}
3762 ActiveView::TextThread { .. }
3763 | ActiveView::History
3764 | ActiveView::Configuration => {}
3765 }
3766 }))
3767 .on_action(cx.listener(Self::toggle_burn_mode))
3768 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3769 if let Some(thread_view) = this.active_thread_view() {
3770 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3771 }
3772 }))
3773 .child(self.render_toolbar(window, cx))
3774 .children(self.render_onboarding(window, cx))
3775 .map(|parent| match &self.active_view {
3776 ActiveView::Thread {
3777 thread,
3778 message_editor,
3779 ..
3780 } => parent
3781 .child(
3782 if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
3783 self.render_thread_empty_state(window, cx)
3784 .into_any_element()
3785 } else {
3786 thread.clone().into_any_element()
3787 },
3788 )
3789 .children(self.render_tool_use_limit_reached(window, cx))
3790 .when_some(thread.read(cx).last_error(), |this, last_error| {
3791 this.child(
3792 div()
3793 .child(match last_error {
3794 ThreadError::PaymentRequired => {
3795 self.render_payment_required_error(thread, cx)
3796 }
3797 ThreadError::ModelRequestLimitReached { plan } => self
3798 .render_model_request_limit_reached_error(plan, thread, cx),
3799 ThreadError::Message { header, message } => {
3800 self.render_error_message(header, message, thread, cx)
3801 }
3802 ThreadError::RetryableError {
3803 message,
3804 can_enable_burn_mode,
3805 } => self.render_retryable_error(
3806 message,
3807 can_enable_burn_mode,
3808 thread,
3809 ),
3810 })
3811 .into_any(),
3812 )
3813 })
3814 .child(h_flex().relative().child(message_editor.clone()).when(
3815 !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
3816 |this| this.child(self.render_backdrop(cx)),
3817 ))
3818 .child(self.render_drag_target(cx)),
3819 ActiveView::ExternalAgentThread { thread_view, .. } => parent
3820 .child(thread_view.clone())
3821 .child(self.render_drag_target(cx)),
3822 ActiveView::History => {
3823 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
3824 parent.child(self.acp_history.clone())
3825 } else {
3826 parent.child(self.history.clone())
3827 }
3828 }
3829 ActiveView::TextThread {
3830 context_editor,
3831 buffer_search_bar,
3832 ..
3833 } => {
3834 let model_registry = LanguageModelRegistry::read_global(cx);
3835 let configuration_error =
3836 model_registry.configuration_error(model_registry.default_model(), cx);
3837 parent
3838 .map(|this| {
3839 if !self.should_render_onboarding(cx)
3840 && let Some(err) = configuration_error.as_ref()
3841 {
3842 this.child(self.render_configuration_error(
3843 true,
3844 err,
3845 &self.focus_handle(cx),
3846 window,
3847 cx,
3848 ))
3849 } else {
3850 this
3851 }
3852 })
3853 .child(self.render_prompt_editor(
3854 context_editor,
3855 buffer_search_bar,
3856 window,
3857 cx,
3858 ))
3859 }
3860 ActiveView::Configuration => parent.children(self.configuration.clone()),
3861 })
3862 .children(self.render_trial_end_upsell(window, cx));
3863
3864 match self.active_view.which_font_size_used() {
3865 WhichFontSize::AgentFont => {
3866 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3867 .size_full()
3868 .child(content)
3869 .into_any()
3870 }
3871 _ => content.into_any(),
3872 }
3873 }
3874}
3875
3876struct PromptLibraryInlineAssist {
3877 workspace: WeakEntity<Workspace>,
3878}
3879
3880impl PromptLibraryInlineAssist {
3881 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3882 Self { workspace }
3883 }
3884}
3885
3886impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3887 fn assist(
3888 &self,
3889 prompt_editor: &Entity<Editor>,
3890 initial_prompt: Option<String>,
3891 window: &mut Window,
3892 cx: &mut Context<RulesLibrary>,
3893 ) {
3894 InlineAssistant::update_global(cx, |assistant, cx| {
3895 let Some(project) = self
3896 .workspace
3897 .upgrade()
3898 .map(|workspace| workspace.read(cx).project().downgrade())
3899 else {
3900 return;
3901 };
3902 let prompt_store = None;
3903 let thread_store = None;
3904 let text_thread_store = None;
3905 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3906 assistant.assist(
3907 prompt_editor,
3908 self.workspace.clone(),
3909 context_store,
3910 project,
3911 prompt_store,
3912 thread_store,
3913 text_thread_store,
3914 initial_prompt,
3915 window,
3916 cx,
3917 )
3918 })
3919 }
3920
3921 fn focus_agent_panel(
3922 &self,
3923 workspace: &mut Workspace,
3924 window: &mut Window,
3925 cx: &mut Context<Workspace>,
3926 ) -> bool {
3927 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3928 }
3929}
3930
3931pub struct ConcreteAssistantPanelDelegate;
3932
3933impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3934 fn active_context_editor(
3935 &self,
3936 workspace: &mut Workspace,
3937 _window: &mut Window,
3938 cx: &mut Context<Workspace>,
3939 ) -> Option<Entity<TextThreadEditor>> {
3940 let panel = workspace.panel::<AgentPanel>(cx)?;
3941 panel.read(cx).active_context_editor()
3942 }
3943
3944 fn open_saved_context(
3945 &self,
3946 workspace: &mut Workspace,
3947 path: Arc<Path>,
3948 window: &mut Window,
3949 cx: &mut Context<Workspace>,
3950 ) -> Task<Result<()>> {
3951 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3952 return Task::ready(Err(anyhow!("Agent panel not found")));
3953 };
3954
3955 panel.update(cx, |panel, cx| {
3956 panel.open_saved_prompt_editor(path, window, cx)
3957 })
3958 }
3959
3960 fn open_remote_context(
3961 &self,
3962 _workspace: &mut Workspace,
3963 _context_id: assistant_context::ContextId,
3964 _window: &mut Window,
3965 _cx: &mut Context<Workspace>,
3966 ) -> Task<Result<Entity<TextThreadEditor>>> {
3967 Task::ready(Err(anyhow!("opening remote context not implemented")))
3968 }
3969
3970 fn quote_selection(
3971 &self,
3972 workspace: &mut Workspace,
3973 selection_ranges: Vec<Range<Anchor>>,
3974 buffer: Entity<MultiBuffer>,
3975 window: &mut Window,
3976 cx: &mut Context<Workspace>,
3977 ) {
3978 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3979 return;
3980 };
3981
3982 if !panel.focus_handle(cx).contains_focused(window, cx) {
3983 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3984 }
3985
3986 panel.update(cx, |_, cx| {
3987 // Wait to create a new context until the workspace is no longer
3988 // being updated.
3989 cx.defer_in(window, move |panel, window, cx| {
3990 if let Some(thread_view) = panel.active_thread_view() {
3991 thread_view.update(cx, |thread_view, cx| {
3992 thread_view.insert_selections(window, cx);
3993 });
3994 } else if let Some(message_editor) = panel.active_message_editor() {
3995 message_editor.update(cx, |message_editor, cx| {
3996 message_editor.context_store().update(cx, |store, cx| {
3997 let buffer = buffer.read(cx);
3998 let selection_ranges = selection_ranges
3999 .into_iter()
4000 .flat_map(|range| {
4001 let (start_buffer, start) =
4002 buffer.text_anchor_for_position(range.start, cx)?;
4003 let (end_buffer, end) =
4004 buffer.text_anchor_for_position(range.end, cx)?;
4005 if start_buffer != end_buffer {
4006 return None;
4007 }
4008 Some((start_buffer, start..end))
4009 })
4010 .collect::<Vec<_>>();
4011
4012 for (buffer, range) in selection_ranges {
4013 store.add_selection(buffer, range, cx);
4014 }
4015 })
4016 })
4017 } else if let Some(context_editor) = panel.active_context_editor() {
4018 let snapshot = buffer.read(cx).snapshot(cx);
4019 let selection_ranges = selection_ranges
4020 .into_iter()
4021 .map(|range| range.to_point(&snapshot))
4022 .collect::<Vec<_>>();
4023
4024 context_editor.update(cx, |context_editor, cx| {
4025 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4026 });
4027 }
4028 });
4029 });
4030 }
4031}
4032
4033struct OnboardingUpsell;
4034
4035impl Dismissable for OnboardingUpsell {
4036 const KEY: &'static str = "dismissed-trial-upsell";
4037}
4038
4039struct TrialEndUpsell;
4040
4041impl Dismissable for TrialEndUpsell {
4042 const KEY: &'static str = "dismissed-trial-end-upsell";
4043}