Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ npx tokscale@latest
# またはbunxを使用
bunx tokscale@latest

# たはdeno dxを使用
dx tokscale@latest

# ライトモード(テーブルレンダリングのみ)
npx tokscale@latest --light
```
Expand Down
3 changes: 3 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ npx tokscale@latest
# 또는 bunx 사용
bunx tokscale@latest

# 또는 deno dx 사용
dx tokscale@latest

# 라이트 모드 (테이블 렌더링만)
npx tokscale@latest --light
```
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ npx tokscale@latest
# Or use bunx
bunx tokscale@latest

# Or use deno dx
dx tokscale@latest

# Light mode (table rendering only)
npx tokscale@latest --light
```
Expand Down
3 changes: 3 additions & 0 deletions README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ npx tokscale@latest
# 或使用 bunx
bunx tokscale@latest

# 或使用 deno dx
dx tokscale@latest

# 轻量模式(仅表格渲染)
npx tokscale@latest --light
```
Expand Down
4 changes: 1 addition & 3 deletions crates/tokscale-cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,7 @@ impl App {
self.set_status("Refresh already in progress");
} else {
self.needs_reload = true;
if self.current_tab == Tab::Usage {
self.fetch_subscription_usage();
}
self.fetch_subscription_usage();
}
}
KeyCode::Char('R') if key.modifiers.contains(KeyModifiers::SHIFT) => {
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ define_clients!(
id: "gemini",
root: PathRoot::Home,
relative: ".gemini/tmp",
pattern: "*.json",
pattern: "*.json|*.jsonl",
headless: false,
parse_local: true,
submit_default: true
Expand Down
80 changes: 80 additions & 0 deletions crates/tokscale-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,13 @@ fn parse_all_messages_with_pricing_with_env_strategy(
};
}

if parsed.unresolved_model_events {
return CachedParseOutcome {
messages,
cache_entry: None,
};
}

let cache_entry = build_codex_cache_entry(
path,
parsed.messages,
Expand Down Expand Up @@ -654,6 +661,12 @@ fn parse_all_messages_with_pricing_with_env_strategy(
fallback_timestamp,
);

if parsed.unresolved_model_events {
return CachedParseOutcome {
messages,
cache_entry: None,
};
}
let cache_entry = build_codex_cache_entry(
path,
raw_messages,
Expand Down Expand Up @@ -3384,6 +3397,73 @@ mod tests {
}
}

#[test]
#[serial_test::serial]
fn test_codex_cache_does_not_persist_unknown_before_later_turn_context() {
let cache_home = tempfile::TempDir::new().unwrap();
let source_home = tempfile::TempDir::new().unwrap();
let original_home = std::env::var("HOME").ok();
std::env::set_var("HOME", cache_home.path());

{
let session_dir = source_home.path().join(".codex/sessions");
std::fs::create_dir_all(&session_dir).unwrap();
let path = session_dir.join("session.jsonl");
std::fs::write(
&path,
concat!(
r#"{"type":"session_meta","payload":{"source":"interactive","model_provider":"openai"}}"#,
"\n",
r#"{"timestamp":"2026-04-27T10:00:00Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":10,"cached_input_tokens":2,"output_tokens":3},"last_token_usage":{"input_tokens":10,"cached_input_tokens":2,"output_tokens":3}}}}"#,
"\n"
),
)
.unwrap();

let initial_messages = parse_all_messages_with_pricing(
source_home.path().to_str().unwrap(),
&["codex".to_string()],
None,
);
assert_eq!(initial_messages.len(), 1);
assert_eq!(initial_messages[0].model_id, "unknown");
assert!(message_cache::SourceMessageCache::load()
.get(&path)
.is_none());

std::thread::sleep(std::time::Duration::from_millis(5));
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
file.write_all(
concat!(
r#"{"timestamp":"2026-04-27T10:00:04Z","type":"turn_context","payload":{"model":"gpt-5.5"}}"#,
"\n"
)
.as_bytes(),
)
.unwrap();
file.flush().unwrap();

let resumed_messages = parse_all_messages_with_pricing(
source_home.path().to_str().unwrap(),
&["codex".to_string()],
None,
);
assert_eq!(resumed_messages.len(), 1);
assert_eq!(resumed_messages[0].model_id, "gpt-5.5");
assert!(message_cache::SourceMessageCache::load()
.get(&path)
.is_some());
}

match original_home {
Some(home) => std::env::set_var("HOME", home),
None => std::env::remove_var("HOME"),
}
}

#[test]
#[serial_test::serial]
fn test_source_cache_does_not_reuse_priced_cost_without_pricing_service() {
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/message_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;

const CACHE_SCHEMA_VERSION: u32 = 8;
const CACHE_SCHEMA_VERSION: u32 = 9;
const CACHE_FILENAME: &str = "source-message-cache.bin";
const CACHE_LOCK_FILENAME: &str = "source-message-cache.lock";
const MAX_CACHE_FILE_BYTES: u64 = 256 * 1024 * 1024;
Expand Down
34 changes: 34 additions & 0 deletions crates/tokscale-core/src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub fn scan_directory(root: &str, pattern: &str) -> Vec<PathBuf> {

match pattern {
"*.json" => file_name.ends_with(".json"),
"*.json|*.jsonl" => file_name.ends_with(".json") || file_name.ends_with(".jsonl"),
"*.jsonl" => file_name.ends_with(".jsonl"),
// OpenClaw: also match archived transcripts
// (<uuid>.jsonl.deleted.<ts>, <uuid>.jsonl.reset.<ts>)
Expand Down Expand Up @@ -1030,6 +1031,26 @@ mod tests {
assert!(json_files.iter().all(|p| p.extension().unwrap() == "json"));
}

#[test]
fn test_scan_directory_json_or_jsonl_pattern() {
let dir = TempDir::new().unwrap();
let path = dir.path();

File::create(path.join("session.json")).unwrap();
File::create(path.join("session.jsonl")).unwrap();
File::create(path.join("session.txt")).unwrap();

let session_files = scan_directory(path.to_str().unwrap(), "*.json|*.jsonl");
assert_eq!(session_files.len(), 2);
assert_eq!(
session_files
.iter()
.map(|path| path.file_name().unwrap().to_str().unwrap())
.collect::<Vec<_>>(),
vec!["session.json", "session.jsonl"]
);
}

#[test]
fn test_scan_directory_jsonl_pattern() {
let dir = TempDir::new().unwrap();
Expand Down Expand Up @@ -1853,6 +1874,19 @@ mod tests {
assert!(result.get(ClientId::OpenCode).is_empty());
}

#[test]
fn test_scan_all_clients_gemini_jsonl_session() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let gemini_path = home.join(".gemini/tmp/123/chats");
fs::create_dir_all(&gemini_path).unwrap();
File::create(gemini_path.join("session-abc.jsonl")).unwrap();

let result = scan_all_clients(home.to_str().unwrap(), &["gemini".to_string()]);
assert_eq!(result.get(ClientId::Gemini).len(), 1);
assert!(result.get(ClientId::Gemini)[0].ends_with("session-abc.jsonl"));
}

#[test]
fn test_scan_all_clients_copilot() {
let dir = TempDir::new().unwrap();
Expand Down
Loading