use std::ops::{Not, Range}; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, acp::AcpThreadView, active_thread::{self, ActiveThread, ActiveThreadEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, agent_diff::AgentDiff, message_editor::{MessageEditor, MessageEditorEvent}, slash_command::SlashCommandCompletionProvider, text_thread_editor::{ AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens, }, thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; use crate::{ExternalAgent, NewExternalAgentThread}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, history_store::{HistoryEntryId, HistoryStore}, thread_store::{TextThreadStore, ThreadStore}, }; use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, }; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, update_settings_file}; use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace, dock::{DockPosition, Panel, PanelEvent}, }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: Option, selected_agent: Option, } pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { workspace .register_action(|workspace, action: &NewThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| panel.new_thread(action, window, cx)); workspace.focus_panel::(window, cx); } }) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| panel.open_history(window, cx)); } }) .register_action(|workspace, _: &OpenSettings, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); } }) .register_action(|workspace, _: &NewTextThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); } }) .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.external_thread(action.agent, None, window, cx) }); } }) .register_action(|workspace, action: &OpenRulesLibrary, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.deploy_rules_library(action, window, cx) }); } }) .register_action(|workspace, _: &OpenAgentDiff, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); match &panel.read(cx).active_view { ActiveView::Thread { thread, .. } => { let thread = thread.read(cx).thread().clone(); AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } } }) .register_action(|workspace, _: &Follow, window, cx| { workspace.follow(CollaboratorId::Agent, window, cx); }) .register_action(|workspace, _: &ExpandMessageEditor, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; }; workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |editor, cx| { editor.expand_message_editor(&ExpandMessageEditor, window, cx); }); } }); }) .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx); }); } }) .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.toggle_options_menu(&ToggleOptionsMenu, window, cx); }); } }) .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); }); } }) .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); }) .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| { OnboardingUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); }); }, ) .detach(); } enum ActiveView { Thread { thread: Entity, change_title_editor: Entity, message_editor: Entity, _subscriptions: Vec, }, ExternalAgentThread { thread_view: Entity, }, TextThread { context_editor: Entity, title_editor: Entity, buffer_search_bar: Entity, _subscriptions: Vec, }, History, Configuration, } enum WhichFontSize { AgentFont, BufferFont, None, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, TextThread, Gemini, ClaudeCode, NativeAgent, } impl AgentType { fn label(self) -> impl Into { match self { Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", Self::Gemini => "Google Gemini", Self::ClaudeCode => "Claude Code", } } fn icon(self) -> IconName { match self { Self::Zed | Self::TextThread => IconName::AiZed, Self::NativeAgent => IconName::ZedAssistant, Self::Gemini => IconName::AiGemini, Self::ClaudeCode => IconName::AiClaude, } } } impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { ActiveView::Thread { .. } | ActiveView::ExternalAgentThread { .. } | ActiveView::History => WhichFontSize::AgentFont, ActiveView::TextThread { .. } => WhichFontSize::BufferFont, ActiveView::Configuration => WhichFontSize::None, } } pub fn thread( active_thread: Entity, message_editor: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let summary = active_thread.read(cx).summary(cx).or_default(); let editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_text(summary.clone(), window, cx); editor }); let subscriptions = vec![ cx.subscribe(&message_editor, |this, _, event, cx| match event { MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => { cx.notify(); } MessageEditorEvent::ScrollThreadToBottom => match &this.active_view { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| { thread.scroll_to_bottom(cx); }); } ActiveView::ExternalAgentThread { .. } => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} }, }), window.subscribe(&editor, cx, { { let thread = active_thread.clone(); move |editor, event, window, cx| match event { EditorEvent::BufferEdited => { let new_summary = editor.read(cx).text(cx); thread.update(cx, |thread, cx| { thread.thread().update(cx, |thread, cx| { thread.set_summary(new_summary, cx); }); }) } EditorEvent::Blurred => { if editor.read(cx).text(cx).is_empty() { let summary = thread.read(cx).summary(cx).or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }); } } _ => {} } } }), cx.subscribe(&active_thread, |_, _, event, cx| match &event { ActiveThreadEvent::EditingMessageTokenCountChanged => { cx.notify(); } }), cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, { let editor = editor.clone(); move |_, thread, event, window, cx| match event { ThreadEvent::SummaryGenerated => { let summary = thread.read(cx).summary().or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }) } ThreadEvent::MessageAdded(_) => { cx.notify(); } _ => {} } }), ]; Self::Thread { change_title_editor: editor, thread: active_thread, message_editor: message_editor, _subscriptions: subscriptions, } } pub fn prompt_editor( context_editor: Entity, history_store: Entity, acp_history_store: Entity, language_registry: Arc, window: &mut Window, cx: &mut App, ) -> Self { let title = context_editor.read(cx).title(cx).to_string(); let editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_text(title, window, cx); editor }); // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would // cause a custom summary to be set. The presence of this custom summary would cause // summarization to not happen. let mut suppress_first_edit = true; let subscriptions = vec![ window.subscribe(&editor, cx, { { let context_editor = context_editor.clone(); move |editor, event, window, cx| match event { EditorEvent::BufferEdited => { if suppress_first_edit { suppress_first_edit = false; return; } let new_summary = editor.read(cx).text(cx); context_editor.update(cx, |context_editor, cx| { context_editor .context() .update(cx, |assistant_context, cx| { assistant_context.set_custom_summary(new_summary, cx); }) }) } EditorEvent::Blurred => { if editor.read(cx).text(cx).is_empty() { let summary = context_editor .read(cx) .context() .read(cx) .summary() .or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }); } } _ => {} } } }), window.subscribe(&context_editor.read(cx).context().clone(), cx, { let editor = editor.clone(); move |assistant_context, event, window, cx| match event { ContextEvent::SummaryGenerated => { let summary = assistant_context.read(cx).summary().or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }) } ContextEvent::PathChanged { old_path, new_path } => { history_store.update(cx, |history_store, cx| { if let Some(old_path) = old_path { history_store .replace_recently_opened_text_thread(old_path, new_path, cx); } else { history_store.push_recently_opened_entry( HistoryEntryId::Context(new_path.clone()), cx, ); } }); acp_history_store.update(cx, |history_store, cx| { if let Some(old_path) = old_path { history_store .replace_recently_opened_text_thread(old_path, new_path, cx); } else { history_store.push_recently_opened_entry( agent2::HistoryEntryId::TextThread(new_path.clone()), cx, ); } }); } _ => {} } }), ]; let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx)); buffer_search_bar.update(cx, |buffer_search_bar, cx| { buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx) }); Self::TextThread { context_editor, title_editor: editor, buffer_search_bar, _subscriptions: subscriptions, } } } pub struct AgentPanel { workspace: WeakEntity, user_store: Entity, project: Entity, fs: Arc, language_registry: Arc, thread_store: Entity, acp_history: Entity, acp_history_store: Entity, _default_model_subscription: Subscription, context_store: Entity, prompt_store: Option>, inline_assist_context_store: Entity, configuration: Option>, configuration_subscription: Option, local_timezone: UtcOffset, active_view: ActiveView, previous_view: Option, history_store: Entity, history: Entity, hovered_recent_history_item: Option, new_thread_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, assistant_navigation_menu_handle: PopoverMenuHandle, assistant_navigation_menu: Option>, width: Option, height: Option, zoomed: bool, pending_serialization: Option>>, onboarding: Entity, selected_agent: AgentType, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; let selected_agent = self.selected_agent; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( AGENT_PANEL_KEY.into(), serde_json::to_string(&SerializedAgentPanel { width, selected_agent: Some(selected_agent), })?, ) .await?; anyhow::Ok(()) })); } pub fn load( workspace: WeakEntity, prompt_builder: Arc, mut cx: AsyncWindowContext, ) -> Task>> { let prompt_store = cx.update(|_window, cx| PromptStore::global(cx)); cx.spawn(async move |cx| { let prompt_store = match prompt_store { Ok(prompt_store) => prompt_store.await.ok(), Err(_) => None, }; let tools = cx.new(|_| ToolWorkingSet::default())?; let thread_store = workspace .update(cx, |workspace, cx| { let project = workspace.project().clone(); ThreadStore::load( project, tools.clone(), prompt_store.clone(), prompt_builder.clone(), cx, ) })? .await?; let slash_commands = Arc::new(SlashCommandWorkingSet::default()); let context_store = workspace .update(cx, |workspace, cx| { let project = workspace.project().clone(); assistant_context::ContextStore::new( project, prompt_builder.clone(), slash_commands, cx, ) })? .await?; let serialized_panel = if let Some(panel) = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) }) .await .log_err() .flatten() { Some(serde_json::from_str::(&panel)?) } else { None }; let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( workspace, thread_store, context_store, prompt_store, window, cx, ) }); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { panel.selected_agent = selected_agent; panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); }); } panel })?; Ok(panel) }) } fn new( workspace: &Workspace, thread_store: Entity, context_store: Entity, prompt_store: Option>, window: &mut Window, cx: &mut Context, ) -> Self { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); let user_store = workspace.app_state().user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); let workspace = workspace.weak_handle(); let weak_self = cx.entity().downgrade(); let message_editor_context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); let thread_id = thread.read(cx).id().clone(); let history_store = cx.new(|cx| { HistoryStore::new( thread_store.clone(), context_store.clone(), [HistoryEntryId::Thread(thread_id)], cx, ) }); let message_editor = cx.new(|cx| { MessageEditor::new( fs.clone(), workspace.clone(), message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), context_store.downgrade(), Some(history_store.downgrade()), thread.clone(), window, cx, ) }); let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); cx.subscribe_in( &acp_history, window, |this, _, event, window, cx| match event { ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => { this.external_thread( Some(crate::ExternalAgent::NativeAgent), Some(thread.clone()), window, cx, ); } ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => { this.open_saved_prompt_editor(thread.path.clone(), window, cx) .detach_and_log_err(cx); } }, ) .detach(); cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); let active_thread = cx.new(|cx| { ActiveThread::new( thread.clone(), thread_store.clone(), context_store.clone(), message_editor_context_store.clone(), language_registry.clone(), workspace.clone(), window, cx, ) }); let panel_type = AgentSettings::get_global(cx).default_view; let active_view = match panel_type { DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx), DefaultView::TextThread => { let context = context_store.update(cx, |context_store, cx| context_store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap(); let context_editor = cx.new(|cx| { let mut editor = TextThreadEditor::for_context( context, fs.clone(), workspace.clone(), project.clone(), lsp_adapter_delegate, window, cx, ); editor.insert_default_prompt(window, cx); editor }); ActiveView::prompt_editor( context_editor, history_store.clone(), acp_history_store.clone(), language_registry.clone(), window, cx, ) } }; AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); let weak_panel = weak_self.clone(); window.defer(cx, move |window, cx| { let panel = weak_panel.clone(); let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { if cx.has_flag::() { menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); } else { menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); } } menu.action("View All", Box::new(OpenHistory)) .end_slot_action(DeleteRecentlyOpenThread.boxed_clone()) .fixed_width(px(320.).into()) .keep_open_on_confirm(false) .key_context("NavigationMenu") }); weak_panel .update(cx, |panel, cx| { cx.subscribe_in( &assistant_navigation_menu, window, |_, menu, _: &DismissEvent, window, cx| { menu.update(cx, |menu, _| { menu.clear_selected(); }); cx.focus_self(window); }, ) .detach(); panel.assistant_navigation_menu = Some(assistant_navigation_menu); }) .ok(); }); let _default_model_subscription = cx.subscribe( &LanguageModelRegistry::global(cx), |this, _, event: &language_model::Event, cx| match event { language_model::Event::DefaultModelChanged => match &this.active_view { ActiveView::Thread { thread, .. } => { thread .read(cx) .thread() .clone() .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); } ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} }, _ => {} }, ); let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( user_store.clone(), client, |_window, cx| { OnboardingUpsell::set_dismissed(true, cx); }, cx, ) }); Self { active_view, workspace, user_store, project: project.clone(), fs: fs.clone(), language_registry, thread_store: thread_store.clone(), _default_model_subscription, context_store, prompt_store, configuration: None, configuration_subscription: None, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), inline_assist_context_store, previous_view: None, history_store: history_store.clone(), history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)), hovered_recent_history_item: None, new_thread_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(), assistant_navigation_menu_handle: PopoverMenuHandle::default(), assistant_navigation_menu: None, width: None, height: None, zoomed: false, pending_serialization: None, onboarding, acp_history, acp_history_store, selected_agent: AgentType::default(), } } pub fn toggle_focus( workspace: &mut Workspace, _: &ToggleFocus, window: &mut Window, cx: &mut Context, ) { if workspace .panel::(cx) .is_some_and(|panel| panel.read(cx).enabled(cx)) && !DisableAiSettings::get_global(cx).disable_ai { workspace.toggle_panel_focus::(window, cx); } } pub(crate) fn local_timezone(&self) -> UtcOffset { self.local_timezone } pub(crate) fn prompt_store(&self) -> &Option> { &self.prompt_store } pub(crate) fn inline_assist_context_store(&self) -> &Entity { &self.inline_assist_context_store } pub(crate) fn thread_store(&self) -> &Entity { &self.thread_store } pub(crate) fn text_thread_store(&self) -> &Entity { &self.context_store } fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context) { match &self.active_view { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } } fn active_message_editor(&self) -> Option<&Entity> { match &self.active_view { ActiveView::Thread { message_editor, .. } => Some(message_editor), ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, } } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context) { // Preserve chat box text when using creating new thread let preserved_text = self .active_message_editor() .map(|editor| editor.read(cx).get_text(cx).trim().to_string()); let thread = self .thread_store .update(cx, |this, cx| this.create_thread(cx)); let context_store = cx.new(|_cx| { ContextStore::new( self.project.downgrade(), Some(self.thread_store.downgrade()), ) }); if let Some(other_thread_id) = action.from_thread_id.clone() { let other_thread_task = self.thread_store.update(cx, |this, cx| { this.open_thread(&other_thread_id, window, cx) }); cx.spawn({ let context_store = context_store.clone(); async move |_panel, cx| { let other_thread = other_thread_task.await?; context_store.update(cx, |this, cx| { this.add_thread(other_thread, false, cx); })?; anyhow::Ok(()) } }) .detach_and_log_err(cx); } let active_thread = cx.new(|cx| { ActiveThread::new( thread.clone(), self.thread_store.clone(), self.context_store.clone(), context_store.clone(), self.language_registry.clone(), self.workspace.clone(), window, cx, ) }); let message_editor = cx.new(|cx| { MessageEditor::new( self.fs.clone(), self.workspace.clone(), context_store.clone(), self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), Some(self.history_store.downgrade()), thread.clone(), window, cx, ) }); if let Some(text) = preserved_text { message_editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); }); } message_editor.focus_handle(cx).focus(window); let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) .log_err() .flatten(); let context_editor = cx.new(|cx| { let mut editor = TextThreadEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, window, cx, ); editor.insert_default_prompt(window, cx); editor }); self.set_active_view( ActiveView::prompt_editor( context_editor.clone(), self.history_store.clone(), self.acp_history_store.clone(), self.language_registry.clone(), window, cx, ), window, cx, ); context_editor.focus_handle(cx).focus(window); } fn external_thread( &mut self, agent_choice: Option, resume_thread: Option, window: &mut Window, cx: &mut Context, ) { let workspace = self.workspace.clone(); let project = self.project.clone(); let fs = self.fs.clone(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; #[derive(Default, Serialize, Deserialize)] struct LastUsedExternalAgent { agent: crate::ExternalAgent, } let thread_store = self.thread_store.clone(); let text_thread_store = self.context_store.clone(); let history = self.acp_history_store.clone(); cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { cx.background_spawn(async move { if let Some(serialized) = serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() { KEY_VALUE_STORE .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) .await .log_err(); } }) .detach(); agent } None => { cx.background_spawn(async move { KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) }) .await .log_err() .flatten() .and_then(|value| { serde_json::from_str::(&value).log_err() }) .unwrap_or_default() .agent } }; let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { match ext_agent { crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { if !cx.has_flag::() { return; } } crate::ExternalAgent::ClaudeCode => { if !cx.has_flag::() { return; } } } let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, resume_thread, workspace.clone(), project, thread_store.clone(), text_thread_store.clone(), window, cx, ) }); this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx); }) }) .detach_and_log_err(cx); } fn deploy_rules_library( &mut self, action: &OpenRulesLibrary, _window: &mut Window, cx: &mut Context, ) { open_rules_library( self.language_registry.clone(), Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())), Rc::new(|| { Rc::new(SlashCommandCompletionProvider::new( Arc::new(SlashCommandWorkingSet::default()), None, None, )) }), action .prompt_to_select .map(|uuid| UserPromptId(uuid).into()), cx, ) .detach_and_log_err(cx); } fn open_history(&mut self, window: &mut Window, cx: &mut Context) { if matches!(self.active_view, ActiveView::History) { if let Some(previous_view) = self.previous_view.take() { self.set_active_view(previous_view, window, cx); } } else { self.thread_store .update(cx, |thread_store, cx| thread_store.reload(cx)) .detach_and_log_err(cx); self.set_active_view(ActiveView::History, window, cx); } cx.notify(); } pub(crate) fn open_saved_prompt_editor( &mut self, path: Arc, window: &mut Window, cx: &mut Context, ) -> Task> { let context = self .context_store .update(cx, |store, cx| store.open_local_context(path, cx)); cx.spawn_in(window, async move |this, cx| { let context = context.await?; this.update_in(cx, |this, window, cx| { this.open_prompt_editor(context, window, cx); }) }) } pub(crate) fn open_prompt_editor( &mut self, context: Entity, window: &mut Window, cx: &mut Context, ) { let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx) .log_err() .flatten(); let editor = cx.new(|cx| { TextThreadEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, window, cx, ) }); self.set_active_view( ActiveView::prompt_editor( editor.clone(), self.history_store.clone(), self.acp_history_store.clone(), self.language_registry.clone(), window, cx, ), window, cx, ); } pub(crate) fn open_thread_by_id( &mut self, thread_id: &ThreadId, window: &mut Window, cx: &mut Context, ) -> Task> { let open_thread_task = self .thread_store .update(cx, |this, cx| this.open_thread(thread_id, window, cx)); cx.spawn_in(window, async move |this, cx| { let thread = open_thread_task.await?; this.update_in(cx, |this, window, cx| { this.open_thread(thread, window, cx); anyhow::Ok(()) })??; Ok(()) }) } pub(crate) fn open_thread( &mut self, thread: Entity, window: &mut Window, cx: &mut Context, ) { let context_store = cx.new(|_cx| { ContextStore::new( self.project.downgrade(), Some(self.thread_store.downgrade()), ) }); let active_thread = cx.new(|cx| { ActiveThread::new( thread.clone(), self.thread_store.clone(), self.context_store.clone(), context_store.clone(), self.language_registry.clone(), self.workspace.clone(), window, cx, ) }); let message_editor = cx.new(|cx| { MessageEditor::new( self.fs.clone(), self.workspace.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), Some(self.history_store.downgrade()), thread.clone(), window, cx, ) }); message_editor.focus_handle(cx).focus(window); let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context) { match self.active_view { ActiveView::Configuration | ActiveView::History => { if let Some(previous_view) = self.previous_view.take() { self.active_view = previous_view; match &self.active_view { ActiveView::Thread { message_editor, .. } => { message_editor.focus_handle(cx).focus(window); } ActiveView::ExternalAgentThread { thread_view } => { thread_view.focus_handle(cx).focus(window); } ActiveView::TextThread { context_editor, .. } => { context_editor.focus_handle(cx).focus(window); } ActiveView::History | ActiveView::Configuration => {} } } cx.notify(); } _ => {} } } pub fn toggle_navigation_menu( &mut self, _: &ToggleNavigationMenu, window: &mut Window, cx: &mut Context, ) { self.assistant_navigation_menu_handle.toggle(window, cx); } pub fn toggle_options_menu( &mut self, _: &ToggleOptionsMenu, window: &mut Window, cx: &mut Context, ) { self.agent_panel_menu_handle.toggle(window, cx); } pub fn toggle_new_thread_menu( &mut self, _: &ToggleNewThreadMenu, window: &mut Window, cx: &mut Context, ) { self.new_thread_menu_handle.toggle(window, cx); } pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, _: &mut Window, cx: &mut Context, ) { self.handle_font_size_action(action.persist, px(1.0), cx); } pub fn decrease_font_size( &mut self, action: &DecreaseBufferFontSize, _: &mut Window, cx: &mut Context, ) { self.handle_font_size_action(action.persist, px(-1.0), cx); } fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context) { match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { if persist { update_settings_file::( self.fs.clone(), cx, move |settings, cx| { let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta; let _ = settings .agent_font_size .insert(Some(theme::clamp_font_size(agent_font_size).into())); }, ); } else { theme::adjust_agent_font_size(cx, |size| size + delta); } } WhichFontSize::BufferFont => { // Prompt editor uses the buffer font size, so allow the action to propagate to the // default handler that changes that font size. cx.propagate(); } WhichFontSize::None => {} } } pub fn reset_font_size( &mut self, action: &ResetBufferFontSize, _: &mut Window, cx: &mut Context, ) { if action.persist { update_settings_file::(self.fs.clone(), cx, move |settings, _| { settings.agent_font_size = None; }); } else { theme::reset_agent_font_size(cx); } } pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context) { if self.zoomed { cx.emit(PanelEvent::ZoomOut); } else { if !self.focus_handle(cx).contains_focused(window, cx) { cx.focus_self(window); } cx.emit(PanelEvent::ZoomIn); } } pub fn open_agent_diff( &mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context, ) { match &self.active_view { ActiveView::Thread { thread, .. } => { let thread = thread.read(cx).thread().clone(); self.workspace .update(cx, |workspace, cx| { AgentDiffPane::deploy_in_workspace( AgentDiffThread::Native(thread), workspace, window, cx, ) }) .log_err(); } ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } } pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context) { let context_server_store = self.project.read(cx).context_server_store(); let tools = self.thread_store.read(cx).tools(); let fs = self.fs.clone(); self.set_active_view(ActiveView::Configuration, window, cx); self.configuration = Some(cx.new(|cx| { AgentConfiguration::new( fs, context_server_store, tools, self.language_registry.clone(), self.workspace.clone(), window, cx, ) })); if let Some(configuration) = self.configuration.as_ref() { self.configuration_subscription = Some(cx.subscribe_in( configuration, window, Self::handle_agent_configuration_event, )); configuration.focus_handle(cx).focus(window); } } pub(crate) fn open_active_thread_as_markdown( &mut self, _: &OpenActiveThreadAsMarkdown, window: &mut Window, cx: &mut Context, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; match &self.active_view { ActiveView::Thread { thread, .. } => { active_thread::open_active_thread_as_markdown( thread.read(cx).thread().clone(), workspace, window, cx, ) .detach_and_log_err(cx); } ActiveView::ExternalAgentThread { thread_view } => { thread_view .update(cx, |thread_view, cx| { thread_view.open_thread_as_markdown(workspace, window, cx) }) .detach_and_log_err(cx); } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } } fn handle_agent_configuration_event( &mut self, _entity: &Entity, event: &AssistantConfigurationEvent, window: &mut Window, cx: &mut Context, ) { match event { AssistantConfigurationEvent::NewThread(provider) => { if LanguageModelRegistry::read_global(cx) .default_model() .is_none_or(|model| model.provider.id() != provider.id()) && let Some(model) = provider.default_model(cx) { update_settings_file::( self.fs.clone(), cx, move |settings, _| settings.set_model(model), ); } self.new_thread(&NewThread::default(), window, cx); if let Some((thread, model)) = self.active_thread(cx).zip(provider.default_model(cx)) { thread.update(cx, |thread, cx| { thread.set_configured_model( Some(ConfiguredModel { provider: provider.clone(), model, }), cx, ); }); } } } } pub(crate) fn active_thread(&self, cx: &App) -> Option> { match &self.active_view { ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), _ => None, } } pub(crate) fn delete_thread( &mut self, thread_id: &ThreadId, cx: &mut Context, ) -> Task> { self.thread_store .update(cx, |this, cx| this.delete_thread(thread_id, cx)) } fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context) { let ActiveView::Thread { thread, .. } = &self.active_view else { return; }; let thread_state = thread.read(cx).thread().read(cx); if !thread_state.tool_use_limit_reached() { return; } let model = thread_state.configured_model().map(|cm| cm.model.clone()); if let Some(model) = model { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, cx| { thread.insert_invisible_continue_message(cx); thread.advance_prompt_id(); thread.send_to_model( model, CompletionIntent::UserPrompt, Some(window.window_handle()), cx, ); }); }); } else { log::warn!("No configured model available for continuation"); } } fn toggle_burn_mode( &mut self, _: &ToggleBurnMode, _window: &mut Window, cx: &mut Context, ) { let ActiveView::Thread { thread, .. } = &self.active_view else { return; }; thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, _cx| { let current_mode = thread.completion_mode(); thread.set_completion_mode(match current_mode { CompletionMode::Burn => CompletionMode::Normal, CompletionMode::Normal => CompletionMode::Burn, }); }); }); } pub(crate) fn active_context_editor(&self) -> Option> { match &self.active_view { ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()), _ => None, } } pub(crate) fn delete_context( &mut self, path: Arc, cx: &mut Context, ) -> Task> { self.context_store .update(cx, |this, cx| this.delete_local_context(path, cx)) } fn set_active_view( &mut self, new_view: ActiveView, window: &mut Window, cx: &mut Context, ) { let current_is_history = matches!(self.active_view, ActiveView::History); let new_is_history = matches!(new_view, ActiveView::History); let current_is_config = matches!(self.active_view, ActiveView::Configuration); let new_is_config = matches!(new_view, ActiveView::Configuration); let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; match &self.active_view { ActiveView::Thread { thread, .. } => { let thread = thread.read(cx); if thread.is_empty() { let id = thread.thread().read(cx).id().clone(); self.history_store.update(cx, |store, cx| { store.remove_recently_opened_thread(id, cx); }); } } _ => {} } match &new_view { ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| { let id = thread.read(cx).thread().read(cx).id().clone(); store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx); }), ActiveView::TextThread { context_editor, .. } => { self.history_store.update(cx, |store, cx| { if let Some(path) = context_editor.read(cx).context().read(cx).path() { store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx) } }) } ActiveView::ExternalAgentThread { .. } => {} ActiveView::History | ActiveView::Configuration => {} } if current_is_special && !new_is_special { self.active_view = new_view; } else if !current_is_special && new_is_special { self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view)); } else { if !new_is_special { self.previous_view = None; } self.active_view = new_view; } self.focus_handle(cx).focus(window); } fn populate_recently_opened_menu_section_old( mut menu: ContextMenu, panel: Entity, cx: &mut Context, ) -> ContextMenu { let entries = panel .read(cx) .history_store .read(cx) .recently_opened_entries(cx); if entries.is_empty() { return menu; } menu = menu.header("Recently Opened"); for entry in entries { let title = entry.title().clone(); let id = entry.id(); menu = menu.entry_with_end_slot_on_hover( title, None, { let panel = panel.downgrade(); let id = id.clone(); move |window, cx| { let id = id.clone(); panel .update(cx, move |this, cx| match id { HistoryEntryId::Thread(id) => this .open_thread_by_id(&id, window, cx) .detach_and_log_err(cx), HistoryEntryId::Context(path) => this .open_saved_prompt_editor(path.clone(), window, cx) .detach_and_log_err(cx), }) .ok(); } }, IconName::Close, "Close Entry".into(), { let panel = panel.downgrade(); let id = id.clone(); move |_window, cx| { panel .update(cx, |this, cx| { this.history_store.update(cx, |history_store, cx| { history_store.remove_recently_opened_entry(&id, cx); }); }) .ok(); } }, ); } menu = menu.separator(); menu } fn populate_recently_opened_menu_section_new( mut menu: ContextMenu, panel: Entity, cx: &mut Context, ) -> ContextMenu { let entries = panel .read(cx) .acp_history_store .read(cx) .recently_opened_entries(cx); if entries.is_empty() { return menu; } menu = menu.header("Recently Opened"); for entry in entries { let title = entry.title().clone(); menu = menu.entry_with_end_slot_on_hover( title, None, { let panel = panel.downgrade(); let entry = entry.clone(); move |window, cx| { let entry = entry.clone(); panel .update(cx, move |this, cx| match &entry { agent2::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), window, cx, ), agent2::HistoryEntry::TextThread(entry) => this .open_saved_prompt_editor(entry.path.clone(), window, cx) .detach_and_log_err(cx), }) .ok(); } }, IconName::Close, "Close Entry".into(), { let panel = panel.downgrade(); let id = entry.id(); move |_window, cx| { panel .update(cx, |this, cx| { this.acp_history_store.update(cx, |history_store, cx| { history_store.remove_recently_opened_entry(&id, cx); }); }) .ok(); } }, ); } menu = menu.separator(); menu } pub fn set_selected_agent( &mut self, agent: AgentType, window: &mut Window, cx: &mut Context, ) { if self.selected_agent != agent { self.selected_agent = agent; self.serialize(cx); } self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { self.selected_agent } pub fn new_agent_thread( &mut self, agent: AgentType, window: &mut Window, cx: &mut Context, ) { match agent { AgentType::Zed => { window.dispatch_action( NewThread { from_thread_id: None, } .boxed_clone(), cx, ); } AgentType::TextThread => { window.dispatch_action(NewTextThread.boxed_clone(), cx); } AgentType::NativeAgent => { self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) } AgentType::Gemini => { self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) } AgentType::ClaudeCode => { self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) } } } } impl Focusable for AgentPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.active_view { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), ActiveView::History => { if cx.has_flag::() { self.acp_history.focus_handle(cx) } else { self.history.focus_handle(cx) } } ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) } else { cx.focus_handle() } } } } } fn agent_panel_dock_position(cx: &App) -> DockPosition { match AgentSettings::get_global(cx).dock { AgentDockPosition::Left => DockPosition::Left, AgentDockPosition::Bottom => DockPosition::Bottom, AgentDockPosition::Right => DockPosition::Right, } } impl EventEmitter for AgentPanel {} impl Panel for AgentPanel { fn persistent_name() -> &'static str { "AgentPanel" } fn position(&self, _window: &Window, cx: &App) -> DockPosition { agent_panel_dock_position(cx) } fn position_is_valid(&self, position: DockPosition) -> bool { position != DockPosition::Bottom } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::(self.fs.clone(), cx, move |settings, _| { let dock = match position { DockPosition::Left => AgentDockPosition::Left, DockPosition::Bottom => AgentDockPosition::Bottom, DockPosition::Right => AgentDockPosition::Right, }; settings.set_dock(dock); }); } fn size(&self, window: &Window, cx: &App) -> Pixels { let settings = AgentSettings::get_global(cx); match self.position(window, cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or(settings.default_width) } DockPosition::Bottom => self.height.unwrap_or(settings.default_height), } } fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { match self.position(window, cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, } self.serialize(cx); cx.notify(); } fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context) {} fn remote_id() -> Option { Some(proto::PanelId::AssistantPanel) } fn icon(&self, _window: &Window, cx: &App) -> Option { (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { Some("Agent Panel") } fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } fn activation_priority(&self) -> u32 { 3 } fn enabled(&self, cx: &App) -> bool { DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled } fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool { self.zoomed } fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context) { self.zoomed = zoomed; cx.notify(); } } impl AgentPanel { fn render_title_view(&self, _window: &mut Window, cx: &Context) -> AnyElement { const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…"; let content = match &self.active_view { ActiveView::Thread { thread: active_thread, change_title_editor, .. } => { let state = { let active_thread = active_thread.read(cx); if active_thread.is_empty() { &ThreadSummary::Pending } else { active_thread.summary(cx) } }; match state { ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) .truncate() .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() .into_any_element(), ThreadSummary::Ready(_) => div() .w_full() .child(change_title_editor.clone()) .into_any_element(), ThreadSummary::Error => h_flex() .w_full() .child(change_title_editor.clone()) .child( IconButton::new("retry-summary-generation", IconName::RotateCcw) .icon_size(IconSize::Small) .on_click({ let active_thread = active_thread.clone(); move |_, _window, cx| { active_thread.update(cx, |thread, cx| { thread.regenerate_summary(cx); }); } }) .tooltip(move |_window, cx| { cx.new(|_| { Tooltip::new("Failed to generate title") .meta("Click to try again") }) .into() }), ) .into_any_element(), } } ActiveView::ExternalAgentThread { thread_view } => { Label::new(thread_view.read(cx).title(cx)) .truncate() .into_any_element() } ActiveView::TextThread { title_editor, context_editor, .. } => { let summary = context_editor.read(cx).context().read(cx).summary(); match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) .truncate() .into_any_element(), ContextSummary::Content(summary) => { if summary.done { div() .w_full() .child(title_editor.clone()) .into_any_element() } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() .into_any_element() } } ContextSummary::Error => h_flex() .w_full() .child(title_editor.clone()) .child( IconButton::new("retry-summary-generation", IconName::RotateCcw) .icon_size(IconSize::Small) .on_click({ let context_editor = context_editor.clone(); move |_, _window, cx| { context_editor.update(cx, |context_editor, cx| { context_editor.regenerate_summary(cx); }); } }) .tooltip(move |_window, cx| { cx.new(|_| { Tooltip::new("Failed to generate title") .meta("Click to try again") }) .into() }), ) .into_any_element(), } } ActiveView::History => Label::new("History").truncate().into_any_element(), ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(), }; h_flex() .key_context("TitleEditor") .id("TitleEditor") .flex_grow() .w_full() .max_w_full() .overflow_x_scroll() .child(content) .into_any() } fn render_panel_options_menu( &self, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let user_store = self.user_store.read(cx); let usage = user_store.model_request_usage(); let account_url = zed_urls::account_url(cx); let focus_handle = self.focus_handle(cx); let full_screen_label = if self.is_zoomed(window, cx) { "Disable Full Screen" } else { "Enable Full Screen" }; PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) .icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Agent Menu", &ToggleOptionsMenu, &focus_handle, window, cx, ) } }, ) .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) .menu({ let focus_handle = focus_handle.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); if let Some(usage) = usage { menu = menu .header_with_link("Prompt Usage", "Manage", account_url.clone()) .custom_entry( move |_window, cx| { let used_percentage = match usage.limit { UsageLimit::Limited(limit) => { Some((usage.amount as f32 / limit as f32) * 100.) } UsageLimit::Unlimited => None, }; h_flex() .flex_1() .gap_1p5() .children(used_percentage.map(|percent| { ProgressBar::new("usage", percent, 100., cx) })) .child( Label::new(match usage.limit { UsageLimit::Limited(limit) => { format!("{} / {limit}", usage.amount) } UsageLimit::Unlimited => { format!("{} / ∞", usage.amount) } }) .size(LabelSize::Small) .color(Color::Muted), ) .into_any_element() }, move |_, cx| cx.open_url(&zed_urls::account_url(cx)), ) .separator() } menu = menu .header("MCP Servers") .action( "View Server Extensions", Box::new(zed_actions::Extensions { category_filter: Some( zed_actions::ExtensionCategoryFilter::ContextServers, ), id: None, }), ) .action("Add Custom Server…", Box::new(AddContextServer)) .separator(); menu = menu .action("Rules…", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); menu })) } }) } fn render_recent_entries_menu(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Recent Threads", &ToggleNavigationMenu, &focus_handle, window, cx, ) } }, ) .anchor(Corner::TopLeft) .with_handle(self.assistant_navigation_menu_handle.clone()) .menu({ let menu = self.assistant_navigation_menu.clone(); move |window, cx| { if let Some(menu) = menu.as_ref() { menu.update(cx, |_, cx| { cx.defer_in(window, |menu, window, cx| { menu.rebuild(window, cx); }); }) } menu.clone() } }) } fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); IconButton::new("go-back", IconName::ArrowLeft) .icon_size(IconSize::Small) .on_click(cx.listener(|this, _, window, cx| { this.go_back(&workspace::GoBack, window, cx); })) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) } }) } fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, }; let new_thread_menu = PopoverMenu::new("new_thread_menu") .trigger_with_tooltip( IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), Tooltip::text("New Thread…"), ) .anchor(Corner::TopRight) .with_handle(self.new_thread_menu_handle.clone()) .menu({ let focus_handle = focus_handle.clone(); move |window, cx| { let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu .context(focus_handle.clone()) .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); if !thread.is_empty() { let thread_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( Box::new(NewThread { from_thread_id: Some(thread_id.clone()), }), cx, ); }), ) } else { this } }) .item( ContextMenuEntry::new("New Thread") .icon(IconName::Thread) .icon_color(Color::Muted) .action(NewThread::default().boxed_clone()) .handler(move |window, cx| { window.dispatch_action( NewThread::default().boxed_clone(), cx, ); }), ) .item( ContextMenuEntry::new("New Text Thread") .icon(IconName::TextThread) .icon_color(Color::Muted) .action(NewTextThread.boxed_clone()) .handler(move |window, cx| { window.dispatch_action(NewTextThread.boxed_clone(), cx); }), ); menu })) } }); h_flex() .id("assistant-toolbar") .h(Tab::container_height(cx)) .max_w_full() .flex_none() .justify_between() .gap_2() .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border) .child( h_flex() .size_full() .pl_1() .gap_1() .child(match &self.active_view { ActiveView::History | ActiveView::Configuration => div() .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), _ => self.render_recent_entries_menu(cx).into_any_element(), }) .child(self.render_title_view(window, cx)), ) .child( h_flex() .h_full() .gap_2() .children(self.render_token_count(cx)) .child( h_flex() .h_full() .gap(DynamicSpacing::Base02.rems(cx)) .px(DynamicSpacing::Base08.rems(cx)) .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) .child(self.render_panel_options_menu(window, cx)), ), ) } fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, }; let new_thread_menu = PopoverMenu::new("new_thread_menu") .trigger_with_tooltip( IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "New…", &ToggleNewThreadMenu, &focus_handle, window, cx, ) } }, ) .anchor(Corner::TopLeft) .with_handle(self.new_thread_menu_handle.clone()) .menu({ let focus_handle = focus_handle.clone(); let workspace = self.workspace.clone(); move |window, cx| { let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu .context(focus_handle.clone()) .header("Zed Agent") .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); if !thread.is_empty() { let thread_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( Box::new(NewThread { from_thread_id: Some(thread_id.clone()), }), cx, ); }), ) } else { this } }) .item( ContextMenuEntry::new("New Thread") .icon(IconName::Thread) .icon_color(Color::Muted) .action(NewThread::default().boxed_clone()) .handler({ let workspace = workspace.clone(); move |window, cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Zed, window, cx, ); }); } }); } } }), ) .item( ContextMenuEntry::new("New Text Thread") .icon(IconName::TextThread) .icon_color(Color::Muted) .action(NewTextThread.boxed_clone()) .handler({ let workspace = workspace.clone(); move |window, cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::TextThread, window, cx, ); }); } }); } } }), ) .item( ContextMenuEntry::new("New Native Agent Thread") .icon(IconName::ZedAssistant) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); move |window, cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::NativeAgent, window, cx, ); }); } }); } } }), ) .separator() .header("External Agents") .when(cx.has_flag::(), |menu| { menu.item( ContextMenuEntry::new("New Gemini Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); move |window, cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Gemini, window, cx, ); }); } }); } } }), ) }) .when(cx.has_flag::(), |menu| { menu.item( ContextMenuEntry::new("New Claude Code Thread") .icon(IconName::AiClaude) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); move |window, cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::ClaudeCode, window, cx, ); }); } }); } } }), ) }); menu })) } }); let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") .px(DynamicSpacing::Base02.rems(cx)) .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) .tooltip(move |window, cx| { Tooltip::with_meta( selected_agent_label.clone(), None, "Selected Agent", window, cx, ) }) .into_any_element(); h_flex() .id("agent-panel-toolbar") .h(Tab::container_height(cx)) .max_w_full() .flex_none() .justify_between() .gap_2() .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border) .child( h_flex() .size_full() .gap(DynamicSpacing::Base04.rems(cx)) .pl(DynamicSpacing::Base04.rems(cx)) .child(match &self.active_view { ActiveView::History | ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() } _ => h_flex() .gap_1() .child(self.render_recent_entries_menu(cx)) .child(Divider::vertical()) .child(selected_agent) .into_any_element(), }) .child(self.render_title_view(window, cx)), ) .child( h_flex() .h_full() .gap_2() .children(self.render_token_count(cx)) .child( h_flex() .h_full() .gap(DynamicSpacing::Base02.rems(cx)) .pl(DynamicSpacing::Base04.rems(cx)) .pr(DynamicSpacing::Base06.rems(cx)) .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) .child(self.render_panel_options_menu(window, cx)), ), ) } fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { if cx.has_flag::() || cx.has_flag::() { self.render_toolbar_new(window, cx).into_any_element() } else { self.render_toolbar_old(window, cx).into_any_element() } } fn render_token_count(&self, cx: &App) -> Option { match &self.active_view { ActiveView::Thread { thread, message_editor, .. } => { let active_thread = thread.read(cx); let message_editor = message_editor.read(cx); let editor_empty = message_editor.is_editor_fully_empty(cx); if active_thread.is_empty() && editor_empty { return None; } let thread = active_thread.thread().read(cx); let is_generating = thread.is_generating(); let conversation_token_usage = thread.total_token_usage()?; let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() { let combined = thread .token_usage_up_to_message(editing_message_id) .add(unsent_tokens); (combined, unsent_tokens > 0) } else { let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0); let combined = conversation_token_usage.add(unsent_tokens); (combined, unsent_tokens > 0) }; let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count(); if total_token_usage.total == 0 { return None; } let token_color = match total_token_usage.ratio() { TokenUsageRatio::Normal if is_estimating => Color::Default, TokenUsageRatio::Normal => Color::Muted, TokenUsageRatio::Warning => Color::Warning, TokenUsageRatio::Exceeded => Color::Error, }; let token_count = h_flex() .id("token-count") .flex_shrink_0() .gap_0p5() .when(!is_generating && is_estimating, |parent| { parent .child( h_flex() .mr_1() .size_2p5() .justify_center() .rounded_full() .bg(cx.theme().colors().text.opacity(0.1)) .child( div().size_1().rounded_full().bg(cx.theme().colors().text), ), ) .tooltip(move |window, cx| { Tooltip::with_meta( "Estimated New Token Count", None, format!( "Current Conversation Tokens: {}", humanize_token_count(conversation_token_usage.total) ), window, cx, ) }) }) .child( Label::new(humanize_token_count(total_token_usage.total)) .size(LabelSize::Small) .color(token_color) .map(|label| { if is_generating || is_waiting_to_update_token_count { label .with_animation( "used-tokens-label", Animation::new(Duration::from_secs(2)) .repeat() .with_easing(pulsating_between(0.6, 1.)), |label, delta| label.alpha(delta), ) .into_any() } else { label.into_any_element() } }), ) .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) .child( Label::new(humanize_token_count(total_token_usage.max)) .size(LabelSize::Small) .color(Color::Muted), ) .into_any(); Some(token_count) } ActiveView::TextThread { context_editor, .. } => { let element = render_remaining_tokens(context_editor, cx)?; Some(element.into_any_element()) } ActiveView::ExternalAgentThread { .. } | ActiveView::History | ActiveView::Configuration => None, } } fn should_render_trial_end_upsell(&self, cx: &mut Context) -> bool { if TrialEndUpsell::dismissed() { return false; } match &self.active_view { ActiveView::Thread { thread, .. } => { if thread .read(cx) .thread() .read(cx) .configured_model() .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { return false; } } ActiveView::TextThread { .. } => { if LanguageModelRegistry::global(cx) .read(cx) .default_model() .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { return false; } } ActiveView::ExternalAgentThread { .. } | ActiveView::History | ActiveView::Configuration => return false, } let plan = self.user_store.read(cx).plan(); let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); matches!(plan, Some(Plan::ZedFree)) && has_previous_trial } fn should_render_onboarding(&self, cx: &mut Context) -> bool { if OnboardingUpsell::dismissed() { return false; } match &self.active_view { ActiveView::History | ActiveView::Configuration => false, ActiveView::ExternalAgentThread { thread_view, .. } if thread_view.read(cx).as_native_thread(cx).is_none() => { false } _ => { let history_is_empty = self .history_store .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx) .providers() .iter() .any(|provider| { provider.is_authenticated(cx) && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }); history_is_empty || !has_configured_non_zed_providers } } } fn render_onboarding( &self, _window: &mut Window, cx: &mut Context, ) -> Option { if !self.should_render_onboarding(cx) { return None; } let thread_view = matches!(&self.active_view, ActiveView::Thread { .. }); let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. }); Some( div() .when(thread_view, |this| { this.size_full().bg(cx.theme().colors().panel_background) }) .when(text_thread_view, |this| { this.bg(cx.theme().colors().editor_background) }) .child(self.onboarding.clone()), ) } fn render_backdrop(&self, cx: &mut Context) -> impl IntoElement { div() .size_full() .absolute() .inset_0() .bg(cx.theme().colors().panel_background) .opacity(0.8) .block_mouse_except_scroll() } fn render_trial_end_upsell( &self, _window: &mut Window, cx: &mut Context, ) -> Option { if !self.should_render_trial_end_upsell(cx) { return None; } Some( v_flex() .absolute() .inset_0() .size_full() .bg(cx.theme().colors().panel_background) .opacity(0.85) .block_mouse_except_scroll() .child(EndTrialUpsell::new(Arc::new({ let this = cx.entity(); move |_, cx| { this.update(cx, |_this, cx| { TrialEndUpsell::set_dismissed(true, cx); cx.notify(); }); } }))), ) } fn render_empty_state_section_header( &self, label: impl Into, action_slot: Option, cx: &mut Context, ) -> impl IntoElement { div().pl_1().pr_1p5().child( h_flex() .mt_2() .pl_1p5() .pb_1() .w_full() .justify_between() .border_b_1() .border_color(cx.theme().colors().border_variant) .child( Label::new(label.into()) .size(LabelSize::Small) .color(Color::Muted), ) .children(action_slot), ) } fn render_thread_empty_state( &self, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let recent_history = self .history_store .update(cx, |this, cx| this.recent_entries(6, cx)); let model_registry = LanguageModelRegistry::read_global(cx); let configuration_error = model_registry.configuration_error(model_registry.default_model(), cx); let no_error = configuration_error.is_none(); let focus_handle = self.focus_handle(cx); v_flex() .size_full() .bg(cx.theme().colors().panel_background) .when(recent_history.is_empty(), |this| { this.child( v_flex() .size_full() .mx_auto() .justify_center() .items_center() .gap_1() .child(h_flex().child(Headline::new("Welcome to the Agent Panel"))) .when(no_error, |parent| { parent .child(h_flex().child( Label::new("Ask and build anything.").color(Color::Muted), )) .child( v_flex() .mt_2() .gap_1() .max_w_48() .child( Button::new("context", "Add Context") .label_size(LabelSize::Small) .icon(IconName::FileCode) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &ToggleContextPicker, &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action( ToggleContextPicker.boxed_clone(), cx, ) }), ) .child( Button::new("mode", "Switch Model") .label_size(LabelSize::Small) .icon(IconName::DatabaseZap) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &ToggleModelSelector, &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action( ToggleModelSelector.boxed_clone(), cx, ) }), ) .child( Button::new("settings", "View Settings") .label_size(LabelSize::Small) .icon(IconName::Settings) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &OpenSettings, &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action( OpenSettings.boxed_clone(), cx, ) }), ), ) }), ) }) .when(!recent_history.is_empty(), |parent| { parent .overflow_hidden() .justify_end() .gap_1() .child( self.render_empty_state_section_header( "Recent", Some( Button::new("view-history", "View All") .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in( &OpenHistory, &self.focus_handle(cx), window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); }) .into_any_element(), ), cx, ), ) .child( v_flex().p_1().pr_1p5().gap_1().children( recent_history .into_iter() .enumerate() .map(|(index, entry)| { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) .hovered(is_hovered) .on_hover(cx.listener( move |this, is_hovered, _window, cx| { if *is_hovered { this.hovered_recent_history_item = Some(index); } else if this.hovered_recent_history_item == Some(index) { this.hovered_recent_history_item = None; } cx.notify(); }, )) .into_any_element() }), ), ) }) .when_some(configuration_error.as_ref(), |this, err| { this.child(self.render_configuration_error(false, err, &focus_handle, window, cx)) }) } fn render_configuration_error( &self, border_bottom: bool, configuration_error: &ConfigurationError, focus_handle: &FocusHandle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { let zed_provider_configured = AgentSettings::get_global(cx) .default_model .as_ref() .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev"); let callout = if zed_provider_configured { Callout::new() .icon(IconName::Warning) .severity(Severity::Warning) .when(border_bottom, |this| { this.border_position(ui::BorderPosition::Bottom) }) .title("Sign in to continue using Zed as your LLM provider.") .actions_slot( Button::new("sign_in", "Sign In") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .on_click({ let workspace = self.workspace.clone(); move |_, _, cx| { let Ok(client) = workspace.update(cx, |workspace, _| workspace.client().clone()) else { return; }; cx.spawn(async move |cx| { client.sign_in_with_optional_connect(true, cx).await }) .detach_and_log_err(cx); } }), ) } else { Callout::new() .icon(IconName::Warning) .severity(Severity::Warning) .when(border_bottom, |this| { this.border_position(ui::BorderPosition::Bottom) }) .title(configuration_error.to_string()) .actions_slot( Button::new("settings", "Configure") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { window.dispatch_action(OpenSettings.boxed_clone(), cx) }), ) }; match configuration_error { ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::NoProvider => callout.into_any_element(), ConfigurationError::ProviderPendingTermsAcceptance(provider) => { Banner::new() .severity(Severity::Warning) .child(h_flex().w_full().children( provider.render_accept_terms( LanguageModelProviderTosView::ThreadEmptyState, cx, ), )) .into_any_element() } } } fn render_tool_use_limit_reached( &self, window: &mut Window, cx: &mut Context, ) -> Option { let active_thread = match &self.active_view { ActiveView::Thread { thread, .. } => thread, ActiveView::ExternalAgentThread { .. } => { return None; } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { return None; } }; let thread = active_thread.read(cx).thread().read(cx); let tool_use_limit_reached = thread.tool_use_limit_reached(); if !tool_use_limit_reached { return None; } let model = thread.configured_model()?.model; let focus_handle = self.focus_handle(cx); let banner = Banner::new() .severity(Severity::Info) .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) .action_slot( h_flex() .gap_1() .child( Button::new("continue-conversation", "Continue") .layer(ElevationIndex::ModalSurface) .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in( &ContinueThread, &focus_handle, window, cx, ) .map(|kb| kb.size(rems_from_px(10.))), ) .on_click(cx.listener(|this, _, window, cx| { this.continue_conversation(window, cx); })), ) .when(model.supports_burn_mode(), |this| { this.child( Button::new("continue-burn-mode", "Continue with Burn Mode") .style(ButtonStyle::Filled) .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .layer(ElevationIndex::ModalSurface) .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in( &ContinueWithBurnMode, &focus_handle, window, cx, ) .map(|kb| kb.size(rems_from_px(10.))), ) .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) .on_click({ let active_thread = active_thread.clone(); cx.listener(move |this, _, window, cx| { active_thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, _cx| { thread.set_completion_mode(CompletionMode::Burn); }); }); this.continue_conversation(window, cx); }) }), ) }), ); Some(div().px_2().pb_2().child(banner).into_any_element()) } fn create_copy_button(&self, message: impl Into) -> impl IntoElement { let message = message.into(); IconButton::new("copy", IconName::Copy) .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip(Tooltip::text("Copy Error Message")) .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) }) } fn dismiss_error_button( &self, thread: &Entity, cx: &mut Context, ) -> impl IntoElement { IconButton::new("dismiss", IconName::Close) .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip(Tooltip::text("Dismiss Error")) .on_click(cx.listener({ let thread = thread.clone(); move |_, _, _, cx| { thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.notify(); } })) } fn upgrade_button( &self, thread: &Entity, cx: &mut Context, ) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click(cx.listener({ let thread = thread.clone(); move |_, _, _, cx| { thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); cx.notify(); } })) } fn render_payment_required_error( &self, thread: &Entity, cx: &mut Context, ) -> AnyElement { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; Callout::new() .severity(Severity::Error) .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( h_flex() .gap_0p5() .child(self.upgrade_button(thread, cx)) .child(self.create_copy_button(ERROR_MESSAGE)), ) .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } fn render_model_request_limit_reached_error( &self, plan: Plan, thread: &Entity, cx: &mut Context, ) -> AnyElement { let error_message = match plan { Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", }; Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") .description(error_message) .actions_slot( h_flex() .gap_0p5() .child(self.upgrade_button(thread, cx)) .child(self.create_copy_button(error_message)), ) .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } fn render_retry_button(&self, thread: &Entity) -> AnyElement { Button::new("retry", "Retry") .icon(IconName::RotateCw) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .label_size(LabelSize::Small) .on_click({ let thread = thread.clone(); move |_, window, cx| { thread.update(cx, |thread, cx| { thread.clear_last_error(); thread.thread().update(cx, |thread, cx| { thread.retry_last_completion(Some(window.window_handle()), cx); }); }); } }) .into_any_element() } fn render_error_message( &self, header: SharedString, message: SharedString, thread: &Entity, cx: &mut Context, ) -> AnyElement { let message_with_header = format!("{}\n{}", header, message); Callout::new() .severity(Severity::Error) .icon(IconName::XCircle) .title(header) .description(message.clone()) .actions_slot( h_flex() .gap_0p5() .child(self.render_retry_button(thread)) .child(self.create_copy_button(message_with_header)), ) .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } fn render_retryable_error( &self, message: SharedString, can_enable_burn_mode: bool, thread: &Entity, ) -> AnyElement { Callout::new() .severity(Severity::Error) .title("Error") .description(message.clone()) .actions_slot( h_flex() .gap_0p5() .when(can_enable_burn_mode, |this| { this.child( Button::new("enable_burn_retry", "Enable Burn Mode and Retry") .icon(IconName::ZedBurnMode) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .label_size(LabelSize::Small) .on_click({ let thread = thread.clone(); move |_, window, cx| { thread.update(cx, |thread, cx| { thread.clear_last_error(); thread.thread().update(cx, |thread, cx| { thread.enable_burn_mode_and_retry( Some(window.window_handle()), cx, ); }); }); } }), ) }) .child(self.render_retry_button(thread)), ) .into_any_element() } fn render_prompt_editor( &self, context_editor: &Entity, buffer_search_bar: &Entity, window: &mut Window, cx: &mut Context, ) -> Div { let mut registrar = buffer_search::DivRegistrar::new( |this, _, _cx| match &this.active_view { ActiveView::TextThread { buffer_search_bar, .. } => Some(buffer_search_bar.clone()), _ => None, }, cx, ); BufferSearchBar::register(&mut registrar); registrar .into_div() .size_full() .relative() .map(|parent| { buffer_search_bar.update(cx, |buffer_search_bar, cx| { if buffer_search_bar.is_dismissed() { return parent; } parent.child( div() .p(DynamicSpacing::Base08.rems(cx)) .border_b_1() .border_color(cx.theme().colors().border_variant) .bg(cx.theme().colors().editor_background) .child(buffer_search_bar.render(window, cx)), ) }) }) .child(context_editor.clone()) .child(self.render_drag_target(cx)) } fn render_drag_target(&self, cx: &Context) -> Div { let is_local = self.project.read(cx).is_local(); div() .invisible() .absolute() .top_0() .right_0() .bottom_0() .left_0() .bg(cx.theme().colors().drop_target_background) .drag_over::(|this, _, _, _| this.visible()) .drag_over::(|this, _, _, _| this.visible()) .when(is_local, |this| { this.drag_over::(|this, _, _, _| this.visible()) }) .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| { let item = tab.pane.read(cx).item_for_index(tab.ix); let project_paths = item .and_then(|item| item.project_path(cx)) .into_iter() .collect::>(); this.handle_drop(project_paths, vec![], window, cx); })) .on_drop( cx.listener(move |this, selection: &DraggedSelection, window, cx| { let project_paths = selection .items() .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx)) .collect::>(); this.handle_drop(project_paths, vec![], window, cx); }), ) .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| { let tasks = paths .paths() .into_iter() .map(|path| { Workspace::project_path_for_path(this.project.clone(), path, false, cx) }) .collect::>(); cx.spawn_in(window, async move |this, cx| { let mut paths = vec![]; let mut added_worktrees = vec![]; let opened_paths = futures::future::join_all(tasks).await; for entry in opened_paths { if let Some((worktree, project_path)) = entry.log_err() { added_worktrees.push(worktree); paths.push(project_path); } } this.update_in(cx, |this, window, cx| { this.handle_drop(paths, added_worktrees, window, cx); }) .ok(); }) .detach(); })) } fn handle_drop( &mut self, paths: Vec, added_worktrees: Vec>, window: &mut Window, cx: &mut Context, ) { match &self.active_view { ActiveView::Thread { thread, .. } => { let context_store = thread.read(cx).context_store().clone(); context_store.update(cx, move |context_store, cx| { let mut tasks = Vec::new(); for project_path in &paths { tasks.push(context_store.add_file_from_path( project_path.clone(), false, cx, )); } cx.background_spawn(async move { futures::future::join_all(tasks).await; // Need to hold onto the worktrees until they have already been used when // opening the buffers. drop(added_worktrees); }) .detach(); }); } ActiveView::ExternalAgentThread { thread_view } => { thread_view.update(cx, |thread_view, cx| { thread_view.insert_dragged_files(paths, added_worktrees, window, cx); }); } ActiveView::TextThread { context_editor, .. } => { context_editor.update(cx, |context_editor, cx| { TextThreadEditor::insert_dragged_files( context_editor, paths, added_worktrees, window, cx, ); }); } ActiveView::History | ActiveView::Configuration => {} } } fn key_context(&self) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("AgentPanel"); match &self.active_view { ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"), ActiveView::TextThread { .. } => key_context.add("prompt_editor"), ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {} } key_context } } impl Render for AgentPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { // WARNING: Changes to this element hierarchy can have // non-obvious implications to the layout of children. // // If you need to change it, please confirm: // - The message editor expands (cmd-option-esc) correctly // - When expanded, the buttons at the bottom of the panel are displayed correctly // - Font size works as expected and can be changed with cmd-+/cmd- // - Scrolling in all views works as expected // - Files can be dropped into the panel let content = v_flex() .relative() .size_full() .justify_between() .key_context(self.key_context()) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); })) .on_action(cx.listener(|this, _: &OpenHistory, window, cx| { this.open_history(window, cx); })) .on_action(cx.listener(|this, _: &OpenSettings, window, cx| { this.open_configuration(window, cx); })) .on_action(cx.listener(Self::open_active_thread_as_markdown)) .on_action(cx.listener(Self::deploy_rules_library)) .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::go_back)) .on_action(cx.listener(Self::toggle_navigation_menu)) .on_action(cx.listener(Self::toggle_options_menu)) .on_action(cx.listener(Self::increase_font_size)) .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) .on_action(cx.listener(|this, _: &ContinueThread, window, cx| { this.continue_conversation(window, cx); })) .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| { match &this.active_view { ActiveView::Thread { thread, .. } => { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, _cx| { thread.set_completion_mode(CompletionMode::Burn); }); }); this.continue_conversation(window, cx); } ActiveView::ExternalAgentThread { .. } => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } })) .on_action(cx.listener(Self::toggle_burn_mode)) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { thread, message_editor, .. } => parent .child( if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) { self.render_thread_empty_state(window, cx) .into_any_element() } else { thread.clone().into_any_element() }, ) .children(self.render_tool_use_limit_reached(window, cx)) .when_some(thread.read(cx).last_error(), |this, last_error| { this.child( div() .child(match last_error { ThreadError::PaymentRequired => { self.render_payment_required_error(thread, cx) } ThreadError::ModelRequestLimitReached { plan } => self .render_model_request_limit_reached_error(plan, thread, cx), ThreadError::Message { header, message } => { self.render_error_message(header, message, thread, cx) } ThreadError::RetryableError { message, can_enable_burn_mode, } => self.render_retryable_error( message, can_enable_burn_mode, thread, ), }) .into_any(), ) }) .child(h_flex().relative().child(message_editor.clone()).when( !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx), |this| this.child(self.render_backdrop(cx)), )) .child(self.render_drag_target(cx)), ActiveView::ExternalAgentThread { thread_view, .. } => parent .child(thread_view.clone()) .child(self.render_drag_target(cx)), ActiveView::History => { if cx.has_flag::() { parent.child(self.acp_history.clone()) } else { parent.child(self.history.clone()) } } ActiveView::TextThread { context_editor, buffer_search_bar, .. } => { let model_registry = LanguageModelRegistry::read_global(cx); let configuration_error = model_registry.configuration_error(model_registry.default_model(), cx); parent .map(|this| { if !self.should_render_onboarding(cx) && let Some(err) = configuration_error.as_ref() { this.child(self.render_configuration_error( true, err, &self.focus_handle(cx), window, cx, )) } else { this } }) .child(self.render_prompt_editor( context_editor, buffer_search_bar, window, cx, )) } ActiveView::Configuration => parent.children(self.configuration.clone()), }) .children(self.render_trial_end_upsell(window, cx)); match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx)) .size_full() .child(content) .into_any() } _ => content.into_any(), } } } struct PromptLibraryInlineAssist { workspace: WeakEntity, } impl PromptLibraryInlineAssist { pub fn new(workspace: WeakEntity) -> Self { Self { workspace } } } impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { fn assist( &self, prompt_editor: &Entity, initial_prompt: Option, window: &mut Window, cx: &mut Context, ) { InlineAssistant::update_global(cx, |assistant, cx| { let Some(project) = self .workspace .upgrade() .map(|workspace| workspace.read(cx).project().downgrade()) else { return; }; let prompt_store = None; let thread_store = None; let text_thread_store = None; let context_store = cx.new(|_| ContextStore::new(project.clone(), None)); assistant.assist( prompt_editor, self.workspace.clone(), context_store, project, prompt_store, thread_store, text_thread_store, initial_prompt, window, cx, ) }) } fn focus_agent_panel( &self, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) -> bool { workspace.focus_panel::(window, cx).is_some() } } pub struct ConcreteAssistantPanelDelegate; impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { fn active_context_editor( &self, workspace: &mut Workspace, _window: &mut Window, cx: &mut Context, ) -> Option> { let panel = workspace.panel::(cx)?; panel.read(cx).active_context_editor() } fn open_saved_context( &self, workspace: &mut Workspace, path: Arc, window: &mut Window, cx: &mut Context, ) -> Task> { let Some(panel) = workspace.panel::(cx) else { return Task::ready(Err(anyhow!("Agent panel not found"))); }; panel.update(cx, |panel, cx| { panel.open_saved_prompt_editor(path, window, cx) }) } fn open_remote_context( &self, _workspace: &mut Workspace, _context_id: assistant_context::ContextId, _window: &mut Window, _cx: &mut Context, ) -> Task>> { Task::ready(Err(anyhow!("opening remote context not implemented"))) } fn quote_selection( &self, workspace: &mut Workspace, selection_ranges: Vec>, buffer: Entity, window: &mut Window, cx: &mut Context, ) { let Some(panel) = workspace.panel::(cx) else { return; }; if !panel.focus_handle(cx).contains_focused(window, cx) { workspace.toggle_panel_focus::(window, cx); } panel.update(cx, |_, cx| { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); let selection_ranges = selection_ranges .into_iter() .flat_map(|range| { let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?; let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?; if start_buffer != end_buffer { return None; } Some((start_buffer, start..end)) }) .collect::>(); for (buffer, range) in selection_ranges { store.add_selection(buffer, range, cx); } }) }) } else if let Some(context_editor) = panel.active_context_editor() { let snapshot = buffer.read(cx).snapshot(cx); let selection_ranges = selection_ranges .into_iter() .map(|range| range.to_point(&snapshot)) .collect::>(); context_editor.update(cx, |context_editor, cx| { context_editor.quote_ranges(selection_ranges, snapshot, window, cx) }); } }); }); } } struct OnboardingUpsell; impl Dismissable for OnboardingUpsell { const KEY: &'static str = "dismissed-trial-upsell"; } struct TrialEndUpsell; impl Dismissable for TrialEndUpsell { const KEY: &'static str = "dismissed-trial-end-upsell"; }