Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ mod tests {
application: Application::ClaudeCode,
date,
project_hash: "proj".into(),
project_path: None,
conversation_hash: "conv".into(),
local_hash: None,
global_hash: "global".into(),
Expand Down
1 change: 1 addition & 0 deletions src/analyzers/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ impl Analyzer for AntigravityCliAnalyzer {
application: Application::AntigravityCli,
date: ts,
project_hash: "".to_string(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: None,
global_hash,
Expand Down
15 changes: 15 additions & 0 deletions src/analyzers/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ pub fn parse_jsonl_file<R: Read>(
let mut fallback_session_name = None;

let mut current_model = None;
let mut current_project_path = None;

// Read entire file at once to avoid per-line allocations
let mut buffer = Vec::new();
Expand All @@ -695,6 +696,9 @@ pub fn parse_jsonl_file<R: Read>(
summaries.insert(summary.leaf_uuid, summary.summary);
}
Ok(ClaudeCodeEntry::Message(entry)) => {
if let Some(cwd) = entry.cwd.as_deref().and_then(normalize_project_path) {
current_project_path = Some(cwd);
}
// Track all UUIDs for summary linking, even if we skip the message
all_uuids.push(entry.uuid.clone());

Expand Down Expand Up @@ -726,6 +730,7 @@ pub fn parse_jsonl_file<R: Read>(
model: model.clone(),
date: timestamp,
project_hash: project_hash.to_string(),
project_path: current_project_path.clone(),
conversation_hash: conversation_hash.to_string(),
stats: Stats::default(), // Will be filled below
role: match role.as_deref() {
Expand Down Expand Up @@ -828,6 +833,16 @@ pub fn parse_jsonl_file<R: Read>(
Ok((messages, summaries, all_uuids, fallback_session_name))
}

fn normalize_project_path(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}

let normalized: PathBuf = Path::new(trimmed).components().collect();
Some(normalized.to_string_lossy().into_owned())
}

// Type alias for token fingerprint
pub type TokenFingerprint = (u64, u64, u64, u64, u64);

Expand Down
1 change: 1 addition & 0 deletions src/analyzers/claude_code_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ mod tests {
application: Application::ClaudeCode,
date: Utc.with_ymd_and_hms(2025, 8, 2, 14, 5, 17).unwrap(),
project_hash: "project".to_string(),
project_path: None,
conversation_hash: conversation.to_string(),
local_hash: Some(local_hash.to_string()),
global_hash: hash.to_string(),
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/cline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
application: Application::Cline,
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down Expand Up @@ -248,6 +249,7 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
application: Application::Cline,
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down
43 changes: 39 additions & 4 deletions src/analyzers/codex_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ pub(crate) fn parse_codex_cli_jsonl_file(
let mut current_tool_call_ids: HashSet<String> = HashSet::with_capacity(20);
let mut session_name: Option<String> = None;
let mut fallback_session_name: Option<String> = None;
let mut project_path: Option<String> = None;
let mut project_hash = String::new();
let mut has_repository_project_id = false;

for line in buffer.split(|&b| b == b'\n') {
// Skip empty lines
Expand All @@ -356,18 +359,37 @@ pub(crate) fn parse_codex_cli_jsonl_file(
"session_meta" => {
// Try to parse the payload as session metadata
let mut payload_bytes = simd_json::to_vec(&wrapper.payload)?;
if let Ok(_session_meta) =
if let Ok(session_meta) =
simd_json::from_slice::<CodexCliSessionMeta>(&mut payload_bytes)
{
session_model =
extract_model_from_value(&wrapper.payload).map(SessionModel::explicit);
project_path = session_meta.cwd.as_deref().and_then(normalize_project_path);
if let Some(repository_url) = session_meta
.git
.as_ref()
.and_then(|git| git.repository_url.as_deref())
.map(str::trim)
.filter(|url| !url.is_empty())
{
project_hash = hash_text(repository_url);
has_repository_project_id = true;
} else if let Some(path) = project_path.as_deref() {
project_hash = hash_text(path);
}
}
}
"turn_context" => {
let mut payload_bytes = simd_json::to_vec(&wrapper.payload)?;
if let Ok(context) =
simd_json::from_slice::<CodexCliTurnContext>(&mut payload_bytes)
{
if let Some(cwd) = context.cwd.as_deref().and_then(normalize_project_path) {
if !has_repository_project_id {
project_hash = hash_text(&cwd);
}
project_path = Some(cwd);
}
if let Some(model_name) = extract_model_from_value(&wrapper.payload) {
session_model = Some(SessionModel::explicit(model_name));
}
Expand Down Expand Up @@ -484,7 +506,8 @@ pub(crate) fn parse_codex_cli_jsonl_file(
local_hash: None,
conversation_hash: hash_text(&session_path_str),
application: Application::CodexCli,
project_hash: "".to_string(),
project_hash: project_hash.clone(),
project_path: project_path.clone(),
model: None,
stats: Stats::default(),
role: MessageRole::User,
Expand Down Expand Up @@ -521,7 +544,8 @@ pub(crate) fn parse_codex_cli_jsonl_file(
local_hash: None,
conversation_hash: hash_text(&session_path_str),
date: wrapper.timestamp,
project_hash: "".to_string(),
project_hash: project_hash.clone(),
project_path: project_path.clone(),
stats: Stats::default(),
role: MessageRole::Assistant,
uuid: None,
Expand Down Expand Up @@ -589,7 +613,8 @@ pub(crate) fn parse_codex_cli_jsonl_file(
local_hash: None,
conversation_hash: hash_text(&session_path_str),
date: wrapper.timestamp,
project_hash: "".to_string(),
project_hash: project_hash.clone(),
project_path: project_path.clone(),
stats,
role: MessageRole::Assistant,
uuid: None,
Expand All @@ -614,6 +639,16 @@ pub(crate) fn parse_codex_cli_jsonl_file(
Ok((entries, detected_model))
}

fn normalize_project_path(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}

let normalized: PathBuf = Path::new(trimmed).components().collect();
Some(normalized.to_string_lossy().into_owned())
}

fn calculate_cost_from_tokens(
usage: &CodexCliTokenUsage,
model_name: &str,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
application: Application::Copilot,
date: user_date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(user_local_hash),
global_hash: user_global_hash,
Expand Down Expand Up @@ -427,6 +428,7 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
application: Application::Copilot,
date: assistant_date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(assistant_local_hash),
global_hash: assistant_global_hash,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/copilot_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ fn push_copilot_cli_user_message(
application: Application::CopilotCli,
date: pending_user.date,
project_hash: project_hash.to_string(),
project_path: None,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
conversation_hash: conversation_hash.to_string(),
local_hash: Some(user_local_hash),
global_hash: user_global_hash,
Expand Down Expand Up @@ -614,6 +615,7 @@ fn flush_copilot_cli_turn(
application: Application::CopilotCli,
date: assistant_date,
project_hash: project_hash.to_string(),
project_path: None,
conversation_hash: conversation_hash.to_string(),
local_hash: Some(assistant_local_hash),
global_hash: assistant_global_hash,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/deepseek_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ fn parse_session_reader<R: Read>(path: &Path, reader: R) -> Result<Vec<Conversat
application: Application::DeepSeekHarness,
date,
project_hash: hash_text(&project_id),
project_path: None,
conversation_hash: hash_text(canonical_session),
local_hash: Some(message_id.clone()),
global_hash: hash_text(&format!("deepseek-harness:{message_id}")),
Expand Down Expand Up @@ -367,6 +368,7 @@ fn parse_session_reader<R: Read>(path: &Path, reader: R) -> Result<Vec<Conversat
application: Application::DeepSeekHarness,
date,
project_hash: hash_text(&project_id),
project_path: None,
conversation_hash: hash_text(canonical_session),
local_hash: Some(message_id.clone()),
global_hash: hash_text(&format!("deepseek-harness:{message_id}")),
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/gemini_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ fn messages_from_session(
date: timestamp,
application: Application::GeminiCli,
project_hash: project_hash.clone(),
project_path: None,
local_hash: None,
global_hash: hash_text(&format!(
"{}_{}",
Expand Down Expand Up @@ -380,6 +381,7 @@ fn messages_from_session(
)),
date: timestamp,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
stats,
role: MessageRole::Assistant,
Expand Down
1 change: 1 addition & 0 deletions src/analyzers/grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ pub fn parse_chat_history_file(path: &Path) -> Result<Vec<ConversationMessage>>
application: Application::Grok,
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(format!("{conversation_hash}:{line_index}")),
global_hash: hash_text(&format!("{file_path}:{line_index}")),
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/kilo_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ fn parse_kilo_code_task_directory(task_dir: &Path) -> Result<Vec<ConversationMes
application: Application::KiloCode,
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down Expand Up @@ -229,6 +230,7 @@ fn parse_kilo_code_task_directory(task_dir: &Path) -> Result<Vec<ConversationMes
application: Application::KiloCode,
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down
1 change: 1 addition & 0 deletions src/analyzers/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ pub(crate) fn build_conversation_message(
application,
date,
project_hash,
project_path: None,
conversation_hash,
local_hash,
global_hash,
Expand Down
1 change: 1 addition & 0 deletions src/analyzers/opencode_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ fn to_conversation_message(
application,
date,
project_hash,
project_path: None,
conversation_hash,
local_hash,
global_hash,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/pi_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ fn parse_jsonl_file<R: Read>(
application: Application::PiAgent,
date: timestamp,
project_hash: project_hash.to_string(),
project_path: None,
conversation_hash: conversation_hash.to_string(),
local_hash: Some(global_hash.clone()),
global_hash,
Expand Down Expand Up @@ -360,6 +361,7 @@ fn parse_jsonl_file<R: Read>(
application: Application::PiAgent,
date: timestamp,
project_hash: project_hash.to_string(),
project_path: None,
conversation_hash: conversation_hash.to_string(),
local_hash: None,
global_hash,
Expand Down
1 change: 1 addition & 0 deletions src/analyzers/piebald.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ fn convert_messages(
application: Application::Piebald,
date,
project_hash,
project_path: None,
conversation_hash,
local_hash: Some(msg.id.to_string()),
global_hash,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/qwen_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ pub fn parse_jsonl_session_file(file_path: &Path) -> Result<Vec<ConversationMess
date: timestamp,
application: Application::QwenCode,
project_hash: project_hash.clone(),
project_path: None,
local_hash: None,
global_hash,
conversation_hash: conversation_hash.clone(),
Expand Down Expand Up @@ -395,6 +396,7 @@ pub fn parse_jsonl_session_file(file_path: &Path) -> Result<Vec<ConversationMess
global_hash,
date: timestamp,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
stats,
role: MessageRole::Assistant,
Expand Down
2 changes: 2 additions & 0 deletions src/analyzers/roo_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ fn parse_roo_code_task_directory(
application: application.clone(),
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down Expand Up @@ -267,6 +268,7 @@ fn parse_roo_code_task_directory(
application: application.clone(),
date,
project_hash: project_hash.clone(),
project_path: None,
conversation_hash: conversation_hash.clone(),
local_hash: Some(local_hash),
global_hash,
Expand Down
8 changes: 8 additions & 0 deletions src/analyzers/tests/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ fn test_deduplicate_messages_resolves_same_local_hash_across_uuids() {
application: Application::ClaudeCode,
date: Utc.with_ymd_and_hms(2025, 8, 2, 16, 0, 0).unwrap(),
project_hash: "project".to_string(),
project_path: None,
conversation_hash: "conversation".to_string(),
local_hash: Some("shared-local-hash".to_string()),
global_hash: "uuid-a".to_string(),
Expand Down Expand Up @@ -125,6 +126,7 @@ fn test_streaming_snapshots_do_not_inflate_identical_fields() {
application: Application::ClaudeCode,
date: Utc.with_ymd_and_hms(2025, 8, 2, 16, 0, 0).unwrap(),
project_hash: "project".to_string(),
project_path: None,
conversation_hash: "conversation".to_string(),
local_hash: Some("streaming-local-hash".to_string()),
global_hash: "uuid-partial".to_string(),
Expand Down Expand Up @@ -171,6 +173,11 @@ fn test_parse_jsonl_file_basic() {
.unwrap();

assert_eq!(messages.len(), 4);
assert!(
messages
.iter()
.all(|message| message.project_path.as_deref() == Some(r"D:\splitrail"))
);

// Check first message (user message)
assert_eq!(messages[0].role, MessageRole::User);
Expand Down Expand Up @@ -451,6 +458,7 @@ fn test_deduplicate_messages_by_local_hash() {
model: Some("test-model".to_string()),
date: Utc.timestamp_opt(1609459200, 0).unwrap(),
project_hash: "project1".to_string(),
project_path: None,
conversation_hash: "conv1".to_string(),
stats: Stats {
input_tokens: 10,
Expand Down
Loading