agent: Add UI for upsell scenarios (#29805)

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
Nate Butler 2025-05-04 20:48:06 -04:00 committed by GitHub
parent a19687a815
commit fe177f5d69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 659 additions and 135 deletions

View file

@ -4,8 +4,12 @@ use std::sync::Arc;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{AgentPreview, AnimatedLabel, MaxModeTooltip};
use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use buffer_diff::BufferDiff;
use client::UserStore;
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::{
@ -27,6 +31,7 @@ use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
@ -53,6 +58,7 @@ pub struct MessageEditor {
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
context_strip: Entity<ContextStrip>,
@ -126,6 +132,7 @@ impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
@ -188,6 +195,7 @@ impl MessageEditor {
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
user_store,
thread,
incompatible_tools_state: incompatible_tools.clone(),
workspace,
@ -1030,79 +1038,72 @@ impl MessageEditor {
})
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let plan = self
.user_store
.read(cx)
.current_plan()
.map(|plan| match plan {
Plan::Free => zed_llm_client::Plan::Free,
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
})
.unwrap_or(zed_llm_client::Plan::Free);
let usage = self.thread.read(cx).last_usage()?;
Some(
div()
.child(UsageCallout::new(plan, usage))
.line_height(line_height),
)
}
fn render_token_limit_callout(
&self,
line_height: Pixels,
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Div {
let heading = if token_usage_ratio == TokenUsageRatio::Exceeded {
) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
h_flex()
.p_2()
.gap_2()
.flex_wrap()
.justify_between()
.bg(
if token_usage_ratio == TokenUsageRatio::Exceeded {
cx.theme().status().error_background.opacity(0.1)
} else {
cx.theme().status().warning_background.opacity(0.1)
})
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.gap_2()
.items_start()
.child(
h_flex()
.h(line_height)
.justify_center()
.child(
if token_usage_ratio == TokenUsageRatio::Exceeded {
Icon::new(IconName::X)
.color(Color::Error)
.size(IconSize::XSmall)
} else {
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::XSmall)
}
),
)
.child(
v_flex()
.mr_auto()
.child(Label::new(heading).size(LabelSize::Small))
.child(
Label::new(
"Start a new thread from a summary to continue the conversation.",
)
.size(LabelSize::Small)
.color(Color::Muted),
),
),
)
.child(
Button::new("new-thread", "Start New Thread")
.on_click(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
let message = "Start a new thread from a summary to continue the conversation.";
window.dispatch_action(Box::new(NewThread {
from_thread_id
}), cx);
}))
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small),
)
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
Icon::new(IconName::X)
.color(Color::Error)
.size(IconSize::XSmall)
} else {
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::XSmall)
};
Some(
div()
.child(ui::Callout::multi_line(
title.into(),
message.into(),
icon,
"Start New Thread".into(),
Box::new(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
})),
))
.line_height(line_height),
)
}
pub fn last_estimated_token_count(&self) -> Option<usize> {
@ -1307,8 +1308,16 @@ impl Render for MessageEditor {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
.children({
let usage_callout = self.render_usage_callout(line_height, cx);
if usage_callout.is_some() {
usage_callout
} else if token_usage_ratio != TokenUsageRatio::Normal {
self.render_token_limit_callout(line_height, token_usage_ratio, cx)
} else {
None
}
})
}
}
@ -1354,6 +1363,12 @@ impl Component for MessageEditor {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn description() -> Option<&'static str> {
Some(
"The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.",
)
}
}
impl AgentPreview for MessageEditor {
@ -1364,16 +1379,18 @@ impl AgentPreview for MessageEditor {
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
if let Some(workspace_entity) = workspace.upgrade() {
let fs = workspace_entity.read(cx).app_state().fs.clone();
let weak_project = workspace_entity.read(cx).project().clone().downgrade();
if let Some(workspace) = workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
let user_store = workspace.read(cx).app_state().user_store.clone();
let weak_project = workspace.read(cx).project().clone().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
let thread = active_thread.read(cx).thread().clone();
let example_message_editor = cx.new(|cx| {
let default_message_editor = cx.new(|cx| {
MessageEditor::new(
fs,
workspace,
workspace.downgrade(),
user_store,
context_store,
None,
thread_store,
@ -1387,8 +1404,15 @@ impl AgentPreview for MessageEditor {
v_flex()
.gap_4()
.children(vec![single_example(
"Default",
example_message_editor.clone().into_any_element(),
"Default Message Editor",
div()
.w(px(540.))
.pt_12()
.bg(cx.theme().colors().panel_background)
.border_1()
.border_color(cx.theme().colors().border)
.child(default_message_editor.clone())
.into_any_element(),
)])
.into_any_element(),
)