agent: Add UI for upsell scenarios (#29805)
Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
a19687a815
commit
fe177f5d69
13 changed files with 659 additions and 135 deletions
|
@ -9,6 +9,7 @@ mod context_picker;
|
||||||
mod context_server_configuration;
|
mod context_server_configuration;
|
||||||
mod context_store;
|
mod context_store;
|
||||||
mod context_strip;
|
mod context_strip;
|
||||||
|
mod debug;
|
||||||
mod history_store;
|
mod history_store;
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod inline_prompt_editor;
|
mod inline_prompt_editor;
|
||||||
|
@ -47,7 +48,7 @@ pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||||
pub use crate::thread_store::ThreadStore;
|
pub use crate::thread_store::ThreadStore;
|
||||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||||
pub use context_store::ContextStore;
|
pub use context_store::ContextStore;
|
||||||
pub use ui::{all_agent_previews, get_agent_preview};
|
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
agent,
|
agent,
|
||||||
|
|
|
@ -50,7 +50,6 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::ui::UsageBanner;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
|
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
|
||||||
InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||||
|
@ -432,6 +431,7 @@ impl AssistantPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
|
user_store.clone(),
|
||||||
message_editor_context_store.clone(),
|
message_editor_context_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
|
@ -735,6 +735,7 @@ impl AssistantPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
|
self.user_store.clone(),
|
||||||
context_store,
|
context_store,
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -933,6 +934,7 @@ impl AssistantPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
|
self.user_store.clone(),
|
||||||
context_store,
|
context_store,
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -1944,22 +1946,6 @@ impl AssistantPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
|
||||||
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(UsageBanner::new(plan, usage).into_any_element())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||||
let tool_use_limit_reached = self
|
let tool_use_limit_reached = self
|
||||||
.thread
|
.thread
|
||||||
|
@ -2277,7 +2263,6 @@ impl Render for AssistantPanel {
|
||||||
ActiveView::Thread { .. } => parent
|
ActiveView::Thread { .. } => parent
|
||||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||||
.children(self.render_tool_use_limit_reached(cx))
|
.children(self.render_tool_use_limit_reached(cx))
|
||||||
.children(self.render_usage_banner(cx))
|
|
||||||
.child(h_flex().child(self.message_editor.clone()))
|
.child(h_flex().child(self.message_editor.clone()))
|
||||||
.children(self.render_last_error(cx)),
|
.children(self.render_last_error(cx)),
|
||||||
ActiveView::History => parent.child(self.history.clone()),
|
ActiveView::History => parent.child(self.history.clone()),
|
||||||
|
|
124
crates/agent/src/debug.rs
Normal file
124
crates/agent/src/debug.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
#![allow(unused, dead_code)]
|
||||||
|
|
||||||
|
use gpui::Global;
|
||||||
|
use language_model::RequestUsage;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use ui::prelude::*;
|
||||||
|
use zed_llm_client::{Plan, UsageLimit};
|
||||||
|
|
||||||
|
/// Debug only: Used for testing various account states
|
||||||
|
///
|
||||||
|
/// Use this by initializing it with
|
||||||
|
/// `cx.set_global(DebugAccountState::default());` somewhere
|
||||||
|
///
|
||||||
|
/// Then call `cx.debug_account()` to get access
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DebugAccountState {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub trial_expired: bool,
|
||||||
|
pub plan: Plan,
|
||||||
|
pub custom_prompt_usage: RequestUsage,
|
||||||
|
pub usage_based_billing_enabled: bool,
|
||||||
|
pub monthly_spending_cap: i32,
|
||||||
|
pub custom_edit_prediction_usage: UsageLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugAccountState {
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_enabled(&mut self, enabled: bool) -> &mut Self {
|
||||||
|
self.enabled = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_trial_expired(&mut self, trial_expired: bool) -> &mut Self {
|
||||||
|
self.trial_expired = trial_expired;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_plan(&mut self, plan: Plan) -> &mut Self {
|
||||||
|
self.plan = plan;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_custom_prompt_usage(&mut self, custom_prompt_usage: RequestUsage) -> &mut Self {
|
||||||
|
self.custom_prompt_usage = custom_prompt_usage;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_usage_based_billing_enabled(
|
||||||
|
&mut self,
|
||||||
|
usage_based_billing_enabled: bool,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.usage_based_billing_enabled = usage_based_billing_enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_monthly_spending_cap(&mut self, monthly_spending_cap: i32) -> &mut Self {
|
||||||
|
self.monthly_spending_cap = monthly_spending_cap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_custom_edit_prediction_usage(
|
||||||
|
&mut self,
|
||||||
|
custom_edit_prediction_usage: UsageLimit,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.custom_edit_prediction_usage = custom_edit_prediction_usage;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DebugAccountState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
trial_expired: false,
|
||||||
|
plan: Plan::Free,
|
||||||
|
custom_prompt_usage: RequestUsage {
|
||||||
|
limit: UsageLimit::Unlimited,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
usage_based_billing_enabled: false,
|
||||||
|
// $50.00
|
||||||
|
monthly_spending_cap: 5000,
|
||||||
|
custom_edit_prediction_usage: UsageLimit::Unlimited,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugAccountState {
|
||||||
|
pub fn get_global(cx: &App) -> &Self {
|
||||||
|
&cx.global::<GlobalDebugAccountState>().0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GlobalDebugAccountState(pub DebugAccountState);
|
||||||
|
|
||||||
|
impl Global for GlobalDebugAccountState {}
|
||||||
|
|
||||||
|
impl Deref for GlobalDebugAccountState {
|
||||||
|
type Target = DebugAccountState;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for GlobalDebugAccountState {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DebugAccount {
|
||||||
|
fn debug_account(&self) -> &DebugAccountState;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugAccount for App {
|
||||||
|
fn debug_account(&self) -> &DebugAccountState {
|
||||||
|
&self.global::<GlobalDebugAccountState>().0
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,12 @@ use std::sync::Arc;
|
||||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||||
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
|
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
|
||||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
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 buffer_diff::BufferDiff;
|
||||||
|
use client::UserStore;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::actions::{MoveUp, Paste};
|
use editor::actions::{MoveUp, Paste};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -27,6 +31,7 @@ use language_model_selector::ToggleModelSelector;
|
||||||
use multi_buffer;
|
use multi_buffer;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
use prompt_store::PromptStore;
|
||||||
|
use proto::Plan;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
|
@ -53,6 +58,7 @@ pub struct MessageEditor {
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
user_store: Entity<UserStore>,
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
context_strip: Entity<ContextStrip>,
|
context_strip: Entity<ContextStrip>,
|
||||||
|
@ -126,6 +132,7 @@ impl MessageEditor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
user_store: Entity<UserStore>,
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: WeakEntity<ThreadStore>,
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
|
@ -188,6 +195,7 @@ impl MessageEditor {
|
||||||
Self {
|
Self {
|
||||||
editor: editor.clone(),
|
editor: editor.clone(),
|
||||||
project: thread.read(cx).project().clone(),
|
project: thread.read(cx).project().clone(),
|
||||||
|
user_store,
|
||||||
thread,
|
thread,
|
||||||
incompatible_tools_state: incompatible_tools.clone(),
|
incompatible_tools_state: incompatible_tools.clone(),
|
||||||
workspace,
|
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(
|
fn render_token_limit_callout(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
token_usage_ratio: TokenUsageRatio,
|
token_usage_ratio: TokenUsageRatio,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Div {
|
) -> Option<Div> {
|
||||||
let heading = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
if !cx.has_flag::<NewBillingFeatureFlag>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
||||||
"Thread reached the token limit"
|
"Thread reached the token limit"
|
||||||
} else {
|
} else {
|
||||||
"Thread reaching the token limit soon"
|
"Thread reaching the token limit soon"
|
||||||
};
|
};
|
||||||
|
|
||||||
h_flex()
|
let message = "Start a new thread from a summary to continue the conversation.";
|
||||||
.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());
|
|
||||||
|
|
||||||
window.dispatch_action(Box::new(NewThread {
|
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
||||||
from_thread_id
|
Icon::new(IconName::X)
|
||||||
}), cx);
|
.color(Color::Error)
|
||||||
}))
|
.size(IconSize::XSmall)
|
||||||
.icon(IconName::Plus)
|
} else {
|
||||||
.icon_position(IconPosition::Start)
|
Icon::new(IconName::Warning)
|
||||||
.icon_size(IconSize::Small)
|
.color(Color::Warning)
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
.size(IconSize::XSmall)
|
||||||
.label_size(LabelSize::Small),
|
};
|
||||||
)
|
|
||||||
|
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> {
|
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))
|
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||||
})
|
})
|
||||||
.child(self.render_editor(font_size, line_height, window, cx))
|
.child(self.render_editor(font_size, line_height, window, cx))
|
||||||
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
|
.children({
|
||||||
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
|
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 {
|
fn scope() -> ComponentScope {
|
||||||
ComponentScope::Agent
|
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 {
|
impl AgentPreview for MessageEditor {
|
||||||
|
@ -1364,16 +1379,18 @@ impl AgentPreview for MessageEditor {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
if let Some(workspace_entity) = workspace.upgrade() {
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
let fs = workspace_entity.read(cx).app_state().fs.clone();
|
let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
let weak_project = workspace_entity.read(cx).project().clone().downgrade();
|
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 context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
||||||
let thread = active_thread.read(cx).thread().clone();
|
let thread = active_thread.read(cx).thread().clone();
|
||||||
|
|
||||||
let example_message_editor = cx.new(|cx| {
|
let default_message_editor = cx.new(|cx| {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs,
|
fs,
|
||||||
workspace,
|
workspace.downgrade(),
|
||||||
|
user_store,
|
||||||
context_store,
|
context_store,
|
||||||
None,
|
None,
|
||||||
thread_store,
|
thread_store,
|
||||||
|
@ -1387,8 +1404,15 @@ impl AgentPreview for MessageEditor {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.children(vec![single_example(
|
.children(vec![single_example(
|
||||||
"Default",
|
"Default Message Editor",
|
||||||
example_message_editor.clone().into_any_element(),
|
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(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -355,6 +355,7 @@ pub struct Thread {
|
||||||
request_token_usage: Vec<TokenUsage>,
|
request_token_usage: Vec<TokenUsage>,
|
||||||
cumulative_token_usage: TokenUsage,
|
cumulative_token_usage: TokenUsage,
|
||||||
exceeded_window_error: Option<ExceededWindowError>,
|
exceeded_window_error: Option<ExceededWindowError>,
|
||||||
|
last_usage: Option<RequestUsage>,
|
||||||
tool_use_limit_reached: bool,
|
tool_use_limit_reached: bool,
|
||||||
feedback: Option<ThreadFeedback>,
|
feedback: Option<ThreadFeedback>,
|
||||||
message_feedback: HashMap<MessageId, ThreadFeedback>,
|
message_feedback: HashMap<MessageId, ThreadFeedback>,
|
||||||
|
@ -418,6 +419,7 @@ impl Thread {
|
||||||
request_token_usage: Vec::new(),
|
request_token_usage: Vec::new(),
|
||||||
cumulative_token_usage: TokenUsage::default(),
|
cumulative_token_usage: TokenUsage::default(),
|
||||||
exceeded_window_error: None,
|
exceeded_window_error: None,
|
||||||
|
last_usage: None,
|
||||||
tool_use_limit_reached: false,
|
tool_use_limit_reached: false,
|
||||||
feedback: None,
|
feedback: None,
|
||||||
message_feedback: HashMap::default(),
|
message_feedback: HashMap::default(),
|
||||||
|
@ -526,6 +528,7 @@ impl Thread {
|
||||||
request_token_usage: serialized.request_token_usage,
|
request_token_usage: serialized.request_token_usage,
|
||||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||||
exceeded_window_error: None,
|
exceeded_window_error: None,
|
||||||
|
last_usage: None,
|
||||||
tool_use_limit_reached: false,
|
tool_use_limit_reached: false,
|
||||||
feedback: None,
|
feedback: None,
|
||||||
message_feedback: HashMap::default(),
|
message_feedback: HashMap::default(),
|
||||||
|
@ -817,6 +820,10 @@ impl Thread {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_usage(&self) -> Option<RequestUsage> {
|
||||||
|
self.last_usage
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tool_use_limit_reached(&self) -> bool {
|
pub fn tool_use_limit_reached(&self) -> bool {
|
||||||
self.tool_use_limit_reached
|
self.tool_use_limit_reached
|
||||||
}
|
}
|
||||||
|
@ -1535,7 +1542,10 @@ impl Thread {
|
||||||
CompletionRequestStatus::UsageUpdated {
|
CompletionRequestStatus::UsageUpdated {
|
||||||
amount, limit
|
amount, limit
|
||||||
} => {
|
} => {
|
||||||
cx.emit(ThreadEvent::UsageUpdated(RequestUsage { limit, amount: amount as i32 }));
|
let usage = RequestUsage { limit, amount: amount as i32 };
|
||||||
|
|
||||||
|
thread.last_usage = Some(usage);
|
||||||
|
cx.emit(ThreadEvent::UsageUpdated(usage));
|
||||||
}
|
}
|
||||||
CompletionRequestStatus::ToolUseLimitReached => {
|
CompletionRequestStatus::ToolUseLimitReached => {
|
||||||
thread.tool_use_limit_reached = true;
|
thread.tool_use_limit_reached = true;
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
pub mod agent_preview;
|
|
||||||
mod animated_label;
|
mod animated_label;
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
mod max_mode_tooltip;
|
mod max_mode_tooltip;
|
||||||
|
pub mod preview;
|
||||||
mod upsell;
|
mod upsell;
|
||||||
mod usage_banner;
|
mod usage_banner;
|
||||||
|
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use agent_preview::*;
|
|
||||||
pub use animated_label::*;
|
pub use animated_label::*;
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
pub use max_mode_tooltip::*;
|
pub use max_mode_tooltip::*;
|
||||||
pub use usage_banner::*;
|
|
||||||
|
|
5
crates/agent/src/ui/preview.rs
Normal file
5
crates/agent/src/ui/preview.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod agent_preview;
|
||||||
|
mod usage_callouts;
|
||||||
|
|
||||||
|
pub use agent_preview::*;
|
||||||
|
pub use usage_callouts::*;
|
|
@ -42,14 +42,14 @@ pub trait AgentPreview: Component + Sized {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! register_agent_preview {
|
macro_rules! register_agent_preview {
|
||||||
($type:ty) => {
|
($type:ty) => {
|
||||||
#[linkme::distributed_slice($crate::ui::agent_preview::__ALL_AGENT_PREVIEWS)]
|
#[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
|
||||||
static __REGISTER_AGENT_PREVIEW: fn() -> (
|
static __REGISTER_AGENT_PREVIEW: fn() -> (
|
||||||
component::ComponentId,
|
component::ComponentId,
|
||||||
$crate::ui::agent_preview::PreviewFn,
|
$crate::ui::preview::PreviewFn,
|
||||||
) = || {
|
) = || {
|
||||||
(
|
(
|
||||||
<$type as component::Component>::id(),
|
<$type as component::Component>::id(),
|
||||||
<$type as $crate::ui::agent_preview::AgentPreview>::agent_preview,
|
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
204
crates/agent/src/ui/preview/usage_callouts.rs
Normal file
204
crates/agent/src/ui/preview/usage_callouts.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
use component::{empty_example, example_group_with_title, single_example};
|
||||||
|
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||||
|
use language_model::RequestUsage;
|
||||||
|
use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
|
||||||
|
use zed_llm_client::{Plan, UsageLimit};
|
||||||
|
|
||||||
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
|
pub struct UsageCallout {
|
||||||
|
plan: Plan,
|
||||||
|
usage: RequestUsage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsageCallout {
|
||||||
|
pub fn new(plan: Plan, usage: RequestUsage) -> Self {
|
||||||
|
Self { plan, usage }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for UsageCallout {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
let (is_limit_reached, is_approaching_limit, remaining) = match self.usage.limit {
|
||||||
|
UsageLimit::Limited(limit) => {
|
||||||
|
let percentage = self.usage.amount as f32 / limit as f32;
|
||||||
|
let is_limit_reached = percentage >= 1.0;
|
||||||
|
let is_near_limit = percentage >= 0.9 && percentage < 1.0;
|
||||||
|
(
|
||||||
|
is_limit_reached,
|
||||||
|
is_near_limit,
|
||||||
|
limit.saturating_sub(self.usage.amount),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
UsageLimit::Unlimited => (false, false, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_limit_reached && !is_approaching_limit {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (title, message, button_text, url) = if is_limit_reached {
|
||||||
|
match self.plan {
|
||||||
|
Plan::Free => (
|
||||||
|
"Out of free prompts",
|
||||||
|
"Upgrade to continue, wait for the next reset, or switch to API key."
|
||||||
|
.to_string(),
|
||||||
|
"Upgrade",
|
||||||
|
"https://zed.dev/pricing",
|
||||||
|
),
|
||||||
|
Plan::ZedProTrial => (
|
||||||
|
"Out of trial prompts",
|
||||||
|
"Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
|
||||||
|
"Upgrade",
|
||||||
|
"https://zed.dev/pricing",
|
||||||
|
),
|
||||||
|
Plan::ZedPro => (
|
||||||
|
"Out of included prompts",
|
||||||
|
"Enable usage based billing to continue.".to_string(),
|
||||||
|
"Manage",
|
||||||
|
"https://zed.dev/account",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match self.plan {
|
||||||
|
Plan::Free => (
|
||||||
|
"Reaching Free tier limit soon",
|
||||||
|
format!(
|
||||||
|
"{} remaining - Upgrade to increase limit, or switch providers",
|
||||||
|
remaining
|
||||||
|
),
|
||||||
|
"Upgrade",
|
||||||
|
"https://zed.dev/pricing",
|
||||||
|
),
|
||||||
|
Plan::ZedProTrial => (
|
||||||
|
"Reaching Trial limit soon",
|
||||||
|
format!(
|
||||||
|
"{} remaining - Upgrade to increase limit, or switch providers",
|
||||||
|
remaining
|
||||||
|
),
|
||||||
|
"Upgrade",
|
||||||
|
"https://zed.dev/pricing",
|
||||||
|
),
|
||||||
|
_ => return div().into_any_element(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = if is_limit_reached {
|
||||||
|
Icon::new(IconName::X)
|
||||||
|
.color(Color::Error)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
} else {
|
||||||
|
Icon::new(IconName::Warning)
|
||||||
|
.color(Color::Warning)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
};
|
||||||
|
|
||||||
|
Callout::multi_line(
|
||||||
|
title.into(),
|
||||||
|
message.into(),
|
||||||
|
icon,
|
||||||
|
button_text.into(),
|
||||||
|
Box::new(move |_, _, cx| {
|
||||||
|
cx.open_url(url);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for UsageCallout {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::Agent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_name() -> &'static str {
|
||||||
|
"AgentUsageCallout"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||||
|
let free_examples = example_group_with_title(
|
||||||
|
"Free Plan",
|
||||||
|
vec![
|
||||||
|
single_example(
|
||||||
|
"Approaching limit (90%)",
|
||||||
|
UsageCallout::new(
|
||||||
|
Plan::Free,
|
||||||
|
RequestUsage {
|
||||||
|
limit: UsageLimit::Limited(50),
|
||||||
|
amount: 45, // 90% of limit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Limit reached (100%)",
|
||||||
|
UsageCallout::new(
|
||||||
|
Plan::Free,
|
||||||
|
RequestUsage {
|
||||||
|
limit: UsageLimit::Limited(50),
|
||||||
|
amount: 50, // 100% of limit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let trial_examples = example_group_with_title(
|
||||||
|
"Zed Pro Trial",
|
||||||
|
vec![
|
||||||
|
single_example(
|
||||||
|
"Approaching limit (90%)",
|
||||||
|
UsageCallout::new(
|
||||||
|
Plan::ZedProTrial,
|
||||||
|
RequestUsage {
|
||||||
|
limit: UsageLimit::Limited(150),
|
||||||
|
amount: 135, // 90% of limit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Limit reached (100%)",
|
||||||
|
UsageCallout::new(
|
||||||
|
Plan::ZedProTrial,
|
||||||
|
RequestUsage {
|
||||||
|
limit: UsageLimit::Limited(150),
|
||||||
|
amount: 150, // 100% of limit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let pro_examples = example_group_with_title(
|
||||||
|
"Zed Pro",
|
||||||
|
vec![
|
||||||
|
single_example(
|
||||||
|
"Limit reached (100%)",
|
||||||
|
UsageCallout::new(
|
||||||
|
Plan::ZedPro,
|
||||||
|
RequestUsage {
|
||||||
|
limit: UsageLimit::Limited(500),
|
||||||
|
amount: 500, // 100% of limit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
empty_example("Unlimited plan (no callout shown)"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
div()
|
||||||
|
.p_4()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_4()
|
||||||
|
.child(free_examples)
|
||||||
|
.child(trial_examples)
|
||||||
|
.child(pro_examples)
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ use std::sync::LazyLock;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, pattern_slash, prelude::*,
|
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
|
||||||
px, rems,
|
prelude::*, px, rems,
|
||||||
};
|
};
|
||||||
use linkme::distributed_slice;
|
use linkme::distributed_slice;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
@ -249,13 +249,20 @@ pub struct ComponentExample {
|
||||||
pub variant_name: SharedString,
|
pub variant_name: SharedString,
|
||||||
pub description: Option<SharedString>,
|
pub description: Option<SharedString>,
|
||||||
pub element: AnyElement,
|
pub element: AnyElement,
|
||||||
|
pub width: Option<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ComponentExample {
|
impl RenderOnce for ComponentExample {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.pt_2()
|
.pt_2()
|
||||||
.w_full()
|
.map(|this| {
|
||||||
|
if let Some(width) = self.width {
|
||||||
|
this.w(width)
|
||||||
|
} else {
|
||||||
|
this.w_full()
|
||||||
|
}
|
||||||
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
|
@ -306,6 +313,7 @@ impl ComponentExample {
|
||||||
variant_name: variant_name.into(),
|
variant_name: variant_name.into(),
|
||||||
element,
|
element,
|
||||||
description: None,
|
description: None,
|
||||||
|
width: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +321,11 @@ impl ComponentExample {
|
||||||
self.description = Some(description.into());
|
self.description = Some(description.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, width: Pixels) -> Self {
|
||||||
|
self.width = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A group of component examples.
|
/// A group of component examples.
|
||||||
|
@ -320,6 +333,7 @@ impl ComponentExample {
|
||||||
pub struct ComponentExampleGroup {
|
pub struct ComponentExampleGroup {
|
||||||
pub title: Option<SharedString>,
|
pub title: Option<SharedString>,
|
||||||
pub examples: Vec<ComponentExample>,
|
pub examples: Vec<ComponentExample>,
|
||||||
|
pub width: Option<Pixels>,
|
||||||
pub grow: bool,
|
pub grow: bool,
|
||||||
pub vertical: bool,
|
pub vertical: bool,
|
||||||
}
|
}
|
||||||
|
@ -330,7 +344,13 @@ impl RenderOnce for ComponentExampleGroup {
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.w_full()
|
.map(|this| {
|
||||||
|
if let Some(width) = self.width {
|
||||||
|
this.w(width)
|
||||||
|
} else {
|
||||||
|
this.w_full()
|
||||||
|
}
|
||||||
|
})
|
||||||
.when_some(self.title, |this, title| {
|
.when_some(self.title, |this, title| {
|
||||||
this.gap_4().child(
|
this.gap_4().child(
|
||||||
div()
|
div()
|
||||||
|
@ -373,6 +393,7 @@ impl ComponentExampleGroup {
|
||||||
Self {
|
Self {
|
||||||
title: None,
|
title: None,
|
||||||
examples,
|
examples,
|
||||||
|
width: None,
|
||||||
grow: false,
|
grow: false,
|
||||||
vertical: false,
|
vertical: false,
|
||||||
}
|
}
|
||||||
|
@ -381,10 +402,15 @@ impl ComponentExampleGroup {
|
||||||
Self {
|
Self {
|
||||||
title: Some(title.into()),
|
title: Some(title.into()),
|
||||||
examples,
|
examples,
|
||||||
|
width: None,
|
||||||
grow: false,
|
grow: false,
|
||||||
vertical: false,
|
vertical: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn width(mut self, width: Pixels) -> Self {
|
||||||
|
self.width = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn grow(mut self) -> Self {
|
pub fn grow(mut self) -> Self {
|
||||||
self.grow = true;
|
self.grow = true;
|
||||||
self
|
self
|
||||||
|
@ -402,6 +428,10 @@ pub fn single_example(
|
||||||
ComponentExample::new(variant_name, example)
|
ComponentExample::new(variant_name, example)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
|
||||||
|
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
|
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
|
||||||
ComponentExampleGroup::new(examples)
|
ComponentExampleGroup::new(examples)
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,6 @@ struct ComponentPreview {
|
||||||
active_page: PreviewPage,
|
active_page: PreviewPage,
|
||||||
components: Vec<ComponentMetadata>,
|
components: Vec<ComponentMetadata>,
|
||||||
component_list: ListState,
|
component_list: ListState,
|
||||||
agent_previews: Vec<ComponentId>,
|
|
||||||
cursor_index: usize,
|
cursor_index: usize,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
@ -179,9 +178,6 @@ impl ComponentPreview {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize agent previews
|
|
||||||
let agent_previews = agent::all_agent_previews();
|
|
||||||
|
|
||||||
let mut component_preview = Self {
|
let mut component_preview = Self {
|
||||||
workspace_id: None,
|
workspace_id: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
@ -195,7 +191,6 @@ impl ComponentPreview {
|
||||||
component_map: components().0,
|
component_map: components().0,
|
||||||
components: sorted_components,
|
components: sorted_components,
|
||||||
component_list,
|
component_list,
|
||||||
agent_previews,
|
|
||||||
cursor_index: selected_index,
|
cursor_index: selected_index,
|
||||||
filter_editor,
|
filter_editor,
|
||||||
filter_text: String::new(),
|
filter_text: String::new(),
|
||||||
|
@ -707,38 +702,22 @@ impl ComponentPreview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_active_thread(
|
fn render_active_thread(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
&self,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("render-active-thread")
|
.id("render-active-thread")
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
v_flex().children(self.agent_previews.iter().filter_map(|component_id| {
|
div()
|
||||||
if let (Some(thread_store), Some(active_thread)) = (
|
.mx_auto()
|
||||||
self.thread_store.as_ref().map(|ts| ts.downgrade()),
|
.w(px(640.))
|
||||||
self.active_thread.clone(),
|
.h_full()
|
||||||
) {
|
.py_8()
|
||||||
agent::get_agent_preview(
|
.bg(cx.theme().colors().panel_background)
|
||||||
component_id,
|
.children(self.active_thread.clone().map(|thread| thread.clone()))
|
||||||
self.workspace.clone(),
|
.when_none(&self.active_thread.clone(), |this| {
|
||||||
active_thread,
|
this.child("No active thread")
|
||||||
thread_store,
|
}),
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|element| div().child(element))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.children(self.active_thread.clone().map(|thread| thread.clone()))
|
|
||||||
.when_none(&self.active_thread.clone(), |this| {
|
|
||||||
this.child("No active thread")
|
|
||||||
})
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,7 +831,7 @@ impl Render for ComponentPreview {
|
||||||
.render_component_page(&id, window, cx)
|
.render_component_page(&id, window, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
PreviewPage::ActiveThread => {
|
PreviewPage::ActiveThread => {
|
||||||
self.render_active_thread(window, cx).into_any_element()
|
self.render_active_thread(cx).into_any_element()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod avatar;
|
mod avatar;
|
||||||
mod banner;
|
mod banner;
|
||||||
mod button;
|
mod button;
|
||||||
|
mod callout;
|
||||||
mod content_group;
|
mod content_group;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod disclosure;
|
mod disclosure;
|
||||||
|
@ -41,6 +42,7 @@ mod stories;
|
||||||
pub use avatar::*;
|
pub use avatar::*;
|
||||||
pub use banner::*;
|
pub use banner::*;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
|
pub use callout::*;
|
||||||
pub use content_group::*;
|
pub use content_group::*;
|
||||||
pub use context_menu::*;
|
pub use context_menu::*;
|
||||||
pub use disclosure::*;
|
pub use disclosure::*;
|
||||||
|
|
162
crates/ui/src/components/callout.rs
Normal file
162
crates/ui/src/components/callout.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use gpui::ClickEvent;
|
||||||
|
|
||||||
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
|
pub struct Callout {
|
||||||
|
title: SharedString,
|
||||||
|
message: Option<SharedString>,
|
||||||
|
icon: Icon,
|
||||||
|
cta_label: SharedString,
|
||||||
|
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
|
line_height: Option<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Callout {
|
||||||
|
pub fn single_line(
|
||||||
|
title: SharedString,
|
||||||
|
icon: Icon,
|
||||||
|
cta_label: SharedString,
|
||||||
|
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
message: None,
|
||||||
|
icon,
|
||||||
|
cta_label,
|
||||||
|
cta_action,
|
||||||
|
line_height: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multi_line(
|
||||||
|
title: SharedString,
|
||||||
|
message: SharedString,
|
||||||
|
icon: Icon,
|
||||||
|
cta_label: SharedString,
|
||||||
|
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
message: Some(message),
|
||||||
|
icon,
|
||||||
|
cta_label,
|
||||||
|
cta_action,
|
||||||
|
line_height: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_height(mut self, line_height: Pixels) -> Self {
|
||||||
|
self.line_height = Some(line_height);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for Callout {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let line_height = self.line_height.unwrap_or(window.line_height());
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.p_2()
|
||||||
|
.gap_2()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.bg(cx.theme().colors().panel_background)
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.overflow_x_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_shrink()
|
||||||
|
.overflow_hidden()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h(line_height)
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(self.icon),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_shrink()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h(line_height)
|
||||||
|
.items_center()
|
||||||
|
.child(Label::new(self.title).size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
.when_some(self.message, |this, message| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex_1()
|
||||||
|
.child(message)
|
||||||
|
.text_ui_sm(cx)
|
||||||
|
.text_color(cx.theme().colors().text_muted),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().flex_none().child(
|
||||||
|
Button::new("cta", self.cta_label)
|
||||||
|
.on_click(self.cta_action)
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.label_size(LabelSize::Small),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Callout {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description() -> Option<&'static str> {
|
||||||
|
Some(
|
||||||
|
"Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||||
|
let callout_examples = vec![
|
||||||
|
single_example(
|
||||||
|
"Single Line",
|
||||||
|
Callout::single_line(
|
||||||
|
"Your settings contain deprecated values, please update them.".into(),
|
||||||
|
Icon::new(IconName::Warning)
|
||||||
|
.color(Color::Warning)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
"Backup & Update".into(),
|
||||||
|
Box::new(|_, _, _| {}),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
.width(px(580.)),
|
||||||
|
single_example(
|
||||||
|
"Multi Line",
|
||||||
|
Callout::multi_line(
|
||||||
|
"Thread reached the token limit".into(),
|
||||||
|
"Start a new thread from a summary to continue the conversation.".into(),
|
||||||
|
Icon::new(IconName::X)
|
||||||
|
.color(Color::Error)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
"Start New Thread".into(),
|
||||||
|
Box::new(|_, _, _| {}),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
.width(px(580.)),
|
||||||
|
];
|
||||||
|
|
||||||
|
Some(
|
||||||
|
example_group(callout_examples)
|
||||||
|
.vertical()
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue