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