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