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