Add end of service notifications (#30982)
Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
c747a57b7e
commit
315321bf8c
9 changed files with 327 additions and 201 deletions
|
@ -85,6 +85,7 @@ actions!(
|
||||||
KeepAll,
|
KeepAll,
|
||||||
Follow,
|
Follow,
|
||||||
ResetTrialUpsell,
|
ResetTrialUpsell,
|
||||||
|
ResetTrialEndUpsell,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use markdown::Markdown;
|
use markdown::Markdown;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
||||||
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||||
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
|
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
|
||||||
ToggleNavigationMenu, ToggleOptionsMenu,
|
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||||
|
@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
|
||||||
window.refresh();
|
window.refresh();
|
||||||
})
|
})
|
||||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
.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 {
|
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
|
||||||
if !matches!(self.active_view, ActiveView::Thread { .. }) {
|
if !matches!(self.active_view, ActiveView::Thread { .. }) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hide_trial_upsell || dismissed_trial_upsell() {
|
if self.hide_trial_upsell || TrialUpsell::dismissed() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1983,125 +1997,115 @@ impl AgentPanel {
|
||||||
move |toggle_state, _window, cx| {
|
move |toggle_state, _window, cx| {
|
||||||
let toggle_state_bool = toggle_state.selected();
|
let toggle_state_bool = toggle_state.selected();
|
||||||
|
|
||||||
set_trial_upsell_dismissed(toggle_state_bool, cx);
|
TrialUpsell::set_dismissed(toggle_state_bool, cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(
|
let contents = div()
|
||||||
div().p_2().child(
|
.size_full()
|
||||||
v_flex()
|
.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()
|
.w_full()
|
||||||
.elevation_2(cx)
|
.px_neg_1()
|
||||||
.rounded(px(8.))
|
.justify_between()
|
||||||
.bg(cx.theme().colors().background.alpha(0.5))
|
.items_center()
|
||||||
.p(px(3.))
|
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||||
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.gap_2()
|
.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(
|
.child(
|
||||||
div()
|
Button::new("dismiss-button", "Not Now")
|
||||||
.absolute()
|
.style(ButtonStyle::Transparent)
|
||||||
.top_0()
|
.color(Color::Muted)
|
||||||
.right(px(-1.0))
|
.on_click({
|
||||||
.w(px(441.))
|
let agent_panel = cx.entity();
|
||||||
.h(px(167.))
|
move |_, _, cx| {
|
||||||
.child(
|
agent_panel.update(cx, |this, cx| {
|
||||||
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
|
this.hide_trial_upsell = true;
|
||||||
)
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
Button::new("cta-button", "Start Trial")
|
||||||
.absolute()
|
.style(ButtonStyle::Transparent)
|
||||||
.top(px(-8.0))
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||||
.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)))
|
Some(self.render_upsell_container(cx, contents))
|
||||||
)
|
}
|
||||||
)
|
|
||||||
// .child(
|
fn render_trial_end_upsell(
|
||||||
// div()
|
&self,
|
||||||
// .absolute()
|
_window: &mut Window,
|
||||||
// .top_0()
|
cx: &mut Context<Self>,
|
||||||
// .right(px(360.))
|
) -> Option<impl IntoElement> {
|
||||||
// .size(px(401.))
|
if !self.should_render_trial_end_upsell(cx) {
|
||||||
// .overflow_hidden()
|
return None;
|
||||||
// .bg(cx.theme().colors().panel_background)
|
}
|
||||||
// )
|
|
||||||
.child(
|
Some(
|
||||||
div()
|
self.render_upsell_container(
|
||||||
.absolute()
|
cx,
|
||||||
.top_0()
|
div()
|
||||||
.right_0()
|
.size_full()
|
||||||
.w(px(660.))
|
.gap_2()
|
||||||
.h(px(401.))
|
.flex()
|
||||||
.overflow_hidden()
|
.flex_col()
|
||||||
.bg(linear_gradient(
|
.child(
|
||||||
75.,
|
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
|
||||||
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
|
)
|
||||||
linear_color_stop(cx.theme().colors().panel_background, 0.45),
|
.child(
|
||||||
))
|
Label::new("You've been automatically reset to the free plan.")
|
||||||
)
|
.size(LabelSize::Small),
|
||||||
.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(
|
||||||
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.px_neg_1()
|
||||||
|
.justify_between()
|
||||||
|
.items_center()
|
||||||
|
.child(div())
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.gap_2()
|
||||||
.px_neg_1()
|
|
||||||
.justify_between()
|
|
||||||
.items_center()
|
|
||||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Button::new("dismiss-button", "Stay on Free")
|
||||||
.gap_2()
|
.style(ButtonStyle::Transparent)
|
||||||
.child(
|
.color(Color::Muted)
|
||||||
Button::new("dismiss-button", "Not Now")
|
.on_click({
|
||||||
.style(ButtonStyle::Transparent)
|
let agent_panel = cx.entity();
|
||||||
.color(Color::Muted)
|
move |_, _, cx| {
|
||||||
.on_click({
|
agent_panel.update(cx, |_this, cx| {
|
||||||
let agent_panel = cx.entity();
|
TrialEndUpsell::set_dismissed(true, cx);
|
||||||
move |_, _, cx| {
|
cx.notify();
|
||||||
agent_panel.update(
|
});
|
||||||
cx,
|
}
|
||||||
|this, cx| {
|
}),
|
||||||
let hidden =
|
)
|
||||||
this.hide_trial_upsell;
|
.child(
|
||||||
println!("hidden: {}", hidden);
|
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||||
this.hide_trial_upsell = true;
|
.style(ButtonStyle::Transparent)
|
||||||
let new_hidden =
|
.on_click(|_, _, cx| {
|
||||||
this.hide_trial_upsell;
|
cx.open_url(&zed_urls::account_url(cx))
|
||||||
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))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
fn render_active_thread_or_empty_state(
|
||||||
&self,
|
&self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -2827,6 +2916,7 @@ impl Render for AgentPanel {
|
||||||
.on_action(cx.listener(Self::toggle_zoom))
|
.on_action(cx.listener(Self::toggle_zoom))
|
||||||
.child(self.render_toolbar(window, cx))
|
.child(self.render_toolbar(window, cx))
|
||||||
.children(self.render_trial_upsell(window, cx))
|
.children(self.render_trial_upsell(window, cx))
|
||||||
|
.children(self.render_trial_end_upsell(window, cx))
|
||||||
.map(|parent| match &self.active_view {
|
.map(|parent| match &self.active_view {
|
||||||
ActiveView::Thread { .. } => parent
|
ActiveView::Thread { .. } => parent
|
||||||
.relative()
|
.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 {
|
impl Dismissable for TrialUpsell {
|
||||||
db::kvp::KEY_VALUE_STORE
|
const KEY: &'static str = "dismissed-trial-upsell";
|
||||||
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
|
|
||||||
.log_err()
|
|
||||||
.map_or(false, |s| s.is_some())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
|
struct TrialEndUpsell;
|
||||||
db::write_and_log(cx, move || async move {
|
|
||||||
if is_dismissed {
|
impl Dismissable for TrialEndUpsell {
|
||||||
db::kvp::KEY_VALUE_STORE
|
const KEY: &'static str = "dismissed-trial-end-upsell";
|
||||||
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
db::kvp::KEY_VALUE_STORE
|
|
||||||
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||||
use client::ErrorExt;
|
use client::ErrorExt;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
use db::kvp::Dismissable;
|
||||||
use editor::display_map::EditorMargins;
|
use editor::display_map::EditorMargins;
|
||||||
use editor::{
|
use editor::{
|
||||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||||
|
@ -33,7 +34,6 @@ use ui::utils::WithRemSize;
|
||||||
use ui::{
|
use ui::{
|
||||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub struct PromptEditor<T> {
|
pub struct PromptEditor<T> {
|
||||||
|
@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||||
.child(CheckboxWithLabel::new(
|
.child(CheckboxWithLabel::new(
|
||||||
"dont-show-again",
|
"dont-show-again",
|
||||||
Label::new("Don't show again"),
|
Label::new("Don't show again"),
|
||||||
if dismissed_rate_limit_notice() {
|
if RateLimitNotice::dismissed() {
|
||||||
ui::ToggleState::Selected
|
ui::ToggleState::Selected
|
||||||
} else {
|
} else {
|
||||||
ui::ToggleState::Unselected
|
ui::ToggleState::Unselected
|
||||||
|
@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||||
ui::ToggleState::Selected => true,
|
ui::ToggleState::Selected => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
RateLimitNotice::set_dismissed(is_dismissed, cx);
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
|
@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
|
||||||
CodegenStatus::Error(error) => {
|
CodegenStatus::Error(error) => {
|
||||||
if cx.has_flag::<ZedProFeatureFlag>()
|
if cx.has_flag::<ZedProFeatureFlag>()
|
||||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||||
&& !dismissed_rate_limit_notice()
|
&& !RateLimitNotice::dismissed()
|
||||||
{
|
{
|
||||||
self.show_rate_limit_notice = true;
|
self.show_rate_limit_notice = true;
|
||||||
cx.notify();
|
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 {
|
impl Dismissable for RateLimitNotice {
|
||||||
db::kvp::KEY_VALUE_STORE
|
const KEY: &'static str = "dismissed-rate-limit-notice";
|
||||||
.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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CodegenStatus {
|
pub enum CodegenStatus {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use gpui::App;
|
||||||
use sqlez_macros::sql;
|
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<()> =
|
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||||
&[sql!(
|
&[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 {
|
impl KeyValueStore {
|
||||||
query! {
|
query! {
|
||||||
pub fn read_kvp(key: &str) -> Result<Option<String>> {
|
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 })
|
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.
|
/// 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
|
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||||
where
|
where
|
||||||
|
|
|
@ -83,6 +83,13 @@ impl EditPredictionUsage {
|
||||||
|
|
||||||
Ok(Self { limit, amount })
|
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 {
|
pub trait EditPredictionProvider: 'static + Sized {
|
||||||
|
|
|
@ -33,7 +33,7 @@ use workspace::{
|
||||||
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
||||||
notifications::NotificationId,
|
notifications::NotificationId,
|
||||||
};
|
};
|
||||||
use zed_actions::OpenBrowser;
|
use zed_actions::{OpenBrowser, OpenZedUrl};
|
||||||
use zed_llm_client::UsageLimit;
|
use zed_llm_client::UsageLimit;
|
||||||
use zeta::RateCompletions;
|
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 show_editor_predictions = self.editor_show_predictions;
|
||||||
|
|
||||||
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
|
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
.when(enabled && !show_editor_predictions, |this| {
|
.when(
|
||||||
this.indicator(Indicator::dot().color(Color::Muted))
|
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))
|
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
||||||
element.tooltip(move |window, cx| {
|
element.tooltip(move |window, cx| {
|
||||||
if enabled {
|
if enabled {
|
||||||
|
@ -440,6 +457,16 @@ impl InlineCompletionButton {
|
||||||
},
|
},
|
||||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
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();
|
.separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub struct ProgressBar {
|
||||||
value: f32,
|
value: f32,
|
||||||
max_value: f32,
|
max_value: f32,
|
||||||
bg_color: Hsla,
|
bg_color: Hsla,
|
||||||
|
over_color: Hsla,
|
||||||
fg_color: Hsla,
|
fg_color: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ impl ProgressBar {
|
||||||
value,
|
value,
|
||||||
max_value,
|
max_value,
|
||||||
bg_color: cx.theme().colors().background,
|
bg_color: cx.theme().colors().background,
|
||||||
|
over_color: cx.theme().status().error,
|
||||||
fg_color: cx.theme().status().info,
|
fg_color: cx.theme().status().info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +52,12 @@ impl ProgressBar {
|
||||||
self.fg_color = color;
|
self.fg_color = color;
|
||||||
self
|
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 {
|
impl RenderOnce for ProgressBar {
|
||||||
|
@ -74,7 +82,8 @@ impl RenderOnce for ProgressBar {
|
||||||
div()
|
div()
|
||||||
.h_full()
|
.h_full()
|
||||||
.rounded_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)),
|
.w(relative(fill_width)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
|
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use client::{Client, UserStore, zed_urls};
|
use client::{Client, UserStore};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -384,47 +384,29 @@ impl Render for ZedPredictModal {
|
||||||
} else {
|
} else {
|
||||||
(IconName::ChevronDown, IconName::ChevronUp)
|
(IconName::ChevronDown, IconName::ChevronUp)
|
||||||
};
|
};
|
||||||
|
let plan = plan.unwrap_or(proto::Plan::Free);
|
||||||
|
|
||||||
base.child(Label::new(copy).color(Color::Muted))
|
base.child(Label::new(copy).color(Color::Muted))
|
||||||
.child(h_flex().map(|parent| {
|
.child(
|
||||||
if let Some(plan) = plan {
|
h_flex().child(
|
||||||
parent.child(
|
Checkbox::new("plan", ToggleState::Selected)
|
||||||
Checkbox::new("plan", ToggleState::Selected)
|
.fill()
|
||||||
.fill()
|
.disabled(true)
|
||||||
.disabled(true)
|
.label(format!(
|
||||||
.label(format!(
|
"You get {} edit predictions through your {}.",
|
||||||
"You get {} edit predictions through your {}.",
|
if plan == proto::Plan::Free {
|
||||||
if plan == proto::Plan::Free {
|
"2,000"
|
||||||
"2,000"
|
} else {
|
||||||
} else {
|
"unlimited"
|
||||||
"unlimited"
|
},
|
||||||
},
|
match plan {
|
||||||
match plan {
|
proto::Plan::Free => "Zed Free plan",
|
||||||
proto::Plan::Free => "Zed Free plan",
|
proto::Plan::ZedPro => "Zed Pro plan",
|
||||||
proto::Plan::ZedPro => "Zed Pro plan",
|
proto::Plan::ZedProTrial => "Zed Pro trial",
|
||||||
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(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.child(
|
.child(
|
||||||
|
@ -495,7 +477,7 @@ impl Render for ZedPredictModal {
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("accept-tos", "Enable Edit Prediction")
|
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))
|
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
.full_width()
|
.full_width()
|
||||||
.on_click(cx.listener(Self::accept_and_enable)),
|
.on_click(cx.listener(Self::accept_and_enable)),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue