Compare commits

..

2 Commits

Author SHA1 Message Date
Nate Butler
3772a467e0 Remove logs 2024-10-07 15:14:54 -04:00
Nate Butler
1be27e7270 wip first approach at advanced entries 2024-10-07 15:14:34 -04:00
29 changed files with 552 additions and 685 deletions

View File

@@ -3,7 +3,8 @@ name: Publish Collab Server Image
on:
push:
tags:
- collab-production
# Pause production deploys while we investigate an issue.
# - collab-production
- collab-staging
env:

105
Cargo.lock generated
View File

@@ -3009,7 +3009,7 @@ dependencies = [
"core-foundation-sys",
"coreaudio-rs",
"dasp_sample",
"jni 0.21.1",
"jni",
"js-sys",
"libc",
"mach2",
@@ -5410,19 +5410,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hoot"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936723a03900105673cf6efb8821399ebdc287a494949c375f2ce50db83bf6c9"
dependencies = [
"http 1.1.0",
"httparse",
"log",
"tinyvec",
"url",
]
[[package]]
name = "hound"
version = "3.5.1"
@@ -6116,20 +6103,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jni"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni"
version = "0.21.1"
@@ -7539,7 +7512,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni 0.21.1",
"jni",
"ndk",
"ndk-context",
"num-derive",
@@ -9731,7 +9704,6 @@ version = "0.23.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -9752,19 +9724,6 @@ dependencies = [
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.0"
@@ -9803,33 +9762,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]]
name = "rustls-platform-verifier"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490"
dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
"jni 0.19.0",
"log",
"once_cell",
"rustls 0.23.13",
"rustls-native-certs 0.7.3",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.8",
"security-framework",
"security-framework-sys",
"webpki-roots 0.26.6",
"winapi",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@@ -10122,7 +10054,6 @@ dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"num-bigint",
"security-framework-sys",
]
@@ -12764,27 +12695,18 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.0.0-rc1"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443349052924188e919be78d5870f6e827435579085947d1ae9ba32bb997e16f"
checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
dependencies = [
"base64 0.22.1",
"cc",
"base64 0.21.7",
"flate2",
"hoot",
"http 1.1.0",
"log",
"once_cell",
"rustls 0.23.13",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"smallvec",
"thiserror",
"utf-8",
"webpki-roots 0.26.6",
"rustls 0.21.12",
"rustls-webpki 0.101.7",
"url",
"webpki-roots 0.25.4",
]
[[package]]
@@ -13688,15 +13610,6 @@ version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.8"

View File

@@ -31,12 +31,10 @@ ENV GITHUB_SHA=$GITHUB_SHA
# - Staging: `4f408ec65a3867278322a189b4eb20f1ab51f508`
# - Production: `fc4c533d0a8c489e5636a4249d2b52a80039fbd7`
#
# Also add `cmake`, since we need it to build `wasmtime`.
#
# Installing these as a temporary workaround, but I think ideally we'd want to figure
# out what caused them to be included in the first place.
RUN apt-get update; \
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev cmake
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \

View File

