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