Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd2d4d45

Browse files
authored
Add support per-session token + cost tracking (#34)
1 parent81a0239 commitd2d4d45

File tree

13 files changed

+1337
-205
lines changed

13 files changed

+1337
-205
lines changed

‎src/analyzers/claude_code.rs‎

Lines changed: 243 additions & 118 deletions
Large diffs are not rendered by default.

‎src/analyzers/cline.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ fn extract_and_hash_project_id_cline(file_path: &Path) -> String {
106106
hash_text(&file_path.to_string_lossy())
107107
}
108108

109+
fnis_probably_tool_json_text(text:&str) ->bool{
110+
let trimmed = text.trim_start();
111+
(trimmed.starts_with('{') || trimmed.starts_with("[{")) && trimmed.contains("\"tool\"")
112+
}
113+
109114
// Parse a single Cline task directory
110115
fnparse_cline_task_directory(task_dir:&Path) ->Result<Vec<ConversationMessage>>{
111116
let project_hash =extract_and_hash_project_id_cline(task_dir);
@@ -138,6 +143,7 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
138143
.context("Failed to parse ui_messages.json")?;
139144

140145
letmut entries =Vec::new();
146+
letmut fallback_session_name:Option<String> =None;
141147

142148
// Process ui_messages to extract API requests with token/cost data
143149
for messagein ui_messages{
@@ -198,13 +204,16 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
198204
model,
199205
stats,
200206
role:MessageRole::Assistant,// API requests are from the assistant
207+
uuid:None,
208+
session_name: fallback_session_name.clone(),
201209
});
202210
}
203211
}
204212
}
205213
ClineUiMessage::Ask{
206214
ts,
207215
ask,
216+
text,
208217
conversation_history_index,
209218
..
210219
} =>{
@@ -219,6 +228,19 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
219228
project_hash, conversation_hash, conversation_history_index, ts
220229
));
221230

231+
if fallback_session_name.is_none() && !text.is_empty(){
232+
let text_str = text;
233+
if !is_probably_tool_json_text(&text_str){
234+
let truncated =if text_str.chars().count() >50{
235+
let chars:String = text_str.chars().take(50).collect();
236+
format!("{}...", chars)
237+
}else{
238+
text_str
239+
};
240+
fallback_session_name =Some(truncated);
241+
}
242+
}
243+
222244
entries.push(ConversationMessage{
223245
application:Application::Cline,
224246
date,
@@ -229,6 +251,8 @@ fn parse_cline_task_directory(task_dir: &Path) -> Result<Vec<ConversationMessage
229251
model:None,
230252
stats:Stats::default(),// User messages don't have token costs
231253
role:MessageRole::User,
254+
uuid:None,
255+
session_name: fallback_session_name.clone(),
232256
});
233257
}
234258
}

‎src/analyzers/codex_cli.rs‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ impl SessionModel {
198198
}
199199
}
200200

