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