@@ -521,10 +521,6 @@ pub struct Usage {
pub input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -485,7 +485,7 @@ impl Telemetry {
worktree_id: WorktreeId,
updated_entries_set: &UpdatedEntriesSet,
) {
let project_type_names: Vec<String> = {
let project_names: Vec<String> = {
let mut state = self.state.lock();
state
.worktree_id_map
@@ -521,8 +521,8 @@ impl Telemetry {
};
// Done on purpose to avoid calling `self.state.lock()` multiple times
for project_type_name in project_type_names {
self.report_app_event(format!("open {} project", project_type_name));
for project_name in project_names {
self.report_app_event(format!("open {} project", project_name));
}
}

View File

@@ -1,11 +0,0 @@
alter table models
add column price_per_million_cache_creation_input_tokens integer not null default 0,
add column price_per_million_cache_read_input_tokens integer not null default 0;
alter table usages
add column cache_creation_input_tokens_this_month bigint not null default 0,
add column cache_read_input_tokens_this_month bigint not null default 0;
alter table lifetime_usages
add column cache_creation_input_tokens bigint not null default 0,
add column cache_read_input_tokens bigint not null default 0;

View File

@@ -1,3 +0,0 @@
alter table usages
drop column cache_creation_input_tokens_this_month,
drop column cache_read_input_tokens_this_month;

View File

@@ -318,31 +318,22 @@ async fn perform_completion(
chunks
.map(move |event| {
let chunk = event?;
let (
input_tokens,
output_tokens,
cache_creation_input_tokens,
cache_read_input_tokens,
) = match &chunk {
let (input_tokens, output_tokens) = match &chunk {
anthropic::Event::MessageStart {
message: anthropic::Response { usage, .. },
}
| anthropic::Event::MessageDelta { usage, .. } => (
usage.input_tokens.unwrap_or(0) as usize,
usage.output_tokens.unwrap_or(0) as usize,
usage.cache_creation_input_tokens.unwrap_or(0) as usize,
usage.cache_read_input_tokens.unwrap_or(0) as usize,
),
_ => (0, 0, 0, 0),
_ => (0, 0),
};
anyhow::Ok(CompletionChunk {
bytes: serde_json::to_vec(&chunk).unwrap(),
anyhow::Ok((
serde_json::to_vec(&chunk).unwrap(),
input_tokens,
output_tokens,
cache_creation_input_tokens,
cache_read_input_tokens,
})
))
})
.boxed()
}
@@ -368,13 +359,11 @@ async fn perform_completion(
chunk.usage.as_ref().map_or(0, |u| u.prompt_tokens) as usize;
let output_tokens =
chunk.usage.as_ref().map_or(0, |u| u.completion_tokens) as usize;
CompletionChunk {
bytes: serde_json::to_vec(&chunk).unwrap(),
(
serde_json::to_vec(&chunk).unwrap(),
input_tokens,
output_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
}
)
})
})
.boxed()
@@ -398,13 +387,13 @@ async fn perform_completion(
.map(|event| {
event.map(|chunk| {
// TODO - implement token counting for Google AI
CompletionChunk {
bytes: serde_json::to_vec(&chunk).unwrap(),
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
}
let input_tokens = 0;
let output_tokens = 0;
(
serde_json::to_vec(&chunk).unwrap(),
input_tokens,
output_tokens,
)
})
})
.boxed()
@@ -418,8 +407,6 @@ async fn perform_completion(
model,
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
inner_stream: stream,
})))
}
@@ -564,14 +551,6 @@ async fn check_usage_limit(
Ok(())
}
struct CompletionChunk {
bytes: Vec<u8>,
input_tokens: usize,
output_tokens: usize,
cache_creation_input_tokens: usize,
cache_read_input_tokens: usize,
}
struct TokenCountingStream<S> {
state: Arc<LlmState>,
claims: LlmTokenClaims,
@@ -579,26 +558,22 @@ struct TokenCountingStream<S> {
model: String,
input_tokens: usize,
output_tokens: usize,
cache_creation_input_tokens: usize,
cache_read_input_tokens: usize,
inner_stream: S,
}
impl<S> Stream for TokenCountingStream<S>
where
S: Stream<Item = Result<CompletionChunk, anyhow::Error>> + Unpin,
S: Stream<Item = Result<(Vec<u8>, usize, usize), anyhow::Error>> + Unpin,
{
type Item = Result<Vec<u8>, anyhow::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.inner_stream).poll_next(cx) {
Poll::Ready(Some(Ok(mut chunk))) => {
chunk.bytes.push(b'\n');
self.input_tokens += chunk.input_tokens;
self.output_tokens += chunk.output_tokens;
self.cache_creation_input_tokens += chunk.cache_creation_input_tokens;
self.cache_read_input_tokens += chunk.cache_read_input_tokens;
Poll::Ready(Some(Ok(chunk.bytes)))
Poll::Ready(Some(Ok((mut bytes, input_tokens, output_tokens)))) => {
bytes.push(b'\n');
self.input_tokens += input_tokens;
self.output_tokens += output_tokens;
Poll::Ready(Some(Ok(bytes)))
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
@@ -615,8 +590,6 @@ impl<S> Drop for TokenCountingStream<S> {
let model = std::mem::take(&mut self.model);
let input_token_count = self.input_tokens;
let output_token_count = self.output_tokens;
let cache_creation_input_token_count = self.cache_creation_input_tokens;
let cache_read_input_token_count = self.cache_read_input_tokens;
self.state.executor.spawn_detached(async move {
let usage = state
.db
@@ -626,8 +599,6 @@ impl<S> Drop for TokenCountingStream<S> {
provider,
&model,
input_token_count,
cache_creation_input_token_count,
cache_read_input_token_count,
output_token_count,
Utc::now(),
)
@@ -659,20 +630,11 @@ impl<S> Drop for TokenCountingStream<S> {
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
cache_creation_input_token_count: cache_creation_input_token_count
as u64,
cache_read_input_token_count: cache_read_input_token_count as u64,
output_token_count: output_token_count as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
cache_creation_input_tokens_this_month: usage
.cache_creation_input_tokens_this_month
as u64,
cache_read_input_tokens_this_month: usage
.cache_read_input_tokens_this_month
as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
lifetime_spending: usage.lifetime_spending as u64,

View File

@@ -14,8 +14,6 @@ pub struct Usage {
pub tokens_this_minute: usize,
pub tokens_this_day: usize,
pub input_tokens_this_month: usize,
pub cache_creation_input_tokens_this_month: usize,
pub cache_read_input_tokens_this_month: usize,
pub output_tokens_this_month: usize,
pub spending_this_month: usize,
pub lifetime_spending: usize,
@@ -162,14 +160,17 @@ impl LlmDatabase {
.all(&*tx)
.await?;
let lifetime_usage = lifetime_usage::Entity::find()
let (lifetime_input_tokens, lifetime_output_tokens) = lifetime_usage::Entity::find()
.filter(
lifetime_usage::Column::UserId
.eq(user_id)
.and(lifetime_usage::Column::ModelId.eq(model.id)),
)
.one(&*tx)
.await?;
.await?
.map_or((0, 0), |usage| {
(usage.input_tokens as usize, usage.output_tokens as usize)
});
let requests_this_minute =
self.get_usage_for_measure(&usages, now, UsageMeasure::RequestsPerMinute)?;
@@ -179,44 +180,18 @@ impl LlmDatabase {
self.get_usage_for_measure(&usages, now, UsageMeasure::TokensPerDay)?;
let input_tokens_this_month =
self.get_usage_for_measure(&usages, now, UsageMeasure::InputTokensPerMonth)?;
let cache_creation_input_tokens_this_month = self.get_usage_for_measure(
&usages,
now,
UsageMeasure::CacheCreationInputTokensPerMonth,
)?;
let cache_read_input_tokens_this_month = self.get_usage_for_measure(
&usages,
now,
UsageMeasure::CacheReadInputTokensPerMonth,
)?;
let output_tokens_this_month =
self.get_usage_for_measure(&usages, now, UsageMeasure::OutputTokensPerMonth)?;
let spending_this_month = calculate_spending(
model,
input_tokens_this_month,
cache_creation_input_tokens_this_month,
cache_read_input_tokens_this_month,
output_tokens_this_month,
);
let lifetime_spending = if let Some(lifetime_usage) = lifetime_usage {
calculate_spending(
model,
lifetime_usage.input_tokens as usize,
lifetime_usage.cache_creation_input_tokens as usize,
lifetime_usage.cache_read_input_tokens as usize,
lifetime_usage.output_tokens as usize,
)
} else {
0
};
let spending_this_month =
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
let lifetime_spending =
calculate_spending(model, lifetime_input_tokens, lifetime_output_tokens);
Ok(Usage {
requests_this_minute,
tokens_this_minute,
tokens_this_day,
input_tokens_this_month,
cache_creation_input_tokens_this_month,
cache_read_input_tokens_this_month,
output_tokens_this_month,
spending_this_month,
lifetime_spending,
@@ -233,8 +208,6 @@ impl LlmDatabase {
provider: LanguageModelProvider,
model_name: &str,
input_token_count: usize,
cache_creation_input_tokens: usize,
cache_read_input_tokens: usize,
output_token_count: usize,
now: DateTimeUtc,
) -> Result<Usage> {
@@ -262,10 +235,6 @@ impl LlmDatabase {
&tx,
)
.await?;
let total_token_count = input_token_count
+ cache_read_input_tokens
+ cache_creation_input_tokens
+ output_token_count;
let tokens_this_minute = self
.update_usage_for_measure(
user_id,
@@ -274,7 +243,7 @@ impl LlmDatabase {
&usages,
UsageMeasure::TokensPerMinute,
now,
total_token_count,
input_token_count + output_token_count,
&tx,
)
.await?;
@@ -286,7 +255,7 @@ impl LlmDatabase {
&usages,
UsageMeasure::TokensPerDay,
now,
total_token_count,
input_token_count + output_token_count,
&tx,
)
.await?;
@@ -302,30 +271,6 @@ impl LlmDatabase {
&tx,
)
.await?;
let cache_creation_input_tokens_this_month = self
.update_usage_for_measure(
user_id,
is_staff,
model.id,
&usages,
UsageMeasure::CacheCreationInputTokensPerMonth,
now,
cache_creation_input_tokens,
&tx,
)
.await?;
let cache_read_input_tokens_this_month = self
.update_usage_for_measure(
user_id,
is_staff,
model.id,
&usages,
UsageMeasure::CacheReadInputTokensPerMonth,
now,
cache_read_input_tokens,
&tx,
)
.await?;
let output_tokens_this_month = self
.update_usage_for_measure(
user_id,
@@ -338,13 +283,8 @@ impl LlmDatabase {
&tx,
)
.await?;
let spending_this_month = calculate_spending(
model,
input_tokens_this_month,
cache_creation_input_tokens_this_month,
cache_read_input_tokens_this_month,
output_tokens_this_month,
);
let spending_this_month =
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
// Update lifetime usage
let lifetime_usage = lifetime_usage::Entity::find()
@@ -363,12 +303,6 @@ impl LlmDatabase {
input_tokens: ActiveValue::set(
usage.input_tokens + input_token_count as i64,
),
cache_creation_input_tokens: ActiveValue::set(
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
),
cache_read_input_tokens: ActiveValue::set(
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
),
output_tokens: ActiveValue::set(
usage.output_tokens + output_token_count as i64,
),
@@ -393,8 +327,6 @@ impl LlmDatabase {
let lifetime_spending = calculate_spending(
model,
lifetime_usage.input_tokens as usize,
lifetime_usage.cache_creation_input_tokens as usize,
lifetime_usage.cache_read_input_tokens as usize,
lifetime_usage.output_tokens as usize,
);
@@ -403,8 +335,6 @@ impl LlmDatabase {
tokens_this_minute,
tokens_this_day,
input_tokens_this_month,
cache_creation_input_tokens_this_month,
cache_read_input_tokens_this_month,
output_tokens_this_month,
spending_this_month,
lifetime_spending,
@@ -571,24 +501,13 @@ impl LlmDatabase {
fn calculate_spending(
model: &model::Model,
input_tokens_this_month: usize,
cache_creation_input_tokens_this_month: usize,
cache_read_input_tokens_this_month: usize,
output_tokens_this_month: usize,
) -> usize {
let input_token_cost =
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
* model.price_per_million_cache_creation_input_tokens as usize
/ 1_000_000;
let cache_read_input_token_cost = cache_read_input_tokens_this_month
* model.price_per_million_cache_read_input_tokens as usize
/ 1_000_000;
let output_token_cost =
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
input_token_cost
+ cache_creation_input_token_cost
+ cache_read_input_token_cost
+ output_token_cost
input_token_cost + output_token_cost
}
const MINUTE_BUCKET_COUNT: usize = 12;
@@ -602,8 +521,6 @@ impl UsageMeasure {
UsageMeasure::TokensPerMinute => MINUTE_BUCKET_COUNT,
UsageMeasure::TokensPerDay => DAY_BUCKET_COUNT,
UsageMeasure::InputTokensPerMonth => MONTH_BUCKET_COUNT,
UsageMeasure::CacheCreationInputTokensPerMonth => MONTH_BUCKET_COUNT,
UsageMeasure::CacheReadInputTokensPerMonth => MONTH_BUCKET_COUNT,
UsageMeasure::OutputTokensPerMonth => MONTH_BUCKET_COUNT,
}
}
@@ -614,8 +531,6 @@ impl UsageMeasure {
UsageMeasure::TokensPerMinute => Duration::minutes(1),
UsageMeasure::TokensPerDay => Duration::hours(24),
UsageMeasure::InputTokensPerMonth => Duration::days(30),
UsageMeasure::CacheCreationInputTokensPerMonth => Duration::days(30),
UsageMeasure::CacheReadInputTokensPerMonth => Duration::days(30),
UsageMeasure::OutputTokensPerMonth => Duration::days(30),
}
}

View File

@@ -9,8 +9,6 @@ pub struct Model {
pub user_id: UserId,
pub model_id: ModelId,
pub input_tokens: i64,
pub cache_creation_input_tokens: i64,
pub cache_read_input_tokens: i64,
pub output_tokens: i64,
}

View File

@@ -14,8 +14,6 @@ pub struct Model {
pub max_tokens_per_minute: i64,
pub max_tokens_per_day: i64,
pub price_per_million_input_tokens: i32,
pub price_per_million_cache_creation_input_tokens: i32,
pub price_per_million_cache_read_input_tokens: i32,
pub price_per_million_output_tokens: i32,
}

View File

@@ -10,8 +10,6 @@ pub enum UsageMeasure {
TokensPerMinute,
TokensPerDay,
InputTokensPerMonth,
CacheCreationInputTokensPerMonth,
CacheReadInputTokensPerMonth,
OutputTokensPerMonth,
}

View File

@@ -33,12 +33,12 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
let user_id = UserId::from_proto(123);
let now = t0;
db.record_usage(user_id, false, provider, model, 1000, 0, 0, 0, now)
db.record_usage(user_id, false, provider, model, 1000, 0, now)
.await
.unwrap();
let now = t0 + Duration::seconds(10);
db.record_usage(user_id, false, provider, model, 2000, 0, 0, 0, now)
db.record_usage(user_id, false, provider, model, 2000, 0, now)
.await
.unwrap();
@@ -50,8 +50,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 3000,
tokens_this_day: 3000,
input_tokens_this_month: 3000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
@@ -67,8 +65,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 2000,
tokens_this_day: 3000,
input_tokens_this_month: 3000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
@@ -76,7 +72,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
);
let now = t0 + Duration::seconds(60);
db.record_usage(user_id, false, provider, model, 3000, 0, 0, 0, now)
db.record_usage(user_id, false, provider, model, 3000, 0, now)
.await
.unwrap();
@@ -88,8 +84,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 5000,
tokens_this_day: 6000,
input_tokens_this_month: 6000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
@@ -106,15 +100,13 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 0,
tokens_this_day: 5000,
input_tokens_this_month: 6000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
}
);
db.record_usage(user_id, false, provider, model, 4000, 0, 0, 0, now)
db.record_usage(user_id, false, provider, model, 4000, 0, now)
.await
.unwrap();
@@ -126,8 +118,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 4000,
tokens_this_day: 9000,
input_tokens_this_month: 10000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
@@ -144,50 +134,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
tokens_this_minute: 0,
tokens_this_day: 0,
input_tokens_this_month: 9000,
cache_creation_input_tokens_this_month: 0,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
}
);
// Test cache creation input tokens
db.record_usage(user_id, false, provider, model, 1000, 500, 0, 0, now)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
usage,
Usage {
requests_this_minute: 1,
tokens_this_minute: 1500,
tokens_this_day: 1500,
input_tokens_this_month: 10000,
cache_creation_input_tokens_this_month: 500,
cache_read_input_tokens_this_month: 0,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,
}
);
// Test cache read input tokens
db.record_usage(user_id, false, provider, model, 1000, 0, 300, 0, now)
.await
.unwrap();
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(
usage,
Usage {
requests_this_minute: 2,
tokens_this_minute: 2800,
tokens_this_day: 2800,
input_tokens_this_month: 11000,
cache_creation_input_tokens_this_month: 500,
cache_read_input_tokens_this_month: 300,
output_tokens_this_month: 0,
spending_this_month: 0,
lifetime_spending: 0,

View File

@@ -12,15 +12,11 @@ pub struct LlmUsageEventRow {
pub model: String,
pub provider: String,
pub input_token_count: u64,
pub cache_creation_input_token_count: u64,
pub cache_read_input_token_count: u64,
pub output_token_count: u64,
pub requests_this_minute: u64,
pub tokens_this_minute: u64,
pub tokens_this_day: u64,
pub input_tokens_this_month: u64,
pub cache_creation_input_tokens_this_month: u64,
pub cache_read_input_tokens_this_month: u64,
pub output_tokens_this_month: u64,
pub spending_this_month: u64,
pub lifetime_spending: u64,

View File

@@ -1533,8 +1533,4 @@ impl HttpClient for NullHttpClient {
fn proxy(&self) -> Option<&http_client::Uri> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
}

View File

@@ -1,4 +1,4 @@
use std::{any::type_name, borrow::Cow, fmt::Debug, io::Read, pin::Pin, task::Poll};
use std::{borrow::Cow, io::Read, pin::Pin, task::Poll};
use futures::{AsyncRead, AsyncReadExt};
@@ -34,21 +34,6 @@ impl AsyncBody {
}
}
impl Debug for AsyncBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(type_name::<Self>())
.field(
"Kind",
&match self.0 {
Inner::Empty => "Empty",
Inner::SyncReader(_) => "SyncReader",
Inner::AsyncReader(_) => "AsyncReader",
},
)
.finish()
}
}
impl Default for AsyncBody {
fn default() -> Self {
Self(Inner::Empty)

View File

@@ -11,7 +11,6 @@ use http::request::Builder;
#[cfg(feature = "test-support")]
use std::fmt;
use std::{
any::type_name,
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
@@ -73,8 +72,6 @@ impl HttpRequestExt for http::request::Builder {
}
pub trait HttpClient: 'static + Send + Sync {
fn type_name(&self) -> &'static str;
fn send(
&self,
req: http::Request<AsyncBody>,
@@ -94,6 +91,7 @@ pub trait HttpClient: 'static + Send + Sync {
RedirectPolicy::NoFollow
})
.body(body);
match request {
Ok(request) => Box::pin(async move { self.send(request).await.map_err(Into::into) }),
Err(e) => Box::pin(async move { Err(e.into()) }),
@@ -156,10 +154,6 @@ impl HttpClient for HttpClientWithProxy {
fn proxy(&self) -> Option<&Uri> {
self.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
}
impl HttpClient for Arc<HttpClientWithProxy> {
@@ -173,10 +167,6 @@ impl HttpClient for Arc<HttpClientWithProxy> {
fn proxy(&self) -> Option<&Uri> {
self.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
}
/// An [`HttpClient`] that has a base URL.
@@ -288,10 +278,6 @@ impl HttpClient for Arc<HttpClientWithUrl> {
fn proxy(&self) -> Option<&Uri> {
self.client.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
}
impl HttpClient for HttpClientWithUrl {
@@ -305,10 +291,6 @@ impl HttpClient for HttpClientWithUrl {
fn proxy(&self) -> Option<&Uri> {
self.client.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
}
pub fn read_proxy_from_env() -> Option<Uri> {
@@ -349,10 +331,6 @@ impl HttpClient for BlockedHttpClient {
fn proxy(&self) -> Option<&Uri> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
}
#[cfg(feature = "test-support")]
@@ -425,8 +403,4 @@ impl HttpClient for FakeHttpClient {
fn proxy(&self) -> Option<&Uri> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
}

View File

@@ -279,13 +279,6 @@ impl DevServerProjects {
match connection.await {
Some(_) => this
.update(&mut cx, |this, cx| {
let _ = this.workspace.update(cx, |workspace, _| {
workspace
.client()
.telemetry()
.report_app_event("create ssh server".to_string())
});
this.add_ssh_server(connection_options, cx);
this.mode = Mode::Default(None);
cx.notify()
@@ -429,15 +422,7 @@ impl DevServerProjects {
);
cx.new_view(|cx| {
let workspace =
Workspace::new(None, project.clone(), app_state.clone(), cx);
workspace
.client()
.telemetry()
.report_app_event("create ssh project".to_string());
workspace
Workspace::new(None, project.clone(), app_state.clone(), cx)
})
})
.log_err();

View File

@@ -16,8 +16,8 @@ path = "src/reqwest_client.rs"
doctest = true
[[example]]
name = "reqwest_example"
path = "examples/reqwest_example.rs"
name = "client"
path = "examples/client.rs"
[dependencies]
anyhow.workspace = true

View File

@@ -1,4 +1,4 @@
use std::{any::type_name, borrow::Cow, io::Read, pin::Pin, task::Poll};
use std::{borrow::Cow, io::Read, pin::Pin, task::Poll};
use anyhow::anyhow;
use bytes::{BufMut, Bytes, BytesMut};
@@ -163,12 +163,8 @@ impl futures::stream::Stream for WrappedBody {
WrappedBodyInner::SyncReader(cursor) => {
let mut buf = Vec::new();
match cursor.read_to_end(&mut buf) {
Ok(bytes) => {
if bytes == 0 {
return Poll::Ready(None);
} else {
return Poll::Ready(Some(Ok(Bytes::from(buf))));
}
Ok(_) => {
return Poll::Ready(Some(Ok(Bytes::from(buf))));
}
Err(e) => return Poll::Ready(Some(Err(e))),
}
@@ -187,10 +183,6 @@ impl http_client::HttpClient for ReqwestClient {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
fn send(
&self,
req: http::Request<http_client::AsyncBody>,
@@ -238,22 +230,3 @@ impl http_client::HttpClient for ReqwestClient {
.boxed()
}
}
#[cfg(test)]
mod test {
use core::str;
use http_client::AsyncBody;
use smol::stream::StreamExt;
use crate::WrappedBody;
#[tokio::test]
async fn test_sync_streaming_upload() {
let mut body = WrappedBody::new(AsyncBody::from("hello there".to_string())).fuse();
let result = body.next().await.unwrap().unwrap();
assert!(body.next().await.is_none());
assert_eq!(str::from_utf8(&result).unwrap(), "hello there");
}
}

View File

@@ -584,7 +584,17 @@ impl TitleBar {
.separator()
})
.action("Settings", zed_actions::OpenSettings.boxed_clone())
.advanced_action(
"Default Settings",
zed_actions::OpenSettings.boxed_clone(),
Some("Settings".into()),
)
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.advanced_action(
"Some Advanced",
Box::new(zed_actions::DecreaseUiFontSize),
None,
)
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.separator()
@@ -613,7 +623,17 @@ impl TitleBar {
.menu(|cx| {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.advanced_action(
"Default Settings",
zed_actions::OpenSettings.boxed_clone(),
Some("Settings".into()),
)
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.advanced_action(
"Some Advanced",
Box::new(zed_actions::DecreaseUiFontSize),
None,
)
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
})

View File

@@ -5,7 +5,7 @@ use crate::{
};
use gpui::{
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
IntoElement, Render, Subscription, View, VisualContext,
IntoElement, Modifiers, ModifiersChangedEvent, Render, Subscription, View, VisualContext,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use settings::Settings;
@@ -24,6 +24,16 @@ enum ContextMenuItem {
action: Option<Box<dyn Action>>,
disabled: bool,
},
AdvancedEntry {
toggle: Option<(IconPosition, bool)>,
label: SharedString,
icon: Option<IconName>,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
action: Option<Box<dyn Action>>,
disabled: bool,
/// Allows the advanced entry to specify a entry to hide when it is visible, enabling a "swapping" effect between the two.
hide_other_entry_when_visible: Option<SharedString>,
},
CustomEntry {
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
@@ -33,11 +43,14 @@ enum ContextMenuItem {
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
item_visibility: Vec<bool>,
focus_handle: FocusHandle,
action_context: Option<FocusHandle>,
selected_index: Option<usize>,
show_advanced: bool,
delayed: bool,
clicked: bool,
init_modifiers: Option<Modifiers>,
_on_blur_subscription: Subscription,
}
@@ -62,18 +75,22 @@ impl ContextMenu {
this.cancel(&menu::Cancel, cx)
});
cx.refresh();
f(
Self {
items: Default::default(),
focus_handle,
action_context: None,
selected_index: None,
delayed: false,
clicked: false,
_on_blur_subscription,
},
cx,
)
let mut menu = Self {
items: Default::default(),
item_visibility: Default::default(),
focus_handle,
action_context: None,
selected_index: None,
delayed: false,
clicked: false,
show_advanced: false,
init_modifiers: None,
_on_blur_subscription,
};
menu = f(menu, cx);
menu.item_visibility = vec![true; menu.items.len()];
menu.handle_update_items(cx);
menu
})
}
@@ -109,6 +126,26 @@ impl ContextMenu {
self
}
/// Adds an entry that is only visible when `show_advanced` is enabled.
pub fn advanced_entry(
mut self,
label: impl Into<SharedString>,
action: Option<Box<dyn Action>>,
handler: impl Fn(&mut WindowContext) + 'static,
hidden_entry_when_visible: impl Into<Option<SharedString>>,
) -> Self {
self.items.push(ContextMenuItem::AdvancedEntry {
toggle: None,
label: label.into(),
handler: Rc::new(move |_, cx| handler(cx)),
icon: None,
action,
disabled: false,
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
});
self
}
pub fn toggleable_entry(
mut self,
label: impl Into<SharedString>,
@@ -176,6 +213,30 @@ impl ContextMenu {
self
}
pub fn advanced_action(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
hidden_entry_when_visible: impl Into<Option<SharedString>>,
) -> Self {
self.items.push(ContextMenuItem::AdvancedEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
handler: Rc::new(move |context, cx| {
if let Some(context) = &context {
cx.focus(context);
}
cx.dispatch_action(action.boxed_clone());
}),
icon: None,
disabled: false,
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
});
self
}
pub fn disabled_action(
mut self,
label: impl Into<SharedString>,
@@ -323,6 +384,64 @@ impl ContextMenu {
self._on_blur_subscription = new_subscription;
self
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) {
let Some(init_modifiers) = self.init_modifiers else {
self.init_modifiers = Some(event.modifiers);
return;
};
if event.modifiers.alt != init_modifiers.alt {
self.show_advanced(event.modifiers.alt, cx);
}
self.init_modifiers = Some(event.modifiers);
}
fn show_advanced(&mut self, show_advanced: bool, cx: &mut ViewContext<Self>) {
self.show_advanced = show_advanced;
self.handle_update_items(cx);
}
fn handle_update_items(&mut self, cx: &mut ViewContext<Self>) {
self.item_visibility.clear();
self.item_visibility.reserve(self.items.len());
for item in &self.items {
match item {
ContextMenuItem::AdvancedEntry {
hide_other_entry_when_visible,
..
} => {
let visible = self.show_advanced;
self.item_visibility.push(visible);
if visible {
if let Some(hidden_label) = hide_other_entry_when_visible {
// Hide the corresponding entry
if let Some(pos) = self.items.iter().position(|i| {
matches!(i, ContextMenuItem::Entry { label, .. } if label == hidden_label)
}) {
self.item_visibility[pos] = false;
}
}
}
}
ContextMenuItem::Entry { label, .. } => {
let visible = !self.show_advanced || !self.items.iter().any(|i| {
matches!(i, ContextMenuItem::AdvancedEntry { hide_other_entry_when_visible: Some(hidden), .. } if hidden == label)
});
self.item_visibility.push(visible);
}
_ => self.item_visibility.push(true),
}
}
cx.notify();
}
}
impl ContextMenuItem {
@@ -332,6 +451,7 @@ impl ContextMenuItem {
| ContextMenuItem::Separator
| ContextMenuItem::Label { .. } => false,
ContextMenuItem::Entry { disabled, .. } => !disabled,
ContextMenuItem::AdvancedEntry { disabled, .. } => !disabled,
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
}
}
@@ -357,6 +477,7 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::select_prev))
.on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel))
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(!self.delayed, |mut el| {
for item in self.items.iter() {
if let ContextMenuItem::Entry {
@@ -374,119 +495,106 @@ impl Render for ContextMenu {
el
})
.flex_none()
.child(List::new().children(self.items.iter_mut().enumerate().map(
|(ix, item)| {
match item {
ContextMenuItem::Separator => ListSeparator.into_any_element(),
ContextMenuItem::Header(header) => {
ListSubHeader::new(header.clone())
.inset(true)
.into_any_element()
.child(
List::new().children(self.items.iter_mut().enumerate().filter_map(
|(ix, item)| {
if !self.item_visibility[ix] {
return None;
}
ContextMenuItem::Label(label) => ListItem::new(ix)
.inset(true)
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry {
toggle,
label,
handler,
icon,
action,
disabled,
} => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let color = if *disabled {
Color::Muted
} else {
Color::Default
};
let label_element = if let Some(icon) = icon {
h_flex()
.gap_1()
.child(Label::new(label.clone()).color(color))
.child(
Icon::new(*icon).size(IconSize::Small).color(color),
)
Some(match item {
ContextMenuItem::Separator => ListSeparator.into_any_element(),
ContextMenuItem::Header(header) => {
ListSubHeader::new(header.clone())
.inset(true)
.into_any_element()
} else {
Label::new(label.clone()).color(color).into_any_element()
};
ListItem::new(ix)
}
ContextMenuItem::Label(label) => ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.selected(Some(ix) == self.selected_index)
.when_some(*toggle, |list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
Icon::new(IconName::Check).color(Color::Accent),
)
} else {
v_flex()
.flex_none()
.size(IconSize::default().rems())
};
match position {
IconPosition::Start => {
list_item.start_slot(contents)
}
IconPosition::End => list_item.end_slot(contents),
}
})
.child(
h_flex()
.w_full()
.justify_between()
.child(label_element)
.debug_selector(|| format!("MENU_ITEM-{}", label))
.children(action.as_ref().and_then(|action| {
self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(
&**action, focus, cx,
)
})
.unwrap_or_else(|| {
KeyBinding::for_action(&**action, cx)
})
.map(|binding| div().ml_4().child(binding))
})),
)
.on_click({
let context = self.action_context.clone();
move |_, cx| {
handler(context.as_ref(), cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
})
.ok();
}
})
.into_any_element()
}
ContextMenuItem::CustomEntry {
entry_render,
handler,
selectable,
} => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.selected(if selectable {
Some(ix) == self.selected_index
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry {
toggle,
label,
handler,
icon,
action,
disabled,
} => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let color = if *disabled {
Color::Muted
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
Color::Default
};
let label_element = if let Some(icon) = icon {
h_flex()
.gap_1()
.child(Label::new(label.clone()).color(color))
.child(
Icon::new(*icon)
.size(IconSize::Small)
.color(color),
)
.into_any_element()
} else {
Label::new(label.clone())
.color(color)
.into_any_element()
};
ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.selected(Some(ix) == self.selected_index)
.when_some(*toggle, |list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
Icon::new(IconName::Check)
.color(Color::Accent),
)
} else {
v_flex()
.flex_none()
.size(IconSize::default().rems())
};
match position {
IconPosition::Start => {
list_item.start_slot(contents)
}
IconPosition::End => {
list_item.end_slot(contents)
}
}
})
.child(
h_flex()
.w_full()
.justify_between()
.child(label_element)
.debug_selector(|| {
format!("MENU_ITEM-{}", label)
})
.children(action.as_ref().and_then(|action| {
self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(
&**action, focus, cx,
)
})
.unwrap_or_else(|| {
KeyBinding::for_action(
&**action, cx,
)
})
.map(|binding| {
div().ml_4().child(binding)
})
})),
)
.on_click({
let context = self.action_context.clone();
move |_, cx| {
handler(context.as_ref(), cx);
@@ -497,13 +605,139 @@ impl Render for ContextMenu {
.ok();
}
})
})
.child(entry_render(cx))
.into_any_element()
}
}
},
))),
.into_any_element()
}
ContextMenuItem::AdvancedEntry {
toggle,
label,
handler,
icon,
action,
disabled,
..
} => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let color = if *disabled {
Color::Muted
} else {
Color::Default
};
let label_element = if let Some(icon) = icon {
h_flex()
.gap_1()
.child(Label::new(label.clone()).color(color))
.child(
Icon::new(*icon)
.size(IconSize::Small)
.color(color),
)
.into_any_element()
} else {
Label::new(label.clone())
.color(color)
.into_any_element()
};
ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.selected(Some(ix) == self.selected_index)
.when_some(*toggle, |list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
Icon::new(IconName::Check)
.color(Color::Accent),
)
} else {
v_flex()
.flex_none()
.size(IconSize::default().rems())
};
match position {
IconPosition::Start => {
list_item.start_slot(contents)
}
IconPosition::End => {
list_item.end_slot(contents)
}
}
})
.child(
h_flex()
.w_full()
.justify_between()
.child(label_element)
.debug_selector(|| {
format!("MENU_ITEM-{}", label)
})
.children(action.as_ref().and_then(|action| {
self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(
&**action, focus, cx,
)
})
.unwrap_or_else(|| {
KeyBinding::for_action(
&**action, cx,
)
})
.map(|binding| {
div().ml_4().child(binding)
})
})),
)
.on_click({
let context = self.action_context.clone();
move |_, cx| {
handler(context.as_ref(), cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
})
.ok();
}
})
.into_any_element()
}
ContextMenuItem::CustomEntry {
entry_render,
handler,
selectable,
} => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.selected(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
move |_, cx| {
handler(context.as_ref(), cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
})
.ok();
}
})
})
.child(entry_render(cx))
.into_any_element()
}
})
},
)),
),
),
)
}