201+
fnis_probably_tool_json_text(text:&str) ->bool{
202+
let trimmed = text.trim_start();
203+
(trimmed.starts_with('{') || trimmed.starts_with("[{")) && trimmed.contains("\"tool\"")
204+
}
205+
206+
fnis_noise_title_candidate(text:&str) ->bool{
207+
let trimmed = text.trim_start();
208+
is_probably_tool_json_text(trimmed)
209+
|| trimmed.starts_with("<environment_context>")
210+
|| trimmed.starts_with("# AGENTS.md instructions for")
211+
}
212+
201213
pub(crate)fnparse_codex_cli_jsonl_file(file_path:&Path) ->Result<Vec<ConversationMessage>>{
202214
letmut entries =Vec::new();
203215
let file_path_str = file_path.to_string_lossy().into_owned();
@@ -210,6 +222,8 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
210222
letmut saw_token_usage =false;
211223
letmut _turn_context:Option<CodexCliTurnContext> =None;
212224
letmut current_tool_call_ids:HashSet<String> =HashSet::new();
225+
letmut session_name:Option<String> =None;
226+
letmut fallback_session_name:Option<String> =None;
213227

214228
for line_resultin reader.lines(){
215229
let line =match line_result{
@@ -246,6 +260,16 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
246260
ifletSome(model_name) =extract_model_from_value(&wrapper.payload){
247261
session_model =Some(SessionModel::explicit(model_name));
248262
}
263+
if session_name.is_none()
264+
&&letSome(summary) = context.summary.clone()
265+
&& !summary.trim().is_empty()
266+
{
267+
let trimmed = summary.trim();
268+
// Skip generic summaries like "auto"
269+
if trimmed.to_lowercase() !="auto"{
270+
session_name =Some(summary);
271+
}
272+
}
249273
_turn_context =Some(context);
250274
}
251275
}
@@ -273,6 +297,71 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
273297
{
274298
match role.as_str(){
275299
"user" =>{
300+
if fallback_session_name.is_none()
301+
&&letSome(content_val) =&message.content
302+
{
303+
let text_opt =match content_val{
304+
simd_json::OwnedValue::String(s) =>{
305+
if s.is_empty(){
306+
None
307+
}else{
308+
Some(s.clone())
309+
}
310+
}
311+
simd_json::OwnedValue::Array(items) =>{
312+
letmut result =None;
313+
for itemin items.iter(){
314+
iflet simd_json::OwnedValue::Object(map) = item{
315+
// Prefer input_text blocks for titles
316+
ifletSome(simd_json::OwnedValue::String(kind)) =
317+
map.get("type")
318+
&& kind =="input_text"
319+
&&letSome(simd_json::OwnedValue::String(text)) =
320+
map.get("text")
321+
&& !text.is_empty()
322+
&& !is_probably_tool_json_text(text)
323+
{
324+
result =Some(text.clone());
325+
break;
326+
}
327+
}
328+
}
329+
result
330+
}
331+
simd_json::OwnedValue::Object(map) =>{
332+
ifletSome(simd_json::OwnedValue::String(text)) =
333+
map.get("text")
334+
{
335+
if !text.is_empty() && !is_probably_tool_json_text(text)
336+
{
337+
Some(text.clone())
338+
}else{
339+
None
340+
}
341+
}else{
342+
None
343+
}
344+
}
345+
_ =>None,
346+
};
347+
348+
ifletSome(text_str) = text_opt
349+
&& !is_noise_title_candidate(&text_str)
350+
{
351+
let truncated =if text_str.chars().count() >50{
352+
let chars:String = text_str.chars().take(50).collect();
353+
format!("{}...", chars)
354+
}else{
355+
text_str
356+
};
357+
fallback_session_name =Some(truncated);
358+
}
359+
}
360+
361+
let effective_name = session_name
362+
.clone()
363+
.or_else(|| fallback_session_name.clone());
364+
276365
entries.push(ConversationMessage{
277366
date: wrapper.timestamp,
278367
global_hash:hash_text(&format!(
@@ -287,6 +376,8 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
287376
model:None,
288377
stats:Stats::default(),
289378
role:MessageRole::User,
379+
uuid:None,
380+
session_name: effective_name,
290381
});
291382
}
292383
"assistant" =>{
@@ -321,6 +412,10 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
321412
project_hash:"".to_string(),
322413
stats:Stats::default(),
323414
role:MessageRole::Assistant,
415+
uuid:None,
416+
session_name: session_name
417+
.clone()
418+
.or_else(|| fallback_session_name.clone()),
324419
});
325420
}
326421
}
@@ -381,6 +476,10 @@ pub(crate) fn parse_codex_cli_jsonl_file(file_path: &Path) -> Result<Vec<Convers
381476
project_hash:"".to_string(),
382477
stats,
383478
role:MessageRole::Assistant,
479+
uuid:None,
480+
session_name: session_name
481+
.clone()
482+
.or_else(|| fallback_session_name.clone()),
384483
});
385484

386485
saw_token_usage =true;

‎src/analyzers/copilot.rs‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ fn extract_and_hash_project_id_copilot(_file_path: &Path) -> String {
162162
hash_text("copilot-global")
163163
}
164164

165+
fnis_probably_tool_json_text(text:&str) ->bool{
166+
let trimmed = text.trim_start();
167+
(trimmed.starts_with('{') || trimmed.starts_with("[{")) && trimmed.contains("\"tool\"")
168+
}
169+
165170
// Helper function to extract model from model_id field
166171
fnextract_model_from_model_id(model_id:&str) ->Option<String>{
167172
// Model ID format examples:
@@ -237,6 +242,7 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
237242
});
238243

