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