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