View File

@@ -16,8 +16,8 @@ path = "src/ureq_client.rs"
doctest = true
[[example]]
name = "ureq_example"
path = "examples/ureq_example.rs"
name = "client"
path = "examples/client.rs"
[dependencies]
anyhow.workspace = true
@@ -27,9 +27,5 @@ http_client.workspace = true
parking_lot.workspace = true
serde.workspace = true
smol.workspace = true
ureq = "3.0.0-rc1"
ureq = "=2.9.1"
util.workspace = true
[dev-dependencies]
gpui = {workspace = true, features = ["test-support"]}

View File

@@ -0,0 +1,24 @@
use futures::AsyncReadExt;
use http_client::{AsyncBody, HttpClient};
use ureq_client::UreqClient;
fn main() {
gpui::App::headless().run(|cx| {
println!("{:?}", std::thread::current().id());
cx.spawn(|cx| async move {
let resp = UreqClient::new(
None,
"Conrad's bot".to_string(),
cx.background_executor().clone(),
)
.get("http://zed.dev", AsyncBody::empty(), true)
.await
.unwrap();
let mut body = String::new();
resp.into_body().read_to_string(&mut body).await.unwrap();
println!("{}", body);
})
.detach();
})
}

View File

@@ -1,40 +0,0 @@
use http_client::{AsyncBody, HttpClient};
use ureq_client::UreqClient;
fn main() {
gpui::App::headless().run(|cx| {
println!("{:?}", std::thread::current().id());
cx.spawn(|cx| async move {
let client = UreqClient::new(
None,
"Conrad's bot".to_string(),
cx.background_executor().clone(),
);
let resp = client
.get("http://zed.dev", AsyncBody::empty(), false)
.await
.unwrap();
let mut body = String::new();
futures::AsyncReadExt::read_to_string(&mut resp.into_body(), &mut body)
.await
.unwrap();
println!("{}", body);
// Test sync read
let resp = client
.get("http://zed.dev", AsyncBody::empty(), false)
.await
.unwrap();
let mut body = String::new();
std::io::Read::read_to_string(&mut resp.into_body(), &mut body).unwrap();
println!("{}", body);
cx.update(|cx| {
cx.quit();
})
})
.detach();
})
}

