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