Read this first, every session. Then .agents/CONTEXT.md, then .agents/lessons.md.
Name: oxidetrack | Binary: oxtrack | Stack: Rust 2021 + Ratatui + SQLite
Purpose: TUI that aggregates AI token usage from OpenCode, Zed, and VS Code Copilot
into a local SQLite database. Displays a Ratatui dashboard with HTML export.
cargo build # debug build
cargo build --release # release build -> target\release\tt.exe
cargo run -- sync # sync without TUI
cargo run -- dashboard # TUI (default command)
cargo run -- inspect <path> # schema discovery for a source DB
cargo run -- export -o report.html # HTML report
cargo fmt --check # formatting check
cargo fmt # auto-format
cargo clippy --all-targets --all-features # lints (fix all warnings)
cargo test # all tests
cargo test <test_name> # single test by name
cargo test --lib config::tests # tests in a specific module
cargo test -- --nocapture # show println output in testsThere is no test suite yet. When adding tests, co-locate #[cfg(test)] mod tests
inside each source file. Integration tests go in tests/.
src/
main.rs CLI entry (clap). Routes to sync / export / dashboard / inspect / recalc.
config.rs Config struct. Loads TOML. Knows pricing + project mapping.
db.rs Aggregation DB. All SQL lives here. UsageRecord is the core type.
export.rs HTML + CSV report builder. No external deps (Chart.js via CDN).
pricing.rs models.dev pricing fetch and cache.
app.rs TUI App state. fmt_tokens / fmt_cost helpers. 7 tabs.
sync/
mod.rs sync_all() orchestrator. inspect_schema() helper.
opencode.rs Reads OpenCode SQLite -> UsageRecord[].
zed.rs Reads Zed SQLite -> UsageRecord[] (placeholder).
ui/
mod.rs Terminal setup/teardown. Event loop. Key handler.
draw.rs All ratatui render functions. Pure -- no DB access.
- Order:
moddeclarations first, thenusestatements - Grouping: 1)
std/anyhow/ external crates -> 2) blank line -> 3)crate::/super::internal - Granularity: import specific items, not globs.
use anyhow::{Context, Result}; - Re-exports from parent: sync adapters import helpers via
use super::{normalise_provider, RecordBuilder};
- 4-space indentation (Rust standard, enforced by
cargo fmt) - Max line width: 100 columns (follow
rustfmtdefaults) - Trailing commas in multi-line struct literals, function args, match arms
- Section dividers: use
// --- Title ---comment blocks (Unicode box-drawing chars OK)
- Structs: PascalCase (
UsageRecord,ModelRow,StatCard) - Functions: snake_case (
sync_all,calc_cost,fmt_tokens) - Constants: SCREAMING_SNAKE_CASE (
TABS,RANGES,C_ACCENT) - Modules: snake_case filenames (
opencode.rs,draw.rs) - Enums: PascalCase variants (
Commands::Sync,Commands::Dashboard,Commands::Recalc) - Builder pattern:
RecordBuilderwithimpl Into<String>for flexible params
anyhow::Resultfor all fallible functions (no custom error enum yet)?operator for propagation; use.with_context(|| ...)for user-friendly messages- Graceful fallback: sync adapters try primary schema, fall back to alternate, then error
unwrap_or_default()for non-critical DB queries in UI (never crash the TUI)- Never
unwrap()in production paths.expect("reason")only where logically impossible
- Data types:
#[derive(Debug, Clone, Serialize, Deserialize)]for anything stored/transferred - Display types:
#[derive(Debug, Clone)]minimum; addDefaultwhere zero-values make sense - CLI:
#[derive(Parser)]/#[derive(Subcommand)]via clap derive
- Module-level
//!doc comments for sync adapters (explain schema, file locations) ///doc comments on all public functions and types- Inline
//comments for non-obvious logic (SQL queries, cost formulas)
draw.rsis pure. Takes&Apponly. Never add DB calls, file I/O, orasync.db.rsowns all SQL. Add new queries asDatabasemethods. No inline SQL elsewhere.session_idis the dedup key. Format:"source:original_id". UNIQUE INDEX prevents dupes.- Cost calculation in
Config::calc_costonly. Never hard-code prices outsideconfig.rs. - Sync adapters tolerate missing columns. Try primary query, fall back, then error.
- Never store raw content. Only token counts, costs, timestamps, and identifiers. No PII.
- Create
src/sync/<source>.rs - Implement
pub fn sync(cfg: &Config, db: &Database) -> Result<usize> - Add
pub mod <source>;insync/mod.rs - Add
if cfg.sources.<source>block insync_all() - Add field to
SourcesConfiginconfig.rs+Defaultimpl - Run
oxtrack inspect <path>to verify schema before writing SQL
- Add label to
app::TABS - Add
draw_<tabname>inui/draw.rs - Add match arm in
draw_content()indraw.rs - Load data in
App::reload()inapp.rs - Handle scroll in
handle_key()inui/mod.rsif tab has a table
Add CREATE TABLE IF NOT EXISTS or ALTER TABLE ... ADD COLUMN in Database::migrate().
Never drop columns or tables -- the DB may hold user data.
pub struct UsageRecord {
pub source: String, // "opencode" | "zed" | "copilot"
pub provider: String, // "anthropic" | "openai" | "google" | "ollama"
pub model: String,
pub project: Option<String>,
pub input_tokens: i64,
pub output_tokens: i64,
pub cost_usd: f64,
pub session_id: Option<String>, // dedup key
pub recorded_at: String, // RFC 3339
}
pub struct UsageSummary { total_tokens, input_tokens, output_tokens, cost_usd, request_count }
pub struct ModelRow { model, provider, total_tokens, input_tokens, output_tokens, cost_usd, request_count }- Read:
AGENTS.md->.agents/CONTEXT.md->.agents/lessons.md - Run:
cargo buildto verify clean state - Report: "Ready. Working on: [task]."
After 2 failed attempts: stop, write to .agents/failures.md, move on.
Format: ## [DATE] [title] | file: X | error: Y | tried: A, B | hypothesis: Z
Update .agents/CONTEXT.md with current state, blockers, and decisions.
- No network calls outside
src/sync/ - No UI framework other than Ratatui
- Do not change the binary name from
oxtrack - Do not store conversation content or file contents in the DB
- Shell: never use
&&in PowerShell -- use;or separate commands