View File

@@ -1,4 +1,6 @@
use std::any::type_name;
use std::collections::HashMap;
use std::io::Read;
use std::sync::Arc;
use std::time::Duration;
use std::{pin::Pin, task::Poll};
@@ -6,14 +8,20 @@ use anyhow::Error;
use futures::channel::mpsc;
use futures::future::BoxFuture;
use futures::{AsyncRead, SinkExt, StreamExt};
use http_client::http::Response;
use http_client::{http, AsyncBody, HttpClient, RedirectPolicy, Request, Uri};
use http_client::{http, AsyncBody, HttpClient, RedirectPolicy, Uri};
use smol::future::FutureExt;
use ureq::{Agent, Proxy, SendBody, Timeouts};
use util::ResultExt;
pub struct UreqClient {
agent: ureq::Agent,
// Note in ureq 2.x the options are stored on the Agent.
// In ureq 3.x we'll be able to set these on the request.
// In practice it's probably "fine" to have many clients, the number of distinct options
// is low; and most requests to the same connection will have the same options so the
// connection pool will work.
clients: Arc<parking_lot::Mutex<HashMap<(Duration, RedirectPolicy), ureq::Agent>>>,
proxy_url: Option<Uri>,
proxy: Option<ureq::Proxy>,
user_agent: String,
background_executor: gpui::BackgroundExecutor,
}
@@ -23,75 +31,90 @@ impl UreqClient {
user_agent: String,
background_executor: gpui::BackgroundExecutor,
) -> Self {
let agent: ureq::Agent = ureq::Config {
http_status_as_error: false,
proxy: proxy_url.and_then(|url| Proxy::new(&url.to_string()).log_err()),
timeouts: Timeouts {
connect: Some(Duration::from_secs(5)),
..Default::default()
},
user_agent: Some(user_agent),
..Default::default()
}
.into();
Self {
agent,
clients: Arc::default(),
proxy_url: proxy_url.clone(),
proxy: proxy_url.and_then(|url| ureq::Proxy::new(url.to_string()).log_err()),
user_agent,
background_executor,
}
}
}
fn agent_for(&self, redirect_policy: RedirectPolicy, timeout: Duration) -> ureq::Agent {
let mut clients = self.clients.lock();
// in case our assumption of distinct options is wrong, we'll sporadically clean it out.
if clients.len() > 50 {
clients.clear()
}
clients
.entry((timeout, redirect_policy.clone()))
.or_insert_with(|| {
let mut builder = ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(5))
.timeout_read(timeout)
.timeout_write(timeout)
.user_agent(&self.user_agent)
.tls_config(http_client::TLS_CONFIG.clone())
.redirects(match redirect_policy {
RedirectPolicy::NoFollow => 0,
RedirectPolicy::FollowLimit(limit) => limit,
RedirectPolicy::FollowAll => 100,
});
if let Some(proxy) = &self.proxy {
builder = builder.proxy(proxy.clone());
}
builder.build()
})
.clone()
}
}
impl HttpClient for UreqClient {
fn proxy(&self) -> Option<&Uri> {
self.agent.config().proxy.as_ref().map(|proxy| proxy.uri())
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
self.proxy_url.as_ref()
}
fn send(
&self,
request: http::Request<AsyncBody>,
) -> BoxFuture<'static, Result<http::Response<AsyncBody>, Error>> {
let redirect_policy = request
.extensions()
.get::<RedirectPolicy>()
.cloned()
.unwrap_or_default();
let timeout = request
.extensions()
.get::<http_client::ReadTimeout>()
.cloned()
.unwrap_or_default()
.0;
let agent = self.agent.clone();
let agent = self.agent_for(
request
.extensions()
.get::<RedirectPolicy>()
.cloned()
.unwrap_or_default(),
request
.extensions()
.get::<http_client::ReadTimeout>()
.cloned()
.unwrap_or_default()
.0,
);
let mut req = agent.request(&request.method().as_ref(), &request.uri().to_string());
for (name, value) in request.headers().into_iter() {
req = req.set(name.as_str(), value.to_str().unwrap());
}
let body = request.into_body();
let executor = self.background_executor.clone();
self.background_executor
.spawn(async move {
let (parts, body) = request.into_parts();
let body = match body.0 {
http_client::Inner::Empty => SendBody::none(),
_ => SendBody::from_owned_reader(body),
};
let mut request = Request::from_parts(parts, body);
let response = req.send(body)?;
let config = agent.configure_request(&mut request);
config.max_redirects = match redirect_policy {
RedirectPolicy::NoFollow => 0,
RedirectPolicy::FollowLimit(limit) => limit,
RedirectPolicy::FollowAll => 100,
};
config.timeouts.recv_body = Some(timeout);
let mut builder = http::Response::builder()
.status(response.status())
.version(http::Version::HTTP_11);
for name in response.headers_names() {
if let Some(value) = response.header(&name) {
builder = builder.header(name, value);
}
}
let response = agent.run(request)?;
let body = AsyncBody::from_reader(UreqResponseReader::new(executor, response));
let http_response = builder.body(body)?;
let (parts, response_body) = response.into_parts();
let response_body =
AsyncBody::from_reader(UreqResponseReader::new(executor, response_body));
Ok(Response::from_parts(parts, response_body))
Ok(http_response)
})
.boxed()
}
@@ -105,14 +128,13 @@ struct UreqResponseReader {
}
impl UreqResponseReader {
fn new(background_executor: gpui::BackgroundExecutor, response: ureq::Body) -> Self {
fn new(background_executor: gpui::BackgroundExecutor, response: ureq::Response) -> Self {
let (mut sender, receiver) = mpsc::channel(1);
let mut reader = response.into_reader();
let task = background_executor.spawn(async move {
let mut buffer = vec![0; 8192];
loop {
let n = match std::io::Read::read(&mut reader, &mut buffer) {
let n = match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
@@ -160,7 +182,6 @@ impl AsyncRead for UreqResponseReader {
self.buffer.clear();
self.idx = 0;
}
Poll::Ready(Ok(n))
}
}

View File

@@ -5575,12 +5575,6 @@ pub fn open_ssh_project(
cx.replace_root_view(|cx| {
let mut workspace =
Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
workspace
.client()
.telemetry()
.report_app_event("open ssh project".to_string());
workspace.set_serialized_ssh_project(serialized_ssh_project);
workspace
});

View File

@@ -528,8 +528,6 @@ fn main() {
session_id,
cx,
);
// We should rename these in the future to `first app open`, `first app open for release channel`, and `app open`
if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) {
match (&system_id, &installation_id) {
(IdType::New(_), IdType::New(_)) => {