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