use crate::prompt::step::StepMeta;
use brazen::Event;
use std::fs;
use std::path::Path;
const STEPS_DIR: &str = "steps";
const RESPONSE_FILE: &str = "response.json";
const META_FILE: &str = "meta.json";
const STEP_SEQ_WIDTH: usize = 3;
pub fn spend(repo: &Path, branch: &str) -> u64 {
sum_over_descent(repo, branch, step_tokens)
}
pub fn wall_seconds(repo: &Path, branch: &str) -> u64 {
sum_over_descent(repo, branch, step_wall)
}
pub fn depth(branch: &str) -> u32 {
(branch.matches('-').count() / 2) as u32
}
fn sum_over_descent(repo: &Path, branch: &str, per_step: fn(&Path) -> u64) -> u64 {
let Ok(entries) = fs::read_dir(repo.join(STEPS_DIR)) else {
return 0;
};
let prefix_dash = format!("{branch}-");
let mut total = 0u64;
for entry in entries.flatten() {
let raw = entry.file_name();
let name = raw.to_string_lossy();
if name != branch && !name.starts_with(&prefix_dash) {
continue;
}
total = total.saturating_add(sum_conv_steps(&entry.path(), per_step));
}
total
}
fn sum_conv_steps(conv_dir: &Path, per_step: fn(&Path) -> u64) -> u64 {
let Ok(entries) = fs::read_dir(conv_dir) else {
return 0;
};
let mut total = 0u64;
for entry in entries.flatten() {
let raw = entry.file_name();
let name = raw.to_string_lossy();
if name.len() == STEP_SEQ_WIDTH && name.bytes().all(|b| b.is_ascii_digit()) {
total = total.saturating_add(per_step(&entry.path()));
}
}
total
}
fn step_tokens(step_dir: &Path) -> u64 {
let Ok(bytes) = fs::read(step_dir.join(RESPONSE_FILE)) else {
return 0;
};
bytes
.split(|b| *b == b'\n')
.filter(|line| !line.is_empty())
.filter_map(|line| serde_json::from_slice::<Event>(line).ok())
.map(usage_tokens)
.sum()
}
fn usage_tokens(event: Event) -> u64 {
match event {
Event::Usage(u) => {
opt(u.input_tokens)
+ opt(u.output_tokens)
+ opt(u.cache_read_tokens)
+ opt(u.cache_write_tokens)
}
_ => 0,
}
}
fn opt(v: Option<u32>) -> u64 {
v.map(u64::from).unwrap_or(0)
}
fn step_wall(step_dir: &Path) -> u64 {
let Ok(bytes) = fs::read(step_dir.join(META_FILE)) else {
return 0;
};
let Ok(meta) = serde_json::from_slice::<StepMeta>(&bytes) else {
return 0;
};
span_seconds(&meta.started_at, &meta.ended_at)
}
fn span_seconds(start: &str, end: &str) -> u64 {
use chrono::DateTime;
let (Ok(s), Ok(e)) = (
DateTime::parse_from_rfc3339(start),
DateTime::parse_from_rfc3339(end),
) else {
return 0;
};
(e - s).num_seconds().max(0) as u64
}