@@ -34,13 +34,15 @@ use rules_library::{RulesLibrary, open_rules_library};
use settings::{Settings, update_settings_file};
use time::UtcOffset;
use ui::{
- Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
+ Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip,
+ prelude::*,
};
use util::ResultExt as _;
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
+use zed_llm_client::UsageLimit;
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
use crate::agent_diff::AgentDiff;
@@ -1369,6 +1371,8 @@ impl AssistantPanel {
let thread = active_thread.thread().read(cx);
let thread_id = thread.id().clone();
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 {
ActiveView::Thread { .. } => !is_empty,
@@ -1454,30 +1458,74 @@ impl AssistantPanel {
.anchor(Corner::TopRight)
.with_handle(self.assistant_dropdown_menu_handle.clone())
.menu(move |window, cx| {
- Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
- menu.when(!is_empty, |menu| {
- menu.action(
- "Start New From Summary",
- Box::new(NewThread {
- from_thread_id: Some(thread_id.clone()),
+ 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,
+ ),
}),
)
- .separator()
- })
- .action("New Text Thread", NewTextThread.boxed_clone())
- .action("Rules Library", Box::new(OpenRulesLibrary::default()))
- .action("Settings", Box::new(OpenConfiguration))
- .separator()
- .header("MCPs")
- .action(
- "View Server Extensions",
- Box::new(zed_actions::Extensions {
- category_filter: Some(
- zed_actions::ExtensionCategoryFilter::ContextServers,
- ),
- }),
- )
- .action("Add Custom Server", Box::new(AddContextServer))
+ .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));
+ menu
}))
});
@@ -16,6 +16,8 @@ use super::Tooltip;
pub enum ContextMenuItem {
Separator,
Header(SharedString),
+ /// title, link_label, link_url
+ HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header
Label(SharedString),
Entry(ContextMenuEntry),
CustomEntry {
@@ -332,6 +334,20 @@ impl ContextMenu {
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 {
self.items.push(ContextMenuItem::Separator);
self
@@ -788,6 +804,25 @@ impl ContextMenu {
ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
.inset(true)
.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)
.inset(true)
.disabled(true)
@@ -1057,6 +1092,7 @@ impl ContextMenuItem {
fn is_selectable(&self) -> bool {
match self {
ContextMenuItem::Header(_)
+ | ContextMenuItem::HeaderWithLink(_, _, _)
| ContextMenuItem::Separator
| ContextMenuItem::Label { .. } => false,
ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
@@ -5,6 +5,7 @@ use crate::{Icon, IconName, IconSize, Label, h_flex};
pub struct ListSubHeader {
label: SharedString,
start_slot: Option<IconName>,
+ end_slot: Option<AnyElement>,
inset: bool,
selected: bool,
}
@@ -14,6 +15,7 @@ impl ListSubHeader {
Self {
label: label.into(),
start_slot: None,
+ end_slot: None,
inset: false,
selected: false,
}
@@ -24,6 +26,11 @@ impl ListSubHeader {
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 {
self.inset = inset;
self
@@ -73,7 +80,8 @@ impl RenderOnce for ListSubHeader {
.color(Color::Muted)
.size(LabelSize::Small),
),
- ),
+ )
+ .children(self.end_slot),
)
}
}