crates/agent/src/agent.rs 🔗
@@ -85,6 +85,7 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
+ ResetTrialEndUpsell,
]
);
Mikayla Maki , Max Brunsfeld , and Marshall Bowers created
Release Notes:
- N/A
---------
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
crates/agent/src/agent.rs | 1
crates/agent/src/agent_panel.rs | 339 +-
crates/agent/src/inline_prompt_editor.rs | 31
crates/db/src/kvp.rs | 27
crates/gpui/src/util.rs | 13
crates/inline_completion/src/inline_completion.rs | 7
crates/inline_completion_button/src/inline_completion_button.rs | 35
crates/ui/src/components/progress/progress_bar.rs | 11
crates/zeta/src/onboarding_modal.rs | 64
9 files changed, 327 insertions(+), 201 deletions(-)
@@ -85,6 +85,7 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
+ ResetTrialEndUpsell,
]
);
@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
-use db::kvp::KEY_VALUE_STORE;
+use db::kvp::{Dismissable, KEY_VALUE_STORE};
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
- OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
- ToggleNavigationMenu, ToggleOptionsMenu,
+ OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
+ ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
- set_trial_upsell_dismissed(false, cx);
+ TrialUpsell::set_dismissed(false, cx);
+ })
+ .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
+ TrialEndUpsell::set_dismissed(false, cx);
});
},
)
@@ -1932,12 +1935,23 @@ impl AgentPanel {
}
}
+ fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> 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<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
- if self.hide_trial_upsell || dismissed_trial_upsell() {
+ if self.hide_trial_upsell || TrialUpsell::dismissed() {
return false;
}
@@ -1983,125 +1997,115 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
- set_trial_upsell_dismissed(toggle_state_bool, cx);
+ TrialUpsell::set_dismissed(toggle_state_bool, cx);
},
);
- Some(
- div().p_2().child(
- v_flex()
+ 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()
- .elevation_2(cx)
- .rounded(px(8.))
- .bg(cx.theme().colors().background.alpha(0.5))
- .p(px(3.))
-
+ .px_neg_1()
+ .justify_between()
+ .items_center()
+ .child(h_flex().items_center().gap_1().child(checkbox))
.child(
- div()
+ h_flex()
.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)))
- )
+ 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_trial_upsell = true;
+ cx.notify();
+ });
+ }
+ }),
)
- // .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(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))
+ Button::new("cta-button", "Start Trial")
+ .style(ButtonStyle::Transparent)
+ .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
+ ),
+ ),
+ );
+
+ Some(self.render_upsell_container(cx, contents))
+ }
+
+ fn render_trial_end_upsell(
+ &self,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<impl IntoElement> {
+ 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()
- .w_full()
- .px_neg_1()
- .justify_between()
- .items_center()
- .child(h_flex().items_center().gap_1().child(checkbox))
+ .gap_2()
.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| {
- let hidden =
- this.hide_trial_upsell;
- println!("hidden: {}", hidden);
- this.hide_trial_upsell = true;
- let new_hidden =
- this.hide_trial_upsell;
- println!(
- "new_hidden: {}",
- new_hidden
- );
-
- cx.notify();
- },
- );
- }
- }),
- )
- .child(
- Button::new("cta-button", "Start Trial")
- .style(ButtonStyle::Transparent)
- .on_click(|_, _, cx| {
- cx.open_url(&zed_urls::account_url(cx))
- }),
- ),
+ 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))
+ }),
),
),
),
@@ -2109,6 +2113,91 @@ impl AgentPanel {
)
}
+ fn render_upsell_container(&self, cx: &mut Context<Self>, 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,
@@ -2827,6 +2916,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
+ .children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -3014,25 +3104,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
-const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
+struct TrialUpsell;
-fn dismissed_trial_upsell() -> bool {
- db::kvp::KEY_VALUE_STORE
- .read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
- .log_err()
- .map_or(false, |s| s.is_some())
+impl Dismissable for TrialUpsell {
+ const KEY: &'static str = "dismissed-trial-upsell";
}
-fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
- db::write_and_log(cx, move || async move {
- if is_dismissed {
- db::kvp::KEY_VALUE_STORE
- .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
- .await
- } else {
- db::kvp::KEY_VALUE_STORE
- .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
- .await
- }
- })
+struct TrialEndUpsell;
+
+impl Dismissable for TrialEndUpsell {
+ const KEY: &'static str = "dismissed-trial-end-upsell";
}
@@ -11,6 +11,7 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
+use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -33,7 +34,6 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
-use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
- if dismissed_rate_limit_notice() {
+ if RateLimitNotice::dismissed() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
- set_rate_limit_notice_dismissed(is_dismissed, cx)
+ RateLimitNotice::set_dismissed(is_dismissed, cx);
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
- && !dismissed_rate_limit_notice()
+ && !RateLimitNotice::dismissed()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,27 +1180,10 @@ impl PromptEditor<TerminalCodegen> {
}
}
-const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
+struct RateLimitNotice;
-fn dismissed_rate_limit_notice() -> bool {
- db::kvp::KEY_VALUE_STORE
- .read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
- .log_err()
- .map_or(false, |s| s.is_some())
-}
-
-fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
- db::write_and_log(cx, move || async move {
- if is_dismissed {
- db::kvp::KEY_VALUE_STORE
- .write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
- .await
- } else {
- db::kvp::KEY_VALUE_STORE
- .delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
- .await
- }
- })
+impl Dismissable for RateLimitNotice {
+ const KEY: &'static str = "dismissed-rate-limit-notice";
}
pub enum CodegenStatus {
@@ -1,6 +1,8 @@
+use gpui::App;
use sqlez_macros::sql;
+use util::ResultExt as _;
-use crate::{define_connection, query};
+use crate::{define_connection, query, write_and_log};
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
&[sql!(
@@ -11,6 +13,29 @@ define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
)];
);
+pub trait Dismissable {
+ const KEY: &'static str;
+
+ fn dismissed() -> bool {
+ KEY_VALUE_STORE
+ .read_kvp(Self::KEY)
+ .log_err()
+ .map_or(false, |s| s.is_some())
+ }
+
+ fn set_dismissed(is_dismissed: bool, cx: &mut App) {
+ write_and_log(cx, move || async move {
+ if is_dismissed {
+ KEY_VALUE_STORE
+ .write_kvp(Self::KEY.into(), "1".into())
+ .await
+ } else {
+ KEY_VALUE_STORE.delete_kvp(Self::KEY.into()).await
+ }
+ })
+ }
+}
+
impl KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {
@@ -27,6 +27,19 @@ pub trait FluentBuilder {
self.map(|this| if condition { then(this) } else { this })
}
+ /// Conditionally modify self with the given closure.
+ fn when_else(
+ self,
+ condition: bool,
+ then: impl FnOnce(Self) -> Self,
+ else_fn: impl FnOnce(Self) -> Self,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| if condition { then(this) } else { else_fn(this) })
+ }
+
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
@@ -83,6 +83,13 @@ impl EditPredictionUsage {
Ok(Self { limit, amount })
}
+
+ pub fn over_limit(&self) -> bool {
+ match self.limit {
+ UsageLimit::Limited(limit) => self.amount >= limit,
+ UsageLimit::Unlimited => false,
+ }
+ }
}
pub trait EditPredictionProvider: 'static + Sized {
@@ -33,7 +33,7 @@ use workspace::{
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
notifications::NotificationId,
};
-use zed_actions::OpenBrowser;
+use zed_actions::{OpenBrowser, OpenZedUrl};
use zed_llm_client::UsageLimit;
use zeta::RateCompletions;
@@ -277,14 +277,31 @@ impl Render for InlineCompletionButton {
);
}
+ let mut over_limit = false;
+
+ if let Some(usage) = self
+ .edit_prediction_provider
+ .as_ref()
+ .and_then(|provider| provider.usage(cx))
+ {
+ over_limit = usage.over_limit()
+ }
+
let show_editor_predictions = self.editor_show_predictions;
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
- .when(enabled && !show_editor_predictions, |this| {
- this.indicator(Indicator::dot().color(Color::Muted))
+ .when(
+ enabled && (!show_editor_predictions || over_limit),
+ |this| {
+ this.indicator(Indicator::dot().when_else(
+ over_limit,
+ |dot| dot.color(Color::Error),
+ |dot| dot.color(Color::Muted),
+ ))
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
- })
+ },
+ )
.when(!self.popover_menu_handle.is_deployed(), |element| {
element.tooltip(move |window, cx| {
if enabled {
@@ -440,6 +457,16 @@ impl InlineCompletionButton {
},
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
)
+ .when(usage.over_limit(), |menu| -> ContextMenu {
+ menu.entry("Subscribe to increase your limit", None, |window, cx| {
+ window.dispatch_action(
+ Box::new(OpenZedUrl {
+ url: zed_urls::account_url(cx),
+ }),
+ cx,
+ );
+ })
+ })
.separator();
}
@@ -13,6 +13,7 @@ pub struct ProgressBar {
value: f32,
max_value: f32,
bg_color: Hsla,
+ over_color: Hsla,
fg_color: Hsla,
}
@@ -23,6 +24,7 @@ impl ProgressBar {
value,
max_value,
bg_color: cx.theme().colors().background,
+ over_color: cx.theme().status().error,
fg_color: cx.theme().status().info,
}
}
@@ -50,6 +52,12 @@ impl ProgressBar {
self.fg_color = color;
self
}
+
+ /// Sets the over limit color of the progress bar.
+ pub fn over_color(mut self, color: Hsla) -> Self {
+ self.over_color = color;
+ self
+ }
}
impl RenderOnce for ProgressBar {
@@ -74,7 +82,8 @@ impl RenderOnce for ProgressBar {
div()
.h_full()
.rounded_full()
- .bg(self.fg_color)
+ .when(self.value > self.max_value, |div| div.bg(self.over_color))
+ .when(self.value <= self.max_value, |div| div.bg(self.fg_color))
.w(relative(fill_width)),
)
}
@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
use anyhow::Context as _;
-use client::{Client, UserStore, zed_urls};
+use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use fs::Fs;
use gpui::{
@@ -384,47 +384,29 @@ impl Render for ZedPredictModal {
} else {
(IconName::ChevronDown, IconName::ChevronUp)
};
+ let plan = plan.unwrap_or(proto::Plan::Free);
base.child(Label::new(copy).color(Color::Muted))
- .child(h_flex().map(|parent| {
- if let Some(plan) = plan {
- parent.child(
- Checkbox::new("plan", ToggleState::Selected)
- .fill()
- .disabled(true)
- .label(format!(
- "You get {} edit predictions through your {}.",
- if plan == proto::Plan::Free {
- "2,000"
- } else {
- "unlimited"
- },
- match plan {
- proto::Plan::Free => "Zed Free plan",
- proto::Plan::ZedPro => "Zed Pro plan",
- proto::Plan::ZedProTrial => "Zed Pro trial",
- }
- )),
- )
- } else {
- parent
- .child(
- Checkbox::new("plan-required", ToggleState::Unselected)
- .fill()
- .disabled(true)
- .label("To get started with edit prediction"),
- )
- .child(
- Button::new("subscribe", "choose a plan")
- .icon(IconName::ArrowUpRight)
- .icon_size(IconSize::Indicator)
- .icon_color(Color::Muted)
- .on_click(|_event, _window, cx| {
- cx.open_url(&zed_urls::account_url(cx));
- }),
- )
- }
- }))
+ .child(
+ h_flex().child(
+ Checkbox::new("plan", ToggleState::Selected)
+ .fill()
+ .disabled(true)
+ .label(format!(
+ "You get {} edit predictions through your {}.",
+ if plan == proto::Plan::Free {
+ "2,000"
+ } else {
+ "unlimited"
+ },
+ match plan {
+ proto::Plan::Free => "Zed Free plan",
+ proto::Plan::ZedPro => "Zed Pro plan",
+ proto::Plan::ZedProTrial => "Zed Pro trial",
+ }
+ )),
+ ),
+ )
.child(
h_flex()
.child(
@@ -495,7 +477,7 @@ impl Render for ZedPredictModal {
.w_full()
.child(
Button::new("accept-tos", "Enable Edit Prediction")
- .disabled(plan.is_none() || !self.terms_of_service)
+ .disabled(!self.terms_of_service)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::accept_and_enable)),