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