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