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