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