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