1use std::ops::{Not, Range};
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Duration;
6
7use acp_thread::AcpThread;
8use agent_servers::AgentServerSettings;
9use agent2::{DbThreadMetadata, HistoryEntry};
10use db::kvp::{Dismissable, KEY_VALUE_STORE};
11use serde::{Deserialize, Serialize};
12use zed_actions::OpenBrowser;
13use zed_actions::agent::ReauthenticateAgent;
14
15use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
16use crate::agent_diff::AgentDiffThread;
17use crate::{
18 AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
19 DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
20 NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
21 ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu,
22 ToggleNewThreadMenu, ToggleOptionsMenu,
23 acp::AcpThreadView,
24 active_thread::{self, ActiveThread, ActiveThreadEvent},
25 agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
26 agent_diff::AgentDiff,
27 message_editor::{MessageEditor, MessageEditorEvent},
28 slash_command::SlashCommandCompletionProvider,
29 text_thread_editor::{
30 AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
31 render_remaining_tokens,
32 },
33 thread_history::{HistoryEntryElement, ThreadHistory},
34 ui::{AgentOnboardingModal, EndTrialUpsell},
35};
36use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
37use agent::{
38 Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
39 context_store::ContextStore,
40 history_store::{HistoryEntryId, HistoryStore},
41 thread_store::{TextThreadStore, ThreadStore},
42};
43use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
44use ai_onboarding::AgentPanelOnboarding;
45use anyhow::{Result, anyhow};
46use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
47use assistant_slash_command::SlashCommandWorkingSet;
48use assistant_tool::ToolWorkingSet;
49use client::{UserStore, zed_urls};
50use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
51use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
52use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag};
53use fs::Fs;
54use gpui::{
55 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
56 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
57 Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
58};
59use language::LanguageRegistry;
60use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
61use project::{DisableAiSettings, Project, ProjectPath, Worktree};
62use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
63use rules_library::{RulesLibrary, open_rules_library};
64use search::{BufferSearchBar, buffer_search};
65use settings::{Settings, update_settings_file};
66use theme::ThemeSettings;
67use time::UtcOffset;
68use ui::utils::WithRemSize;
69use ui::{
70 Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
71 PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
72};
73use util::ResultExt as _;
74use workspace::{
75 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
76 dock::{DockPosition, Panel, PanelEvent},
77};
78use zed_actions::{
79 DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
80 agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
81 assistant::{OpenRulesLibrary, ToggleFocus},
82};
83
84const AGENT_PANEL_KEY: &str = "agent_panel";
85
86#[derive(Serialize, Deserialize)]
87struct SerializedAgentPanel {
88 width: Option<Pixels>,
89 selected_agent: Option<AgentType>,
90}
91
92pub fn init(cx: &mut App) {
93 cx.observe_new(
94 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
95 workspace
96 .register_action(|workspace, action: &NewThread, window, cx| {
97 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
98 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
99 workspace.focus_panel::<AgentPanel>(window, cx);
100 }
101 })
102 .register_action(
103 |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
104 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
105 panel.update(cx, |panel, cx| {
106 panel.new_native_agent_thread_from_summary(action, window, cx)
107 });
108 workspace.focus_panel::<AgentPanel>(window, cx);
109 }
110 },
111 )
112 .register_action(|workspace, _: &OpenHistory, window, cx| {
113 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
114 workspace.focus_panel::<AgentPanel>(window, cx);
115 panel.update(cx, |panel, cx| panel.open_history(window, cx));
116 }
117 })
118 .register_action(|workspace, _: &OpenSettings, window, cx| {
119 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
120 workspace.focus_panel::<AgentPanel>(window, cx);
121 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
122 }
123 })
124 .register_action(|workspace, _: &NewTextThread, window, cx| {
125 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
126 workspace.focus_panel::<AgentPanel>(window, cx);
127 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
128 }
129 })
130 .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
131 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
132 workspace.focus_panel::<AgentPanel>(window, cx);
133 panel.update(cx, |panel, cx| {
134 panel.external_thread(action.agent.clone(), None, None, window, cx)
135 });
136 }
137 })
138 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
139 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
140 workspace.focus_panel::<AgentPanel>(window, cx);
141 panel.update(cx, |panel, cx| {
142 panel.deploy_rules_library(action, window, cx)
143 });
144 }
145 })
146 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
147 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
148 workspace.focus_panel::<AgentPanel>(window, cx);
149 match &panel.read(cx).active_view {
150 ActiveView::Thread { thread, .. } => {
151 let thread = thread.read(cx).thread().clone();
152 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
153 }
154 ActiveView::ExternalAgentThread { .. }
155 | ActiveView::TextThread { .. }
156 | ActiveView::History
157 | ActiveView::Configuration => {}
158 }
159 }
160 })
161 .register_action(|workspace, _: &Follow, window, cx| {
162 workspace.follow(CollaboratorId::Agent, window, cx);
163 })
164 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
165 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
166 return;
167 };
168 workspace.focus_panel::<AgentPanel>(window, cx);
169 panel.update(cx, |panel, cx| {
170 if let Some(message_editor) = panel.active_message_editor() {
171 message_editor.update(cx, |editor, cx| {
172 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
173 });
174 }
175 });
176 })
177 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
178 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
179 workspace.focus_panel::<AgentPanel>(window, cx);
180 panel.update(cx, |panel, cx| {
181 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
182 });
183 }
184 })
185 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
186 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
187 workspace.focus_panel::<AgentPanel>(window, cx);
188 panel.update(cx, |panel, cx| {
189 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
190 });
191 }
192 })
193 .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
194 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
195 workspace.focus_panel::<AgentPanel>(window, cx);
196 panel.update(cx, |panel, cx| {
197 panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
198 });
199 }
200 })
201 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
202 AgentOnboardingModal::toggle(workspace, window, cx)
203 })
204 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
205 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
206 window.refresh();
207 })
208 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
209 OnboardingUpsell::set_dismissed(false, cx);
210 })
211 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
212 TrialEndUpsell::set_dismissed(false, cx);
213 });
214 },
215 )
216 .detach();
217}
218
219enum ActiveView {
220 Thread {
221 thread: Entity<ActiveThread>,
222 change_title_editor: Entity<Editor>,
223 message_editor: Entity<MessageEditor>,
224 _subscriptions: Vec<gpui::Subscription>,
225 },
226 ExternalAgentThread {
227 thread_view: Entity<AcpThreadView>,
228 },
229 TextThread {
230 context_editor: Entity<TextThreadEditor>,
231 title_editor: Entity<Editor>,
232 buffer_search_bar: Entity<BufferSearchBar>,
233 _subscriptions: Vec<gpui::Subscription>,
234 },
235 History,
236 Configuration,
237}
238
239enum WhichFontSize {
240 AgentFont,
241 BufferFont,
242 None,
243}
244
245// TODO unify this with ExternalAgent
246#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
247pub enum AgentType {
248 #[default]
249 Zed,
250 TextThread,
251 Gemini,
252 ClaudeCode,
253 NativeAgent,
254 Custom {
255 name: SharedString,
256 settings: AgentServerSettings,
257 },
258}
259
260impl AgentType {
261 fn label(&self) -> SharedString {
262 match self {
263 Self::Zed | Self::TextThread => "Zed Agent".into(),
264 Self::NativeAgent => "Agent 2".into(),
265 Self::Gemini => "Gemini CLI".into(),
266 Self::ClaudeCode => "Claude Code".into(),
267 Self::Custom { name, .. } => name.into(),
268 }
269 }
270
271 fn icon(&self) -> Option<IconName> {
272 match self {
273 Self::Zed | Self::NativeAgent | Self::TextThread => None,
274 Self::Gemini => Some(IconName::AiGemini),
275 Self::ClaudeCode => Some(IconName::AiClaude),
276 Self::Custom { .. } => Some(IconName::Terminal),
277 }
278 }
279}
280
281impl ActiveView {
282 pub fn which_font_size_used(&self) -> WhichFontSize {
283 match self {
284 ActiveView::Thread { .. }
285 | ActiveView::ExternalAgentThread { .. }
286 | ActiveView::History => WhichFontSize::AgentFont,
287 ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
288 ActiveView::Configuration => WhichFontSize::None,
289 }
290 }
291
292 pub fn thread(
293 active_thread: Entity<ActiveThread>,
294 message_editor: Entity<MessageEditor>,
295 window: &mut Window,
296 cx: &mut Context<AgentPanel>,
297 ) -> Self {
298 let summary = active_thread.read(cx).summary(cx).or_default();
299
300 let editor = cx.new(|cx| {
301 let mut editor = Editor::single_line(window, cx);
302 editor.set_text(summary.clone(), window, cx);
303 editor
304 });
305
306 let subscriptions = vec![
307 cx.subscribe(&message_editor, |this, _, event, cx| match event {
308 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
309 cx.notify();
310 }
311 MessageEditorEvent::ScrollThreadToBottom => match &this.active_view {
312 ActiveView::Thread { thread, .. } => {
313 thread.update(cx, |thread, cx| {
314 thread.scroll_to_bottom(cx);
315 });
316 }
317 ActiveView::ExternalAgentThread { .. } => {}
318 ActiveView::TextThread { .. }
319 | ActiveView::History
320 | ActiveView::Configuration => {}
321 },
322 }),
323 window.subscribe(&editor, cx, {
324 {
325 let thread = active_thread.clone();
326 move |editor, event, window, cx| match event {
327 EditorEvent::BufferEdited => {
328 let new_summary = editor.read(cx).text(cx);
329
330 thread.update(cx, |thread, cx| {
331 thread.thread().update(cx, |thread, cx| {
332 thread.set_summary(new_summary, cx);
333 });
334 })
335 }
336 EditorEvent::Blurred => {
337 if editor.read(cx).text(cx).is_empty() {
338 let summary = thread.read(cx).summary(cx).or_default();
339
340 editor.update(cx, |editor, cx| {
341 editor.set_text(summary, window, cx);
342 });
343 }
344 }
345 _ => {}
346 }
347 }
348 }),
349 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
350 ActiveThreadEvent::EditingMessageTokenCountChanged => {
351 cx.notify();
352 }
353 }),
354 cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
355 let editor = editor.clone();
356 move |_, thread, event, window, cx| match event {
357 ThreadEvent::SummaryGenerated => {
358 let summary = thread.read(cx).summary().or_default();
359
360 editor.update(cx, |editor, cx| {
361 editor.set_text(summary, window, cx);
362 })
363 }
364 ThreadEvent::MessageAdded(_) => {
365 cx.notify();
366 }
367 _ => {}
368 }
369 }),
370 ];
371
372 Self::Thread {
373 change_title_editor: editor,
374 thread: active_thread,
375 message_editor,
376 _subscriptions: subscriptions,
377 }
378 }
379
380 pub fn prompt_editor(
381 context_editor: Entity<TextThreadEditor>,
382 history_store: Entity<HistoryStore>,
383 acp_history_store: Entity<agent2::HistoryStore>,
384 language_registry: Arc<LanguageRegistry>,
385 window: &mut Window,
386 cx: &mut App,
387 ) -> Self {
388 let title = context_editor.read(cx).title(cx).to_string();
389
390 let editor = cx.new(|cx| {
391 let mut editor = Editor::single_line(window, cx);
392 editor.set_text(title, window, cx);
393 editor
394 });
395
396 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
397 // cause a custom summary to be set. The presence of this custom summary would cause
398 // summarization to not happen.
399 let mut suppress_first_edit = true;
400
401 let subscriptions = vec![
402 window.subscribe(&editor, cx, {
403 {
404 let context_editor = context_editor.clone();
405 move |editor, event, window, cx| match event {
406 EditorEvent::BufferEdited => {
407 if suppress_first_edit {
408 suppress_first_edit = false;
409 return;
410 }
411 let new_summary = editor.read(cx).text(cx);
412
413 context_editor.update(cx, |context_editor, cx| {
414 context_editor
415 .context()
416 .update(cx, |assistant_context, cx| {
417 assistant_context.set_custom_summary(new_summary, cx);
418 })
419 })
420 }
421 EditorEvent::Blurred => {
422 if editor.read(cx).text(cx).is_empty() {
423 let summary = context_editor
424 .read(cx)
425 .context()
426 .read(cx)
427 .summary()
428 .or_default();
429
430 editor.update(cx, |editor, cx| {
431 editor.set_text(summary, window, cx);
432 });
433 }
434 }
435 _ => {}
436 }
437 }
438 }),
439 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
440 let editor = editor.clone();
441 move |assistant_context, event, window, cx| match event {
442 ContextEvent::SummaryGenerated => {
443 let summary = assistant_context.read(cx).summary().or_default();
444
445 editor.update(cx, |editor, cx| {
446 editor.set_text(summary, window, cx);
447 })
448 }
449 ContextEvent::PathChanged { old_path, new_path } => {
450 history_store.update(cx, |history_store, cx| {
451 if let Some(old_path) = old_path {
452 history_store
453 .replace_recently_opened_text_thread(old_path, new_path, cx);
454 } else {
455 history_store.push_recently_opened_entry(
456 HistoryEntryId::Context(new_path.clone()),
457 cx,
458 );
459 }
460 });
461
462 acp_history_store.update(cx, |history_store, cx| {
463 if let Some(old_path) = old_path {
464 history_store
465 .replace_recently_opened_text_thread(old_path, new_path, cx);
466 } else {
467 history_store.push_recently_opened_entry(
468 agent2::HistoryEntryId::TextThread(new_path.clone()),
469 cx,
470 );
471 }
472 });
473 }
474 _ => {}
475 }
476 }),
477 ];
478
479 let buffer_search_bar =
480 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
481 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
482 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
483 });
484
485 Self::TextThread {
486 context_editor,
487 title_editor: editor,
488 buffer_search_bar,
489 _subscriptions: subscriptions,
490 }
491 }
492}
493
494pub struct AgentPanel {
495 workspace: WeakEntity<Workspace>,
496 user_store: Entity<UserStore>,
497 project: Entity<Project>,
498 fs: Arc<dyn Fs>,
499 language_registry: Arc<LanguageRegistry>,
500 thread_store: Entity<ThreadStore>,
501 acp_history: Entity<AcpThreadHistory>,
502 acp_history_store: Entity<agent2::HistoryStore>,
503 _default_model_subscription: Subscription,
504 context_store: Entity<TextThreadStore>,
505 prompt_store: Option<Entity<PromptStore>>,
506 inline_assist_context_store: Entity<ContextStore>,
507 configuration: Option<Entity<AgentConfiguration>>,
508 configuration_subscription: Option<Subscription>,
509 local_timezone: UtcOffset,
510 active_view: ActiveView,
511 previous_view: Option<ActiveView>,
512 history_store: Entity<HistoryStore>,
513 history: Entity<ThreadHistory>,
514 hovered_recent_history_item: Option<usize>,
515 new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
516 agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
517 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
518 assistant_navigation_menu: Option<Entity<ContextMenu>>,
519 width: Option<Pixels>,
520 height: Option<Pixels>,
521 zoomed: bool,
522 pending_serialization: Option<Task<Result<()>>>,
523 onboarding: Entity<AgentPanelOnboarding>,
524 selected_agent: AgentType,
525}
526
527impl AgentPanel {
528 fn serialize(&mut self, cx: &mut Context<Self>) {
529 let width = self.width;
530 let selected_agent = self.selected_agent.clone();
531 self.pending_serialization = Some(cx.background_spawn(async move {
532 KEY_VALUE_STORE
533 .write_kvp(
534 AGENT_PANEL_KEY.into(),
535 serde_json::to_string(&SerializedAgentPanel {
536 width,
537 selected_agent: Some(selected_agent),
538 })?,
539 )
540 .await?;
541 anyhow::Ok(())
542 }));
543 }
544
545 pub fn load(
546 workspace: WeakEntity<Workspace>,
547 prompt_builder: Arc<PromptBuilder>,
548 mut cx: AsyncWindowContext,
549 ) -> Task<Result<Entity<Self>>> {
550 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
551 cx.spawn(async move |cx| {
552 let prompt_store = match prompt_store {
553 Ok(prompt_store) => prompt_store.await.ok(),
554 Err(_) => None,
555 };
556 let tools = cx.new(|_| ToolWorkingSet::default())?;
557 let thread_store = workspace
558 .update(cx, |workspace, cx| {
559 let project = workspace.project().clone();
560 ThreadStore::load(
561 project,
562 tools.clone(),
563 prompt_store.clone(),
564 prompt_builder.clone(),
565 cx,
566 )
567 })?
568 .await?;
569
570 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
571 let context_store = workspace
572 .update(cx, |workspace, cx| {
573 let project = workspace.project().clone();
574 assistant_context::ContextStore::new(
575 project,
576 prompt_builder.clone(),
577 slash_commands,
578 cx,
579 )
580 })?
581 .await?;
582
583 let serialized_panel = if let Some(panel) = cx
584 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
585 .await
586 .log_err()
587 .flatten()
588 {
589 Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
590 } else {
591 None
592 };
593
594 // Wait for the Gemini/Native feature flag to be available.
595 let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
596 if !client.status().borrow().is_signed_out() {
597 cx.update(|_, cx| {
598 cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
599 Duration::from_secs(2),
600 )
601 })?
602 .await;
603 }
604
605 let panel = workspace.update_in(cx, |workspace, window, cx| {
606 let panel = cx.new(|cx| {
607 Self::new(
608 workspace,
609 thread_store,
610 context_store,
611 prompt_store,
612 window,
613 cx,
614 )
615 });
616 if let Some(serialized_panel) = serialized_panel {
617 panel.update(cx, |panel, cx| {
618 panel.width = serialized_panel.width.map(|w| w.round());
619 if let Some(selected_agent) = serialized_panel.selected_agent {
620 panel.selected_agent = selected_agent.clone();
621 panel.new_agent_thread(selected_agent, window, cx);
622 }
623 cx.notify();
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 set_selected_agent(
1856 &mut self,
1857 agent: AgentType,
1858 window: &mut Window,
1859 cx: &mut Context<Self>,
1860 ) {
1861 if self.selected_agent != agent {
1862 self.selected_agent = agent.clone();
1863 self.serialize(cx);
1864 }
1865 self.new_agent_thread(agent, window, cx);
1866 }
1867
1868 pub fn selected_agent(&self) -> AgentType {
1869 self.selected_agent.clone()
1870 }
1871
1872 pub fn new_agent_thread(
1873 &mut self,
1874 agent: AgentType,
1875 window: &mut Window,
1876 cx: &mut Context<Self>,
1877 ) {
1878 match agent {
1879 AgentType::Zed => {
1880 window.dispatch_action(
1881 NewThread {
1882 from_thread_id: None,
1883 }
1884 .boxed_clone(),
1885 cx,
1886 );
1887 }
1888 AgentType::TextThread => {
1889 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1890 }
1891 AgentType::NativeAgent => self.external_thread(
1892 Some(crate::ExternalAgent::NativeAgent),
1893 None,
1894 None,
1895 window,
1896 cx,
1897 ),
1898 AgentType::Gemini => {
1899 self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1900 }
1901 AgentType::ClaudeCode => self.external_thread(
1902 Some(crate::ExternalAgent::ClaudeCode),
1903 None,
1904 None,
1905 window,
1906 cx,
1907 ),
1908 AgentType::Custom { name, settings } => self.external_thread(
1909 Some(crate::ExternalAgent::Custom { name, settings }),
1910 None,
1911 None,
1912 window,
1913 cx,
1914 ),
1915 }
1916 }
1917
1918 pub fn load_agent_thread(
1919 &mut self,
1920 thread: DbThreadMetadata,
1921 window: &mut Window,
1922 cx: &mut Context<Self>,
1923 ) {
1924 self.external_thread(
1925 Some(ExternalAgent::NativeAgent),
1926 Some(thread),
1927 None,
1928 window,
1929 cx,
1930 );
1931 }
1932}
1933
1934impl Focusable for AgentPanel {
1935 fn focus_handle(&self, cx: &App) -> FocusHandle {
1936 match &self.active_view {
1937 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1938 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1939 ActiveView::History => {
1940 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
1941 self.acp_history.focus_handle(cx)
1942 } else {
1943 self.history.focus_handle(cx)
1944 }
1945 }
1946 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1947 ActiveView::Configuration => {
1948 if let Some(configuration) = self.configuration.as_ref() {
1949 configuration.focus_handle(cx)
1950 } else {
1951 cx.focus_handle()
1952 }
1953 }
1954 }
1955 }
1956}
1957
1958fn agent_panel_dock_position(cx: &App) -> DockPosition {
1959 match AgentSettings::get_global(cx).dock {
1960 AgentDockPosition::Left => DockPosition::Left,
1961 AgentDockPosition::Bottom => DockPosition::Bottom,
1962 AgentDockPosition::Right => DockPosition::Right,
1963 }
1964}
1965
1966impl EventEmitter<PanelEvent> for AgentPanel {}
1967
1968impl Panel for AgentPanel {
1969 fn persistent_name() -> &'static str {
1970 "AgentPanel"
1971 }
1972
1973 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1974 agent_panel_dock_position(cx)
1975 }
1976
1977 fn position_is_valid(&self, position: DockPosition) -> bool {
1978 position != DockPosition::Bottom
1979 }
1980
1981 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1982 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1983 let dock = match position {
1984 DockPosition::Left => AgentDockPosition::Left,
1985 DockPosition::Bottom => AgentDockPosition::Bottom,
1986 DockPosition::Right => AgentDockPosition::Right,
1987 };
1988 settings.set_dock(dock);
1989 });
1990 }
1991
1992 fn size(&self, window: &Window, cx: &App) -> Pixels {
1993 let settings = AgentSettings::get_global(cx);
1994 match self.position(window, cx) {
1995 DockPosition::Left | DockPosition::Right => {
1996 self.width.unwrap_or(settings.default_width)
1997 }
1998 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1999 }
2000 }
2001
2002 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
2003 match self.position(window, cx) {
2004 DockPosition::Left | DockPosition::Right => self.width = size,
2005 DockPosition::Bottom => self.height = size,
2006 }
2007 self.serialize(cx);
2008 cx.notify();
2009 }
2010
2011 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
2012
2013 fn remote_id() -> Option<proto::PanelId> {
2014 Some(proto::PanelId::AssistantPanel)
2015 }
2016
2017 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2018 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2019 }
2020
2021 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2022 Some("Agent Panel")
2023 }
2024
2025 fn toggle_action(&self) -> Box<dyn Action> {
2026 Box::new(ToggleFocus)
2027 }
2028
2029 fn activation_priority(&self) -> u32 {
2030 3
2031 }
2032
2033 fn enabled(&self, cx: &App) -> bool {
2034 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
2035 }
2036
2037 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2038 self.zoomed
2039 }
2040
2041 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2042 self.zoomed = zoomed;
2043 cx.notify();
2044 }
2045}
2046
2047impl AgentPanel {
2048 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2049 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2050
2051 let content = match &self.active_view {
2052 ActiveView::Thread {
2053 thread: active_thread,
2054 change_title_editor,
2055 ..
2056 } => {
2057 let state = {
2058 let active_thread = active_thread.read(cx);
2059 if active_thread.is_empty() {
2060 &ThreadSummary::Pending
2061 } else {
2062 active_thread.summary(cx)
2063 }
2064 };
2065
2066 match state {
2067 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
2068 .truncate()
2069 .color(Color::Muted)
2070 .into_any_element(),
2071 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
2072 .truncate()
2073 .color(Color::Muted)
2074 .into_any_element(),
2075 ThreadSummary::Ready(_) => div()
2076 .w_full()
2077 .child(change_title_editor.clone())
2078 .into_any_element(),
2079 ThreadSummary::Error => h_flex()
2080 .w_full()
2081 .child(change_title_editor.clone())
2082 .child(
2083 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2084 .icon_size(IconSize::Small)
2085 .on_click({
2086 let active_thread = active_thread.clone();
2087 move |_, _window, cx| {
2088 active_thread.update(cx, |thread, cx| {
2089 thread.regenerate_summary(cx);
2090 });
2091 }
2092 })
2093 .tooltip(move |_window, cx| {
2094 cx.new(|_| {
2095 Tooltip::new("Failed to generate title")
2096 .meta("Click to try again")
2097 })
2098 .into()
2099 }),
2100 )
2101 .into_any_element(),
2102 }
2103 }
2104 ActiveView::ExternalAgentThread { thread_view } => {
2105 if let Some(title_editor) = thread_view.read(cx).title_editor() {
2106 div()
2107 .w_full()
2108 .on_action({
2109 let thread_view = thread_view.downgrade();
2110 move |_: &menu::Confirm, window, cx| {
2111 if let Some(thread_view) = thread_view.upgrade() {
2112 thread_view.focus_handle(cx).focus(window);
2113 }
2114 }
2115 })
2116 .on_action({
2117 let thread_view = thread_view.downgrade();
2118 move |_: &editor::actions::Cancel, window, cx| {
2119 if let Some(thread_view) = thread_view.upgrade() {
2120 thread_view.focus_handle(cx).focus(window);
2121 }
2122 }
2123 })
2124 .child(title_editor)
2125 .into_any_element()
2126 } else {
2127 Label::new(thread_view.read(cx).title())
2128 .color(Color::Muted)
2129 .truncate()
2130 .into_any_element()
2131 }
2132 }
2133 ActiveView::TextThread {
2134 title_editor,
2135 context_editor,
2136 ..
2137 } => {
2138 let summary = context_editor.read(cx).context().read(cx).summary();
2139
2140 match summary {
2141 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
2142 .color(Color::Muted)
2143 .truncate()
2144 .into_any_element(),
2145 ContextSummary::Content(summary) => {
2146 if summary.done {
2147 div()
2148 .w_full()
2149 .child(title_editor.clone())
2150 .into_any_element()
2151 } else {
2152 Label::new(LOADING_SUMMARY_PLACEHOLDER)
2153 .truncate()
2154 .color(Color::Muted)
2155 .into_any_element()
2156 }
2157 }
2158 ContextSummary::Error => h_flex()
2159 .w_full()
2160 .child(title_editor.clone())
2161 .child(
2162 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2163 .icon_size(IconSize::Small)
2164 .on_click({
2165 let context_editor = context_editor.clone();
2166 move |_, _window, cx| {
2167 context_editor.update(cx, |context_editor, cx| {
2168 context_editor.regenerate_summary(cx);
2169 });
2170 }
2171 })
2172 .tooltip(move |_window, cx| {
2173 cx.new(|_| {
2174 Tooltip::new("Failed to generate title")
2175 .meta("Click to try again")
2176 })
2177 .into()
2178 }),
2179 )
2180 .into_any_element(),
2181 }
2182 }
2183 ActiveView::History => Label::new("History").truncate().into_any_element(),
2184 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2185 };
2186
2187 h_flex()
2188 .key_context("TitleEditor")
2189 .id("TitleEditor")
2190 .flex_grow()
2191 .w_full()
2192 .max_w_full()
2193 .overflow_x_scroll()
2194 .child(content)
2195 .into_any()
2196 }
2197
2198 fn render_panel_options_menu(
2199 &self,
2200 window: &mut Window,
2201 cx: &mut Context<Self>,
2202 ) -> impl IntoElement {
2203 let user_store = self.user_store.read(cx);
2204 let usage = user_store.model_request_usage();
2205 let account_url = zed_urls::account_url(cx);
2206
2207 let focus_handle = self.focus_handle(cx);
2208
2209 let full_screen_label = if self.is_zoomed(window, cx) {
2210 "Disable Full Screen"
2211 } else {
2212 "Enable Full Screen"
2213 };
2214
2215 let selected_agent = self.selected_agent.clone();
2216
2217 PopoverMenu::new("agent-options-menu")
2218 .trigger_with_tooltip(
2219 IconButton::new("agent-options-menu", IconName::Ellipsis)
2220 .icon_size(IconSize::Small),
2221 {
2222 let focus_handle = focus_handle.clone();
2223 move |window, cx| {
2224 Tooltip::for_action_in(
2225 "Toggle Agent Menu",
2226 &ToggleOptionsMenu,
2227 &focus_handle,
2228 window,
2229 cx,
2230 )
2231 }
2232 },
2233 )
2234 .anchor(Corner::TopRight)
2235 .with_handle(self.agent_panel_menu_handle.clone())
2236 .menu({
2237 move |window, cx| {
2238 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2239 menu = menu.context(focus_handle.clone());
2240 if let Some(usage) = usage {
2241 menu = menu
2242 .header_with_link("Prompt Usage", "Manage", account_url.clone())
2243 .custom_entry(
2244 move |_window, cx| {
2245 let used_percentage = match usage.limit {
2246 UsageLimit::Limited(limit) => {
2247 Some((usage.amount as f32 / limit as f32) * 100.)
2248 }
2249 UsageLimit::Unlimited => None,
2250 };
2251
2252 h_flex()
2253 .flex_1()
2254 .gap_1p5()
2255 .children(used_percentage.map(|percent| {
2256 ProgressBar::new("usage", percent, 100., cx)
2257 }))
2258 .child(
2259 Label::new(match usage.limit {
2260 UsageLimit::Limited(limit) => {
2261 format!("{} / {limit}", usage.amount)
2262 }
2263 UsageLimit::Unlimited => {
2264 format!("{} / ∞", usage.amount)
2265 }
2266 })
2267 .size(LabelSize::Small)
2268 .color(Color::Muted),
2269 )
2270 .into_any_element()
2271 },
2272 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
2273 )
2274 .separator()
2275 }
2276
2277 menu = menu
2278 .header("MCP Servers")
2279 .action(
2280 "View Server Extensions",
2281 Box::new(zed_actions::Extensions {
2282 category_filter: Some(
2283 zed_actions::ExtensionCategoryFilter::ContextServers,
2284 ),
2285 id: None,
2286 }),
2287 )
2288 .action("Add Custom Server…", Box::new(AddContextServer))
2289 .separator();
2290
2291 menu = menu
2292 .action("Rules…", Box::new(OpenRulesLibrary::default()))
2293 .action("Settings", Box::new(OpenSettings))
2294 .separator()
2295 .action(full_screen_label, Box::new(ToggleZoom));
2296
2297 if selected_agent == AgentType::Gemini {
2298 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2299 }
2300
2301 menu
2302 }))
2303 }
2304 })
2305 }
2306
2307 fn render_recent_entries_menu(
2308 &self,
2309 icon: IconName,
2310 corner: Corner,
2311 cx: &mut Context<Self>,
2312 ) -> impl IntoElement {
2313 let focus_handle = self.focus_handle(cx);
2314
2315 PopoverMenu::new("agent-nav-menu")
2316 .trigger_with_tooltip(
2317 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2318 {
2319 move |window, cx| {
2320 Tooltip::for_action_in(
2321 "Toggle Recent Threads",
2322 &ToggleNavigationMenu,
2323 &focus_handle,
2324 window,
2325 cx,
2326 )
2327 }
2328 },
2329 )
2330 .anchor(corner)
2331 .with_handle(self.assistant_navigation_menu_handle.clone())
2332 .menu({
2333 let menu = self.assistant_navigation_menu.clone();
2334 move |window, cx| {
2335 telemetry::event!("View Thread History Clicked");
2336
2337 if let Some(menu) = menu.as_ref() {
2338 menu.update(cx, |_, cx| {
2339 cx.defer_in(window, |menu, window, cx| {
2340 menu.rebuild(window, cx);
2341 });
2342 })
2343 }
2344 menu.clone()
2345 }
2346 })
2347 }
2348
2349 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2350 let focus_handle = self.focus_handle(cx);
2351
2352 IconButton::new("go-back", IconName::ArrowLeft)
2353 .icon_size(IconSize::Small)
2354 .on_click(cx.listener(|this, _, window, cx| {
2355 this.go_back(&workspace::GoBack, window, cx);
2356 }))
2357 .tooltip({
2358 move |window, cx| {
2359 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
2360 }
2361 })
2362 }
2363
2364 fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2365 let focus_handle = self.focus_handle(cx);
2366
2367 let active_thread = match &self.active_view {
2368 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2369 ActiveView::ExternalAgentThread { .. }
2370 | ActiveView::TextThread { .. }
2371 | ActiveView::History
2372 | ActiveView::Configuration => None,
2373 };
2374
2375 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2376 .trigger_with_tooltip(
2377 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2378 Tooltip::text("New Thread…"),
2379 )
2380 .anchor(Corner::TopRight)
2381 .with_handle(self.new_thread_menu_handle.clone())
2382 .menu({
2383 move |window, cx| {
2384 let active_thread = active_thread.clone();
2385 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2386 menu = menu
2387 .context(focus_handle.clone())
2388 .when_some(active_thread, |this, active_thread| {
2389 let thread = active_thread.read(cx);
2390
2391 if !thread.is_empty() {
2392 let thread_id = thread.id().clone();
2393 this.item(
2394 ContextMenuEntry::new("New From Summary")
2395 .icon(IconName::ThreadFromSummary)
2396 .icon_color(Color::Muted)
2397 .handler(move |window, cx| {
2398 window.dispatch_action(
2399 Box::new(NewThread {
2400 from_thread_id: Some(thread_id.clone()),
2401 }),
2402 cx,
2403 );
2404 }),
2405 )
2406 } else {
2407 this
2408 }
2409 })
2410 .item(
2411 ContextMenuEntry::new("New Thread")
2412 .icon(IconName::Thread)
2413 .icon_color(Color::Muted)
2414 .action(NewThread::default().boxed_clone())
2415 .handler(move |window, cx| {
2416 window.dispatch_action(
2417 NewThread::default().boxed_clone(),
2418 cx,
2419 );
2420 }),
2421 )
2422 .item(
2423 ContextMenuEntry::new("New Text Thread")
2424 .icon(IconName::TextThread)
2425 .icon_color(Color::Muted)
2426 .action(NewTextThread.boxed_clone())
2427 .handler(move |window, cx| {
2428 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2429 }),
2430 );
2431 menu
2432 }))
2433 }
2434 });
2435
2436 h_flex()
2437 .id("assistant-toolbar")
2438 .h(Tab::container_height(cx))
2439 .max_w_full()
2440 .flex_none()
2441 .justify_between()
2442 .gap_2()
2443 .bg(cx.theme().colors().tab_bar_background)
2444 .border_b_1()
2445 .border_color(cx.theme().colors().border)
2446 .child(
2447 h_flex()
2448 .size_full()
2449 .pl_1()
2450 .gap_1()
2451 .child(match &self.active_view {
2452 ActiveView::History | ActiveView::Configuration => div()
2453 .pl(DynamicSpacing::Base04.rems(cx))
2454 .child(self.render_toolbar_back_button(cx))
2455 .into_any_element(),
2456 _ => self
2457 .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx)
2458 .into_any_element(),
2459 })
2460 .child(self.render_title_view(window, cx)),
2461 )
2462 .child(
2463 h_flex()
2464 .h_full()
2465 .gap_2()
2466 .children(self.render_token_count(cx))
2467 .child(
2468 h_flex()
2469 .h_full()
2470 .gap(DynamicSpacing::Base02.rems(cx))
2471 .px(DynamicSpacing::Base08.rems(cx))
2472 .border_l_1()
2473 .border_color(cx.theme().colors().border)
2474 .child(new_thread_menu)
2475 .child(self.render_panel_options_menu(window, cx)),
2476 ),
2477 )
2478 }
2479
2480 fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2481 let focus_handle = self.focus_handle(cx);
2482
2483 let active_thread = match &self.active_view {
2484 ActiveView::ExternalAgentThread { thread_view } => {
2485 thread_view.read(cx).as_native_thread(cx)
2486 }
2487 ActiveView::Thread { .. }
2488 | ActiveView::TextThread { .. }
2489 | ActiveView::History
2490 | ActiveView::Configuration => None,
2491 };
2492
2493 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2494 .trigger_with_tooltip(
2495 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2496 {
2497 let focus_handle = focus_handle.clone();
2498 move |window, cx| {
2499 Tooltip::for_action_in(
2500 "New…",
2501 &ToggleNewThreadMenu,
2502 &focus_handle,
2503 window,
2504 cx,
2505 )
2506 }
2507 },
2508 )
2509 .anchor(Corner::TopLeft)
2510 .with_handle(self.new_thread_menu_handle.clone())
2511 .menu({
2512 let workspace = self.workspace.clone();
2513
2514 move |window, cx| {
2515 telemetry::event!("New Thread Clicked");
2516
2517 let active_thread = active_thread.clone();
2518 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2519 menu = menu
2520 .context(focus_handle.clone())
2521 .header("Zed Agent")
2522 .when_some(active_thread, |this, active_thread| {
2523 let thread = active_thread.read(cx);
2524
2525 if !thread.is_empty() {
2526 let session_id = thread.id().clone();
2527 this.item(
2528 ContextMenuEntry::new("New From Summary")
2529 .icon(IconName::ThreadFromSummary)
2530 .icon_color(Color::Muted)
2531 .handler(move |window, cx| {
2532 window.dispatch_action(
2533 Box::new(NewNativeAgentThreadFromSummary {
2534 from_session_id: session_id.clone(),
2535 }),
2536 cx,
2537 );
2538 }),
2539 )
2540 } else {
2541 this
2542 }
2543 })
2544 .item(
2545 ContextMenuEntry::new("New Thread")
2546 .action(NewThread::default().boxed_clone())
2547 .icon(IconName::Thread)
2548 .icon_color(Color::Muted)
2549 .handler({
2550 let workspace = workspace.clone();
2551 move |window, cx| {
2552 if let Some(workspace) = workspace.upgrade() {
2553 workspace.update(cx, |workspace, cx| {
2554 if let Some(panel) =
2555 workspace.panel::<AgentPanel>(cx)
2556 {
2557 panel.update(cx, |panel, cx| {
2558 panel.set_selected_agent(
2559 AgentType::NativeAgent,
2560 window,
2561 cx,
2562 );
2563 });
2564 }
2565 });
2566 }
2567 }
2568 }),
2569 )
2570 .item(
2571 ContextMenuEntry::new("New Text Thread")
2572 .icon(IconName::TextThread)
2573 .icon_color(Color::Muted)
2574 .action(NewTextThread.boxed_clone())
2575 .handler({
2576 let workspace = workspace.clone();
2577 move |window, cx| {
2578 if let Some(workspace) = workspace.upgrade() {
2579 workspace.update(cx, |workspace, cx| {
2580 if let Some(panel) =
2581 workspace.panel::<AgentPanel>(cx)
2582 {
2583 panel.update(cx, |panel, cx| {
2584 panel.set_selected_agent(
2585 AgentType::TextThread,
2586 window,
2587 cx,
2588 );
2589 });
2590 }
2591 });
2592 }
2593 }
2594 }),
2595 )
2596 .separator()
2597 .header("External Agents")
2598 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2599 menu.item(
2600 ContextMenuEntry::new("New Gemini CLI Thread")
2601 .icon(IconName::AiGemini)
2602 .icon_color(Color::Muted)
2603 .handler({
2604 let workspace = workspace.clone();
2605 move |window, cx| {
2606 if let Some(workspace) = workspace.upgrade() {
2607 workspace.update(cx, |workspace, cx| {
2608 if let Some(panel) =
2609 workspace.panel::<AgentPanel>(cx)
2610 {
2611 panel.update(cx, |panel, cx| {
2612 panel.set_selected_agent(
2613 AgentType::Gemini,
2614 window,
2615 cx,
2616 );
2617 });
2618 }
2619 });
2620 }
2621 }
2622 }),
2623 )
2624 })
2625 .when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| {
2626 menu.item(
2627 ContextMenuEntry::new("New Claude Code Thread")
2628 .icon(IconName::AiClaude)
2629 .icon_color(Color::Muted)
2630 .handler({
2631 let workspace = workspace.clone();
2632 move |window, cx| {
2633 if let Some(workspace) = workspace.upgrade() {
2634 workspace.update(cx, |workspace, cx| {
2635 if let Some(panel) =
2636 workspace.panel::<AgentPanel>(cx)
2637 {
2638 panel.update(cx, |panel, cx| {
2639 panel.set_selected_agent(
2640 AgentType::ClaudeCode,
2641 window,
2642 cx,
2643 );
2644 });
2645 }
2646 });
2647 }
2648 }
2649 }),
2650 )
2651 })
2652 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
2653 // Add custom agents from settings
2654 let settings =
2655 agent_servers::AllAgentServersSettings::get_global(cx);
2656 for (agent_name, agent_settings) in &settings.custom {
2657 menu = menu.item(
2658 ContextMenuEntry::new(format!("New {} Thread", agent_name))
2659 .icon(IconName::Terminal)
2660 .icon_color(Color::Muted)
2661 .handler({
2662 let workspace = workspace.clone();
2663 let agent_name = agent_name.clone();
2664 let agent_settings = agent_settings.clone();
2665 move |window, cx| {
2666 if let Some(workspace) = workspace.upgrade() {
2667 workspace.update(cx, |workspace, cx| {
2668 if let Some(panel) =
2669 workspace.panel::<AgentPanel>(cx)
2670 {
2671 panel.update(cx, |panel, cx| {
2672 panel.set_selected_agent(
2673 AgentType::Custom {
2674 name: agent_name
2675 .clone(),
2676 settings:
2677 agent_settings
2678 .clone(),
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 }
2685 });
2686 }
2687 }
2688 }),
2689 );
2690 }
2691
2692 menu
2693 })
2694 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2695 menu.separator().link(
2696 "Add Your Own Agent",
2697 OpenBrowser {
2698 url: "https://agentclientprotocol.com/".into(),
2699 }
2700 .boxed_clone(),
2701 )
2702 });
2703 menu
2704 }))
2705 }
2706 });
2707
2708 let selected_agent_label = self.selected_agent.label();
2709 let selected_agent = div()
2710 .id("selected_agent_icon")
2711 .when_some(self.selected_agent.icon(), |this, icon| {
2712 this.px(DynamicSpacing::Base02.rems(cx))
2713 .child(Icon::new(icon).color(Color::Muted))
2714 .tooltip(move |window, cx| {
2715 Tooltip::with_meta(
2716 selected_agent_label.clone(),
2717 None,
2718 "Selected Agent",
2719 window,
2720 cx,
2721 )
2722 })
2723 })
2724 .into_any_element();
2725
2726 h_flex()
2727 .id("agent-panel-toolbar")
2728 .h(Tab::container_height(cx))
2729 .max_w_full()
2730 .flex_none()
2731 .justify_between()
2732 .gap_2()
2733 .bg(cx.theme().colors().tab_bar_background)
2734 .border_b_1()
2735 .border_color(cx.theme().colors().border)
2736 .child(
2737 h_flex()
2738 .size_full()
2739 .gap(DynamicSpacing::Base04.rems(cx))
2740 .pl(DynamicSpacing::Base04.rems(cx))
2741 .child(match &self.active_view {
2742 ActiveView::History | ActiveView::Configuration => {
2743 self.render_toolbar_back_button(cx).into_any_element()
2744 }
2745 _ => selected_agent.into_any_element(),
2746 })
2747 .child(self.render_title_view(window, cx)),
2748 )
2749 .child(
2750 h_flex()
2751 .flex_none()
2752 .gap(DynamicSpacing::Base02.rems(cx))
2753 .pl(DynamicSpacing::Base04.rems(cx))
2754 .pr(DynamicSpacing::Base06.rems(cx))
2755 .child(new_thread_menu)
2756 .child(self.render_recent_entries_menu(
2757 IconName::MenuAltTemp,
2758 Corner::TopRight,
2759 cx,
2760 ))
2761 .child(self.render_panel_options_menu(window, cx)),
2762 )
2763 }
2764
2765 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2766 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>()
2767 || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>()
2768 {
2769 self.render_toolbar_new(window, cx).into_any_element()
2770 } else {
2771 self.render_toolbar_old(window, cx).into_any_element()
2772 }
2773 }
2774
2775 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
2776 match &self.active_view {
2777 ActiveView::Thread {
2778 thread,
2779 message_editor,
2780 ..
2781 } => {
2782 let active_thread = thread.read(cx);
2783 let message_editor = message_editor.read(cx);
2784
2785 let editor_empty = message_editor.is_editor_fully_empty(cx);
2786
2787 if active_thread.is_empty() && editor_empty {
2788 return None;
2789 }
2790
2791 let thread = active_thread.thread().read(cx);
2792 let is_generating = thread.is_generating();
2793 let conversation_token_usage = thread.total_token_usage()?;
2794
2795 let (total_token_usage, is_estimating) =
2796 if let Some((editing_message_id, unsent_tokens)) =
2797 active_thread.editing_message_id()
2798 {
2799 let combined = thread
2800 .token_usage_up_to_message(editing_message_id)
2801 .add(unsent_tokens);
2802
2803 (combined, unsent_tokens > 0)
2804 } else {
2805 let unsent_tokens =
2806 message_editor.last_estimated_token_count().unwrap_or(0);
2807 let combined = conversation_token_usage.add(unsent_tokens);
2808
2809 (combined, unsent_tokens > 0)
2810 };
2811
2812 let is_waiting_to_update_token_count =
2813 message_editor.is_waiting_to_update_token_count();
2814
2815 if total_token_usage.total == 0 {
2816 return None;
2817 }
2818
2819 let token_color = match total_token_usage.ratio() {
2820 TokenUsageRatio::Normal if is_estimating => Color::Default,
2821 TokenUsageRatio::Normal => Color::Muted,
2822 TokenUsageRatio::Warning => Color::Warning,
2823 TokenUsageRatio::Exceeded => Color::Error,
2824 };
2825
2826 let token_count = h_flex()
2827 .id("token-count")
2828 .flex_shrink_0()
2829 .gap_0p5()
2830 .when(!is_generating && is_estimating, |parent| {
2831 parent
2832 .child(
2833 h_flex()
2834 .mr_1()
2835 .size_2p5()
2836 .justify_center()
2837 .rounded_full()
2838 .bg(cx.theme().colors().text.opacity(0.1))
2839 .child(
2840 div().size_1().rounded_full().bg(cx.theme().colors().text),
2841 ),
2842 )
2843 .tooltip(move |window, cx| {
2844 Tooltip::with_meta(
2845 "Estimated New Token Count",
2846 None,
2847 format!(
2848 "Current Conversation Tokens: {}",
2849 humanize_token_count(conversation_token_usage.total)
2850 ),
2851 window,
2852 cx,
2853 )
2854 })
2855 })
2856 .child(
2857 Label::new(humanize_token_count(total_token_usage.total))
2858 .size(LabelSize::Small)
2859 .color(token_color)
2860 .map(|label| {
2861 if is_generating || is_waiting_to_update_token_count {
2862 label
2863 .with_animation(
2864 "used-tokens-label",
2865 Animation::new(Duration::from_secs(2))
2866 .repeat()
2867 .with_easing(pulsating_between(0.6, 1.)),
2868 |label, delta| label.alpha(delta),
2869 )
2870 .into_any()
2871 } else {
2872 label.into_any_element()
2873 }
2874 }),
2875 )
2876 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2877 .child(
2878 Label::new(humanize_token_count(total_token_usage.max))
2879 .size(LabelSize::Small)
2880 .color(Color::Muted),
2881 )
2882 .into_any();
2883
2884 Some(token_count)
2885 }
2886 ActiveView::TextThread { context_editor, .. } => {
2887 let element = render_remaining_tokens(context_editor, cx)?;
2888
2889 Some(element.into_any_element())
2890 }
2891 ActiveView::ExternalAgentThread { .. }
2892 | ActiveView::History
2893 | ActiveView::Configuration => None,
2894 }
2895 }
2896
2897 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2898 if TrialEndUpsell::dismissed() {
2899 return false;
2900 }
2901
2902 match &self.active_view {
2903 ActiveView::Thread { thread, .. } => {
2904 if thread
2905 .read(cx)
2906 .thread()
2907 .read(cx)
2908 .configured_model()
2909 .is_some_and(|model| {
2910 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2911 })
2912 {
2913 return false;
2914 }
2915 }
2916 ActiveView::TextThread { .. } => {
2917 if LanguageModelRegistry::global(cx)
2918 .read(cx)
2919 .default_model()
2920 .is_some_and(|model| {
2921 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2922 })
2923 {
2924 return false;
2925 }
2926 }
2927 ActiveView::ExternalAgentThread { .. }
2928 | ActiveView::History
2929 | ActiveView::Configuration => return false,
2930 }
2931
2932 let plan = self.user_store.read(cx).plan();
2933 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2934
2935 matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
2936 }
2937
2938 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2939 if OnboardingUpsell::dismissed() {
2940 return false;
2941 }
2942
2943 match &self.active_view {
2944 ActiveView::History | ActiveView::Configuration => false,
2945 ActiveView::ExternalAgentThread { thread_view, .. }
2946 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2947 {
2948 false
2949 }
2950 _ => {
2951 let history_is_empty = if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
2952 self.acp_history_store.read(cx).is_empty(cx)
2953 } else {
2954 self.history_store
2955 .update(cx, |store, cx| store.recent_entries(1, cx).is_empty())
2956 };
2957
2958 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2959 .providers()
2960 .iter()
2961 .any(|provider| {
2962 provider.is_authenticated(cx)
2963 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2964 });
2965
2966 history_is_empty || !has_configured_non_zed_providers
2967 }
2968 }
2969 }
2970
2971 fn render_onboarding(
2972 &self,
2973 _window: &mut Window,
2974 cx: &mut Context<Self>,
2975 ) -> Option<impl IntoElement> {
2976 if !self.should_render_onboarding(cx) {
2977 return None;
2978 }
2979
2980 let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
2981 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2982
2983 Some(
2984 div()
2985 .when(thread_view, |this| {
2986 this.size_full().bg(cx.theme().colors().panel_background)
2987 })
2988 .when(text_thread_view, |this| {
2989 this.bg(cx.theme().colors().editor_background)
2990 })
2991 .child(self.onboarding.clone()),
2992 )
2993 }
2994
2995 fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
2996 div()
2997 .size_full()
2998 .absolute()
2999 .inset_0()
3000 .bg(cx.theme().colors().panel_background)
3001 .opacity(0.8)
3002 .block_mouse_except_scroll()
3003 }
3004
3005 fn render_trial_end_upsell(
3006 &self,
3007 _window: &mut Window,
3008 cx: &mut Context<Self>,
3009 ) -> Option<impl IntoElement> {
3010 if !self.should_render_trial_end_upsell(cx) {
3011 return None;
3012 }
3013
3014 Some(
3015 v_flex()
3016 .absolute()
3017 .inset_0()
3018 .size_full()
3019 .bg(cx.theme().colors().panel_background)
3020 .opacity(0.85)
3021 .block_mouse_except_scroll()
3022 .child(EndTrialUpsell::new(Arc::new({
3023 let this = cx.entity();
3024 move |_, cx| {
3025 this.update(cx, |_this, cx| {
3026 TrialEndUpsell::set_dismissed(true, cx);
3027 cx.notify();
3028 });
3029 }
3030 }))),
3031 )
3032 }
3033
3034 fn render_empty_state_section_header(
3035 &self,
3036 label: impl Into<SharedString>,
3037 action_slot: Option<AnyElement>,
3038 cx: &mut Context<Self>,
3039 ) -> impl IntoElement {
3040 div().pl_1().pr_1p5().child(
3041 h_flex()
3042 .mt_2()
3043 .pl_1p5()
3044 .pb_1()
3045 .w_full()
3046 .justify_between()
3047 .border_b_1()
3048 .border_color(cx.theme().colors().border_variant)
3049 .child(
3050 Label::new(label.into())
3051 .size(LabelSize::Small)
3052 .color(Color::Muted),
3053 )
3054 .children(action_slot),
3055 )
3056 }
3057
3058 fn render_thread_empty_state(
3059 &self,
3060 window: &mut Window,
3061 cx: &mut Context<Self>,
3062 ) -> impl IntoElement {
3063 let recent_history = self
3064 .history_store
3065 .update(cx, |this, cx| this.recent_entries(6, cx));
3066
3067 let model_registry = LanguageModelRegistry::read_global(cx);
3068
3069 let configuration_error =
3070 model_registry.configuration_error(model_registry.default_model(), cx);
3071
3072 let no_error = configuration_error.is_none();
3073 let focus_handle = self.focus_handle(cx);
3074
3075 v_flex()
3076 .size_full()
3077 .bg(cx.theme().colors().panel_background)
3078 .when(recent_history.is_empty(), |this| {
3079 this.child(
3080 v_flex()
3081 .size_full()
3082 .mx_auto()
3083 .justify_center()
3084 .items_center()
3085 .gap_1()
3086 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
3087 .when(no_error, |parent| {
3088 parent
3089 .child(h_flex().child(
3090 Label::new("Ask and build anything.").color(Color::Muted),
3091 ))
3092 .child(
3093 v_flex()
3094 .mt_2()
3095 .gap_1()
3096 .max_w_48()
3097 .child(
3098 Button::new("context", "Add Context")
3099 .label_size(LabelSize::Small)
3100 .icon(IconName::FileCode)
3101 .icon_position(IconPosition::Start)
3102 .icon_size(IconSize::Small)
3103 .icon_color(Color::Muted)
3104 .full_width()
3105 .key_binding(KeyBinding::for_action_in(
3106 &ToggleContextPicker,
3107 &focus_handle,
3108 window,
3109 cx,
3110 ))
3111 .on_click(|_event, window, cx| {
3112 window.dispatch_action(
3113 ToggleContextPicker.boxed_clone(),
3114 cx,
3115 )
3116 }),
3117 )
3118 .child(
3119 Button::new("mode", "Switch Model")
3120 .label_size(LabelSize::Small)
3121 .icon(IconName::DatabaseZap)
3122 .icon_position(IconPosition::Start)
3123 .icon_size(IconSize::Small)
3124 .icon_color(Color::Muted)
3125 .full_width()
3126 .key_binding(KeyBinding::for_action_in(
3127 &ToggleModelSelector,
3128 &focus_handle,
3129 window,
3130 cx,
3131 ))
3132 .on_click(|_event, window, cx| {
3133 window.dispatch_action(
3134 ToggleModelSelector.boxed_clone(),
3135 cx,
3136 )
3137 }),
3138 )
3139 .child(
3140 Button::new("settings", "View Settings")
3141 .label_size(LabelSize::Small)
3142 .icon(IconName::Settings)
3143 .icon_position(IconPosition::Start)
3144 .icon_size(IconSize::Small)
3145 .icon_color(Color::Muted)
3146 .full_width()
3147 .key_binding(KeyBinding::for_action_in(
3148 &OpenSettings,
3149 &focus_handle,
3150 window,
3151 cx,
3152 ))
3153 .on_click(|_event, window, cx| {
3154 window.dispatch_action(
3155 OpenSettings.boxed_clone(),
3156 cx,
3157 )
3158 }),
3159 ),
3160 )
3161 }),
3162 )
3163 })
3164 .when(!recent_history.is_empty(), |parent| {
3165 parent
3166 .overflow_hidden()
3167 .justify_end()
3168 .gap_1()
3169 .child(
3170 self.render_empty_state_section_header(
3171 "Recent",
3172 Some(
3173 Button::new("view-history", "View All")
3174 .style(ButtonStyle::Subtle)
3175 .label_size(LabelSize::Small)
3176 .key_binding(
3177 KeyBinding::for_action_in(
3178 &OpenHistory,
3179 &self.focus_handle(cx),
3180 window,
3181 cx,
3182 )
3183 .map(|kb| kb.size(rems_from_px(12.))),
3184 )
3185 .on_click(move |_event, window, cx| {
3186 window.dispatch_action(OpenHistory.boxed_clone(), cx);
3187 })
3188 .into_any_element(),
3189 ),
3190 cx,
3191 ),
3192 )
3193 .child(
3194 v_flex().p_1().pr_1p5().gap_1().children(
3195 recent_history
3196 .into_iter()
3197 .enumerate()
3198 .map(|(index, entry)| {
3199 // TODO: Add keyboard navigation.
3200 let is_hovered =
3201 self.hovered_recent_history_item == Some(index);
3202 HistoryEntryElement::new(entry, cx.entity().downgrade())
3203 .hovered(is_hovered)
3204 .on_hover(cx.listener(
3205 move |this, is_hovered, _window, cx| {
3206 if *is_hovered {
3207 this.hovered_recent_history_item = Some(index);
3208 } else if this.hovered_recent_history_item
3209 == Some(index)
3210 {
3211 this.hovered_recent_history_item = None;
3212 }
3213 cx.notify();
3214 },
3215 ))
3216 .into_any_element()
3217 }),
3218 ),
3219 )
3220 })
3221 .when_some(configuration_error.as_ref(), |this, err| {
3222 this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
3223 })
3224 }
3225
3226 fn render_configuration_error(
3227 &self,
3228 border_bottom: bool,
3229 configuration_error: &ConfigurationError,
3230 focus_handle: &FocusHandle,
3231 window: &mut Window,
3232 cx: &mut App,
3233 ) -> impl IntoElement {
3234 let zed_provider_configured = AgentSettings::get_global(cx)
3235 .default_model
3236 .as_ref()
3237 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
3238
3239 let callout = if zed_provider_configured {
3240 Callout::new()
3241 .icon(IconName::Warning)
3242 .severity(Severity::Warning)
3243 .when(border_bottom, |this| {
3244 this.border_position(ui::BorderPosition::Bottom)
3245 })
3246 .title("Sign in to continue using Zed as your LLM provider.")
3247 .actions_slot(
3248 Button::new("sign_in", "Sign In")
3249 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3250 .label_size(LabelSize::Small)
3251 .on_click({
3252 let workspace = self.workspace.clone();
3253 move |_, _, cx| {
3254 let Ok(client) =
3255 workspace.update(cx, |workspace, _| workspace.client().clone())
3256 else {
3257 return;
3258 };
3259
3260 cx.spawn(async move |cx| {
3261 client.sign_in_with_optional_connect(true, cx).await
3262 })
3263 .detach_and_log_err(cx);
3264 }
3265 }),
3266 )
3267 } else {
3268 Callout::new()
3269 .icon(IconName::Warning)
3270 .severity(Severity::Warning)
3271 .when(border_bottom, |this| {
3272 this.border_position(ui::BorderPosition::Bottom)
3273 })
3274 .title(configuration_error.to_string())
3275 .actions_slot(
3276 Button::new("settings", "Configure")
3277 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3278 .label_size(LabelSize::Small)
3279 .key_binding(
3280 KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
3281 .map(|kb| kb.size(rems_from_px(12.))),
3282 )
3283 .on_click(|_event, window, cx| {
3284 window.dispatch_action(OpenSettings.boxed_clone(), cx)
3285 }),
3286 )
3287 };
3288
3289 match configuration_error {
3290 ConfigurationError::ModelNotFound
3291 | ConfigurationError::ProviderNotAuthenticated(_)
3292 | ConfigurationError::NoProvider => callout.into_any_element(),
3293 }
3294 }
3295
3296 fn render_tool_use_limit_reached(
3297 &self,
3298 window: &mut Window,
3299 cx: &mut Context<Self>,
3300 ) -> Option<AnyElement> {
3301 let active_thread = match &self.active_view {
3302 ActiveView::Thread { thread, .. } => thread,
3303 ActiveView::ExternalAgentThread { .. } => {
3304 return None;
3305 }
3306 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
3307 return None;
3308 }
3309 };
3310
3311 let thread = active_thread.read(cx).thread().read(cx);
3312
3313 let tool_use_limit_reached = thread.tool_use_limit_reached();
3314 if !tool_use_limit_reached {
3315 return None;
3316 }
3317
3318 let model = thread.configured_model()?.model;
3319
3320 let focus_handle = self.focus_handle(cx);
3321
3322 let banner = Banner::new()
3323 .severity(Severity::Info)
3324 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
3325 .action_slot(
3326 h_flex()
3327 .gap_1()
3328 .child(
3329 Button::new("continue-conversation", "Continue")
3330 .layer(ElevationIndex::ModalSurface)
3331 .label_size(LabelSize::Small)
3332 .key_binding(
3333 KeyBinding::for_action_in(
3334 &ContinueThread,
3335 &focus_handle,
3336 window,
3337 cx,
3338 )
3339 .map(|kb| kb.size(rems_from_px(10.))),
3340 )
3341 .on_click(cx.listener(|this, _, window, cx| {
3342 this.continue_conversation(window, cx);
3343 })),
3344 )
3345 .when(model.supports_burn_mode(), |this| {
3346 this.child(
3347 Button::new("continue-burn-mode", "Continue with Burn Mode")
3348 .style(ButtonStyle::Filled)
3349 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3350 .layer(ElevationIndex::ModalSurface)
3351 .label_size(LabelSize::Small)
3352 .key_binding(
3353 KeyBinding::for_action_in(
3354 &ContinueWithBurnMode,
3355 &focus_handle,
3356 window,
3357 cx,
3358 )
3359 .map(|kb| kb.size(rems_from_px(10.))),
3360 )
3361 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
3362 .on_click({
3363 let active_thread = active_thread.clone();
3364 cx.listener(move |this, _, window, cx| {
3365 active_thread.update(cx, |active_thread, cx| {
3366 active_thread.thread().update(cx, |thread, _cx| {
3367 thread.set_completion_mode(CompletionMode::Burn);
3368 });
3369 });
3370 this.continue_conversation(window, cx);
3371 })
3372 }),
3373 )
3374 }),
3375 );
3376
3377 Some(div().px_2().pb_2().child(banner).into_any_element())
3378 }
3379
3380 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3381 let message = message.into();
3382
3383 IconButton::new("copy", IconName::Copy)
3384 .icon_size(IconSize::Small)
3385 .icon_color(Color::Muted)
3386 .tooltip(Tooltip::text("Copy Error Message"))
3387 .on_click(move |_, _, cx| {
3388 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3389 })
3390 }
3391
3392 fn dismiss_error_button(
3393 &self,
3394 thread: &Entity<ActiveThread>,
3395 cx: &mut Context<Self>,
3396 ) -> impl IntoElement {
3397 IconButton::new("dismiss", IconName::Close)
3398 .icon_size(IconSize::Small)
3399 .icon_color(Color::Muted)
3400 .tooltip(Tooltip::text("Dismiss Error"))
3401 .on_click(cx.listener({
3402 let thread = thread.clone();
3403 move |_, _, _, cx| {
3404 thread.update(cx, |this, _cx| {
3405 this.clear_last_error();
3406 });
3407
3408 cx.notify();
3409 }
3410 }))
3411 }
3412
3413 fn upgrade_button(
3414 &self,
3415 thread: &Entity<ActiveThread>,
3416 cx: &mut Context<Self>,
3417 ) -> impl IntoElement {
3418 Button::new("upgrade", "Upgrade")
3419 .label_size(LabelSize::Small)
3420 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3421 .on_click(cx.listener({
3422 let thread = thread.clone();
3423 move |_, _, _, cx| {
3424 thread.update(cx, |this, _cx| {
3425 this.clear_last_error();
3426 });
3427
3428 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
3429 cx.notify();
3430 }
3431 }))
3432 }
3433
3434 fn render_payment_required_error(
3435 &self,
3436 thread: &Entity<ActiveThread>,
3437 cx: &mut Context<Self>,
3438 ) -> AnyElement {
3439 const ERROR_MESSAGE: &str =
3440 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
3441
3442 Callout::new()
3443 .severity(Severity::Error)
3444 .icon(IconName::XCircle)
3445 .title("Free Usage Exceeded")
3446 .description(ERROR_MESSAGE)
3447 .actions_slot(
3448 h_flex()
3449 .gap_0p5()
3450 .child(self.upgrade_button(thread, cx))
3451 .child(self.create_copy_button(ERROR_MESSAGE)),
3452 )
3453 .dismiss_action(self.dismiss_error_button(thread, cx))
3454 .into_any_element()
3455 }
3456
3457 fn render_model_request_limit_reached_error(
3458 &self,
3459 plan: Plan,
3460 thread: &Entity<ActiveThread>,
3461 cx: &mut Context<Self>,
3462 ) -> AnyElement {
3463 let error_message = match plan {
3464 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
3465 Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
3466 };
3467
3468 Callout::new()
3469 .severity(Severity::Error)
3470 .title("Model Prompt Limit Reached")
3471 .description(error_message)
3472 .actions_slot(
3473 h_flex()
3474 .gap_0p5()
3475 .child(self.upgrade_button(thread, cx))
3476 .child(self.create_copy_button(error_message)),
3477 )
3478 .dismiss_action(self.dismiss_error_button(thread, cx))
3479 .into_any_element()
3480 }
3481
3482 fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
3483 Button::new("retry", "Retry")
3484 .icon(IconName::RotateCw)
3485 .icon_position(IconPosition::Start)
3486 .icon_size(IconSize::Small)
3487 .label_size(LabelSize::Small)
3488 .on_click({
3489 let thread = thread.clone();
3490 move |_, window, cx| {
3491 thread.update(cx, |thread, cx| {
3492 thread.clear_last_error();
3493 thread.thread().update(cx, |thread, cx| {
3494 thread.retry_last_completion(Some(window.window_handle()), cx);
3495 });
3496 });
3497 }
3498 })
3499 .into_any_element()
3500 }
3501
3502 fn render_error_message(
3503 &self,
3504 header: SharedString,
3505 message: SharedString,
3506 thread: &Entity<ActiveThread>,
3507 cx: &mut Context<Self>,
3508 ) -> AnyElement {
3509 let message_with_header = format!("{}\n{}", header, message);
3510
3511 Callout::new()
3512 .severity(Severity::Error)
3513 .icon(IconName::XCircle)
3514 .title(header)
3515 .description(message)
3516 .actions_slot(
3517 h_flex()
3518 .gap_0p5()
3519 .child(self.render_retry_button(thread))
3520 .child(self.create_copy_button(message_with_header)),
3521 )
3522 .dismiss_action(self.dismiss_error_button(thread, cx))
3523 .into_any_element()
3524 }
3525
3526 fn render_retryable_error(
3527 &self,
3528 message: SharedString,
3529 can_enable_burn_mode: bool,
3530 thread: &Entity<ActiveThread>,
3531 ) -> AnyElement {
3532 Callout::new()
3533 .severity(Severity::Error)
3534 .title("Error")
3535 .description(message)
3536 .actions_slot(
3537 h_flex()
3538 .gap_0p5()
3539 .when(can_enable_burn_mode, |this| {
3540 this.child(
3541 Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
3542 .icon(IconName::ZedBurnMode)
3543 .icon_position(IconPosition::Start)
3544 .icon_size(IconSize::Small)
3545 .label_size(LabelSize::Small)
3546 .on_click({
3547 let thread = thread.clone();
3548 move |_, window, cx| {
3549 thread.update(cx, |thread, cx| {
3550 thread.clear_last_error();
3551 thread.thread().update(cx, |thread, cx| {
3552 thread.enable_burn_mode_and_retry(
3553 Some(window.window_handle()),
3554 cx,
3555 );
3556 });
3557 });
3558 }
3559 }),
3560 )
3561 })
3562 .child(self.render_retry_button(thread)),
3563 )
3564 .into_any_element()
3565 }
3566
3567 fn render_prompt_editor(
3568 &self,
3569 context_editor: &Entity<TextThreadEditor>,
3570 buffer_search_bar: &Entity<BufferSearchBar>,
3571 window: &mut Window,
3572 cx: &mut Context<Self>,
3573 ) -> Div {
3574 let mut registrar = buffer_search::DivRegistrar::new(
3575 |this, _, _cx| match &this.active_view {
3576 ActiveView::TextThread {
3577 buffer_search_bar, ..
3578 } => Some(buffer_search_bar.clone()),
3579 _ => None,
3580 },
3581 cx,
3582 );
3583 BufferSearchBar::register(&mut registrar);
3584 registrar
3585 .into_div()
3586 .size_full()
3587 .relative()
3588 .map(|parent| {
3589 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3590 if buffer_search_bar.is_dismissed() {
3591 return parent;
3592 }
3593 parent.child(
3594 div()
3595 .p(DynamicSpacing::Base08.rems(cx))
3596 .border_b_1()
3597 .border_color(cx.theme().colors().border_variant)
3598 .bg(cx.theme().colors().editor_background)
3599 .child(buffer_search_bar.render(window, cx)),
3600 )
3601 })
3602 })
3603 .child(context_editor.clone())
3604 .child(self.render_drag_target(cx))
3605 }
3606
3607 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3608 let is_local = self.project.read(cx).is_local();
3609 div()
3610 .invisible()
3611 .absolute()
3612 .top_0()
3613 .right_0()
3614 .bottom_0()
3615 .left_0()
3616 .bg(cx.theme().colors().drop_target_background)
3617 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3618 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3619 .when(is_local, |this| {
3620 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3621 })
3622 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3623 let item = tab.pane.read(cx).item_for_index(tab.ix);
3624 let project_paths = item
3625 .and_then(|item| item.project_path(cx))
3626 .into_iter()
3627 .collect::<Vec<_>>();
3628 this.handle_drop(project_paths, vec![], window, cx);
3629 }))
3630 .on_drop(
3631 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3632 let project_paths = selection
3633 .items()
3634 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3635 .collect::<Vec<_>>();
3636 this.handle_drop(project_paths, vec![], window, cx);
3637 }),
3638 )
3639 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3640 let tasks = paths
3641 .paths()
3642 .iter()
3643 .map(|path| {
3644 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3645 })
3646 .collect::<Vec<_>>();
3647 cx.spawn_in(window, async move |this, cx| {
3648 let mut paths = vec![];
3649 let mut added_worktrees = vec![];
3650 let opened_paths = futures::future::join_all(tasks).await;
3651 for entry in opened_paths {
3652 if let Some((worktree, project_path)) = entry.log_err() {
3653 added_worktrees.push(worktree);
3654 paths.push(project_path);
3655 }
3656 }
3657 this.update_in(cx, |this, window, cx| {
3658 this.handle_drop(paths, added_worktrees, window, cx);
3659 })
3660 .ok();
3661 })
3662 .detach();
3663 }))
3664 }
3665
3666 fn handle_drop(
3667 &mut self,
3668 paths: Vec<ProjectPath>,
3669 added_worktrees: Vec<Entity<Worktree>>,
3670 window: &mut Window,
3671 cx: &mut Context<Self>,
3672 ) {
3673 match &self.active_view {
3674 ActiveView::Thread { thread, .. } => {
3675 let context_store = thread.read(cx).context_store().clone();
3676 context_store.update(cx, move |context_store, cx| {
3677 let mut tasks = Vec::new();
3678 for project_path in &paths {
3679 tasks.push(context_store.add_file_from_path(
3680 project_path.clone(),
3681 false,
3682 cx,
3683 ));
3684 }
3685 cx.background_spawn(async move {
3686 futures::future::join_all(tasks).await;
3687 // Need to hold onto the worktrees until they have already been used when
3688 // opening the buffers.
3689 drop(added_worktrees);
3690 })
3691 .detach();
3692 });
3693 }
3694 ActiveView::ExternalAgentThread { thread_view } => {
3695 thread_view.update(cx, |thread_view, cx| {
3696 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3697 });
3698 }
3699 ActiveView::TextThread { context_editor, .. } => {
3700 context_editor.update(cx, |context_editor, cx| {
3701 TextThreadEditor::insert_dragged_files(
3702 context_editor,
3703 paths,
3704 added_worktrees,
3705 window,
3706 cx,
3707 );
3708 });
3709 }
3710 ActiveView::History | ActiveView::Configuration => {}
3711 }
3712 }
3713
3714 fn key_context(&self) -> KeyContext {
3715 let mut key_context = KeyContext::new_with_defaults();
3716 key_context.add("AgentPanel");
3717 match &self.active_view {
3718 ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
3719 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3720 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3721 }
3722 key_context
3723 }
3724}
3725
3726impl Render for AgentPanel {
3727 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3728 // WARNING: Changes to this element hierarchy can have
3729 // non-obvious implications to the layout of children.
3730 //
3731 // If you need to change it, please confirm:
3732 // - The message editor expands (cmd-option-esc) correctly
3733 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3734 // - Font size works as expected and can be changed with cmd-+/cmd-
3735 // - Scrolling in all views works as expected
3736 // - Files can be dropped into the panel
3737 let content = v_flex()
3738 .relative()
3739 .size_full()
3740 .justify_between()
3741 .key_context(self.key_context())
3742 .on_action(cx.listener(Self::cancel))
3743 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3744 this.new_thread(action, window, cx);
3745 }))
3746 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3747 this.open_history(window, cx);
3748 }))
3749 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3750 this.open_configuration(window, cx);
3751 }))
3752 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3753 .on_action(cx.listener(Self::deploy_rules_library))
3754 .on_action(cx.listener(Self::open_agent_diff))
3755 .on_action(cx.listener(Self::go_back))
3756 .on_action(cx.listener(Self::toggle_navigation_menu))
3757 .on_action(cx.listener(Self::toggle_options_menu))
3758 .on_action(cx.listener(Self::increase_font_size))
3759 .on_action(cx.listener(Self::decrease_font_size))
3760 .on_action(cx.listener(Self::reset_font_size))
3761 .on_action(cx.listener(Self::toggle_zoom))
3762 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3763 this.continue_conversation(window, cx);
3764 }))
3765 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3766 match &this.active_view {
3767 ActiveView::Thread { thread, .. } => {
3768 thread.update(cx, |active_thread, cx| {
3769 active_thread.thread().update(cx, |thread, _cx| {
3770 thread.set_completion_mode(CompletionMode::Burn);
3771 });
3772 });
3773 this.continue_conversation(window, cx);
3774 }
3775 ActiveView::ExternalAgentThread { .. } => {}
3776 ActiveView::TextThread { .. }
3777 | ActiveView::History
3778 | ActiveView::Configuration => {}
3779 }
3780 }))
3781 .on_action(cx.listener(Self::toggle_burn_mode))
3782 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3783 if let Some(thread_view) = this.active_thread_view() {
3784 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3785 }
3786 }))
3787 .child(self.render_toolbar(window, cx))
3788 .children(self.render_onboarding(window, cx))
3789 .map(|parent| match &self.active_view {
3790 ActiveView::Thread {
3791 thread,
3792 message_editor,
3793 ..
3794 } => parent
3795 .child(
3796 if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
3797 self.render_thread_empty_state(window, cx)
3798 .into_any_element()
3799 } else {
3800 thread.clone().into_any_element()
3801 },
3802 )
3803 .children(self.render_tool_use_limit_reached(window, cx))
3804 .when_some(thread.read(cx).last_error(), |this, last_error| {
3805 this.child(
3806 div()
3807 .child(match last_error {
3808 ThreadError::PaymentRequired => {
3809 self.render_payment_required_error(thread, cx)
3810 }
3811 ThreadError::ModelRequestLimitReached { plan } => self
3812 .render_model_request_limit_reached_error(plan, thread, cx),
3813 ThreadError::Message { header, message } => {
3814 self.render_error_message(header, message, thread, cx)
3815 }
3816 ThreadError::RetryableError {
3817 message,
3818 can_enable_burn_mode,
3819 } => self.render_retryable_error(
3820 message,
3821 can_enable_burn_mode,
3822 thread,
3823 ),
3824 })
3825 .into_any(),
3826 )
3827 })
3828 .child(h_flex().relative().child(message_editor.clone()).when(
3829 !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
3830 |this| this.child(self.render_backdrop(cx)),
3831 ))
3832 .child(self.render_drag_target(cx)),
3833 ActiveView::ExternalAgentThread { thread_view, .. } => parent
3834 .child(thread_view.clone())
3835 .child(self.render_drag_target(cx)),
3836 ActiveView::History => {
3837 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
3838 parent.child(self.acp_history.clone())
3839 } else {
3840 parent.child(self.history.clone())
3841 }
3842 }
3843 ActiveView::TextThread {
3844 context_editor,
3845 buffer_search_bar,
3846 ..
3847 } => {
3848 let model_registry = LanguageModelRegistry::read_global(cx);
3849 let configuration_error =
3850 model_registry.configuration_error(model_registry.default_model(), cx);
3851 parent
3852 .map(|this| {
3853 if !self.should_render_onboarding(cx)
3854 && let Some(err) = configuration_error.as_ref()
3855 {
3856 this.child(self.render_configuration_error(
3857 true,
3858 err,
3859 &self.focus_handle(cx),
3860 window,
3861 cx,
3862 ))
3863 } else {
3864 this
3865 }
3866 })
3867 .child(self.render_prompt_editor(
3868 context_editor,
3869 buffer_search_bar,
3870 window,
3871 cx,
3872 ))
3873 }
3874 ActiveView::Configuration => parent.children(self.configuration.clone()),
3875 })
3876 .children(self.render_trial_end_upsell(window, cx));
3877
3878 match self.active_view.which_font_size_used() {
3879 WhichFontSize::AgentFont => {
3880 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3881 .size_full()
3882 .child(content)
3883 .into_any()
3884 }
3885 _ => content.into_any(),
3886 }
3887 }
3888}
3889
3890struct PromptLibraryInlineAssist {
3891 workspace: WeakEntity<Workspace>,
3892}
3893
3894impl PromptLibraryInlineAssist {
3895 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3896 Self { workspace }
3897 }
3898}
3899
3900impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3901 fn assist(
3902 &self,
3903 prompt_editor: &Entity<Editor>,
3904 initial_prompt: Option<String>,
3905 window: &mut Window,
3906 cx: &mut Context<RulesLibrary>,
3907 ) {
3908 InlineAssistant::update_global(cx, |assistant, cx| {
3909 let Some(project) = self
3910 .workspace
3911 .upgrade()
3912 .map(|workspace| workspace.read(cx).project().downgrade())
3913 else {
3914 return;
3915 };
3916 let prompt_store = None;
3917 let thread_store = None;
3918 let text_thread_store = None;
3919 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3920 assistant.assist(
3921 prompt_editor,
3922 self.workspace.clone(),
3923 context_store,
3924 project,
3925 prompt_store,
3926 thread_store,
3927 text_thread_store,
3928 initial_prompt,
3929 window,
3930 cx,
3931 )
3932 })
3933 }
3934
3935 fn focus_agent_panel(
3936 &self,
3937 workspace: &mut Workspace,
3938 window: &mut Window,
3939 cx: &mut Context<Workspace>,
3940 ) -> bool {
3941 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3942 }
3943}
3944
3945pub struct ConcreteAssistantPanelDelegate;
3946
3947impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3948 fn active_context_editor(
3949 &self,
3950 workspace: &mut Workspace,
3951 _window: &mut Window,
3952 cx: &mut Context<Workspace>,
3953 ) -> Option<Entity<TextThreadEditor>> {
3954 let panel = workspace.panel::<AgentPanel>(cx)?;
3955 panel.read(cx).active_context_editor()
3956 }
3957
3958 fn open_saved_context(
3959 &self,
3960 workspace: &mut Workspace,
3961 path: Arc<Path>,
3962 window: &mut Window,
3963 cx: &mut Context<Workspace>,
3964 ) -> Task<Result<()>> {
3965 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3966 return Task::ready(Err(anyhow!("Agent panel not found")));
3967 };
3968
3969 panel.update(cx, |panel, cx| {
3970 panel.open_saved_prompt_editor(path, window, cx)
3971 })
3972 }
3973
3974 fn open_remote_context(
3975 &self,
3976 _workspace: &mut Workspace,
3977 _context_id: assistant_context::ContextId,
3978 _window: &mut Window,
3979 _cx: &mut Context<Workspace>,
3980 ) -> Task<Result<Entity<TextThreadEditor>>> {
3981 Task::ready(Err(anyhow!("opening remote context not implemented")))
3982 }
3983
3984 fn quote_selection(
3985 &self,
3986 workspace: &mut Workspace,
3987 selection_ranges: Vec<Range<Anchor>>,
3988 buffer: Entity<MultiBuffer>,
3989 window: &mut Window,
3990 cx: &mut Context<Workspace>,
3991 ) {
3992 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3993 return;
3994 };
3995
3996 if !panel.focus_handle(cx).contains_focused(window, cx) {
3997 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3998 }
3999
4000 panel.update(cx, |_, cx| {
4001 // Wait to create a new context until the workspace is no longer
4002 // being updated.
4003 cx.defer_in(window, move |panel, window, cx| {
4004 if let Some(thread_view) = panel.active_thread_view() {
4005 thread_view.update(cx, |thread_view, cx| {
4006 thread_view.insert_selections(window, cx);
4007 });
4008 } else if let Some(message_editor) = panel.active_message_editor() {
4009 message_editor.update(cx, |message_editor, cx| {
4010 message_editor.context_store().update(cx, |store, cx| {
4011 let buffer = buffer.read(cx);
4012 let selection_ranges = selection_ranges
4013 .into_iter()
4014 .flat_map(|range| {
4015 let (start_buffer, start) =
4016 buffer.text_anchor_for_position(range.start, cx)?;
4017 let (end_buffer, end) =
4018 buffer.text_anchor_for_position(range.end, cx)?;
4019 if start_buffer != end_buffer {
4020 return None;
4021 }
4022 Some((start_buffer, start..end))
4023 })
4024 .collect::<Vec<_>>();
4025
4026 for (buffer, range) in selection_ranges {
4027 store.add_selection(buffer, range, cx);
4028 }
4029 })
4030 })
4031 } else if let Some(context_editor) = panel.active_context_editor() {
4032 let snapshot = buffer.read(cx).snapshot(cx);
4033 let selection_ranges = selection_ranges
4034 .into_iter()
4035 .map(|range| range.to_point(&snapshot))
4036 .collect::<Vec<_>>();
4037
4038 context_editor.update(cx, |context_editor, cx| {
4039 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4040 });
4041 }
4042 });
4043 });
4044 }
4045}
4046
4047struct OnboardingUpsell;
4048
4049impl Dismissable for OnboardingUpsell {
4050 const KEY: &'static str = "dismissed-trial-upsell";
4051}
4052
4053struct TrialEndUpsell;
4054
4055impl Dismissable for TrialEndUpsell {
4056 const KEY: &'static str = "dismissed-trial-end-upsell";
4057}