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