pub mod amp;
use std::io::Read;
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
pub mod antigravity;
pub mod antigravity_cli;
pub mod augment;
pub mod cherrystudio;
pub mod claudecode;
pub mod cline;
pub mod codebuddy;
pub mod codebuff;
pub mod codex;
pub mod commandcode;
pub mod copilot;
pub mod copilot_desktop;
pub mod copilot_vscode;
pub mod crush;
pub mod cursor;
pub mod devin;
pub mod droid;
pub mod dsh;
pub mod freebuff;
pub mod fx;
pub mod gemini;
pub mod gjc;
pub mod goose;
pub mod grok;
pub mod hermes;
pub mod jcode;
pub mod junie;
pub mod kilo;
pub mod kilocode;
pub mod kimchi;
pub mod kimi;
pub mod kiro;
pub mod lmstudio;
pub mod mcode;
pub mod micode;
pub mod mux;
pub mod omp;
pub mod openclaw;
pub mod opencode;
pub(crate) mod opencode_schema;
pub mod opencodereview;
pub mod pi;
pub mod prime_agent;
pub mod qwen;
pub mod reasonix;
pub mod roocode;
pub mod senpi;
pub mod synthetic;
pub(crate) mod tencent_buddy;
pub mod trae;
pub mod unsloth;
pub(crate) mod utils;
pub mod warp;
pub mod workbuddy;
pub mod zcode;
pub mod zed;
use crate::TokenBreakdown;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CostSource {
#[default]
Unknown,
ProviderReported,
Estimated,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UnifiedMessage {
pub client: String,
pub model_id: String,
pub provider_id: String,
pub session_id: String,
pub workspace_key: Option<String>,
pub workspace_label: Option<String>,
pub timestamp: i64,
pub date: String,
pub tokens: TokenBreakdown,
pub cost: f64,
#[serde(default)]
pub cost_source: CostSource,
#[serde(default)]
pub duration_ms: Option<i64>,
#[serde(default = "default_message_count")]
pub message_count: i32,
pub agent: Option<String>,
pub dedup_key: Option<String>,
#[serde(default)]
pub session_title: Option<String>,
#[serde(default)]
pub is_turn_start: bool,
}
const fn default_message_count() -> i32 {
1
}
pub fn normalize_agent_name(agent: &str) -> String {
let cleaned = strip_zero_width_chars(agent);
let trimmed = cleaned.trim();
let stripped = strip_agent_prefix(trimmed);
let canonical = canonicalize_agent_name(stripped);
let agent_lower = canonical.to_lowercase();
if agent_lower.contains("plan") {
if agent_lower.contains("omo") || agent_lower.contains("sisyphus") {
return "Planner-Sisyphus".to_string();
}
return titlecase_agent(&canonical);
}
if agent_lower == "omo" || agent_lower == "sisyphus" {
return "Sisyphus".to_string();
}
if agent_lower == "orchestrator-sisyphus" {
return "Atlas".to_string();
}
titlecase_agent(&canonical)
}
pub fn normalize_opencode_agent_name(agent: &str) -> String {
let cleaned = strip_zero_width_chars(agent);
let trimmed = cleaned.trim();
let stripped = strip_agent_prefix(trimmed);
let canonical = canonicalize_agent_name(stripped);
let agent_lower = canonical.to_lowercase();
if let Some(normalized) = normalize_oh_my_opencode_agent_name(&agent_lower) {
return normalized;
}
normalize_agent_name(&canonical)
}
pub fn normalize_copilot_agent_name(agent: &str) -> String {
if agent.eq_ignore_ascii_case("github.copilot.default") {
return "GitHub Copilot".to_string();
}
const GITHUB_COPILOT_PREFIX: &str = "github.copilot.";
if agent
.get(..GITHUB_COPILOT_PREFIX.len())
.is_some_and(|head| head.eq_ignore_ascii_case(GITHUB_COPILOT_PREFIX))
{
let remainder = &agent[GITHUB_COPILOT_PREFIX.len()..];
let hyphenated = remainder.replace('.', "-");
return titlecase_agent(&hyphenated);
}
const PLUGIN_PREFIX: &str = "Plugin:";
if agent
.get(..PLUGIN_PREFIX.len())
.is_some_and(|head| head.eq_ignore_ascii_case(PLUGIN_PREFIX))
{
let rest = &agent[PLUGIN_PREFIX.len()..];
let parts: Vec<&str> = rest.splitn(2, ':').collect();
if parts.len() == 2 {
let team = titlecase_agent(parts[0]);
let slug = titlecase_agent(parts[1]);
return format!("{}: {}", team, slug);
}
return titlecase_agent(rest);
}
normalize_agent_name(agent)
}
fn normalize_oh_my_opencode_agent_name(agent_lower: &str) -> Option<String> {
let normalized = match agent_lower {
"sisyphus (ultraworker)"
| "sisyphus - ultraworker"
| "sisyphus ultraworker"
| "sisyphus" => "Sisyphus",
"hephaestus (deep agent)"
| "hephaestus - deep agent"
| "hephaestus deep agent"
| "hephaestus" => "Hephaestus",
"prometheus (plan builder)"
| "prometheus - plan builder"
| "prometheus plan builder"
| "prometheus (planner)"
| "prometheus" => "Prometheus",
"atlas (plan executor)" | "atlas - plan executor" | "atlas plan executor" | "atlas" => {
"Atlas"
}
"metis (plan consultant)"
| "metis - plan consultant"
| "metis plan consultant"
| "metis" => "Metis",
"momus (plan critic)"
| "momus - plan critic"
| "momus plan critic"
| "momus (plan reviewer)"
| "momus" => "Momus",
"orchestrator-sisyphus" => "Atlas",
"sisyphus-junior" => "Sisyphus-Junior",
"planner-sisyphus" => "Planner-Sisyphus",
_ => return None,
};
Some(normalized.to_string())
}
fn strip_zero_width_chars(s: &str) -> String {
if !s.contains(['\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}']) {
return s.to_string();
}
s.chars()
.filter(|c| !matches!(c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{FEFF}'))
.collect()
}
fn strip_agent_prefix(name: &str) -> &str {
for prefix in &["astrape:", "oh-my-claudecode:", "oh-my-codex:"] {
if name
.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
{
return &name[prefix.len()..];
}
}
name
}
fn canonicalize_agent_name(name: &str) -> String {
name.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn titlecase_word(word: &str) -> String {
match word.to_lowercase().as_str() {
"ui" => "UI".to_string(),
"ux" => "UX".to_string(),
"api" => "API".to_string(),
_ => {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => {
let upper: String = c.to_uppercase().collect();
upper + &chars.collect::<String>()
}
}
}
}
}
fn titlecase_agent(name: &str) -> String {
if name.is_empty() {
return String::new();
}
name.split('-')
.flat_map(|part| part.split_whitespace())
.map(titlecase_word)
.collect::<Vec<_>>()
.join(" ")
}
impl UnifiedMessage {
pub fn new(
client: impl Into<String>,
model_id: impl Into<String>,
provider_id: impl Into<String>,
session_id: impl Into<String>,
timestamp: i64,
tokens: TokenBreakdown,
cost: f64,
) -> Self {
Self::new_full(
client,
model_id,
provider_id,
session_id,
timestamp,
tokens,
cost,
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_agent(
client: impl Into<String>,
model_id: impl Into<String>,
provider_id: impl Into<String>,
session_id: impl Into<String>,
timestamp: i64,
tokens: TokenBreakdown,
cost: f64,
agent: Option<String>,
) -> Self {
Self::new_full(
client,
model_id,
provider_id,
session_id,
timestamp,
tokens,
cost,
agent,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_dedup(
client: impl Into<String>,
model_id: impl Into<String>,
provider_id: impl Into<String>,
session_id: impl Into<String>,
timestamp: i64,
tokens: TokenBreakdown,
cost: f64,
dedup_key: Option<String>,
) -> Self {
Self::new_full(
client,
model_id,
provider_id,
session_id,
timestamp,
tokens,
cost,
None,
dedup_key,
)
}
#[allow(clippy::too_many_arguments)]
fn new_full(
client: impl Into<String>,
model_id: impl Into<String>,
provider_id: impl Into<String>,
session_id: impl Into<String>,
timestamp: i64,
tokens: TokenBreakdown,
cost: f64,
agent: Option<String>,
dedup_key: Option<String>,
) -> Self {
let client = client.into();
let model_id = model_id.into();
let date = timestamp_to_date(timestamp);
Self {
client,
model_id,
provider_id: provider_id.into(),
session_id: session_id.into(),
workspace_key: None,
workspace_label: None,
timestamp,
date,
tokens,
cost,
cost_source: CostSource::Unknown,
duration_ms: None,
message_count: default_message_count(),
agent,
dedup_key,
session_title: None,
is_turn_start: false,
}
}
pub fn set_workspace(
&mut self,
workspace_key: Option<String>,
workspace_label: Option<String>,
) {
self.workspace_key = workspace_key;
self.workspace_label = workspace_label;
}
pub(crate) fn refresh_derived_fields(&mut self) {
self.date = timestamp_to_date(self.timestamp);
}
pub(crate) fn set_timestamp(&mut self, timestamp: i64) {
self.timestamp = timestamp;
self.refresh_derived_fields();
}
pub fn mark_provider_reported_cost(&mut self) {
self.cost_source = CostSource::ProviderReported;
}
pub(crate) fn mark_estimated_cost(&mut self) {
self.cost_source = CostSource::Estimated;
}
pub(crate) fn has_authoritative_cost(&self) -> bool {
self.cost_source == CostSource::ProviderReported
}
}
pub fn normalize_workspace_key(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let preserve_unc_prefix = trimmed.starts_with("\\\\") || trimmed.starts_with("//");
let mut normalized = trimmed.replace('\\', "/");
if preserve_unc_prefix {
let body = normalized.trim_start_matches('/');
let mut collapsed = body.to_string();
while collapsed.contains("//") {
collapsed = collapsed.replace("//", "/");
}
normalized = format!("//{}", collapsed);
} else {
while normalized.contains("//") {
normalized = normalized.replace("//", "/");
}
}
let minimum_len = if preserve_unc_prefix { 2 } else { 1 };
if normalized.len() > minimum_len {
normalized = normalized.trim_end_matches('/').to_string();
}
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
fn is_absolute_workspace_key(key: &str) -> bool {
normalize_workspace_key(key)
.is_some_and(|key| key.starts_with('/') || windows_drive_root(&key, ':', '/').is_some())
}
fn windows_drive_root(key: &str, colon: char, separator: char) -> Option<(char, &str)> {
let drive = key.chars().next()?;
if !drive.is_ascii_alphabetic() {
return None;
}
let remaining = key[1..].strip_prefix(colon)?;
remaining
.starts_with(separator)
.then_some((drive, remaining))
}
pub fn workspace_label_from_key(key: &str) -> Option<String> {
key.rsplit('/')
.find(|segment| !segment.is_empty())
.map(|segment| segment.to_string())
}
pub const WORKTREE_SEPARATOR: &str = " ⑃ ";
const WORKTREE_MARKERS: [&str; 2] = [".claude/worktrees/", ".git/worktrees/"];
const ENCODED_WORKTREE_MARKERS: [&str; 2] = ["--claude-worktrees-", "--git-worktrees-"];
fn split_encoded_worktree(key: &str) -> Option<(String, String)> {
let (index, marker_len) = first_encoded_worktree_marker(key)?;
let repo = &key[..index];
let mut worktree = &key[index + marker_len..];
while let Some((inner, inner_len)) = first_encoded_worktree_marker(worktree) {
worktree = &worktree[inner + inner_len..];
}
Some((repo.to_string(), worktree.to_string()))
}
fn first_encoded_worktree_marker(key: &str) -> Option<(usize, usize)> {
ENCODED_WORKTREE_MARKERS
.iter()
.filter_map(|marker| key.find(marker).map(|index| (index, marker.len())))
.filter(|(index, marker_len)| *index > 0 && index + marker_len < key.len())
.min()
}
pub fn workspace_repo_root(key: &str) -> Option<String> {
let key = normalize_workspace_key(key)?;
let index = WORKTREE_MARKERS
.iter()
.filter_map(|marker| find_segment_marker(&key, marker))
.filter(|index| !key[..*index].trim_end_matches('/').is_empty())
.min()?;
Some(key[..index].trim_end_matches('/').to_string())
}
fn find_segment_marker(key: &str, marker: &str) -> Option<usize> {
let mut from = 0;
while let Some(offset) = key[from..].find(marker) {
let index = from + offset;
if index == 0 || key.as_bytes()[index - 1] == b'/' {
return Some(index);
}
from = index + 1;
}
None
}
pub fn workspace_git_worktree_root(path: &str) -> Option<String> {
if !is_absolute_workspace_key(path) || !Path::new(path).is_absolute() {
return None;
}
let contents = read_git_pointer_file(&Path::new(path).join(".git"))?;
let pointer = contents
.lines()
.find_map(|line| line.trim().strip_prefix("gitdir:"))?
.trim();
let joined = if Path::new(pointer).is_absolute() || pointer.starts_with('/') {
normalize_workspace_key(pointer)?
} else {
normalize_workspace_key(&Path::new(path).join(pointer).to_string_lossy())?
};
repo_root_from_gitdir(&lexically_normalize(&joined)?)
}
const GIT_POINTER_MAX_BYTES: u64 = 64 * 1024;
fn read_git_pointer_file(path: &Path) -> Option<String> {
let metadata = std::fs::symlink_metadata(path).ok()?;
let metadata = if metadata.is_symlink() {
std::fs::metadata(path).ok()?
} else {
metadata
};
if !metadata.is_file() || metadata.len() > GIT_POINTER_MAX_BYTES {
return None;
}
let mut contents = String::new();
std::fs::File::open(path)
.ok()?
.take(GIT_POINTER_MAX_BYTES)
.read_to_string(&mut contents)
.ok()?;
Some(contents)
}
fn lexically_normalize(key: &str) -> Option<String> {
let prefix_len = if key.starts_with("//") {
2
} else if key.starts_with('/') {
1
} else {
0
};
let (prefix, rest) = key.split_at(prefix_len);
let mut segments: Vec<&str> = Vec::new();
for segment in rest.split('/') {
match segment {
"" | "." => {}
".." => {
if segments.last().is_some_and(|last| *last != "..") {
segments.pop();
} else if prefix.is_empty() {
segments.push("..");
}
}
other => segments.push(other),
}
}
normalize_workspace_key(&format!("{prefix}{}", segments.join("/")))
}
fn repo_root_from_gitdir(gitdir: &str) -> Option<String> {
let (git_dir, worktree) = gitdir.rsplit_once("/worktrees/")?;
if worktree.is_empty() {
return None;
}
let root = git_dir.strip_suffix("/.git").unwrap_or(git_dir);
(!root.is_empty()).then(|| root.to_string())
}
pub fn workspace_repo_root_resolved(path: &str) -> Option<String> {
workspace_repo_root(path).or_else(|| workspace_git_worktree_root(path))
}
pub fn workspace_path_for_key(key: &str) -> Option<String> {
workspace_path_for_decoded_key(key, decode_claude_project_slug(key).as_deref())
}
pub fn workspace_path_for_decoded_key(key: &str, decoded: Option<&str>) -> Option<String> {
if let Some(decoded) = decoded {
return Some(decoded.to_string());
}
let normalized = normalize_workspace_key(key)?;
normalized.contains('/').then_some(normalized)
}
pub fn workspace_display_label(key: &str) -> Option<String> {
workspace_display_label_for_decoded_key(key, decode_claude_project_slug(key).as_deref())
}
pub fn workspace_display_label_for_decoded_key(key: &str, decoded: Option<&str>) -> Option<String> {
let path = decoded
.map(str::to_string)
.or_else(|| normalize_workspace_key(key))
.unwrap_or_else(|| key.to_string());
if let Some(root) = workspace_repo_root_resolved(&path) {
let repo = workspace_label_from_key(&root)?;
return match workspace_label_from_key(&path) {
Some(worktree) => Some(format!("{repo}{WORKTREE_SEPARATOR}{worktree}")),
None => Some(repo),
};
}
if let Some((repo_slug, worktree)) = split_encoded_worktree(&path) {
let repo = decode_claude_project_slug(&repo_slug)
.and_then(|decoded| workspace_label_from_key(&decoded))
.or_else(|| last_slug_segment(&repo_slug))?;
return Some(format!("{repo}{WORKTREE_SEPARATOR}{worktree}"));
}
workspace_label_from_key(&path)
}
pub fn workspace_repo_root_from_slug(key: &str) -> Option<String> {
let (repo_slug, _) = split_encoded_worktree(key)?;
Some(decode_claude_project_slug(&repo_slug).unwrap_or(repo_slug))
}
fn last_slug_segment(slug: &str) -> Option<String> {
slug.rsplit('-')
.find(|segment| !segment.is_empty())
.map(|segment| segment.to_string())
}
pub fn decode_claude_project_slug(key: &str) -> Option<String> {
if key.contains('/') {
return None;
}
let (root, remaining) = slug_root_and_remainder(key)?;
let mut budget = SLUG_DECODE_STEP_BUDGET;
match resolve_slug_under(&root, remaining, &mut budget) {
SlugResolution::Resolved(path) => Some(path),
SlugResolution::DeadEnd | SlugResolution::Refused => None,
}
}
const SLUG_DECODE_STEP_BUDGET: u32 = 4_096;
fn spend_slug_budget(budget: &mut u32, cost: u32) -> bool {
match budget.checked_sub(cost) {
Some(remaining) => {
*budget = remaining;
true
}
None => {
*budget = 0;
false
}
}
}
fn slug_root_and_remainder(key: &str) -> Option<(PathBuf, &str)> {
if key.starts_with('-') {
return Some((PathBuf::from(MAIN_SEPARATOR_STR), key));
}
let (drive, remaining) = windows_drive_root(key, '-', '-')?;
Some((
PathBuf::from(format!("{drive}:{MAIN_SEPARATOR_STR}")),
remaining,
))
}
#[derive(Debug, PartialEq, Eq)]
enum SlugResolution {
Resolved(String),
DeadEnd,
Refused,
}
fn resolve_slug_under(dir: &Path, remaining: &str, budget: &mut u32) -> SlugResolution {
if remaining.is_empty() {
return match normalize_workspace_key(&dir.to_string_lossy()) {
Some(path) => SlugResolution::Resolved(path),
None => SlugResolution::DeadEnd,
};
}
if !spend_slug_budget(budget, 1) {
return SlugResolution::Refused;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return SlugResolution::DeadEnd;
};
let matched: Vec<String> = entries
.flatten()
.map(|entry| entry.file_name().to_string_lossy().to_string())
.filter(|name| slug_matches_prefix(remaining, name))
.collect();
if !spend_slug_budget(budget, matched.len() as u32) {
return SlugResolution::Refused;
}
let mut candidates: Vec<String> = matched
.into_iter()
.filter(|name| dir.join(name).is_dir())
.collect();
candidates.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
let mut resolved: Option<String> = None;
for name in candidates {
let consumed = slugify_path_segment(&name).len() + 1;
match resolve_slug_under(&dir.join(&name), &remaining[consumed..], budget) {
SlugResolution::Resolved(path) => {
if resolved.is_some() {
return SlugResolution::Refused;
}
resolved = Some(path);
}
SlugResolution::Refused => return SlugResolution::Refused,
SlugResolution::DeadEnd => {}
}
}
match resolved {
Some(path) => SlugResolution::Resolved(path),
None => SlugResolution::DeadEnd,
}
}
fn slug_matches_prefix(remaining: &str, name: &str) -> bool {
let encoded = slugify_path_segment(name);
let Some(rest) = remaining.strip_prefix('-') else {
return false;
};
let Some(tail) = rest.strip_prefix(encoded.as_str()) else {
return false;
};
tail.is_empty() || tail.starts_with('-')
}
fn slugify_path_segment(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}
#[cfg(test)]
pub(crate) fn canonical_tempdir() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().unwrap();
let canonical = std::fs::canonicalize(temp.path()).unwrap();
let spelled = canonical.to_string_lossy().to_string();
let stripped = spelled
.strip_prefix(r"\\?\")
.map(PathBuf::from)
.unwrap_or(canonical);
(temp, stripped)
}
fn timestamp_to_date(timestamp_ms: i64) -> String {
crate::bucket_tz::bucket_timezone().date_of_ms(timestamp_ms)
}
fn timestamp_to_date_with_timezone<Tz>(timestamp_ms: i64, timezone: &Tz) -> String
where
Tz: chrono::TimeZone,
Tz::Offset: std::fmt::Display,
{
match timezone.timestamp_millis_opt(timestamp_ms) {
chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d").to_string(),
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::FixedOffset;
#[test]
fn warp_cache_parser_preserves_requests_and_spend_without_tokens() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
file.path(),
r#"{
"version": 1,
"syncedAt": "2026-05-29T12:00:00Z",
"usage": {
"requestsUsed": 42,
"requestLimit": 100,
"spendCents": 1234,
"nextRefreshTime": "2026-06-01T00:00:00Z"
},
"workspaces": [
{
"id": "workspace-1",
"name": "Personal",
"requestsUsed": 12,
"spendCents": 345
}
]
}"#,
)
.unwrap();
let messages = crate::sessions::warp::parse_warp_file(file.path());
assert_eq!(messages.len(), 1);
let workspace = messages
.iter()
.find(|message| message.session_id == "warp-aggregate-workspace-1")
.unwrap();
assert_eq!(workspace.client, "warp");
assert_eq!(workspace.model_id, "aggregate-requests");
assert_eq!(workspace.provider_id, "warp");
assert_eq!(workspace.workspace_label.as_deref(), Some("Personal"));
assert_eq!(workspace.message_count, 12);
assert_eq!(workspace.tokens, TokenBreakdown::default());
assert!((workspace.cost - 3.45).abs() < 1e-9);
std::fs::write(
file.path(),
r#"{
"version": 1,
"syncedAt": "2026-05-29T12:00:00Z",
"usage": {
"requestsUsed": 42,
"requestLimit": 100,
"spendCents": 1234,
"nextRefreshTime": "2026-06-01T00:00:00Z"
},
"workspaces": []
}"#,
)
.unwrap();
let messages = crate::sessions::warp::parse_warp_file(file.path());
assert_eq!(messages.len(), 1);
let account = &messages[0];
assert_eq!(account.session_id, "warp-aggregate-account");
assert_eq!(account.message_count, 42);
assert_eq!(account.tokens, TokenBreakdown::default());
assert!((account.cost - 12.34).abs() < 1e-9);
}
#[test]
fn test_timestamp_to_date_with_positive_offset() {
let kst = FixedOffset::east_opt(9 * 60 * 60).unwrap();
let ts = 1772512200000_i64; let date = timestamp_to_date_with_timezone(ts, &kst);
assert_eq!(date, "2026-03-03");
}
#[test]
fn test_timestamp_to_date_with_negative_offset() {
let pst = FixedOffset::west_opt(8 * 60 * 60).unwrap();
let ts = 1772512200000_i64; let date = timestamp_to_date_with_timezone(ts, &pst);
assert_eq!(date, "2026-03-02");
}
#[test]
fn test_timestamp_to_date_invalid_timestamp() {
let utc = FixedOffset::east_opt(0).unwrap();
let date = timestamp_to_date_with_timezone(i64::MAX, &utc);
assert_eq!(date, "");
}
#[test]
fn test_unified_message_creation() {
let tokens = TokenBreakdown {
input: 100,
output: 50,
cache_read: 0,
cache_write: 0,
reasoning: 0,
};
let msg = UnifiedMessage::new(
"opencode",
"claude-3-5-sonnet",
"anthropic",
"test-session-id",
1733011200000,
tokens,
0.05,
);
assert_eq!(msg.client, "opencode");
assert_eq!(msg.model_id, "claude-3-5-sonnet");
assert_eq!(msg.session_id, "test-session-id");
assert_eq!(msg.date, timestamp_to_date(1733011200000));
assert_eq!(msg.cost, 0.05);
assert_eq!(msg.agent, None);
assert_eq!(msg.workspace_key, None);
assert_eq!(msg.workspace_label, None);
}
#[test]
fn test_normalize_workspace_key_normalizes_slashes_and_trailing_separator() {
assert_eq!(
normalize_workspace_key(r"C:\Users\alice\repo\"),
Some("C:/Users/alice/repo".to_string())
);
assert_eq!(
normalize_workspace_key("/Users/alice//repo/"),
Some("/Users/alice/repo".to_string())
);
}
#[test]
fn test_normalize_workspace_key_preserves_unc_prefix() {
assert_eq!(
normalize_workspace_key(r"\\server\share\repo\"),
Some("//server/share/repo".to_string())
);
assert_eq!(
normalize_workspace_key("//server//share///repo/"),
Some("//server/share/repo".to_string())
);
}
#[test]
fn test_workspace_label_from_key_uses_last_path_segment() {
assert_eq!(
workspace_label_from_key("/Users/alice/my-repo"),
Some("my-repo".to_string())
);
assert_eq!(
workspace_label_from_key("encoded-project-key"),
Some("encoded-project-key".to_string())
);
}
#[test]
fn test_normalize_agent_name() {
assert_eq!(normalize_agent_name("OmO"), "Sisyphus");
assert_eq!(normalize_agent_name("Sisyphus"), "Sisyphus");
assert_eq!(normalize_agent_name("omo"), "Sisyphus");
assert_eq!(normalize_agent_name("sisyphus"), "Sisyphus");
assert_eq!(
normalize_agent_name("Sisyphus (Ultraworker)"),
"Sisyphus (Ultraworker)"
);
assert_eq!(
normalize_opencode_agent_name("Sisyphus (Ultraworker)"),
"Sisyphus"
);
assert_eq!(normalize_opencode_agent_name("hephaestus"), "Hephaestus");
assert_eq!(normalize_opencode_agent_name("prometheus"), "Prometheus");
assert_eq!(normalize_opencode_agent_name("atlas"), "Atlas");
assert_eq!(normalize_opencode_agent_name("metis"), "Metis");
assert_eq!(normalize_opencode_agent_name("momus"), "Momus");
assert_eq!(
normalize_opencode_agent_name("sisyphus-junior"),
"Sisyphus-Junior"
);
assert_eq!(
normalize_opencode_agent_name("planner-sisyphus"),
"Planner-Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("Hephaestus (Deep Agent)"),
"Hephaestus"
);
assert_eq!(
normalize_opencode_agent_name("Prometheus (Plan Builder)"),
"Prometheus"
);
assert_eq!(
normalize_opencode_agent_name("Prometheus (Planner)"),
"Prometheus"
);
assert_eq!(
normalize_opencode_agent_name("Atlas (Plan Executor)"),
"Atlas"
);
assert_eq!(
normalize_opencode_agent_name("Metis (Plan Consultant)"),
"Metis"
);
assert_eq!(
normalize_opencode_agent_name("Momus (Plan Critic)"),
"Momus"
);
assert_eq!(
normalize_opencode_agent_name("Momus (Plan Reviewer)"),
"Momus"
);
assert_eq!(normalize_agent_name("OmO-Plan"), "Planner-Sisyphus");
assert_eq!(normalize_agent_name("Planner-Sisyphus"), "Planner-Sisyphus");
assert_eq!(normalize_agent_name("omo-plan"), "Planner-Sisyphus");
assert_eq!(normalize_agent_name("orchestrator-sisyphus"), "Atlas");
assert_eq!(
normalize_opencode_agent_name("orchestrator-sisyphus"),
"Atlas"
);
assert_eq!(normalize_agent_name("explore"), "Explore");
assert_eq!(normalize_agent_name("CustomAgent"), "CustomAgent");
assert_eq!(normalize_agent_name("executor"), "Executor");
assert_eq!(
normalize_agent_name("task-orchestrator"),
"Task Orchestrator"
);
assert_eq!(normalize_agent_name("git-committer"), "Git Committer");
assert_eq!(
normalize_agent_name("frontend-ui-ux-engineer"),
"Frontend UI UX Engineer"
);
assert_eq!(
normalize_agent_name("astrape:executor-high"),
"Executor High"
);
assert_eq!(
normalize_agent_name("oh-my-claudecode:code-reviewer"),
"Code Reviewer"
);
}
#[test]
fn test_normalize_copilot_agent_name() {
assert_eq!(
normalize_copilot_agent_name("github.copilot.default"),
"GitHub Copilot"
);
assert_eq!(
normalize_copilot_agent_name("GITHUB.COPILOT.DEFAULT"),
"GitHub Copilot"
);
assert_eq!(normalize_copilot_agent_name("github.copilot.chat"), "Chat");
assert_eq!(
normalize_copilot_agent_name("Plugin:software-engineering-team:se-ux-ui-designer"),
"Software Engineering Team: Se UX UI Designer"
);
assert_eq!(
normalize_copilot_agent_name("plugin:my-team:my-agent"),
"My Team: My Agent"
);
assert_eq!(
normalize_copilot_agent_name("Plugin:code-review-team:api-reviewer"),
"Code Review Team: API Reviewer"
);
assert_eq!(
normalize_copilot_agent_name("some-custom-agent"),
"Some Custom Agent"
);
assert_eq!(normalize_agent_name("oh-my-codex:librarian"), "Librarian");
assert_eq!(normalize_agent_name("astrape:executor"), "Executor");
assert_eq!(normalize_agent_name("plan-reviewer"), "Plan Reviewer");
assert_eq!(normalize_agent_name("astrape:planner"), "Planner");
assert_eq!(
normalize_opencode_agent_name("astrape:sisyphus"),
"Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("oh-my-claudecode:executor"),
"Executor"
);
assert_eq!(
normalize_opencode_agent_name("Sisyphus - Ultraworker"),
"Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("Hephaestus - Deep Agent"),
"Hephaestus"
);
assert_eq!(
normalize_opencode_agent_name("Prometheus - Plan Builder"),
"Prometheus"
);
assert_eq!(
normalize_opencode_agent_name("Atlas - Plan Executor"),
"Atlas"
);
assert_eq!(
normalize_opencode_agent_name("Metis - Plan Consultant"),
"Metis"
);
assert_eq!(
normalize_opencode_agent_name("Momus - Plan Critic"),
"Momus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}Sisyphus - Ultraworker"),
"Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}\u{200B}\u{200B}Prometheus - Plan Builder"),
"Prometheus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}\u{200B}\u{200B}\u{200B}Atlas - Plan Executor"),
"Atlas"
);
assert_eq!(
normalize_opencode_agent_name("\u{FEFF}Momus - Plan Critic"),
"Momus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}sisyphus-junior"),
"Sisyphus-Junior"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}sisyphus"),
"Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B} Sisyphus - Ultraworker "),
"Sisyphus"
);
assert_eq!(
normalize_opencode_agent_name("\u{200B}\u{200B}\u{200B} Prometheus Plan Builder"),
"Prometheus"
);
}
#[test]
fn test_strip_zero_width_chars() {
assert_eq!(strip_zero_width_chars("hello"), "hello");
assert_eq!(strip_zero_width_chars("\u{200B}hello"), "hello");
assert_eq!(
strip_zero_width_chars("\u{200B}\u{200B}\u{200B}hello"),
"hello"
);
assert_eq!(strip_zero_width_chars("\u{FEFF}hello"), "hello");
assert_eq!(strip_zero_width_chars("\u{200C}hello\u{200D}"), "hello");
assert_eq!(strip_zero_width_chars(""), "");
assert_eq!(
strip_zero_width_chars("no special chars"),
"no special chars"
);
}
}