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