Show prompt usage in agent overflow menu (#29922)
This PR adds prompt usage information, and easy access to managing your account, to the agent overflow menu:  Currently this UI will only show after making a request. We'll work on eagerly getting the usage info later. Release Notes: - Added current prompt usage information to the agent menu (`...`) for Zed AI users
This commit is contained in:
parent
1c44cabaea
commit
a72ade8762
3 changed files with 116 additions and 24 deletions
|
@ -34,13 +34,15 @@ use rules_library::{RulesLibrary, open_rules_library};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
use ui::{
|
use ui::{
|
||||||
Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
|
Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip,
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::OpenConfiguration;
|
use zed_actions::agent::OpenConfiguration;
|
||||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||||
|
use zed_llm_client::UsageLimit;
|
||||||
|
|
||||||
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
||||||
use crate::agent_diff::AgentDiff;
|
use crate::agent_diff::AgentDiff;
|
||||||
|
@ -1369,6 +1371,8 @@ impl AssistantPanel {
|
||||||
let thread = active_thread.thread().read(cx);
|
let thread = active_thread.thread().read(cx);
|
||||||
let thread_id = thread.id().clone();
|
let thread_id = thread.id().clone();
|
||||||
let is_empty = active_thread.is_empty();
|
let is_empty = active_thread.is_empty();
|
||||||
|
let last_usage = active_thread.thread().read(cx).last_usage();
|
||||||
|
let account_url = zed_urls::account_url(cx);
|
||||||
|
|
||||||
let show_token_count = match &self.active_view {
|
let show_token_count = match &self.active_view {
|
||||||
ActiveView::Thread { .. } => !is_empty,
|
ActiveView::Thread { .. } => !is_empty,
|
||||||
|
@ -1454,30 +1458,74 @@ impl AssistantPanel {
|
||||||
.anchor(Corner::TopRight)
|
.anchor(Corner::TopRight)
|
||||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||||
.menu(move |window, cx| {
|
.menu(move |window, cx| {
|
||||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
|
||||||
menu.when(!is_empty, |menu| {
|
menu = menu
|
||||||
menu.action(
|
.action("New Thread", NewThread::default().boxed_clone())
|
||||||
"Start New From Summary",
|
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||||
Box::new(NewThread {
|
.when(!is_empty, |menu| {
|
||||||
from_thread_id: Some(thread_id.clone()),
|
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,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.separator()
|
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||||
})
|
.separator();
|
||||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
|
||||||
.action("Rules Library", Box::new(OpenRulesLibrary::default()))
|
if let Some(usage) = last_usage {
|
||||||
.action("Settings", Box::new(OpenConfiguration))
|
menu = menu
|
||||||
.separator()
|
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||||
.header("MCPs")
|
.custom_entry(
|
||||||
.action(
|
move |_window, cx| {
|
||||||
"View Server Extensions",
|
let used_percentage = match usage.limit {
|
||||||
Box::new(zed_actions::Extensions {
|
UsageLimit::Limited(limit) => {
|
||||||
category_filter: Some(
|
Some((usage.amount as f32 / limit as f32) * 100.)
|
||||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
}
|
||||||
),
|
UsageLimit::Unlimited => None,
|
||||||
}),
|
};
|
||||||
)
|
|
||||||
.action("Add Custom Server", Box::new(AddContextServer))
|
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));
|
||||||
|
menu
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ use super::Tooltip;
|
||||||
pub enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
Separator,
|
Separator,
|
||||||
Header(SharedString),
|
Header(SharedString),
|
||||||
|
/// title, link_label, link_url
|
||||||
|
HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header
|
||||||
Label(SharedString),
|
Label(SharedString),
|
||||||
Entry(ContextMenuEntry),
|
Entry(ContextMenuEntry),
|
||||||
CustomEntry {
|
CustomEntry {
|
||||||
|
@ -332,6 +334,20 @@ impl ContextMenu {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn header_with_link(
|
||||||
|
mut self,
|
||||||
|
title: impl Into<SharedString>,
|
||||||
|
link_label: impl Into<SharedString>,
|
||||||
|
link_url: impl Into<SharedString>,
|
||||||
|
) -> Self {
|
||||||
|
self.items.push(ContextMenuItem::HeaderWithLink(
|
||||||
|
title.into(),
|
||||||
|
link_label.into(),
|
||||||
|
link_url.into(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn separator(mut self) -> Self {
|
pub fn separator(mut self) -> Self {
|
||||||
self.items.push(ContextMenuItem::Separator);
|
self.items.push(ContextMenuItem::Separator);
|
||||||
self
|
self
|
||||||
|
@ -788,6 +804,25 @@ impl ContextMenu {
|
||||||
ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
|
ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
ContextMenuItem::HeaderWithLink(header, label, url) => {
|
||||||
|
let url = url.clone();
|
||||||
|
let link_id = ElementId::Name(format!("link-{}", url).into());
|
||||||
|
ListSubHeader::new(header.clone())
|
||||||
|
.inset(true)
|
||||||
|
.end_slot(
|
||||||
|
Button::new(link_id, label.clone())
|
||||||
|
.color(Color::Muted)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.size(ButtonSize::None)
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
let url = url.clone();
|
||||||
|
cx.open_url(&url);
|
||||||
|
})
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
ContextMenuItem::Label(label) => ListItem::new(ix)
|
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
|
@ -1057,6 +1092,7 @@ impl ContextMenuItem {
|
||||||
fn is_selectable(&self) -> bool {
|
fn is_selectable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ContextMenuItem::Header(_)
|
ContextMenuItem::Header(_)
|
||||||
|
| ContextMenuItem::HeaderWithLink(_, _, _)
|
||||||
| ContextMenuItem::Separator
|
| ContextMenuItem::Separator
|
||||||
| ContextMenuItem::Label { .. } => false,
|
| ContextMenuItem::Label { .. } => false,
|
||||||
ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
|
ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::{Icon, IconName, IconSize, Label, h_flex};
|
||||||
pub struct ListSubHeader {
|
pub struct ListSubHeader {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
start_slot: Option<IconName>,
|
start_slot: Option<IconName>,
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
@ -14,6 +15,7 @@ impl ListSubHeader {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
start_slot: None,
|
start_slot: None,
|
||||||
|
end_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
}
|
}
|
||||||
|
@ -24,6 +26,11 @@ impl ListSubHeader {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn end_slot(mut self, end_slot: AnyElement) -> Self {
|
||||||
|
self.end_slot = Some(end_slot);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn inset(mut self, inset: bool) -> Self {
|
pub fn inset(mut self, inset: bool) -> Self {
|
||||||
self.inset = inset;
|
self.inset = inset;
|
||||||
self
|
self
|
||||||
|
@ -73,7 +80,8 @@ impl RenderOnce for ListSubHeader {
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(LabelSize::Small),
|
.size(LabelSize::Small),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
.children(self.end_slot),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue