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