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