use std::ops::Range; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; use anyhow::{Result, anyhow}; use assistant_context_editor::{ AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent, ContextSummary, SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens, }; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use assistant_context_editor::language_model_selector::ToggleModelSelector; use client::{UserStore, zed_urls}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID, }; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use proto::Plan; 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, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*, }; use util::{ResultExt as _, maybe}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace, }; use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding}; use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus}; use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; use zed_llm_client::{CompletionIntent, UsageLimit}; use crate::active_thread::{self, ActiveThread, ActiveThreadEvent}; use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}; use crate::agent_diff::AgentDiff; use crate::history_store::{HistoryStore, RecentEntry}; use crate::message_editor::{MessageEditor, MessageEditorEvent}; use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio}; use crate::thread_history::{HistoryEntryElement, ThreadHistory}; use crate::thread_store::ThreadStore; use crate::ui::AgentOnboardingModal; use crate::{ AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, }; const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: 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, _: &OpenConfiguration, 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: &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); let thread = panel.read(cx).thread.read(cx).thread().clone(); AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } }) .register_action(|workspace, _: &Follow, window, cx| { workspace.follow(CollaboratorId::Agent, window, cx); }) .register_action(|workspace, _: &ExpandMessageEditor, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { panel.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, _: &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| { Upsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); }); }, ) .detach(); } enum ActiveView { Thread { change_title_editor: Entity, thread: WeakEntity, _subscriptions: Vec, }, TextThread { context_editor: Entity, title_editor: Entity, buffer_search_bar: Entity, _subscriptions: Vec, }, History, Configuration, } enum WhichFontSize { AgentFont, BufferFont, None, } impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont, ActiveView::TextThread { .. } => WhichFontSize::BufferFont, ActiveView::Configuration => WhichFontSize::None, } } pub fn thread(thread: Entity, window: &mut Window, cx: &mut App) -> Self { let summary = thread.read(cx).summary().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![ window.subscribe(&editor, cx, { { let thread = 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.set_summary(new_summary, cx); }) } EditorEvent::Blurred => { if editor.read(cx).text(cx).is_empty() { let summary = thread.read(cx).summary().or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }); } } _ => {} } } }), window.subscribe(&thread, cx, { 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); }) } _ => {} } }), ]; Self::Thread { change_title_editor: editor, thread: thread.downgrade(), _subscriptions: subscriptions, } } pub fn prompt_editor( context_editor: 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); }) } _ => {} } }), ]; 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, thread: Entity, message_editor: Entity, _active_thread_subscriptions: Vec, _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, assistant_dropdown_menu_handle: PopoverMenuHandle, assistant_navigation_menu_handle: PopoverMenuHandle, assistant_navigation_menu: Option>, width: Option, height: Option, zoomed: bool, pending_serialization: Option>>, hide_upsell: bool, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( AGENT_PANEL_KEY.into(), serde_json::to_string(&SerializedAgentPanel { width })?, ) .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_editor::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()); 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 workspace = workspace.weak_handle(); let weak_self = cx.entity().downgrade(); let message_editor_context_store = cx.new(|_cx| { crate::context_store::ContextStore::new( project.downgrade(), Some(thread_store.downgrade()), ) }); let inline_assist_context_store = cx.new(|_cx| { crate::context_store::ContextStore::new( project.downgrade(), Some(thread_store.downgrade()), ) }); let message_editor = cx.new(|cx| { MessageEditor::new( fs.clone(), workspace.clone(), user_store.clone(), message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), context_store.downgrade(), thread.clone(), window, cx, ) }); let message_editor_subscription = cx.subscribe(&message_editor, |_, _, event, cx| match event { MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => { cx.notify(); } }); let thread_id = thread.read(cx).id().clone(); let history_store = cx.new(|cx| { HistoryStore::new( thread_store.clone(), context_store.clone(), [RecentEntry::Thread(thread_id, thread.clone())], window, cx, ) }); cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); let panel_type = AgentSettings::get_global(cx).default_view; let active_view = match panel_type { DefaultView::Thread => ActiveView::thread(thread.clone(), 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 = ContextEditor::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, language_registry.clone(), window, cx) } }; let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| { if let ThreadEvent::MessageAdded(_) = &event { // needed to leave empty state cx.notify(); } }); 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, ) }); AgentDiff::set_active_thread(&workspace, &thread, window, cx); let active_thread_subscription = cx.subscribe(&active_thread, |_, _, event, cx| match &event { ActiveThreadEvent::EditingMessageTokenCountChanged => { cx.notify(); } }); 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| { let recently_opened = panel .update(cx, |this, cx| { this.history_store.update(cx, |history_store, cx| { history_store.recently_opened_entries(cx) }) }) .unwrap_or_default(); if !recently_opened.is_empty() { menu = menu.header("Recently Opened"); for entry in recently_opened.iter() { if let RecentEntry::Context(context) = entry { if context.read(cx).path().is_none() { log::error!( "bug: text thread in recent history list was never saved" ); continue; } } let summary = entry.summary(cx); menu = menu.entry_with_end_slot_on_hover( summary, None, { let panel = panel.clone(); let entry = entry.clone(); move |window, cx| { panel .update(cx, { let entry = entry.clone(); move |this, cx| match entry { RecentEntry::Thread(_, thread) => { this.open_thread(thread, window, cx) } RecentEntry::Context(context) => { let Some(path) = context.read(cx).path() else { return; }; this.open_saved_prompt_editor( path.clone(), window, cx, ) .detach_and_log_err(cx) } } }) .ok(); } }, IconName::Close, "Close Entry".into(), { let panel = panel.clone(); let entry = entry.clone(); move |_window, cx| { panel .update(cx, |this, cx| { this.history_store.update( cx, |history_store, cx| { history_store.remove_recently_opened_entry( &entry, cx, ); }, ); }) .ok(); } }, ); } menu = menu.separator(); } 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 => { this.thread .read(cx) .thread() .clone() .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); } _ => {} }, ); Self { active_view, workspace, user_store, project: project.clone(), fs: fs.clone(), language_registry, thread_store: thread_store.clone(), thread: active_thread, message_editor, _active_thread_subscriptions: vec![ thread_subscription, active_thread_subscription, message_editor_subscription, ], _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, assistant_dropdown_menu_handle: PopoverMenuHandle::default(), assistant_navigation_menu_handle: PopoverMenuHandle::default(), assistant_navigation_menu: None, width: None, height: None, zoomed: false, pending_serialization: None, hide_upsell: false, } } 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)) { 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) { self.thread .update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context) { let thread = self .thread_store .update(cx, |this, cx| this.create_thread(cx)); let thread_view = ActiveView::thread(thread.clone(), window, cx); self.set_active_view(thread_view, window, cx); let context_store = cx.new(|_cx| { crate::context_store::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 thread_subscription = cx.subscribe(&thread, |_, _, event, cx| { if let ThreadEvent::MessageAdded(_) = &event { // needed to leave empty state cx.notify(); } }); self.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, ) }); AgentDiff::set_active_thread(&self.workspace, &thread, window, cx); let active_thread_subscription = cx.subscribe(&self.thread, |_, _, event, cx| match &event { ActiveThreadEvent::EditingMessageTokenCountChanged => { cx.notify(); } }); self.message_editor = cx.new(|cx| { MessageEditor::new( self.fs.clone(), self.workspace.clone(), self.user_store.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), thread, window, cx, ) }); self.message_editor.focus_handle(cx).focus(window); let message_editor_subscription = cx.subscribe(&self.message_editor, |_, _, event, cx| match event { MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => { cx.notify(); } }); self._active_thread_subscriptions = vec![ thread_subscription, active_thread_subscription, message_editor_subscription, ]; } 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 = ContextEditor::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.language_registry.clone(), window, cx, ), window, cx, ); context_editor.focus_handle(cx).focus(window); } 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| { ContextEditor::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.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 thread_view = ActiveView::thread(thread.clone(), window, cx); self.set_active_view(thread_view, window, cx); let context_store = cx.new(|_cx| { crate::context_store::ContextStore::new( self.project.downgrade(), Some(self.thread_store.downgrade()), ) }); let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| { if let ThreadEvent::MessageAdded(_) = &event { // needed to leave empty state cx.notify(); } }); self.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, ) }); AgentDiff::set_active_thread(&self.workspace, &thread, window, cx); let active_thread_subscription = cx.subscribe(&self.thread, |_, _, event, cx| match &event { ActiveThreadEvent::EditingMessageTokenCountChanged => { cx.notify(); } }); self.message_editor = cx.new(|cx| { MessageEditor::new( self.fs.clone(), self.workspace.clone(), self.user_store.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), thread, window, cx, ) }); self.message_editor.focus_handle(cx).focus(window); let message_editor_subscription = cx.subscribe(&self.message_editor, |_, _, event, cx| match event { MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => { cx.notify(); } }); self._active_thread_subscriptions = vec![ thread_subscription, active_thread_subscription, message_editor_subscription, ]; } 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 { .. } => { self.message_editor.focus_handle(cx).focus(window); } ActiveView::TextThread { context_editor, .. } => { context_editor.focus_handle(cx).focus(window); } _ => {} } } else { self.active_view = ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx); self.message_editor.focus_handle(cx).focus(window); } 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.assistant_dropdown_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(theme::clamp_font_size(agent_font_size).0); }, ); } 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, ) { let thread = self.thread.read(cx).thread().clone(); self.workspace .update(cx, |workspace, cx| { AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx) }) .log_err(); } 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, 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; }; let Some(thread) = self.active_thread() else { return; }; active_thread::open_active_thread_as_markdown(thread, workspace, window, cx) .detach_and_log_err(cx); } 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() .map_or(true, |model| model.provider.id() != provider.id()) { if 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); } } } pub(crate) fn active_thread(&self) -> Option> { match &self.active_view { ActiveView::Thread { thread, .. } => thread.upgrade(), _ => 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)) } pub(crate) fn has_active_thread(&self) -> bool { matches!(self.active_view, ActiveView::Thread { .. }) } fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context) { let thread_state = self.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 { self.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, ) { self.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, .. } => { if let Some(thread) = thread.upgrade() { if thread.read(cx).is_empty() { let id = thread.read(cx).id().clone(); self.history_store.update(cx, |store, cx| { store.remove_recently_opened_thread(id, cx); }); } } } ActiveView::TextThread { context_editor, .. } => { let context = context_editor.read(cx).context(); // When switching away from an unsaved text thread, delete its entry. if context.read(cx).path().is_none() { let context = context.clone(); self.history_store.update(cx, |store, cx| { store.remove_recently_opened_entry(&RecentEntry::Context(context), cx); }); } } _ => {} } match &new_view { ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| { if let Some(thread) = thread.upgrade() { let id = thread.read(cx).id().clone(); store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx); } }), ActiveView::TextThread { context_editor, .. } => { self.history_store.update(cx, |store, cx| { let context = context_editor.read(cx).context().clone(); store.push_recently_opened_entry(RecentEntry::Context(context), cx) }) } _ => {} } 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); } } impl Focusable for AgentPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.active_view { ActiveView::Thread { .. } => self.message_editor.focus_handle(cx), ActiveView::History => 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 { 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 { change_title_editor, .. } => { let active_thread = self.thread.read(cx); let state = 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( ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) .on_click({ let active_thread = self.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::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( ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) .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_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let active_thread = self.thread.read(cx); let user_store = self.user_store.read(cx); let thread = active_thread.thread().read(cx); let thread_id = thread.id().clone(); let is_empty = active_thread.is_empty(); let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx); let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| { maybe!({ let amount = user_store.model_request_usage_amount()?; let limit = user_store.model_request_usage_limit()?.variant?; Some(RequestUsage { amount: amount as i32, limit: match limit { proto::usage_limit::Variant::Limited(limited) => { zed_llm_client::UsageLimit::Limited(limited.limit as i32) } proto::usage_limit::Variant::Unlimited(_) => { zed_llm_client::UsageLimit::Unlimited } }, }) }) }); let account_url = zed_urls::account_url(cx); let show_token_count = match &self.active_view { ActiveView::Thread { .. } => !is_empty || !editor_empty, ActiveView::TextThread { .. } => true, _ => false, }; let focus_handle = self.focus_handle(cx); let go_back_button = div().child( 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, ) } }), ); let recent_entries_menu = div().child( PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( IconButton::new("agent-nav-menu", IconName::MenuAlt) .icon_size(IconSize::Small) .style(ui::ButtonStyle::Subtle), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Panel Menu", &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() } }), ); let zoom_in_label = if self.is_zoomed(window, cx) { "Zoom Out" } else { "Zoom In" }; let agent_extra_menu = 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.assistant_dropdown_menu_handle.clone()) .menu(move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| { menu = menu .action("New Thread", NewThread::default().boxed_clone()) .action("New Text Thread", NewTextThread.boxed_clone()) .when(!is_empty, |menu| { menu.action( "New From Summary", Box::new(NewThread { from_thread_id: Some(thread_id.clone()), }), ) }) .separator(); menu = menu .header("MCP Servers") .action( "View Server Extensions", Box::new(zed_actions::Extensions { category_filter: Some( zed_actions::ExtensionCategoryFilter::ContextServers, ), }), ) .action("Add Custom Server…", Box::new(AddContextServer)) .separator(); if let Some(usage) = last_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 .action("Rules…", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenConfiguration)) .action(zoom_in_label, Box::new(ToggleZoom)); 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 => go_back_button, _ => recent_entries_menu, }) .child(self.render_title_view(window, cx)), ) .child( h_flex() .h_full() .gap_2() .when(show_token_count, |parent| { parent.children(self.render_token_count(&thread, 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( IconButton::new("new", IconName::Plus) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip(move |window, cx| { Tooltip::for_action_in( "New Thread", &NewThread::default(), &focus_handle, window, cx, ) }) .on_click(move |_event, window, cx| { window.dispatch_action( NewThread::default().boxed_clone(), cx, ); }), ) .child(agent_extra_menu), ), ) } fn render_token_count(&self, thread: &Thread, cx: &App) -> Option { let is_generating = thread.is_generating(); let message_editor = self.message_editor.read(cx); let conversation_token_usage = thread.total_token_usage()?; let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) = self.thread.read(cx).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(); match &self.active_view { ActiveView::Thread { .. } => { 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()) } _ => None, } } fn should_render_trial_end_upsell(&self, cx: &mut Context) -> bool { if TrialEndUpsell::dismissed() { return false; } let plan = self.user_store.read(cx).current_plan(); let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); matches!(plan, Some(Plan::Free)) && has_previous_trial } fn should_render_upsell(&self, cx: &mut Context) -> bool { if !matches!(self.active_view, ActiveView::Thread { .. }) { return false; } if self.hide_upsell || Upsell::dismissed() { return false; } let is_using_zed_provider = self .thread .read(cx) .thread() .read(cx) .configured_model() .map_or(false, |model| { model.provider.id().0 == ZED_CLOUD_PROVIDER_ID }); if !is_using_zed_provider { return false; } let plan = self.user_store.read(cx).current_plan(); if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) { return false; } let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); if has_previous_trial { return false; } true } fn render_upsell( &self, _window: &mut Window, cx: &mut Context, ) -> Option { if !self.should_render_upsell(cx) { return None; } if self.user_store.read(cx).account_too_young() { Some(self.render_young_account_upsell(cx).into_any_element()) } else { Some(self.render_trial_upsell(cx).into_any_element()) } } fn render_young_account_upsell(&self, cx: &mut Context) -> impl IntoElement { let checkbox = CheckboxWithLabel::new( "dont-show-again", Label::new("Don't show again").color(Color::Muted), ToggleState::Unselected, move |toggle_state, _window, cx| { let toggle_state_bool = toggle_state.selected(); Upsell::set_dismissed(toggle_state_bool, cx); }, ); let contents = div() .size_full() .gap_2() .flex() .flex_col() .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) .child( Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.") .size(LabelSize::Small), ) .child( Label::new( "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.", ) .color(Color::Muted), ) .child( h_flex() .w_full() .px_neg_1() .justify_between() .items_center() .child(h_flex().items_center().gap_1().child(checkbox)) .child( h_flex() .gap_2() .child( Button::new("dismiss-button", "Not Now") .style(ButtonStyle::Transparent) .color(Color::Muted) .on_click({ let agent_panel = cx.entity(); move |_, _, cx| { agent_panel.update(cx, |this, cx| { this.hide_upsell = true; cx.notify(); }); } }), ) .child( Button::new("cta-button", "Upgrade to Zed Pro") .style(ButtonStyle::Transparent) .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), ), ), ); self.render_upsell_container(cx, contents) } fn render_trial_upsell(&self, cx: &mut Context) -> impl IntoElement { let checkbox = CheckboxWithLabel::new( "dont-show-again", Label::new("Don't show again").color(Color::Muted), ToggleState::Unselected, move |toggle_state, _window, cx| { let toggle_state_bool = toggle_state.selected(); Upsell::set_dismissed(toggle_state_bool, cx); }, ); let contents = div() .size_full() .gap_2() .flex() .flex_col() .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) .child( Label::new("Try Zed Pro for free for 14 days - no credit card required.") .size(LabelSize::Small), ) .child( Label::new( "Use your own API keys or enable usage-based billing once you hit the cap.", ) .color(Color::Muted), ) .child( h_flex() .w_full() .px_neg_1() .justify_between() .items_center() .child(h_flex().items_center().gap_1().child(checkbox)) .child( h_flex() .gap_2() .child( Button::new("dismiss-button", "Not Now") .style(ButtonStyle::Transparent) .color(Color::Muted) .on_click({ let agent_panel = cx.entity(); move |_, _, cx| { agent_panel.update(cx, |this, cx| { this.hide_upsell = true; cx.notify(); }); } }), ) .child( Button::new("cta-button", "Start Trial") .style(ButtonStyle::Transparent) .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), ), ), ); self.render_upsell_container(cx, contents) } fn render_trial_end_upsell( &self, _window: &mut Window, cx: &mut Context, ) -> Option { if !self.should_render_trial_end_upsell(cx) { return None; } Some( self.render_upsell_container( cx, div() .size_full() .gap_2() .flex() .flex_col() .child( Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small), ) .child( Label::new("You've been automatically reset to the free plan.") .size(LabelSize::Small), ) .child( h_flex() .w_full() .px_neg_1() .justify_between() .items_center() .child(div()) .child( h_flex() .gap_2() .child( Button::new("dismiss-button", "Stay on Free") .style(ButtonStyle::Transparent) .color(Color::Muted) .on_click({ let agent_panel = cx.entity(); move |_, _, cx| { agent_panel.update(cx, |_this, cx| { TrialEndUpsell::set_dismissed(true, cx); cx.notify(); }); } }), ) .child( Button::new("cta-button", "Upgrade to Zed Pro") .style(ButtonStyle::Transparent) .on_click(|_, _, cx| { cx.open_url(&zed_urls::account_url(cx)) }), ), ), ), ), ) } fn render_upsell_container(&self, cx: &mut Context, content: Div) -> Div { div().p_2().child( v_flex() .w_full() .elevation_2(cx) .rounded(px(8.)) .bg(cx.theme().colors().background.alpha(0.5)) .p(px(3.)) .child( div() .gap_2() .flex() .flex_col() .size_full() .border_1() .rounded(px(5.)) .border_color(cx.theme().colors().text.alpha(0.1)) .overflow_hidden() .relative() .bg(cx.theme().colors().panel_background) .px_4() .py_3() .child( div() .absolute() .top_0() .right(px(-1.0)) .w(px(441.)) .h(px(167.)) .child( Vector::new( VectorName::Grid, rems_from_px(441.), rems_from_px(167.), ) .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))), ), ) .child( div() .absolute() .top(px(-8.0)) .right_0() .w(px(400.)) .h(px(92.)) .child( Vector::new( VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.), ) .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))), ), ) // .child( // div() // .absolute() // .top_0() // .right(px(360.)) // .size(px(401.)) // .overflow_hidden() // .bg(cx.theme().colors().panel_background) // ) .child( div() .absolute() .top_0() .right_0() .w(px(660.)) .h(px(401.)) .overflow_hidden() .bg(linear_gradient( 75., linear_color_stop( cx.theme().colors().panel_background.alpha(0.01), 1.0, ), linear_color_stop(cx.theme().colors().panel_background, 0.45), )), ) .child(content), ), ) } fn render_active_thread_or_empty_state( &self, window: &mut Window, cx: &mut Context, ) -> AnyElement { if self.thread.read(cx).is_empty() { return self .render_thread_empty_state(window, cx) .into_any_element(); } self.thread.clone().into_any_element() } fn configuration_error(&self, cx: &App) -> Option { let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else { return Some(ConfigurationError::NoProvider); }; if !model.provider.is_authenticated(cx) { return Some(ConfigurationError::ProviderNotAuthenticated); } if model.provider.must_accept_terms(cx) { return Some(ConfigurationError::ProviderPendingTermsAcceptance( model.provider, )); } None } 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 configuration_error = self.configuration_error(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| { let configuration_error_ref = &configuration_error; this.child( v_flex() .size_full() .max_w_80() .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) .mb_2p5(), ), ) .child( Button::new("new-thread", "Start New Thread") .icon(IconName::Plus) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &NewThread::default(), &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action(NewThread::default().boxed_clone(), cx) }), ) .child( Button::new("context", "Add Context") .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") .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") .icon(IconName::Settings) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &OpenConfiguration, &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action(OpenConfiguration.boxed_clone(), cx) }), ) }) .map(|parent| { match configuration_error_ref { Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { parent .child( h_flex().child( Label::new("To start using the agent, configure at least one LLM provider.") .color(Color::Muted) .mb_2p5() ) ) .child( Button::new("settings", "Configure a Provider") .icon(IconName::Settings) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .icon_color(Color::Muted) .full_width() .key_binding(KeyBinding::for_action_in( &OpenConfiguration, &focus_handle, window, cx, )) .on_click(|_event, window, cx| { window.dispatch_action(OpenConfiguration.boxed_clone(), cx) }), ) } Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { parent.children( provider.render_accept_terms( LanguageModelProviderTosView::ThreadFreshStart, cx, ), ) } None => parent, } }) ) }) .when(!recent_history.is_empty(), |parent| { let focus_handle = focus_handle.clone(); let configuration_error_ref = &configuration_error; parent .overflow_hidden() .p_1p5() .justify_end() .gap_1() .child( h_flex() .pl_1p5() .pb_1() .w_full() .justify_between() .border_b_1() .border_color(cx.theme().colors().border_variant) .child( Label::new("Recent") .size(LabelSize::Small) .color(Color::Muted), ) .child( 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); }), ), ) .child( v_flex() .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() }), ) ) .map(|parent| { match configuration_error_ref { Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { parent .child( Banner::new() .severity(ui::Severity::Warning) .child( Label::new( "Configure at least one LLM provider to start using the panel.", ) .size(LabelSize::Small), ) .action_slot( Button::new("settings", "Configure Provider") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in( &OpenConfiguration, &focus_handle, window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { window.dispatch_action( OpenConfiguration.boxed_clone(), cx, ) }), ), ) } Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { parent .child( Banner::new() .severity(ui::Severity::Warning) .child( h_flex() .w_full() .children( provider.render_accept_terms( LanguageModelProviderTosView::ThreadtEmptyState, cx, ), ), ), ) } None => parent, } }) }) } fn render_tool_use_limit_reached( &self, window: &mut Window, cx: &mut Context, ) -> Option { let tool_use_limit_reached = self .thread .read(cx) .thread() .read(cx) .tool_use_limit_reached(); if !tool_use_limit_reached { return None; } let model = self .thread .read(cx) .thread() .read(cx) .configured_model()? .model; let focus_handle = self.focus_handle(cx); let banner = Banner::new() .severity(ui::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_max_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(cx.listener(|this, _, window, cx| { this.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 render_last_error(&self, cx: &mut Context) -> Option { let last_error = self.thread.read(cx).last_error()?; Some( div() .absolute() .right_3() .bottom_12() .max_w_96() .py_2() .px_3() .elevation_2(cx) .occlude() .child(match last_error { ThreadError::PaymentRequired => self.render_payment_required_error(cx), ThreadError::ModelRequestLimitReached { plan } => { self.render_model_request_limit_reached_error(plan, cx) } ThreadError::Message { header, message } => { self.render_error_message(header, message, cx) } }) .into_any(), ) } fn render_payment_required_error(&self, cx: &mut Context) -> AnyElement { const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used."; v_flex() .gap_0p5() .child( h_flex() .gap_1p5() .items_center() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)), ) .child( div() .id("error-message") .max_h_24() .overflow_y_scroll() .child(Label::new(ERROR_MESSAGE)), ) .child( h_flex() .justify_end() .mt_1() .gap_1() .child(self.create_copy_button(ERROR_MESSAGE)) .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }, ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.notify(); }, ))), ) .into_any() } fn render_model_request_limit_reached_error( &self, plan: Plan, cx: &mut Context, ) -> AnyElement { let error_message = match plan { Plan::ZedPro => { "Model request limit reached. Upgrade to usage-based billing for more requests." } Plan::ZedProTrial => { "Model request limit reached. Upgrade to Zed Pro for more requests." } Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.", }; let call_to_action = match plan { Plan::ZedPro => "Upgrade to usage-based billing", Plan::ZedProTrial => "Upgrade to Zed Pro", Plan::Free => "Upgrade to Zed Pro", }; v_flex() .gap_0p5() .child( h_flex() .gap_1p5() .items_center() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)), ) .child( div() .id("error-message") .max_h_24() .overflow_y_scroll() .child(Label::new(error_message)), ) .child( h_flex() .justify_end() .mt_1() .gap_1() .child(self.create_copy_button(error_message)) .child( Button::new("subscribe", call_to_action).on_click(cx.listener( |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }, )), ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.notify(); }, ))), ) .into_any() } fn render_error_message( &self, header: SharedString, message: SharedString, cx: &mut Context, ) -> AnyElement { let message_with_header = format!("{}\n{}", header, message); v_flex() .gap_0p5() .child( h_flex() .gap_1p5() .items_center() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new(header).weight(FontWeight::MEDIUM)), ) .child( div() .id("error-message") .max_h_32() .overflow_y_scroll() .child(Label::new(message.clone())), ) .child( h_flex() .justify_end() .mt_1() .gap_1() .child(self.create_copy_button(message_with_header)) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); cx.notify(); }, ))), ) .into_any() } 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 { .. } => { let context_store = self.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::TextThread { context_editor, .. } => { context_editor.update(cx, |context_editor, cx| { ContextEditor::insert_dragged_files( context_editor, paths, added_worktrees, window, cx, ); }); } ActiveView::History | ActiveView::Configuration => {} } } fn create_copy_button(&self, message: impl Into) -> impl IntoElement { let message = message.into(); IconButton::new("copy", IconName::Copy) .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) }) .tooltip(Tooltip::text("Copy Error Message")) } fn key_context(&self) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("AgentPanel"); if matches!(self.active_view, ActiveView::TextThread { .. }) { key_context.add("prompt_editor"); } 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() .key_context(self.key_context()) .justify_between() .size_full() .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, _: &OpenConfiguration, 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| { this.thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, _cx| { thread.set_completion_mode(CompletionMode::Burn); }); }); this.continue_conversation(window, cx); })) .on_action(cx.listener(Self::toggle_burn_mode)) .child(self.render_toolbar(window, cx)) .children(self.render_upsell(window, cx)) .children(self.render_trial_end_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { .. } => parent .relative() .child(self.render_active_thread_or_empty_state(window, cx)) .children(self.render_tool_use_limit_reached(window, cx)) .child(h_flex().child(self.message_editor.clone())) .children(self.render_last_error(cx)) .child(self.render_drag_target(cx)), ActiveView::History => parent.child(self.history.clone()), ActiveView::TextThread { context_editor, buffer_search_bar, .. } => parent.child(self.render_prompt_editor( context_editor, buffer_search_bar, window, cx, )), ActiveView::Configuration => parent.children(self.configuration.clone()), }); 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_editor::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 panel.has_active_thread() { panel.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 Upsell; impl Dismissable for Upsell { const KEY: &'static str = "dismissed-trial-upsell"; } struct TrialEndUpsell; impl Dismissable for TrialEndUpsell { const KEY: &'static str = "dismissed-trial-end-upsell"; }