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 => self.external_thread(
1915 Some(crate::ExternalAgent::ClaudeCode),
1916 None,
1917 None,
1918 window,
1919 cx,
1920 ),
1921 AgentType::Custom { name, command } => self.external_thread(
1922 Some(crate::ExternalAgent::Custom { name, command }),
1923 None,
1924 None,
1925 window,
1926 cx,
1927 ),
1928 }
1929 }
1930
1931 pub fn load_agent_thread(
1932 &mut self,
1933 thread: DbThreadMetadata,
1934 window: &mut Window,
1935 cx: &mut Context<Self>,
1936 ) {
1937 self.external_thread(
1938 Some(ExternalAgent::NativeAgent),
1939 Some(thread),
1940 None,
1941 window,
1942 cx,
1943 );
1944 }
1945}
1946
1947impl Focusable for AgentPanel {
1948 fn focus_handle(&self, cx: &App) -> FocusHandle {
1949 match &self.active_view {
1950 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1951 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1952 ActiveView::History => {
1953 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
1954 self.acp_history.focus_handle(cx)
1955 } else {
1956 self.history.focus_handle(cx)
1957 }
1958 }
1959 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1960 ActiveView::Configuration => {
1961 if let Some(configuration) = self.configuration.as_ref() {
1962 configuration.focus_handle(cx)
1963 } else {
1964 cx.focus_handle()
1965 }
1966 }
1967 }
1968 }
1969}
1970
1971fn agent_panel_dock_position(cx: &App) -> DockPosition {
1972 match AgentSettings::get_global(cx).dock {
1973 AgentDockPosition::Left => DockPosition::Left,
1974 AgentDockPosition::Bottom => DockPosition::Bottom,
1975 AgentDockPosition::Right => DockPosition::Right,
1976 }
1977}
1978
1979impl EventEmitter<PanelEvent> for AgentPanel {}
1980
1981impl Panel for AgentPanel {
1982 fn persistent_name() -> &'static str {
1983 "AgentPanel"
1984 }
1985
1986 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1987 agent_panel_dock_position(cx)
1988 }
1989
1990 fn position_is_valid(&self, position: DockPosition) -> bool {
1991 position != DockPosition::Bottom
1992 }
1993
1994 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1995 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1996 let dock = match position {
1997 DockPosition::Left => AgentDockPosition::Left,
1998 DockPosition::Bottom => AgentDockPosition::Bottom,
1999 DockPosition::Right => AgentDockPosition::Right,
2000 };
2001 settings.set_dock(dock);
2002 });
2003 }
2004
2005 fn size(&self, window: &Window, cx: &App) -> Pixels {
2006 let settings = AgentSettings::get_global(cx);
2007 match self.position(window, cx) {
2008 DockPosition::Left | DockPosition::Right => {
2009 self.width.unwrap_or(settings.default_width)
2010 }
2011 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
2012 }
2013 }
2014
2015 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
2016 match self.position(window, cx) {
2017 DockPosition::Left | DockPosition::Right => self.width = size,
2018 DockPosition::Bottom => self.height = size,
2019 }
2020 self.serialize(cx);
2021 cx.notify();
2022 }
2023
2024 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
2025
2026 fn remote_id() -> Option<proto::PanelId> {
2027 Some(proto::PanelId::AssistantPanel)
2028 }
2029
2030 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2031 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2032 }
2033
2034 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2035 Some("Agent Panel")
2036 }
2037
2038 fn toggle_action(&self) -> Box<dyn Action> {
2039 Box::new(ToggleFocus)
2040 }
2041
2042 fn activation_priority(&self) -> u32 {
2043 3
2044 }
2045
2046 fn enabled(&self, cx: &App) -> bool {
2047 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
2048 }
2049
2050 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2051 self.zoomed
2052 }
2053
2054 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2055 self.zoomed = zoomed;
2056 cx.notify();
2057 }
2058}
2059
2060impl AgentPanel {
2061 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2062 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2063
2064 let content = match &self.active_view {
2065 ActiveView::Thread {
2066 thread: active_thread,
2067 change_title_editor,
2068 ..
2069 } => {
2070 let state = {
2071 let active_thread = active_thread.read(cx);
2072 if active_thread.is_empty() {
2073 &ThreadSummary::Pending
2074 } else {
2075 active_thread.summary(cx)
2076 }
2077 };
2078
2079 match state {
2080 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
2081 .truncate()
2082 .color(Color::Muted)
2083 .into_any_element(),
2084 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
2085 .truncate()
2086 .color(Color::Muted)
2087 .into_any_element(),
2088 ThreadSummary::Ready(_) => div()
2089 .w_full()
2090 .child(change_title_editor.clone())
2091 .into_any_element(),
2092 ThreadSummary::Error => h_flex()
2093 .w_full()
2094 .child(change_title_editor.clone())
2095 .child(
2096 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2097 .icon_size(IconSize::Small)
2098 .on_click({
2099 let active_thread = active_thread.clone();
2100 move |_, _window, cx| {
2101 active_thread.update(cx, |thread, cx| {
2102 thread.regenerate_summary(cx);
2103 });
2104 }
2105 })
2106 .tooltip(move |_window, cx| {
2107 cx.new(|_| {
2108 Tooltip::new("Failed to generate title")
2109 .meta("Click to try again")
2110 })
2111 .into()
2112 }),
2113 )
2114 .into_any_element(),
2115 }
2116 }
2117 ActiveView::ExternalAgentThread { thread_view } => {
2118 if let Some(title_editor) = thread_view.read(cx).title_editor() {
2119 div()
2120 .w_full()
2121 .on_action({
2122 let thread_view = thread_view.downgrade();
2123 move |_: &menu::Confirm, window, cx| {
2124 if let Some(thread_view) = thread_view.upgrade() {
2125 thread_view.focus_handle(cx).focus(window);
2126 }
2127 }
2128 })
2129 .on_action({
2130 let thread_view = thread_view.downgrade();
2131 move |_: &editor::actions::Cancel, window, cx| {
2132 if let Some(thread_view) = thread_view.upgrade() {
2133 thread_view.focus_handle(cx).focus(window);
2134 }
2135 }
2136 })
2137 .child(title_editor)
2138 .into_any_element()
2139 } else {
2140 Label::new(thread_view.read(cx).title(cx))
2141 .color(Color::Muted)
2142 .truncate()
2143 .into_any_element()
2144 }
2145 }
2146 ActiveView::TextThread {
2147 title_editor,
2148 context_editor,
2149 ..
2150 } => {
2151 let summary = context_editor.read(cx).context().read(cx).summary();
2152
2153 match summary {
2154 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
2155 .color(Color::Muted)
2156 .truncate()
2157 .into_any_element(),
2158 ContextSummary::Content(summary) => {
2159 if summary.done {
2160 div()
2161 .w_full()
2162 .child(title_editor.clone())
2163 .into_any_element()
2164 } else {
2165 Label::new(LOADING_SUMMARY_PLACEHOLDER)
2166 .truncate()
2167 .color(Color::Muted)
2168 .into_any_element()
2169 }
2170 }
2171 ContextSummary::Error => h_flex()
2172 .w_full()
2173 .child(title_editor.clone())
2174 .child(
2175 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2176 .icon_size(IconSize::Small)
2177 .on_click({
2178 let context_editor = context_editor.clone();
2179 move |_, _window, cx| {
2180 context_editor.update(cx, |context_editor, cx| {
2181 context_editor.regenerate_summary(cx);
2182 });
2183 }
2184 })
2185 .tooltip(move |_window, cx| {
2186 cx.new(|_| {
2187 Tooltip::new("Failed to generate title")
2188 .meta("Click to try again")
2189 })
2190 .into()
2191 }),
2192 )
2193 .into_any_element(),
2194 }
2195 }
2196 ActiveView::History => Label::new("History").truncate().into_any_element(),
2197 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2198 };
2199
2200 h_flex()
2201 .key_context("TitleEditor")
2202 .id("TitleEditor")
2203 .flex_grow()
2204 .w_full()
2205 .max_w_full()
2206 .overflow_x_scroll()
2207 .child(content)
2208 .into_any()
2209 }
2210
2211 fn render_panel_options_menu(
2212 &self,
2213 window: &mut Window,
2214 cx: &mut Context<Self>,
2215 ) -> impl IntoElement {
2216 let user_store = self.user_store.read(cx);
2217 let usage = user_store.model_request_usage();
2218 let account_url = zed_urls::account_url(cx);
2219
2220 let focus_handle = self.focus_handle(cx);
2221
2222 let full_screen_label = if self.is_zoomed(window, cx) {
2223 "Disable Full Screen"
2224 } else {
2225 "Enable Full Screen"
2226 };
2227
2228 let selected_agent = self.selected_agent.clone();
2229
2230 PopoverMenu::new("agent-options-menu")
2231 .trigger_with_tooltip(
2232 IconButton::new("agent-options-menu", IconName::Ellipsis)
2233 .icon_size(IconSize::Small),
2234 {
2235 let focus_handle = focus_handle.clone();
2236 move |window, cx| {
2237 Tooltip::for_action_in(
2238 "Toggle Agent Menu",
2239 &ToggleOptionsMenu,
2240 &focus_handle,
2241 window,
2242 cx,
2243 )
2244 }
2245 },
2246 )
2247 .anchor(Corner::TopRight)
2248 .with_handle(self.agent_panel_menu_handle.clone())
2249 .menu({
2250 move |window, cx| {
2251 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2252 menu = menu.context(focus_handle.clone());
2253 if let Some(usage) = usage {
2254 menu = menu
2255 .header_with_link("Prompt Usage", "Manage", account_url.clone())
2256 .custom_entry(
2257 move |_window, cx| {
2258 let used_percentage = match usage.limit {
2259 UsageLimit::Limited(limit) => {
2260 Some((usage.amount as f32 / limit as f32) * 100.)
2261 }
2262 UsageLimit::Unlimited => None,
2263 };
2264
2265 h_flex()
2266 .flex_1()
2267 .gap_1p5()
2268 .children(used_percentage.map(|percent| {
2269 ProgressBar::new("usage", percent, 100., cx)
2270 }))
2271 .child(
2272 Label::new(match usage.limit {
2273 UsageLimit::Limited(limit) => {
2274 format!("{} / {limit}", usage.amount)
2275 }
2276 UsageLimit::Unlimited => {
2277 format!("{} / ∞", usage.amount)
2278 }
2279 })
2280 .size(LabelSize::Small)
2281 .color(Color::Muted),
2282 )
2283 .into_any_element()
2284 },
2285 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
2286 )
2287 .separator()
2288 }
2289
2290 menu = menu
2291 .header("MCP Servers")
2292 .action(
2293 "View Server Extensions",
2294 Box::new(zed_actions::Extensions {
2295 category_filter: Some(
2296 zed_actions::ExtensionCategoryFilter::ContextServers,
2297 ),
2298 id: None,
2299 }),
2300 )
2301 .action("Add Custom Server…", Box::new(AddContextServer))
2302 .separator();
2303
2304 menu = menu
2305 .action("Rules…", Box::new(OpenRulesLibrary::default()))
2306 .action("Settings", Box::new(OpenSettings))
2307 .separator()
2308 .action(full_screen_label, Box::new(ToggleZoom));
2309
2310 if selected_agent == AgentType::Gemini {
2311 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2312 }
2313
2314 menu
2315 }))
2316 }
2317 })
2318 }
2319
2320 fn render_recent_entries_menu(
2321 &self,
2322 icon: IconName,
2323 corner: Corner,
2324 cx: &mut Context<Self>,
2325 ) -> impl IntoElement {
2326 let focus_handle = self.focus_handle(cx);
2327
2328 PopoverMenu::new("agent-nav-menu")
2329 .trigger_with_tooltip(
2330 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2331 {
2332 move |window, cx| {
2333 Tooltip::for_action_in(
2334 "Toggle Recent Threads",
2335 &ToggleNavigationMenu,
2336 &focus_handle,
2337 window,
2338 cx,
2339 )
2340 }
2341 },
2342 )
2343 .anchor(corner)
2344 .with_handle(self.assistant_navigation_menu_handle.clone())
2345 .menu({
2346 let menu = self.assistant_navigation_menu.clone();
2347 move |window, cx| {
2348 telemetry::event!("View Thread History Clicked");
2349
2350 if let Some(menu) = menu.as_ref() {
2351 menu.update(cx, |_, cx| {
2352 cx.defer_in(window, |menu, window, cx| {
2353 menu.rebuild(window, cx);
2354 });
2355 })
2356 }
2357 menu.clone()
2358 }
2359 })
2360 }
2361
2362 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2363 let focus_handle = self.focus_handle(cx);
2364
2365 IconButton::new("go-back", IconName::ArrowLeft)
2366 .icon_size(IconSize::Small)
2367 .on_click(cx.listener(|this, _, window, cx| {
2368 this.go_back(&workspace::GoBack, window, cx);
2369 }))
2370 .tooltip({
2371 move |window, cx| {
2372 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
2373 }
2374 })
2375 }
2376
2377 fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2378 let focus_handle = self.focus_handle(cx);
2379
2380 let active_thread = match &self.active_view {
2381 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
2382 ActiveView::ExternalAgentThread { .. }
2383 | ActiveView::TextThread { .. }
2384 | ActiveView::History
2385 | ActiveView::Configuration => None,
2386 };
2387
2388 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2389 .trigger_with_tooltip(
2390 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2391 Tooltip::text("New Thread…"),
2392 )
2393 .anchor(Corner::TopRight)
2394 .with_handle(self.new_thread_menu_handle.clone())
2395 .menu({
2396 move |window, cx| {
2397 let active_thread = active_thread.clone();
2398 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2399 menu = menu
2400 .context(focus_handle.clone())
2401 .when_some(active_thread, |this, active_thread| {
2402 let thread = active_thread.read(cx);
2403
2404 if !thread.is_empty() {
2405 let thread_id = thread.id().clone();
2406 this.item(
2407 ContextMenuEntry::new("New From Summary")
2408 .icon(IconName::ThreadFromSummary)
2409 .icon_color(Color::Muted)
2410 .handler(move |window, cx| {
2411 window.dispatch_action(
2412 Box::new(NewThread {
2413 from_thread_id: Some(thread_id.clone()),
2414 }),
2415 cx,
2416 );
2417 }),
2418 )
2419 } else {
2420 this
2421 }
2422 })
2423 .item(
2424 ContextMenuEntry::new("New Thread")
2425 .icon(IconName::Thread)
2426 .icon_color(Color::Muted)
2427 .action(NewThread::default().boxed_clone())
2428 .handler(move |window, cx| {
2429 window.dispatch_action(
2430 NewThread::default().boxed_clone(),
2431 cx,
2432 );
2433 }),
2434 )
2435 .item(
2436 ContextMenuEntry::new("New Text Thread")
2437 .icon(IconName::TextThread)
2438 .icon_color(Color::Muted)
2439 .action(NewTextThread.boxed_clone())
2440 .handler(move |window, cx| {
2441 window.dispatch_action(NewTextThread.boxed_clone(), cx);
2442 }),
2443 );
2444 menu
2445 }))
2446 }
2447 });
2448
2449 h_flex()
2450 .id("assistant-toolbar")
2451 .h(Tab::container_height(cx))
2452 .max_w_full()
2453 .flex_none()
2454 .justify_between()
2455 .gap_2()
2456 .bg(cx.theme().colors().tab_bar_background)
2457 .border_b_1()
2458 .border_color(cx.theme().colors().border)
2459 .child(
2460 h_flex()
2461 .size_full()
2462 .pl_1()
2463 .gap_1()
2464 .child(match &self.active_view {
2465 ActiveView::History | ActiveView::Configuration => div()
2466 .pl(DynamicSpacing::Base04.rems(cx))
2467 .child(self.render_toolbar_back_button(cx))
2468 .into_any_element(),
2469 _ => self
2470 .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx)
2471 .into_any_element(),
2472 })
2473 .child(self.render_title_view(window, cx)),
2474 )
2475 .child(
2476 h_flex()
2477 .h_full()
2478 .gap_2()
2479 .children(self.render_token_count(cx))
2480 .child(
2481 h_flex()
2482 .h_full()
2483 .gap(DynamicSpacing::Base02.rems(cx))
2484 .px(DynamicSpacing::Base08.rems(cx))
2485 .border_l_1()
2486 .border_color(cx.theme().colors().border)
2487 .child(new_thread_menu)
2488 .child(self.render_panel_options_menu(window, cx)),
2489 ),
2490 )
2491 }
2492
2493 fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2494 let focus_handle = self.focus_handle(cx);
2495
2496 let active_thread = match &self.active_view {
2497 ActiveView::ExternalAgentThread { thread_view } => {
2498 thread_view.read(cx).as_native_thread(cx)
2499 }
2500 ActiveView::Thread { .. }
2501 | ActiveView::TextThread { .. }
2502 | ActiveView::History
2503 | ActiveView::Configuration => None,
2504 };
2505
2506 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2507 .trigger_with_tooltip(
2508 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2509 {
2510 let focus_handle = focus_handle.clone();
2511 move |window, cx| {
2512 Tooltip::for_action_in(
2513 "New…",
2514 &ToggleNewThreadMenu,
2515 &focus_handle,
2516 window,
2517 cx,
2518 )
2519 }
2520 },
2521 )
2522 .anchor(Corner::TopLeft)
2523 .with_handle(self.new_thread_menu_handle.clone())
2524 .menu({
2525 let workspace = self.workspace.clone();
2526
2527 move |window, cx| {
2528 telemetry::event!("New Thread Clicked");
2529
2530 let active_thread = active_thread.clone();
2531 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
2532 menu = menu
2533 .context(focus_handle.clone())
2534 .header("Zed Agent")
2535 .when_some(active_thread, |this, active_thread| {
2536 let thread = active_thread.read(cx);
2537
2538 if !thread.is_empty() {
2539 let session_id = thread.id().clone();
2540 this.item(
2541 ContextMenuEntry::new("New From Summary")
2542 .icon(IconName::ThreadFromSummary)
2543 .icon_color(Color::Muted)
2544 .handler(move |window, cx| {
2545 window.dispatch_action(
2546 Box::new(NewNativeAgentThreadFromSummary {
2547 from_session_id: session_id.clone(),
2548 }),
2549 cx,
2550 );
2551 }),
2552 )
2553 } else {
2554 this
2555 }
2556 })
2557 .item(
2558 ContextMenuEntry::new("New Thread")
2559 .action(NewThread::default().boxed_clone())
2560 .icon(IconName::Thread)
2561 .icon_color(Color::Muted)
2562 .handler({
2563 let workspace = workspace.clone();
2564 move |window, cx| {
2565 if let Some(workspace) = workspace.upgrade() {
2566 workspace.update(cx, |workspace, cx| {
2567 if let Some(panel) =
2568 workspace.panel::<AgentPanel>(cx)
2569 {
2570 panel.update(cx, |panel, cx| {
2571 panel.new_agent_thread(
2572 AgentType::NativeAgent,
2573 window,
2574 cx,
2575 );
2576 });
2577 }
2578 });
2579 }
2580 }
2581 }),
2582 )
2583 .item(
2584 ContextMenuEntry::new("New Text Thread")
2585 .icon(IconName::TextThread)
2586 .icon_color(Color::Muted)
2587 .action(NewTextThread.boxed_clone())
2588 .handler({
2589 let workspace = workspace.clone();
2590 move |window, cx| {
2591 if let Some(workspace) = workspace.upgrade() {
2592 workspace.update(cx, |workspace, cx| {
2593 if let Some(panel) =
2594 workspace.panel::<AgentPanel>(cx)
2595 {
2596 panel.update(cx, |panel, cx| {
2597 panel.new_agent_thread(
2598 AgentType::TextThread,
2599 window,
2600 cx,
2601 );
2602 });
2603 }
2604 });
2605 }
2606 }
2607 }),
2608 )
2609 .separator()
2610 .header("External Agents")
2611 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2612 menu.item(
2613 ContextMenuEntry::new("New Gemini CLI Thread")
2614 .icon(IconName::AiGemini)
2615 .icon_color(Color::Muted)
2616 .handler({
2617 let workspace = workspace.clone();
2618 move |window, cx| {
2619 if let Some(workspace) = workspace.upgrade() {
2620 workspace.update(cx, |workspace, cx| {
2621 if let Some(panel) =
2622 workspace.panel::<AgentPanel>(cx)
2623 {
2624 panel.update(cx, |panel, cx| {
2625 panel.new_agent_thread(
2626 AgentType::Gemini,
2627 window,
2628 cx,
2629 );
2630 });
2631 }
2632 });
2633 }
2634 }
2635 }),
2636 )
2637 })
2638 .when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| {
2639 menu.item(
2640 ContextMenuEntry::new("New Claude Code Thread")
2641 .icon(IconName::AiClaude)
2642 .icon_color(Color::Muted)
2643 .handler({
2644 let workspace = workspace.clone();
2645 move |window, cx| {
2646 if let Some(workspace) = workspace.upgrade() {
2647 workspace.update(cx, |workspace, cx| {
2648 if let Some(panel) =
2649 workspace.panel::<AgentPanel>(cx)
2650 {
2651 panel.update(cx, |panel, cx| {
2652 panel.new_agent_thread(
2653 AgentType::ClaudeCode,
2654 window,
2655 cx,
2656 );
2657 });
2658 }
2659 });
2660 }
2661 }
2662 }),
2663 )
2664 })
2665 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
2666 // Add custom agents from settings
2667 let settings =
2668 agent_servers::AllAgentServersSettings::get_global(cx);
2669 for (agent_name, agent_settings) in &settings.custom {
2670 menu = menu.item(
2671 ContextMenuEntry::new(format!("New {} Thread", agent_name))
2672 .icon(IconName::Terminal)
2673 .icon_color(Color::Muted)
2674 .handler({
2675 let workspace = workspace.clone();
2676 let agent_name = agent_name.clone();
2677 let agent_settings = agent_settings.clone();
2678 move |window, cx| {
2679 if let Some(workspace) = workspace.upgrade() {
2680 workspace.update(cx, |workspace, cx| {
2681 if let Some(panel) =
2682 workspace.panel::<AgentPanel>(cx)
2683 {
2684 panel.update(cx, |panel, cx| {
2685 panel.new_agent_thread(
2686 AgentType::Custom {
2687 name: agent_name
2688 .clone(),
2689 command: agent_settings
2690 .command
2691 .clone(),
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 }
2698 });
2699 }
2700 }
2701 }),
2702 );
2703 }
2704
2705 menu
2706 })
2707 .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
2708 menu.separator().link(
2709 "Add Other Agents",
2710 OpenBrowser {
2711 url: zed_urls::external_agents_docs(cx),
2712 }
2713 .boxed_clone(),
2714 )
2715 });
2716 menu
2717 }))
2718 }
2719 });
2720
2721 let selected_agent_label = self.selected_agent.label();
2722 let selected_agent = div()
2723 .id("selected_agent_icon")
2724 .when_some(self.selected_agent.icon(), |this, icon| {
2725 this.px(DynamicSpacing::Base02.rems(cx))
2726 .child(Icon::new(icon).color(Color::Muted))
2727 .tooltip(move |window, cx| {
2728 Tooltip::with_meta(
2729 selected_agent_label.clone(),
2730 None,
2731 "Selected Agent",
2732 window,
2733 cx,
2734 )
2735 })
2736 })
2737 .into_any_element();
2738
2739 h_flex()
2740 .id("agent-panel-toolbar")
2741 .h(Tab::container_height(cx))
2742 .max_w_full()
2743 .flex_none()
2744 .justify_between()
2745 .gap_2()
2746 .bg(cx.theme().colors().tab_bar_background)
2747 .border_b_1()
2748 .border_color(cx.theme().colors().border)
2749 .child(
2750 h_flex()
2751 .size_full()
2752 .gap(DynamicSpacing::Base04.rems(cx))
2753 .pl(DynamicSpacing::Base04.rems(cx))
2754 .child(match &self.active_view {
2755 ActiveView::History | ActiveView::Configuration => {
2756 self.render_toolbar_back_button(cx).into_any_element()
2757 }
2758 _ => selected_agent.into_any_element(),
2759 })
2760 .child(self.render_title_view(window, cx)),
2761 )
2762 .child(
2763 h_flex()
2764 .flex_none()
2765 .gap(DynamicSpacing::Base02.rems(cx))
2766 .pl(DynamicSpacing::Base04.rems(cx))
2767 .pr(DynamicSpacing::Base06.rems(cx))
2768 .child(new_thread_menu)
2769 .child(self.render_recent_entries_menu(
2770 IconName::MenuAltTemp,
2771 Corner::TopRight,
2772 cx,
2773 ))
2774 .child(self.render_panel_options_menu(window, cx)),
2775 )
2776 }
2777
2778 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2779 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>()
2780 || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>()
2781 {
2782 self.render_toolbar_new(window, cx).into_any_element()
2783 } else {
2784 self.render_toolbar_old(window, cx).into_any_element()
2785 }
2786 }
2787
2788 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
2789 match &self.active_view {
2790 ActiveView::Thread {
2791 thread,
2792 message_editor,
2793 ..
2794 } => {
2795 let active_thread = thread.read(cx);
2796 let message_editor = message_editor.read(cx);
2797
2798 let editor_empty = message_editor.is_editor_fully_empty(cx);
2799
2800 if active_thread.is_empty() && editor_empty {
2801 return None;
2802 }
2803
2804 let thread = active_thread.thread().read(cx);
2805 let is_generating = thread.is_generating();
2806 let conversation_token_usage = thread.total_token_usage()?;
2807
2808 let (total_token_usage, is_estimating) =
2809 if let Some((editing_message_id, unsent_tokens)) =
2810 active_thread.editing_message_id()
2811 {
2812 let combined = thread
2813 .token_usage_up_to_message(editing_message_id)
2814 .add(unsent_tokens);
2815
2816 (combined, unsent_tokens > 0)
2817 } else {
2818 let unsent_tokens =
2819 message_editor.last_estimated_token_count().unwrap_or(0);
2820 let combined = conversation_token_usage.add(unsent_tokens);
2821
2822 (combined, unsent_tokens > 0)
2823 };
2824
2825 let is_waiting_to_update_token_count =
2826 message_editor.is_waiting_to_update_token_count();
2827
2828 if total_token_usage.total == 0 {
2829 return None;
2830 }
2831
2832 let token_color = match total_token_usage.ratio() {
2833 TokenUsageRatio::Normal if is_estimating => Color::Default,
2834 TokenUsageRatio::Normal => Color::Muted,
2835 TokenUsageRatio::Warning => Color::Warning,
2836 TokenUsageRatio::Exceeded => Color::Error,
2837 };
2838
2839 let token_count = h_flex()
2840 .id("token-count")
2841 .flex_shrink_0()
2842 .gap_0p5()
2843 .when(!is_generating && is_estimating, |parent| {
2844 parent
2845 .child(
2846 h_flex()
2847 .mr_1()
2848 .size_2p5()
2849 .justify_center()
2850 .rounded_full()
2851 .bg(cx.theme().colors().text.opacity(0.1))
2852 .child(
2853 div().size_1().rounded_full().bg(cx.theme().colors().text),
2854 ),
2855 )
2856 .tooltip(move |window, cx| {
2857 Tooltip::with_meta(
2858 "Estimated New Token Count",
2859 None,
2860 format!(
2861 "Current Conversation Tokens: {}",
2862 humanize_token_count(conversation_token_usage.total)
2863 ),
2864 window,
2865 cx,
2866 )
2867 })
2868 })
2869 .child(
2870 Label::new(humanize_token_count(total_token_usage.total))
2871 .size(LabelSize::Small)
2872 .color(token_color)
2873 .map(|label| {
2874 if is_generating || is_waiting_to_update_token_count {
2875 label
2876 .with_animation(
2877 "used-tokens-label",
2878 Animation::new(Duration::from_secs(2))
2879 .repeat()
2880 .with_easing(pulsating_between(0.6, 1.)),
2881 |label, delta| label.alpha(delta),
2882 )
2883 .into_any()
2884 } else {
2885 label.into_any_element()
2886 }
2887 }),
2888 )
2889 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2890 .child(
2891 Label::new(humanize_token_count(total_token_usage.max))
2892 .size(LabelSize::Small)
2893 .color(Color::Muted),
2894 )
2895 .into_any();
2896
2897 Some(token_count)
2898 }
2899 ActiveView::ExternalAgentThread { .. }
2900 | ActiveView::TextThread { .. }
2901 | ActiveView::History
2902 | ActiveView::Configuration => None,
2903 }
2904 }
2905
2906 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2907 if TrialEndUpsell::dismissed() {
2908 return false;
2909 }
2910
2911 match &self.active_view {
2912 ActiveView::Thread { thread, .. } => {
2913 if thread
2914 .read(cx)
2915 .thread()
2916 .read(cx)
2917 .configured_model()
2918 .is_some_and(|model| {
2919 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2920 })
2921 {
2922 return false;
2923 }
2924 }
2925 ActiveView::TextThread { .. } => {
2926 if LanguageModelRegistry::global(cx)
2927 .read(cx)
2928 .default_model()
2929 .is_some_and(|model| {
2930 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2931 })
2932 {
2933 return false;
2934 }
2935 }
2936 ActiveView::ExternalAgentThread { .. }
2937 | ActiveView::History
2938 | ActiveView::Configuration => return false,
2939 }
2940
2941 let plan = self.user_store.read(cx).plan();
2942 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2943
2944 matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
2945 }
2946
2947 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2948 if OnboardingUpsell::dismissed() {
2949 return false;
2950 }
2951
2952 match &self.active_view {
2953 ActiveView::History | ActiveView::Configuration => false,
2954 ActiveView::ExternalAgentThread { thread_view, .. }
2955 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2956 {
2957 false
2958 }
2959 _ => {
2960 let history_is_empty = if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
2961 self.acp_history_store.read(cx).is_empty(cx)
2962 } else {
2963 self.history_store
2964 .update(cx, |store, cx| store.recent_entries(1, cx).is_empty())
2965 };
2966
2967 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2968 .providers()
2969 .iter()
2970 .any(|provider| {
2971 provider.is_authenticated(cx)
2972 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2973 });
2974
2975 history_is_empty || !has_configured_non_zed_providers
2976 }
2977 }
2978 }
2979
2980 fn render_onboarding(
2981 &self,
2982 _window: &mut Window,
2983 cx: &mut Context<Self>,
2984 ) -> Option<impl IntoElement> {
2985 if !self.should_render_onboarding(cx) {
2986 return None;
2987 }
2988
2989 let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
2990 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2991
2992 Some(
2993 div()
2994 .when(thread_view, |this| {
2995 this.size_full().bg(cx.theme().colors().panel_background)
2996 })
2997 .when(text_thread_view, |this| {
2998 this.bg(cx.theme().colors().editor_background)
2999 })
3000 .child(self.onboarding.clone()),
3001 )
3002 }
3003
3004 fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
3005 div()
3006 .size_full()
3007 .absolute()
3008 .inset_0()
3009 .bg(cx.theme().colors().panel_background)
3010 .opacity(0.8)
3011 .block_mouse_except_scroll()
3012 }
3013
3014 fn render_trial_end_upsell(
3015 &self,
3016 _window: &mut Window,
3017 cx: &mut Context<Self>,
3018 ) -> Option<impl IntoElement> {
3019 if !self.should_render_trial_end_upsell(cx) {
3020 return None;
3021 }
3022
3023 Some(
3024 v_flex()
3025 .absolute()
3026 .inset_0()
3027 .size_full()
3028 .bg(cx.theme().colors().panel_background)
3029 .opacity(0.85)
3030 .block_mouse_except_scroll()
3031 .child(EndTrialUpsell::new(Arc::new({
3032 let this = cx.entity();
3033 move |_, cx| {
3034 this.update(cx, |_this, cx| {
3035 TrialEndUpsell::set_dismissed(true, cx);
3036 cx.notify();
3037 });
3038 }
3039 }))),
3040 )
3041 }
3042
3043 fn render_empty_state_section_header(
3044 &self,
3045 label: impl Into<SharedString>,
3046 action_slot: Option<AnyElement>,
3047 cx: &mut Context<Self>,
3048 ) -> impl IntoElement {
3049 div().pl_1().pr_1p5().child(
3050 h_flex()
3051 .mt_2()
3052 .pl_1p5()
3053 .pb_1()
3054 .w_full()
3055 .justify_between()
3056 .border_b_1()
3057 .border_color(cx.theme().colors().border_variant)
3058 .child(
3059 Label::new(label.into())
3060 .size(LabelSize::Small)
3061 .color(Color::Muted),
3062 )
3063 .children(action_slot),
3064 )
3065 }
3066
3067 fn render_thread_empty_state(
3068 &self,
3069 window: &mut Window,
3070 cx: &mut Context<Self>,
3071 ) -> impl IntoElement {
3072 let recent_history = self
3073 .history_store
3074 .update(cx, |this, cx| this.recent_entries(6, cx));
3075
3076 let model_registry = LanguageModelRegistry::read_global(cx);
3077
3078 let configuration_error =
3079 model_registry.configuration_error(model_registry.default_model(), cx);
3080
3081 let no_error = configuration_error.is_none();
3082 let focus_handle = self.focus_handle(cx);
3083
3084 v_flex()
3085 .size_full()
3086 .bg(cx.theme().colors().panel_background)
3087 .when(recent_history.is_empty(), |this| {
3088 this.child(
3089 v_flex()
3090 .size_full()
3091 .mx_auto()
3092 .justify_center()
3093 .items_center()
3094 .gap_1()
3095 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
3096 .when(no_error, |parent| {
3097 parent
3098 .child(h_flex().child(
3099 Label::new("Ask and build anything.").color(Color::Muted),
3100 ))
3101 .child(
3102 v_flex()
3103 .mt_2()
3104 .gap_1()
3105 .max_w_48()
3106 .child(
3107 Button::new("context", "Add Context")
3108 .label_size(LabelSize::Small)
3109 .icon(IconName::FileCode)
3110 .icon_position(IconPosition::Start)
3111 .icon_size(IconSize::Small)
3112 .icon_color(Color::Muted)
3113 .full_width()
3114 .key_binding(KeyBinding::for_action_in(
3115 &ToggleContextPicker,
3116 &focus_handle,
3117 window,
3118 cx,
3119 ))
3120 .on_click(|_event, window, cx| {
3121 window.dispatch_action(
3122 ToggleContextPicker.boxed_clone(),
3123 cx,
3124 )
3125 }),
3126 )
3127 .child(
3128 Button::new("mode", "Switch Model")
3129 .label_size(LabelSize::Small)
3130 .icon(IconName::DatabaseZap)
3131 .icon_position(IconPosition::Start)
3132 .icon_size(IconSize::Small)
3133 .icon_color(Color::Muted)
3134 .full_width()
3135 .key_binding(KeyBinding::for_action_in(
3136 &ToggleModelSelector,
3137 &focus_handle,
3138 window,
3139 cx,
3140 ))
3141 .on_click(|_event, window, cx| {
3142 window.dispatch_action(
3143 ToggleModelSelector.boxed_clone(),
3144 cx,
3145 )
3146 }),
3147 )
3148 .child(
3149 Button::new("settings", "View Settings")
3150 .label_size(LabelSize::Small)
3151 .icon(IconName::Settings)
3152 .icon_position(IconPosition::Start)
3153 .icon_size(IconSize::Small)
3154 .icon_color(Color::Muted)
3155 .full_width()
3156 .key_binding(KeyBinding::for_action_in(
3157 &OpenSettings,
3158 &focus_handle,
3159 window,
3160 cx,
3161 ))
3162 .on_click(|_event, window, cx| {
3163 window.dispatch_action(
3164 OpenSettings.boxed_clone(),
3165 cx,
3166 )
3167 }),
3168 ),
3169 )
3170 }),
3171 )
3172 })
3173 .when(!recent_history.is_empty(), |parent| {
3174 parent
3175 .overflow_hidden()
3176 .justify_end()
3177 .gap_1()
3178 .child(
3179 self.render_empty_state_section_header(
3180 "Recent",
3181 Some(
3182 Button::new("view-history", "View All")
3183 .style(ButtonStyle::Subtle)
3184 .label_size(LabelSize::Small)
3185 .key_binding(
3186 KeyBinding::for_action_in(
3187 &OpenHistory,
3188 &self.focus_handle(cx),
3189 window,
3190 cx,
3191 )
3192 .map(|kb| kb.size(rems_from_px(12.))),
3193 )
3194 .on_click(move |_event, window, cx| {
3195 window.dispatch_action(OpenHistory.boxed_clone(), cx);
3196 })
3197 .into_any_element(),
3198 ),
3199 cx,
3200 ),
3201 )
3202 .child(
3203 v_flex().p_1().pr_1p5().gap_1().children(
3204 recent_history
3205 .into_iter()
3206 .enumerate()
3207 .map(|(index, entry)| {
3208 // TODO: Add keyboard navigation.
3209 let is_hovered =
3210 self.hovered_recent_history_item == Some(index);
3211 HistoryEntryElement::new(entry, cx.entity().downgrade())
3212 .hovered(is_hovered)
3213 .on_hover(cx.listener(
3214 move |this, is_hovered, _window, cx| {
3215 if *is_hovered {
3216 this.hovered_recent_history_item = Some(index);
3217 } else if this.hovered_recent_history_item
3218 == Some(index)
3219 {
3220 this.hovered_recent_history_item = None;
3221 }
3222 cx.notify();
3223 },
3224 ))
3225 .into_any_element()
3226 }),
3227 ),
3228 )
3229 })
3230 .when_some(configuration_error.as_ref(), |this, err| {
3231 this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
3232 })
3233 }
3234
3235 fn render_configuration_error(
3236 &self,
3237 border_bottom: bool,
3238 configuration_error: &ConfigurationError,
3239 focus_handle: &FocusHandle,
3240 window: &mut Window,
3241 cx: &mut App,
3242 ) -> impl IntoElement {
3243 let zed_provider_configured = AgentSettings::get_global(cx)
3244 .default_model
3245 .as_ref()
3246 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
3247
3248 let callout = if zed_provider_configured {
3249 Callout::new()
3250 .icon(IconName::Warning)
3251 .severity(Severity::Warning)
3252 .when(border_bottom, |this| {
3253 this.border_position(ui::BorderPosition::Bottom)
3254 })
3255 .title("Sign in to continue using Zed as your LLM provider.")
3256 .actions_slot(
3257 Button::new("sign_in", "Sign In")
3258 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3259 .label_size(LabelSize::Small)
3260 .on_click({
3261 let workspace = self.workspace.clone();
3262 move |_, _, cx| {
3263 let Ok(client) =
3264 workspace.update(cx, |workspace, _| workspace.client().clone())
3265 else {
3266 return;
3267 };
3268
3269 cx.spawn(async move |cx| {
3270 client.sign_in_with_optional_connect(true, cx).await
3271 })
3272 .detach_and_log_err(cx);
3273 }
3274 }),
3275 )
3276 } else {
3277 Callout::new()
3278 .icon(IconName::Warning)
3279 .severity(Severity::Warning)
3280 .when(border_bottom, |this| {
3281 this.border_position(ui::BorderPosition::Bottom)
3282 })
3283 .title(configuration_error.to_string())
3284 .actions_slot(
3285 Button::new("settings", "Configure")
3286 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3287 .label_size(LabelSize::Small)
3288 .key_binding(
3289 KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
3290 .map(|kb| kb.size(rems_from_px(12.))),
3291 )
3292 .on_click(|_event, window, cx| {
3293 window.dispatch_action(OpenSettings.boxed_clone(), cx)
3294 }),
3295 )
3296 };
3297
3298 match configuration_error {
3299 ConfigurationError::ModelNotFound
3300 | ConfigurationError::ProviderNotAuthenticated(_)
3301 | ConfigurationError::NoProvider => callout.into_any_element(),
3302 }
3303 }
3304
3305 fn render_tool_use_limit_reached(
3306 &self,
3307 window: &mut Window,
3308 cx: &mut Context<Self>,
3309 ) -> Option<AnyElement> {
3310 let active_thread = match &self.active_view {
3311 ActiveView::Thread { thread, .. } => thread,
3312 ActiveView::ExternalAgentThread { .. } => {
3313 return None;
3314 }
3315 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
3316 return None;
3317 }
3318 };
3319
3320 let thread = active_thread.read(cx).thread().read(cx);
3321
3322 let tool_use_limit_reached = thread.tool_use_limit_reached();
3323 if !tool_use_limit_reached {
3324 return None;
3325 }
3326
3327 let model = thread.configured_model()?.model;
3328
3329 let focus_handle = self.focus_handle(cx);
3330
3331 let banner = Banner::new()
3332 .severity(Severity::Info)
3333 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
3334 .action_slot(
3335 h_flex()
3336 .gap_1()
3337 .child(
3338 Button::new("continue-conversation", "Continue")
3339 .layer(ElevationIndex::ModalSurface)
3340 .label_size(LabelSize::Small)
3341 .key_binding(
3342 KeyBinding::for_action_in(
3343 &ContinueThread,
3344 &focus_handle,
3345 window,
3346 cx,
3347 )
3348 .map(|kb| kb.size(rems_from_px(10.))),
3349 )
3350 .on_click(cx.listener(|this, _, window, cx| {
3351 this.continue_conversation(window, cx);
3352 })),
3353 )
3354 .when(model.supports_burn_mode(), |this| {
3355 this.child(
3356 Button::new("continue-burn-mode", "Continue with Burn Mode")
3357 .style(ButtonStyle::Filled)
3358 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3359 .layer(ElevationIndex::ModalSurface)
3360 .label_size(LabelSize::Small)
3361 .key_binding(
3362 KeyBinding::for_action_in(
3363 &ContinueWithBurnMode,
3364 &focus_handle,
3365 window,
3366 cx,
3367 )
3368 .map(|kb| kb.size(rems_from_px(10.))),
3369 )
3370 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
3371 .on_click({
3372 let active_thread = active_thread.clone();
3373 cx.listener(move |this, _, window, cx| {
3374 active_thread.update(cx, |active_thread, cx| {
3375 active_thread.thread().update(cx, |thread, _cx| {
3376 thread.set_completion_mode(CompletionMode::Burn);
3377 });
3378 });
3379 this.continue_conversation(window, cx);
3380 })
3381 }),
3382 )
3383 }),
3384 );
3385
3386 Some(div().px_2().pb_2().child(banner).into_any_element())
3387 }
3388
3389 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3390 let message = message.into();
3391
3392 IconButton::new("copy", IconName::Copy)
3393 .icon_size(IconSize::Small)
3394 .icon_color(Color::Muted)
3395 .tooltip(Tooltip::text("Copy Error Message"))
3396 .on_click(move |_, _, cx| {
3397 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3398 })
3399 }
3400
3401 fn dismiss_error_button(
3402 &self,
3403 thread: &Entity<ActiveThread>,
3404 cx: &mut Context<Self>,
3405 ) -> impl IntoElement {
3406 IconButton::new("dismiss", IconName::Close)
3407 .icon_size(IconSize::Small)
3408 .icon_color(Color::Muted)
3409 .tooltip(Tooltip::text("Dismiss Error"))
3410 .on_click(cx.listener({
3411 let thread = thread.clone();
3412 move |_, _, _, cx| {
3413 thread.update(cx, |this, _cx| {
3414 this.clear_last_error();
3415 });
3416
3417 cx.notify();
3418 }
3419 }))
3420 }
3421
3422 fn upgrade_button(
3423 &self,
3424 thread: &Entity<ActiveThread>,
3425 cx: &mut Context<Self>,
3426 ) -> impl IntoElement {
3427 Button::new("upgrade", "Upgrade")
3428 .label_size(LabelSize::Small)
3429 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3430 .on_click(cx.listener({
3431 let thread = thread.clone();
3432 move |_, _, _, cx| {
3433 thread.update(cx, |this, _cx| {
3434 this.clear_last_error();
3435 });
3436
3437 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
3438 cx.notify();
3439 }
3440 }))
3441 }
3442
3443 fn render_payment_required_error(
3444 &self,
3445 thread: &Entity<ActiveThread>,
3446 cx: &mut Context<Self>,
3447 ) -> AnyElement {
3448 const ERROR_MESSAGE: &str =
3449 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
3450
3451 Callout::new()
3452 .severity(Severity::Error)
3453 .icon(IconName::XCircle)
3454 .title("Free Usage Exceeded")
3455 .description(ERROR_MESSAGE)
3456 .actions_slot(
3457 h_flex()
3458 .gap_0p5()
3459 .child(self.upgrade_button(thread, cx))
3460 .child(self.create_copy_button(ERROR_MESSAGE)),
3461 )
3462 .dismiss_action(self.dismiss_error_button(thread, cx))
3463 .into_any_element()
3464 }
3465
3466 fn render_model_request_limit_reached_error(
3467 &self,
3468 plan: Plan,
3469 thread: &Entity<ActiveThread>,
3470 cx: &mut Context<Self>,
3471 ) -> AnyElement {
3472 let error_message = match plan {
3473 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
3474 Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
3475 };
3476
3477 Callout::new()
3478 .severity(Severity::Error)
3479 .title("Model Prompt Limit Reached")
3480 .description(error_message)
3481 .actions_slot(
3482 h_flex()
3483 .gap_0p5()
3484 .child(self.upgrade_button(thread, cx))
3485 .child(self.create_copy_button(error_message)),
3486 )
3487 .dismiss_action(self.dismiss_error_button(thread, cx))
3488 .into_any_element()
3489 }
3490
3491 fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
3492 Button::new("retry", "Retry")
3493 .icon(IconName::RotateCw)
3494 .icon_position(IconPosition::Start)
3495 .icon_size(IconSize::Small)
3496 .label_size(LabelSize::Small)
3497 .on_click({
3498 let thread = thread.clone();
3499 move |_, window, cx| {
3500 thread.update(cx, |thread, cx| {
3501 thread.clear_last_error();
3502 thread.thread().update(cx, |thread, cx| {
3503 thread.retry_last_completion(Some(window.window_handle()), cx);
3504 });
3505 });
3506 }
3507 })
3508 .into_any_element()
3509 }
3510
3511 fn render_error_message(
3512 &self,
3513 header: SharedString,
3514 message: SharedString,
3515 thread: &Entity<ActiveThread>,
3516 cx: &mut Context<Self>,
3517 ) -> AnyElement {
3518 let message_with_header = format!("{}\n{}", header, message);
3519
3520 Callout::new()
3521 .severity(Severity::Error)
3522 .icon(IconName::XCircle)
3523 .title(header)
3524 .description(message)
3525 .actions_slot(
3526 h_flex()
3527 .gap_0p5()
3528 .child(self.render_retry_button(thread))
3529 .child(self.create_copy_button(message_with_header)),
3530 )
3531 .dismiss_action(self.dismiss_error_button(thread, cx))
3532 .into_any_element()
3533 }
3534
3535 fn render_retryable_error(
3536 &self,
3537 message: SharedString,
3538 can_enable_burn_mode: bool,
3539 thread: &Entity<ActiveThread>,
3540 ) -> AnyElement {
3541 Callout::new()
3542 .severity(Severity::Error)
3543 .title("Error")
3544 .description(message)
3545 .actions_slot(
3546 h_flex()
3547 .gap_0p5()
3548 .when(can_enable_burn_mode, |this| {
3549 this.child(
3550 Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
3551 .icon(IconName::ZedBurnMode)
3552 .icon_position(IconPosition::Start)
3553 .icon_size(IconSize::Small)
3554 .label_size(LabelSize::Small)
3555 .on_click({
3556 let thread = thread.clone();
3557 move |_, window, cx| {
3558 thread.update(cx, |thread, cx| {
3559 thread.clear_last_error();
3560 thread.thread().update(cx, |thread, cx| {
3561 thread.enable_burn_mode_and_retry(
3562 Some(window.window_handle()),
3563 cx,
3564 );
3565 });
3566 });
3567 }
3568 }),
3569 )
3570 })
3571 .child(self.render_retry_button(thread)),
3572 )
3573 .into_any_element()
3574 }
3575
3576 fn render_prompt_editor(
3577 &self,
3578 context_editor: &Entity<TextThreadEditor>,
3579 buffer_search_bar: &Entity<BufferSearchBar>,
3580 window: &mut Window,
3581 cx: &mut Context<Self>,
3582 ) -> Div {
3583 let mut registrar = buffer_search::DivRegistrar::new(
3584 |this, _, _cx| match &this.active_view {
3585 ActiveView::TextThread {
3586 buffer_search_bar, ..
3587 } => Some(buffer_search_bar.clone()),
3588 _ => None,
3589 },
3590 cx,
3591 );
3592 BufferSearchBar::register(&mut registrar);
3593 registrar
3594 .into_div()
3595 .size_full()
3596 .relative()
3597 .map(|parent| {
3598 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3599 if buffer_search_bar.is_dismissed() {
3600 return parent;
3601 }
3602 parent.child(
3603 div()
3604 .p(DynamicSpacing::Base08.rems(cx))
3605 .border_b_1()
3606 .border_color(cx.theme().colors().border_variant)
3607 .bg(cx.theme().colors().editor_background)
3608 .child(buffer_search_bar.render(window, cx)),
3609 )
3610 })
3611 })
3612 .child(context_editor.clone())
3613 .child(self.render_drag_target(cx))
3614 }
3615
3616 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3617 let is_local = self.project.read(cx).is_local();
3618 div()
3619 .invisible()
3620 .absolute()
3621 .top_0()
3622 .right_0()
3623 .bottom_0()
3624 .left_0()
3625 .bg(cx.theme().colors().drop_target_background)
3626 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3627 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3628 .when(is_local, |this| {
3629 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3630 })
3631 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3632 let item = tab.pane.read(cx).item_for_index(tab.ix);
3633 let project_paths = item
3634 .and_then(|item| item.project_path(cx))
3635 .into_iter()
3636 .collect::<Vec<_>>();
3637 this.handle_drop(project_paths, vec![], window, cx);
3638 }))
3639 .on_drop(
3640 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3641 let project_paths = selection
3642 .items()
3643 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3644 .collect::<Vec<_>>();
3645 this.handle_drop(project_paths, vec![], window, cx);
3646 }),
3647 )
3648 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3649 let tasks = paths
3650 .paths()
3651 .iter()
3652 .map(|path| {
3653 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3654 })
3655 .collect::<Vec<_>>();
3656 cx.spawn_in(window, async move |this, cx| {
3657 let mut paths = vec![];
3658 let mut added_worktrees = vec![];
3659 let opened_paths = futures::future::join_all(tasks).await;
3660 for entry in opened_paths {
3661 if let Some((worktree, project_path)) = entry.log_err() {
3662 added_worktrees.push(worktree);
3663 paths.push(project_path);
3664 }
3665 }
3666 this.update_in(cx, |this, window, cx| {
3667 this.handle_drop(paths, added_worktrees, window, cx);
3668 })
3669 .ok();
3670 })
3671 .detach();
3672 }))
3673 }
3674
3675 fn handle_drop(
3676 &mut self,
3677 paths: Vec<ProjectPath>,
3678 added_worktrees: Vec<Entity<Worktree>>,
3679 window: &mut Window,
3680 cx: &mut Context<Self>,
3681 ) {
3682 match &self.active_view {
3683 ActiveView::Thread { thread, .. } => {
3684 let context_store = thread.read(cx).context_store().clone();
3685 context_store.update(cx, move |context_store, cx| {
3686 let mut tasks = Vec::new();
3687 for project_path in &paths {
3688 tasks.push(context_store.add_file_from_path(
3689 project_path.clone(),
3690 false,
3691 cx,
3692 ));
3693 }
3694 cx.background_spawn(async move {
3695 futures::future::join_all(tasks).await;
3696 // Need to hold onto the worktrees until they have already been used when
3697 // opening the buffers.
3698 drop(added_worktrees);
3699 })
3700 .detach();
3701 });
3702 }
3703 ActiveView::ExternalAgentThread { thread_view } => {
3704 thread_view.update(cx, |thread_view, cx| {
3705 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3706 });
3707 }
3708 ActiveView::TextThread { context_editor, .. } => {
3709 context_editor.update(cx, |context_editor, cx| {
3710 TextThreadEditor::insert_dragged_files(
3711 context_editor,
3712 paths,
3713 added_worktrees,
3714 window,
3715 cx,
3716 );
3717 });
3718 }
3719 ActiveView::History | ActiveView::Configuration => {}
3720 }
3721 }
3722
3723 fn key_context(&self) -> KeyContext {
3724 let mut key_context = KeyContext::new_with_defaults();
3725 key_context.add("AgentPanel");
3726 match &self.active_view {
3727 ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
3728 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3729 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3730 }
3731 key_context
3732 }
3733}
3734
3735impl Render for AgentPanel {
3736 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3737 // WARNING: Changes to this element hierarchy can have
3738 // non-obvious implications to the layout of children.
3739 //
3740 // If you need to change it, please confirm:
3741 // - The message editor expands (cmd-option-esc) correctly
3742 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3743 // - Font size works as expected and can be changed with cmd-+/cmd-
3744 // - Scrolling in all views works as expected
3745 // - Files can be dropped into the panel
3746 let content = v_flex()
3747 .relative()
3748 .size_full()
3749 .justify_between()
3750 .key_context(self.key_context())
3751 .on_action(cx.listener(Self::cancel))
3752 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3753 this.new_thread(action, window, cx);
3754 }))
3755 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3756 this.open_history(window, cx);
3757 }))
3758 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3759 this.open_configuration(window, cx);
3760 }))
3761 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3762 .on_action(cx.listener(Self::deploy_rules_library))
3763 .on_action(cx.listener(Self::open_agent_diff))
3764 .on_action(cx.listener(Self::go_back))
3765 .on_action(cx.listener(Self::toggle_navigation_menu))
3766 .on_action(cx.listener(Self::toggle_options_menu))
3767 .on_action(cx.listener(Self::increase_font_size))
3768 .on_action(cx.listener(Self::decrease_font_size))
3769 .on_action(cx.listener(Self::reset_font_size))
3770 .on_action(cx.listener(Self::toggle_zoom))
3771 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3772 this.continue_conversation(window, cx);
3773 }))
3774 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3775 match &this.active_view {
3776 ActiveView::Thread { thread, .. } => {
3777 thread.update(cx, |active_thread, cx| {
3778 active_thread.thread().update(cx, |thread, _cx| {
3779 thread.set_completion_mode(CompletionMode::Burn);
3780 });
3781 });
3782 this.continue_conversation(window, cx);
3783 }
3784 ActiveView::ExternalAgentThread { .. } => {}
3785 ActiveView::TextThread { .. }
3786 | ActiveView::History
3787 | ActiveView::Configuration => {}
3788 }
3789 }))
3790 .on_action(cx.listener(Self::toggle_burn_mode))
3791 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3792 if let Some(thread_view) = this.active_thread_view() {
3793 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3794 }
3795 }))
3796 .child(self.render_toolbar(window, cx))
3797 .children(self.render_onboarding(window, cx))
3798 .map(|parent| match &self.active_view {
3799 ActiveView::Thread {
3800 thread,
3801 message_editor,
3802 ..
3803 } => parent
3804 .child(
3805 if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
3806 self.render_thread_empty_state(window, cx)
3807 .into_any_element()
3808 } else {
3809 thread.clone().into_any_element()
3810 },
3811 )
3812 .children(self.render_tool_use_limit_reached(window, cx))
3813 .when_some(thread.read(cx).last_error(), |this, last_error| {
3814 this.child(
3815 div()
3816 .child(match last_error {
3817 ThreadError::PaymentRequired => {
3818 self.render_payment_required_error(thread, cx)
3819 }
3820 ThreadError::ModelRequestLimitReached { plan } => self
3821 .render_model_request_limit_reached_error(plan, thread, cx),
3822 ThreadError::Message { header, message } => {
3823 self.render_error_message(header, message, thread, cx)
3824 }
3825 ThreadError::RetryableError {
3826 message,
3827 can_enable_burn_mode,
3828 } => self.render_retryable_error(
3829 message,
3830 can_enable_burn_mode,
3831 thread,
3832 ),
3833 })
3834 .into_any(),
3835 )
3836 })
3837 .child(h_flex().relative().child(message_editor.clone()).when(
3838 !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
3839 |this| this.child(self.render_backdrop(cx)),
3840 ))
3841 .child(self.render_drag_target(cx)),
3842 ActiveView::ExternalAgentThread { thread_view, .. } => parent
3843 .child(thread_view.clone())
3844 .child(self.render_drag_target(cx)),
3845 ActiveView::History => {
3846 if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
3847 parent.child(self.acp_history.clone())
3848 } else {
3849 parent.child(self.history.clone())
3850 }
3851 }
3852 ActiveView::TextThread {
3853 context_editor,
3854 buffer_search_bar,
3855 ..
3856 } => {
3857 let model_registry = LanguageModelRegistry::read_global(cx);
3858 let configuration_error =
3859 model_registry.configuration_error(model_registry.default_model(), cx);
3860 parent
3861 .map(|this| {
3862 if !self.should_render_onboarding(cx)
3863 && let Some(err) = configuration_error.as_ref()
3864 {
3865 this.child(self.render_configuration_error(
3866 true,
3867 err,
3868 &self.focus_handle(cx),
3869 window,
3870 cx,
3871 ))
3872 } else {
3873 this
3874 }
3875 })
3876 .child(self.render_prompt_editor(
3877 context_editor,
3878 buffer_search_bar,
3879 window,
3880 cx,
3881 ))
3882 }
3883 ActiveView::Configuration => parent.children(self.configuration.clone()),
3884 })
3885 .children(self.render_trial_end_upsell(window, cx));
3886
3887 match self.active_view.which_font_size_used() {
3888 WhichFontSize::AgentFont => {
3889 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3890 .size_full()
3891 .child(content)
3892 .into_any()
3893 }
3894 _ => content.into_any(),
3895 }
3896 }
3897}
3898
3899struct PromptLibraryInlineAssist {
3900 workspace: WeakEntity<Workspace>,
3901}
3902
3903impl PromptLibraryInlineAssist {
3904 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3905 Self { workspace }
3906 }
3907}
3908
3909impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3910 fn assist(
3911 &self,
3912 prompt_editor: &Entity<Editor>,
3913 initial_prompt: Option<String>,
3914 window: &mut Window,
3915 cx: &mut Context<RulesLibrary>,
3916 ) {
3917 InlineAssistant::update_global(cx, |assistant, cx| {
3918 let Some(project) = self
3919 .workspace
3920 .upgrade()
3921 .map(|workspace| workspace.read(cx).project().downgrade())
3922 else {
3923 return;
3924 };
3925 let prompt_store = None;
3926 let thread_store = None;
3927 let text_thread_store = None;
3928 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3929 assistant.assist(
3930 prompt_editor,
3931 self.workspace.clone(),
3932 context_store,
3933 project,
3934 prompt_store,
3935 thread_store,
3936 text_thread_store,
3937 initial_prompt,
3938 window,
3939 cx,
3940 )
3941 })
3942 }
3943
3944 fn focus_agent_panel(
3945 &self,
3946 workspace: &mut Workspace,
3947 window: &mut Window,
3948 cx: &mut Context<Workspace>,
3949 ) -> bool {
3950 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3951 }
3952}
3953
3954pub struct ConcreteAssistantPanelDelegate;
3955
3956impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3957 fn active_context_editor(
3958 &self,
3959 workspace: &mut Workspace,
3960 _window: &mut Window,
3961 cx: &mut Context<Workspace>,
3962 ) -> Option<Entity<TextThreadEditor>> {
3963 let panel = workspace.panel::<AgentPanel>(cx)?;
3964 panel.read(cx).active_context_editor()
3965 }
3966
3967 fn open_saved_context(
3968 &self,
3969 workspace: &mut Workspace,
3970 path: Arc<Path>,
3971 window: &mut Window,
3972 cx: &mut Context<Workspace>,
3973 ) -> Task<Result<()>> {
3974 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3975 return Task::ready(Err(anyhow!("Agent panel not found")));
3976 };
3977
3978 panel.update(cx, |panel, cx| {
3979 panel.open_saved_prompt_editor(path, window, cx)
3980 })
3981 }
3982
3983 fn open_remote_context(
3984 &self,
3985 _workspace: &mut Workspace,
3986 _context_id: assistant_context::ContextId,
3987 _window: &mut Window,
3988 _cx: &mut Context<Workspace>,
3989 ) -> Task<Result<Entity<TextThreadEditor>>> {
3990 Task::ready(Err(anyhow!("opening remote context not implemented")))
3991 }
3992
3993 fn quote_selection(
3994 &self,
3995 workspace: &mut Workspace,
3996 selection_ranges: Vec<Range<Anchor>>,
3997 buffer: Entity<MultiBuffer>,
3998 window: &mut Window,
3999 cx: &mut Context<Workspace>,
4000 ) {
4001 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4002 return;
4003 };
4004
4005 if !panel.focus_handle(cx).contains_focused(window, cx) {
4006 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4007 }
4008
4009 panel.update(cx, |_, cx| {
4010 // Wait to create a new context until the workspace is no longer
4011 // being updated.
4012 cx.defer_in(window, move |panel, window, cx| {
4013 if let Some(thread_view) = panel.active_thread_view() {
4014 thread_view.update(cx, |thread_view, cx| {
4015 thread_view.insert_selections(window, cx);
4016 });
4017 } else if let Some(message_editor) = panel.active_message_editor() {
4018 message_editor.update(cx, |message_editor, cx| {
4019 message_editor.context_store().update(cx, |store, cx| {
4020 let buffer = buffer.read(cx);
4021 let selection_ranges = selection_ranges
4022 .into_iter()
4023 .flat_map(|range| {
4024 let (start_buffer, start) =
4025 buffer.text_anchor_for_position(range.start, cx)?;
4026 let (end_buffer, end) =
4027 buffer.text_anchor_for_position(range.end, cx)?;
4028 if start_buffer != end_buffer {
4029 return None;
4030 }
4031 Some((start_buffer, start..end))
4032 })
4033 .collect::<Vec<_>>();
4034
4035 for (buffer, range) in selection_ranges {
4036 store.add_selection(buffer, range, cx);
4037 }
4038 })
4039 })
4040 } else if let Some(context_editor) = panel.active_context_editor() {
4041 let snapshot = buffer.read(cx).snapshot(cx);
4042 let selection_ranges = selection_ranges
4043 .into_iter()
4044 .map(|range| range.to_point(&snapshot))
4045 .collect::<Vec<_>>();
4046
4047 context_editor.update(cx, |context_editor, cx| {
4048 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4049 });
4050 }
4051 });
4052 });
4053 }
4054}
4055
4056struct OnboardingUpsell;
4057
4058impl Dismissable for OnboardingUpsell {
4059 const KEY: &'static str = "dismissed-trial-upsell";
4060}
4061
4062struct TrialEndUpsell;
4063
4064impl Dismissable for TrialEndUpsell {
4065 const KEY: &'static str = "dismissed-trial-end-upsell";
4066}