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