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