239244
letmut entries =Vec::new();
245+
letmut fallback_session_name:Option<String> =None;
240246

241247
// Process each request-response pair
242248
for(idx, request)in session.requests.iter().enumerate(){
@@ -296,6 +302,20 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
296302
}
297303
}
298304

305+
// Capture fallback session name from the first user message text
306+
if fallback_session_name.is_none() && !request.message.text.is_empty(){
307+
let text_str = request.message.text.clone();
308+
if !is_probably_tool_json_text(&text_str){
309+
let truncated =if text_str.chars().count() >50{
310+
let chars:String = text_str.chars().take(50).collect();
311+
format!("{}...", chars)
312+
}else{
313+
text_str
314+
};
315+
fallback_session_name =Some(truncated);
316+
}
317+
}
318+
299319
// Create user message
300320
let user_date =DateTime::from_timestamp_millis(request.timestamp).unwrap_or_else(Utc::now);
301321

@@ -315,6 +335,8 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
315335
model:None,
316336
stats:Stats::default(),
317337
role:MessageRole::User,
338+
uuid:None,
339+
session_name: fallback_session_name.clone(),
318340
});
319341

320342
// Create assistant message
@@ -359,6 +381,8 @@ pub(crate) fn parse_copilot_session_file(session_file: &Path) -> Result<Vec<Conv
359381
model,
360382
stats,
361383
role:MessageRole::Assistant,
384+
uuid:None,
385+
session_name: fallback_session_name.clone(),
362386
});
363387
}
364388

@@ -374,14 +398,15 @@ impl Analyzer for CopilotAnalyzer {
374398
fnget_data_glob_patterns(&self) ->Vec<String>{
375399
letmut patterns =Vec::new();
376400

377-
// VSCode forks that might have Copilot installed: Code, Cursor, Windsurf, VSCodium, Positron
401+
// VSCode forks that might have Copilot installed: Code, Cursor, Windsurf, VSCodium, Positron, Antigravity
378402
let vscode_forks =[
379403
"Code",
380404
"Cursor",
381405
"Windsurf",
382406
"VSCodium",
383407
"Positron",
384408
"Code - Insiders",
409+
"Antigravity",
385410
];
386411

387412
ifletSome(home_dir) = std::env::home_dir(){

‎src/analyzers/gemini_cli.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ fn parse_json_session_file(file_path: &Path) -> Result<Vec<ConversationMessage>>
194194
let project_hash =extract_and_hash_project_id_gemini_cli(file_path);
195195
let file_path_str = file_path.to_string_lossy();
196196
letmut entries =Vec::new();
197+
letmut fallback_session_name:Option<String> =None;
197198

198199
// Parse the complete session JSON
199200
let session:GeminiCliSession =
@@ -205,8 +206,19 @@ fn parse_json_session_file(file_path: &Path) -> Result<Vec<ConversationMessage>>
205206
GeminiCliMessage::User{
206207
id: _,
207208
timestamp,
208-
content: _,
209+
content,
209210
} =>{
211+
if fallback_session_name.is_none() && !content.is_empty(){
212+
let text_str = content;
213+
let truncated =if text_str.chars().count() >50{
214+
let chars:String = text_str.chars().take(50).collect();
215+
format!("{}...", chars)
216+
}else{
217+
text_str
218+
};
219+
fallback_session_name =Some(truncated);
220+
}
221+
210222
entries.push(ConversationMessage{
211223
date: timestamp,
212224
application:Application::GeminiCli,
@@ -221,6 +233,8 @@ fn parse_json_session_file(file_path: &Path) -> Result<Vec<ConversationMessage>>
221233
model:None,
222234
stats:Stats::default(),
223235
role:MessageRole::User,
236+
uuid:None,
237+
session_name: fallback_session_name.clone(),
224238
});
225239
}
226240
GeminiCliMessage::Gemini{
@@ -257,6 +271,8 @@ fn parse_json_session_file(file_path: &Path) -> Result<Vec<ConversationMessage>>
257271
conversation_hash:hash_text(&file_path.to_string_lossy()),
258272
stats,
259273
role:MessageRole::Assistant,
274+
uuid:None,
275+
session_name: fallback_session_name.clone(),
260276
});
261277
}
262278
_ =>{}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp