From be3d1e0ab2900a4f0d0a44e3afd6ee09dd18f228 Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 13:32:30 +0800 Subject: [PATCH 01/67] feat(sources): add Codex usage support with multi-agent scopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn the Claude-only dashboard into a multi-agent one behind a UsageSource-style adapter layer; Codex CLI is the first new source. Backend: - store.rs: multi-root incremental ingest; Codex adapter parses ~/.codex/sessions rollout JSONL (token_count turn deltas via last_token_usage, model from turn_context, session from session_meta/ filename, mcp__* function calls). Normalizes Codex cached_input_tokens (a subset of input) into the shared input/cacheRead schema. Persists the newest rate_limits snapshot. Store cache bumped to v5. - parser.rs: dashboard now returns scopes — a single scope (classic UI, zero change) with one source, or All + per-agent scopes with several. All scope carries per-agent slices (hero split bar, stacked chart) and agent-tinted model palettes (Claude coral / Codex teal). - config.rs: Codex MCP whitelist from ~/.codex/config.toml [mcp_servers.*], '-'/'_' normalized to match logged tool names. - lib.rs: fs watcher covers all source roots; milestones read the aggregate scope; quieten pre-existing objc/cocoa dependency warnings. Frontend: - Agent filter chips (shown only when >=2 sources have data); filtering to one agent re-tints the whole panel with its accent per theme. - All view stacks the trend chart and hero bar by agent; single-agent views keep the Input/Output split. - Codex quota card: 5h + weekly rate-limit windows, plan, reset countdown, stale marker, amber warning at >=80%. - Skills section hides for Codex (no such concept); cost donut keeps agent-tinted model colors in multi-source mode. Co-Authored-By: Claude Fable 5 --- docs/DESIGN-multi-agent.md | 225 +++++++++++++++++++++++++++ src-tauri/Cargo.toml | 6 + src-tauri/src/config.rs | 46 +++++- src-tauri/src/lib.rs | 44 ++++-- src-tauri/src/model.rs | 54 ++++++- src-tauri/src/parser.rs | 219 ++++++++++++++++++++++----- src-tauri/src/store.rs | 302 ++++++++++++++++++++++++++++++++----- src/App.tsx | 148 +++++++++++++++--- src/charts.tsx | 62 +++++--- src/data.ts | 40 ++++- 10 files changed, 1012 insertions(+), 134 deletions(-) create mode 100644 docs/DESIGN-multi-agent.md diff --git a/docs/DESIGN-multi-agent.md b/docs/DESIGN-multi-agent.md new file mode 100644 index 0000000..1bda36c --- /dev/null +++ b/docs/DESIGN-multi-agent.md @@ -0,0 +1,225 @@ +# TokenScope 多智能体用量设计方案(Multi-Agent Design) + +> 版本:v0.1 草案 · 2026-07-02 +> 目标:从「Claude 用量仪表盘」演进为「本地所有 AI Agent 的统一用量仪表盘」。 +> 第一步:支持 Codex CLI。 + +--- + +## 1. 产品定位升级 + +### 1.1 新的一句话定位 + +> 一个常驻菜单栏的小工具,统一展示本机所有 AI 编码工具(Claude Code、Codex、未来更多)的 Token 用量、花费与使用习惯。 + +### 1.2 为什么是这个方向 + +- 开发者普遍**同时使用多个 CLI Agent**(Claude Code + Codex 双持很常见),但每个工具的用量入口互相割裂。 +- 各工具都把会话日志落在本地(`~/.claude`、`~/.codex`…),零侵入解析的技术路线可以完全复用。 +- 「跨 Agent 对比」本身就是新价值:这周 Claude 和 Codex 各烧了多少?哪个项目在用哪个工具? + +### 1.3 核心原则(继承自现有 PRD,不变) + +1. 零侵入:只读日志,不装 hook、不代理流量。 +2. 全本地:不上传任何数据。 +3. 花费永远标注「估算 (est.)」。 + +--- + +## 2. 数据源抽象:Source Adapter + +这是本次架构的核心:把「Claude」从硬编码变成第一个 **UsageSource 适配器**。 + +``` +trait UsageSource { + fn id() -> "claude" | "codex" | ... // 稳定标识 + fn display_name() -> "Claude Code" | "Codex" + fn detect() -> bool // 本机是否安装/有数据 + fn watch_paths() -> Vec // FS 监听目录 + fn parse_increment(file, offset) -> Vec // 增量解析 +} +``` + +### 2.1 统一事件模型(Normalized Schema) + +所有适配器解析后归一化为同一结构,聚合层/UI 层完全 source-agnostic: + +```jsonc +{ + "source": "codex", // 归属 Agent + "sessionId": "...", // 会话去重 + "eventId": "...", // 消息/回合去重 + "timestamp": "...", + "model": "gpt-5.5", + "tokens": { + "input": 470, // 非缓存输入(已扣除 cached) + "output": 274, + "cacheRead": 78208, + "cacheWrite": 0, // Codex 无此概念,置 0 + "reasoning": 0 // Claude 无此字段,置 0(含在 output 内) + }, + "cwd": "/Users/.../project", + "toolCalls": [{ "kind": "mcp", "server": "...", "name": "..." }] +} +``` + +### 2.2 Claude 适配器(现状迁移) + +现有 parser.rs 逻辑原样搬进适配器,无行为变化。 + +### 2.3 Codex 适配器(本期新增) + +**数据源**:`~/.codex/sessions/YYYY/MM/DD/rollout--.jsonl` +(目录可被 `CODEX_HOME` 环境变量重定向,设置里允许自定义路径。) + +关键事件与字段(已在本机实测验证,codex-cli 0.142): + +| 事件 | 用途 | 关键字段 | +|------|------|---------| +| `session_meta` | 会话元信息 | `session_id`、`cwd`、`cli_version`、`model_provider`、`source.subagent`(子代理标记)、`parent_thread_id` | +| `turn_context` | 回合上下文 | `turn_id`、`model`(如 `gpt-5.5`)、`cwd`、`reasoning_effort` | +| `event_msg / token_count` | **Token 用量** | `info.last_token_usage`(本回合增量)与 `info.total_token_usage`(会话累计):`input_tokens` / `cached_input_tokens` / `output_tokens` / `reasoning_output_tokens` | +| `event_msg / token_count` | **配额** | `rate_limits.primary`(5h 窗口 used_percent / resets_at)、`rate_limits.secondary`(周窗口)、`plan_type` | +| `response_item / function_call` | 工具调用 | MCP / 自定义工具调用统计 | + +**解析要点**: + +1. **Token 口径归一**:Codex 的 `cached_input_tokens` 是 `input_tokens` 的子集(Claude 的 cache_read 则是独立字段)。归一化时 Codex 的 `input = input_tokens - cached_input_tokens`、`cacheRead = cached_input_tokens`,保证跨 Agent 口径一致。 +2. **按回合取增量**:用 `last_token_usage` 作为单事件用量,按时间戳归入天/小时桶;模型归属取最近一条 `turn_context.model`(同一会话可中途换模型)。 +3. **去重**:`token_count` 无消息 id,用 `(file, 行号/字节偏移)` 作为 eventId;会话被 resume/fork 时是新文件新 session_id,`parent_thread_id` 仅作参考,不合并。 +4. **子代理**:`source.subagent`(如 review)的会话照常计入 token,会话数统计时归入主线程(可后期再细化)。 +5. **兜底校验**:每个文件最终的 `total_token_usage` 可用来校验增量求和是否漏算(容错:坏行跳过)。 +6. **花费**:`model` + `model_provider` 映射 LiteLLM key(`gpt-5.5` 等 OpenAI 系模型价格表已覆盖);缓存部分按 `cache_read_input_token_cost` 计价。 + +### 2.4 未来适配器(占位,不实现) + +Gemini CLI、Cursor CLI、OpenCode、Copilot CLI……只要实现 UsageSource 即插入。UI 按「已检测到数据的 source」动态展示,代码里不出现写死的双 Agent 假设。 + +--- + +## 3. 视觉与交互方案 + +### 3.1 总体思路:一套面板,一个过滤器,一套颜色语言 + +不做多页签、不做双栏对比视图。在现有单列面板上加一个 **Agent 过滤器**,并引入 **按 Agent 着色** 的颜色语言。理由: + +- 现有面板信息架构(总量 → 趋势 → 模型 → 花费 → 工具 → 热力图)对任何 Agent 都成立; +- 菜单栏浮窗宽度有限,双栏对比放不下也没必要; +- 「All 聚合视图」才是日常主视图,单 Agent 视图是下钻。 + +### 3.2 Agent 颜色语言 + +每个 Agent 分配一个固定的品牌色调,贯穿全部图表(堆叠条、圆环、列表圆点、过滤 chip): + +| Agent | 色调 | 示例 | +|-------|------|------| +| Claude Code | 珊瑚橙 `#D97757`(Anthropic 品牌色系) | 深浅两档用于 input/output | +| Codex | 青绿 `#10A37F`(OpenAI 品牌色系) | 同上 | +| All(聚合) | 沿用现有绿色主题 | 中性 | +| 未来 Agent | 从预置调色板顺序取(蓝/紫/黄…) | — | + +交互细节:**过滤到单个 Agent 时,整个面板的强调色切换为该 Agent 的色调**(图表、进度条、菜单栏图标 tint 不变)。用户一眼就知道当前在看谁。深浅色主题各配一组通过对比度校验的色值。 + +### 3.3 面板布局(All 视图) + +``` +┌──────────────────────────────────────────┐ +│ ◉ Tokenscope [Day|Week|Month] │ ← 不变 +│ ┌─────┬────────┬───────┐ │ +│ │ All │ ⬤Claude│ ⬤Codex│ ← Agent 过滤 chips(新增,仅检测到≥2个源时显示) +│ └─────┴────────┴───────┘ │ +│ TOTAL TOKENS Est.cost │ +│ 12.40M ▲14% $46.10 │ ← 全 Agent 合计 +│ ⬤ Claude 8.1M ⬤ Codex 4.3M │ ← 原 Input/Output 行改为按 Agent 分段条 +│ │ +│ ▐▐▐▐▐▐▐ 周趋势堆叠柱状图 │ ← 堆叠维度改为 Agent(橙+青绿) +│ │ hover 提示各 Agent 当日量 +│ TOKENS BY MODEL │ +│ ⬤ Claude Sonnet 4.5 ████████ 5.8M 47% │ ← 圆点用所属 Agent 色 +│ ⬤ GPT-5.5 ███ 1.9M 16% │ +│ │ +│ COST BY MODEL ◔ $46.10 │ ← 圆环切片按 Agent 色系分深浅 +│ │ +│ [REQUESTS 2,847] [COST TREND $46.10] │ ← 不变,数据为合计 +│ │ +│ MCP CALLS 1,284 · 14 servers │ ← 两源合并,行尾小圆点标 Agent +│ SKILL CALLS (Claude 专属,见 3.5) │ +│ DAILY ACTIVITY 热力图(合计) │ +└──────────────────────────────────────────┘ +``` + +要点: +- **Input/Output 分段条 → Agent 分段条**:All 视图下最重要的问题从「输入输出比」变成「谁在烧」。Input/Output 拆分下沉到单 Agent 视图。 +- 只装了一个 Agent 的用户**看不到任何变化**:chips 不出现,面板与今天完全一致(着色沿用现有绿色)。 + +### 3.4 单 Agent 视图(点击 chip 下钻) + +- 布局与现在的 Claude 视图一致:总量条恢复为 **Input / Output** 拆分,趋势图恢复 input/output 堆叠。 +- 强调色切换为该 Agent 色调。 +- **Codex 视图专属卡片:配额(Rate Limits)** —— 数据直接来自日志,Claude 没有的差异化能力: + +``` +┌ CODEX QUOTA ────────────────────────────┐ +│ 5h window ████████░░░░░░ 62% │ +│ Weekly ███░░░░░░░░░░░ 23% │ +│ Plan: Pro resets in 2h 14m │ +└─────────────────────────────────────────┘ +``` + + 取**最新一条** `token_count.rate_limits`;数据超过 1 小时未更新时置灰并标注「as of HH:MM」。≥80% 时进度条转警示色,可选系统通知(后期)。 +- Codex 视图中 output 条内再细分 **reasoning tokens**(浅色纹理段),hover 显示占比。 + +### 3.5 各区块在不同过滤态下的行为 + +| 区块 | All | Claude | Codex | +|------|-----|--------|-------| +| 总量卡 | 合计 + Agent 分段条 | Input/Output 条 | Input/Output 条(含 reasoning 细分) | +| 趋势柱状图 | 按 Agent 堆叠 | 按 In/Out 堆叠 | 按 In/Out 堆叠 | +| By Model | 全部模型,圆点带 Agent 色 | 仅 claude-* | 仅该源模型 | +| Cost 圆环 | 按模型,色相随 Agent | 现状 | 同左 | +| MCP Calls | 合并 + 行尾 Agent 圆点 | 仅 Claude | 仅 Codex | +| Skill Calls | 显示(仅 Claude 数据) | 显示 | **隐藏**(Codex 无此概念) | +| Quota 卡 | 隐藏 | 隐藏 | **显示** | +| 热力图 | 合计 | 单源 | 单源 | + +原则:**没有数据的区块整块隐藏,不显示空表**;概念不存在的功能不硬造对应物。 + +### 3.6 菜单栏 + +- 数字 = **所有 Agent 今日合计**(延续「只显示 token 不显示钱」)。 +- 设置项「菜单栏统计范围」:All(默认)/ 仅某个 Agent。 +- 图标不按 Agent 换色,保持系统级中性。 + +### 3.7 空态与引导 + +- 首次启动自动探测:`~/.claude/projects` 与 `~/.codex/sessions` 谁有数据就启用谁。 +- 检测到新数据源时,面板顶部出现一次性提示条:「检测到 Codex 用量数据,已自动纳入统计 ✕」。 +- 设置页:每个数据源一行 —— 开关 + 路径(默认路径 / 自定义)+ 检测状态(`已检测到 N 个会话` / `未找到数据`)。 + +--- + +## 4. 分期计划 + +### Phase 1 — Codex 支持(本期) +1. Rust 端抽出 `UsageSource` trait,Claude 逻辑迁入适配器(行为不变,回归验证)。 +2. Codex 适配器:目录扫描 + FS 监听 + token_count/turn_context 增量解析 + LiteLLM 计价。 +3. UI:Agent 过滤 chips、Agent 颜色语言、All 视图分段条/堆叠图、单源视图。 +4. Codex Quota 卡片。 +5. 设置页数据源管理 + 空态。 + +### Phase 2 — 打磨 +- MCP 合并视图的 Agent 标注;reasoning token 细分;配额 ≥80% 通知。 +- 「按项目 (cwd)」切片:两源都有 cwd,天然可做跨 Agent 项目视图。 + +### Phase 3 — 更多 Agent +- Gemini CLI / Cursor / OpenCode / Copilot CLI 适配器,chips 超过 4 个时折叠为下拉。 + +--- + +## 5. 关键决策记录 + +1. **过滤器而非页签/双栏**:All 聚合是主场景,单源是下钻;浮窗宽度也不允许并排对比。 +2. **All 视图堆叠维度从 In/Out 改为 Agent**:多源场景下「谁在烧」比「输入输出比」更重要,后者下沉到单源视图。 +3. **Codex 用 last_token_usage 增量而非 total 快照**:才能落到小时/天粒度桶;total 仅作校验。 +4. **单源用户零变化**:只有检测到 ≥2 个数据源才出现任何多 Agent UI,避免打扰存量用户。 +5. **Quota 卡做成 source 专属能力**:适配器可声明可选能力(capabilities),UI 按能力渲染,未来别的 Agent 有独有数据同理接入。 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3705467..6086258 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -9,6 +9,12 @@ edition = "2021" name = "tokenscope_lib" crate-type = ["staticlib", "cdylib", "rlib"] +[lints.rust] +# The old `objc` crate (pulled in via tauri-nspanel) emits cfg(feature = +# "cargo-clippy") from its macros, which trips this lint at every msg_send! +# call site in our crate. Nothing we can fix locally — silence it. +unexpected_cfgs = "allow" + [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index d7baa78..39a2573 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -5,7 +5,8 @@ use std::fs; use std::path::{Path, PathBuf}; pub struct UserConfig { - pub mcp_servers: HashSet, + pub mcp_servers: HashSet, // claude, from ~/.claude.json + pub codex_mcp_servers: HashSet, // codex, from ~/.codex/config.toml pub skills: HashSet, } @@ -66,19 +67,58 @@ fn load_user_skills() -> HashSet { set } +/// Codex normalizes MCP server names to snake_case in tool names (a config +/// entry `chrome-devtools` calls tools named `mcp__chrome_devtools__…`), so +/// both the whitelist and lookups go through this. +fn norm_mcp(name: &str) -> String { + name.replace('-', "_") +} + +/// MCP servers from ~/.codex/config.toml (honoring CODEX_HOME): every +/// `[mcp_servers.]` table header. A structural TOML parse isn't needed +/// for section names, so we scan headers instead of adding a toml dependency. +fn load_codex_mcps() -> HashSet { + let mut set = HashSet::new(); + let path = std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .or_else(|| home().map(|h| h.join(".codex"))) + .map(|d| d.join("config.toml")); + let Some(text) = path.and_then(|p| fs::read_to_string(p).ok()) else { + return set; + }; + for line in text.lines() { + let line = line.trim(); + // Match [mcp_servers.] and [mcp_servers.""], but not deeper + // sub-tables like [mcp_servers..env]. + let Some(rest) = line.strip_prefix("[mcp_servers.") else { continue }; + let Some(inner) = rest.strip_suffix(']') else { continue }; + let name = inner.trim_matches('"'); + if !name.is_empty() && !name.contains('.') { + set.insert(norm_mcp(name)); + } + } + set +} + impl UserConfig { pub fn load() -> Self { // Parse ~/.claude.json a single time and derive the MCP whitelist from it. let json = read_user_config(); UserConfig { mcp_servers: mcps_from(json.as_ref()), + codex_mcp_servers: load_codex_mcps(), skills: load_user_skills(), } } /// A tool name like "mcp____" → is server user-installed? - pub fn is_user_mcp(&self, server: &str) -> bool { - self.mcp_servers.contains(server) + /// Checked against the owning agent's own config. + pub fn is_user_mcp(&self, agent: &str, server: &str) -> bool { + if agent == crate::store::AGENT_CODEX { + self.codex_mcp_servers.contains(&norm_mcp(server)) + } else { + self.mcp_servers.contains(server) + } } /// A skill id (may be "plugin:skill") → strip plugin prefix, check dir. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 60e1add..d3a2268 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -181,13 +181,17 @@ fn check_milestones(app: &tauri::AppHandle, dash: &Dashboard) { let Some(state) = app.try_state::() else { return; }; + // The first scope is always the aggregate view (all agents combined). + let Some(primary) = dash.scopes.first() else { + return; + }; // total_tokens is already in millions, so a 100M milestone is total / 100. let (week_id, month_id) = period_ids(); let cur = MilestoneState { week_id, - week_floor: (dash.week.metrics.total_tokens / 100.0).floor() as i64, + week_floor: (primary.week.metrics.total_tokens / 100.0).floor() as i64, month_id, - month_floor: (dash.month.metrics.total_tokens / 100.0).floor() as i64, + month_floor: (primary.month.metrics.total_tokens / 100.0).floor() as i64, }; let mut g = state.state.lock().unwrap(); @@ -224,6 +228,7 @@ fn celebrate(app: &tauri::AppHandle) { /// Show (or reuse) a full-screen, click-through, non-activating overlay on the /// primary monitor and run the confetti animation, then hide it after it plays. /// Must be called on the main thread. +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) fn show_celebration(app: &tauri::AppHandle) { let Some(state) = app.try_state::() else { return; @@ -505,6 +510,7 @@ fn position_popover_windows(app: &tauri::AppHandle) { /// True if our (Accessory) app is currently the frontmost application. #[cfg(target_os = "macos")] +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) fn app_is_frontmost() -> bool { use tauri_nspanel::cocoa::base::id; use tauri_nspanel::objc::{class, msg_send, sel, sel_impl}; @@ -541,6 +547,7 @@ fn hide_panel_on_context_switch(app: &tauri::AppHandle) { /// activation (mirrors tauri-nspanel's menu-bar example). The observers live for /// the whole app lifetime, so the returned tokens are intentionally dropped. #[cfg(target_os = "macos")] +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) fn register_panel_autohide(app: &tauri::AppHandle) { use std::ffi::CString; use tauri_nspanel::block::ConcreteBlock; @@ -578,6 +585,7 @@ fn register_panel_autohide(app: &tauri::AppHandle) { /// (and thus the webview's `prefers-color-scheme`) can lag the real system value. /// The user default reflects the system setting directly, regardless of focus. #[cfg(target_os = "macos")] +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) fn system_is_dark() -> bool { use std::ffi::CStr; use tauri_nspanel::cocoa::base::{id, nil}; @@ -609,6 +617,7 @@ fn system_is_dark() -> bool { /// lives for the whole app lifetime, so the returned token is intentionally /// dropped (same as register_panel_autohide). #[cfg(target_os = "macos")] +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) fn watch_system_theme(app: &tauri::AppHandle) { use std::ffi::CString; use tauri_nspanel::block::ConcreteBlock; @@ -727,6 +736,7 @@ fn fmt_tokens_m(m: f64) -> String { } #[cfg_attr(mobile, tauri::mobile_entry_point)] +#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) pub fn run() { // Tracks when the popover was last hidden, so a click on the tray icon // while it's open (which first blurs/hides it) doesn't immediately reopen. @@ -1007,12 +1017,14 @@ pub fn run() { }); // Filesystem watcher: reflect a log write within ~1s instead of - // waiting up to the 30s poll (PRD wants <=5s). Writes land in - // ~/.claude/projects; our own cache lives elsewhere, so this never - // self-triggers. Debounced so a burst of writes coalesces into one - // rebuild; the 30s poll above stays as a fallback. (build_dashboard - // serializes on BUILD_LOCK, so this and the poll can't race the cache.) - if let Some(projects) = dirs::home_dir().map(|h| h.join(".claude").join("projects")) { + // waiting up to the 30s poll (PRD wants <=5s). One watcher covers + // every source root (~/.claude/projects, ~/.codex/sessions, …); our + // own cache lives elsewhere, so this never self-triggers. Debounced + // so a burst of writes coalesces into one rebuild; the 30s poll + // above stays as a fallback. (build_dashboard serializes on + // BUILD_LOCK, so this and the poll can't race the cache.) + { + let roots = store::source_roots(); let handle = app.handle().clone(); std::thread::spawn(move || { use notify::{RecursiveMode, Watcher}; @@ -1027,11 +1039,17 @@ pub fn run() { Ok(w) => w, Err(_) => return, }; - // Claude Code may not have created the dir yet on a fresh - // machine; create it so watch() registers instead of silently - // falling back to the 30s poll for the whole session. - let _ = std::fs::create_dir_all(&projects); - if watcher.watch(&projects, RecursiveMode::Recursive).is_err() { + let mut watching = false; + for (_agent, dir) in &roots { + // The CLI may not have created its dir yet on a fresh + // machine; create it so watch() registers instead of + // silently falling back to the 30s poll all session. + let _ = std::fs::create_dir_all(dir); + if watcher.watch(dir, RecursiveMode::Recursive).is_ok() { + watching = true; + } + } + if !watching { return; } // Block for the first change, then drain the burst until quiet. diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 9cbd406..7c20aba 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -18,6 +18,9 @@ pub struct ModelStat { pub cost: f64, // USD estimate pub color: String, pub priced: bool, // false = no pricing data in LiteLLM (cost is unknown, not $0) + // Owning agent id ("claude" / "codex"). In the All scope the same model name + // can appear once per agent (e.g. gpt-5 via a router AND via Codex). + pub agent: String, } #[derive(Debug, Clone, Serialize)] @@ -51,6 +54,17 @@ pub struct Metrics { pub skills: u64, } +/// Per-agent share of a period, used only by the All scope: `tokens` drives the +/// hero split bar, `values` (aligned with `series`) drives the stacked chart. +#[derive(Debug, Clone, Serialize)] +pub struct AgentSlice { + pub id: String, // "claude" | "codex" + pub label: String, // "Claude" | "Codex" + pub color: String, // base hex, same across charts + pub tokens: f64, // M tokens in the period + pub values: Vec, // M tokens per series bucket +} + #[derive(Debug, Clone, Serialize)] pub struct PeriodReport { pub metrics: Metrics, @@ -62,6 +76,8 @@ pub struct PeriodReport { pub req_trend: Vec, #[serde(rename = "costTrend")] pub cost_trend: Vec, + // Non-empty only in the All scope when >=2 agents have data. + pub agents: Vec, } #[derive(Debug, Clone, Serialize)] @@ -71,14 +87,48 @@ pub struct HeatDay { pub level: u8, // 0..4 } +/// Codex rate-limit snapshot, straight from the newest token_count event's +/// rate_limits. `as_of_ms` lets the UI flag a stale reading. +#[derive(Debug, Clone, Serialize, serde::Deserialize)] +pub struct Quota { + pub plan: String, + #[serde(rename = "primaryPct")] + pub primary_pct: f64, + #[serde(rename = "primaryMinutes")] + pub primary_minutes: u64, + #[serde(rename = "primaryResetsAt")] + pub primary_resets_at: i64, // unix seconds + #[serde(rename = "secondaryPct")] + pub secondary_pct: f64, + #[serde(rename = "secondaryMinutes")] + pub secondary_minutes: u64, + #[serde(rename = "secondaryResetsAt")] + pub secondary_resets_at: i64, // unix seconds + #[serde(rename = "asOfMs")] + pub as_of_ms: i64, +} + +/// One selectable view of the dashboard. With a single data source there is +/// exactly one scope (id "all", empty color → default theme, UI unchanged); +/// with several sources the first scope is the aggregate ("all") followed by +/// one scope per agent, and the UI shows filter chips. #[derive(Debug, Clone, Serialize)] -pub struct Dashboard { +pub struct Scope { + pub id: String, // "all" | "claude" | "codex" + pub label: String, // "All" | "Claude" | "Codex" + pub color: String, // agent accent hex; "" = default theme accent pub day: PeriodReport, pub week: PeriodReport, pub month: PeriodReport, pub heatmap: Vec, + pub quota: Option, // Codex only +} + +#[derive(Debug, Clone, Serialize)] +pub struct Dashboard { + pub scopes: Vec, #[serde(rename = "todayTokens")] - pub today_tokens: f64, // M tokens, for the tray label + pub today_tokens: f64, // M tokens across all agents, for the tray label #[serde(rename = "generatedAt")] pub generated_at: String, } diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index b2a15a3..33661a0 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -1,10 +1,12 @@ -// Parse ~/.claude/projects/**/*.jsonl, dedupe assistant messages by id, -// classify tool calls (user-installed MCP / Skill only), and aggregate -// into Day / Week / Month reports + a daily heatmap. +// Aggregate the store's normalized events (Claude + Codex) into per-scope +// Day / Week / Month reports + a daily heatmap. With one data source the +// dashboard has a single scope and looks exactly like the classic single-agent +// UI; with several, the first scope aggregates everything ("All") and is +// followed by one scope per agent, each with its own accent palette. use crate::config::UserConfig; use crate::model::*; use crate::pricing::Pricing; -use crate::store::{RawEvent, Store}; +use crate::store::{RawEvent, Store, AGENT_CLAUDE, AGENT_CODEX}; use chrono::{DateTime, Datelike, Duration, Local, Timelike}; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; @@ -13,8 +15,8 @@ use std::sync::Mutex; // handler never touch the incremental cache files concurrently. static BUILD_LOCK: Mutex<()> = Mutex::new(()); -// One assistant API response, with config + pricing applied (derived per request -// from a RawEvent, since user config / prices / time windows can all change). +// One API response, with config + pricing applied (derived per request from a +// RawEvent, since user config / prices / time windows can all change). struct Event { ts: DateTime, session: String, @@ -24,11 +26,42 @@ struct Event { output: f64, // raw tokens cost: f64, // USD (differentiated by token type), 0 if unknown model priced: bool, // whether a price was found for this model + agent: &'static str, // owning agent id (interned via agent_def) mcp: Vec, // user-installed server names called in this msg skills: Vec, // user-installed skill names called in this msg } -// Top-5 models keep the green/slate scheme; everything beyond is uniform gray. +/// Static per-agent identity: label + accent + a 5-step chart palette (rank +/// shades of the accent, ending in a muted overflow tone). +pub struct AgentDef { + pub id: &'static str, + pub label: &'static str, + pub color: &'static str, + palette: [&'static str; 5], +} + +/// Registry order = display order (chips, stacked charts, slices). +const AGENTS: &[AgentDef] = &[ + AgentDef { + id: AGENT_CLAUDE, + label: "Claude", + color: "#d97757", // Anthropic coral + palette: ["#c9683f", "#d97757", "#e59a78", "#f0c0a8", "#6d5147"], + }, + AgentDef { + id: AGENT_CODEX, + label: "Codex", + color: "#10a37f", // OpenAI teal + palette: ["#0f8a6c", "#10a37f", "#4dbf9f", "#93dcc5", "#4a5f57"], + }, +]; + +fn agent_def(id: &str) -> &'static AgentDef { + AGENTS.iter().find(|a| a.id == id).unwrap_or(&AGENTS[0]) +} + +// Single-source palette: the classic green/slate scheme (kept so a one-agent +// install looks exactly like before). Top-5 models get these; beyond is gray. const PALETTE: &[&str] = &["#1f9d63", "#34c27e", "#6ad0a0", "#a7e3c5", "#4b5a52"]; const OVERFLOW_GRAY: &str = "#79817b"; @@ -52,7 +85,7 @@ fn vendor_of(model: &str) -> &'static str { let m = model.to_lowercase(); if m.contains("claude") { "Anthropic" - } else if m.contains("gpt") || m.contains("o1") || m.contains("o3") { + } else if m.contains("gpt") || m.contains("o1") || m.contains("o3") || m.contains("codex") { "OpenAI" } else if m.contains("gemini") { "Google" @@ -84,6 +117,7 @@ pub fn build_dashboard() -> Dashboard { if dirty { store.save(); } + let codex_quota = store.codex_quota.clone(); // 2. Aggregate: apply current config + prices, slice by current time. let cfg = UserConfig::load(); @@ -99,21 +133,66 @@ pub fn build_dashboard() -> Dashboard { let now = Local::now(); let today = now.date_naive(); - let mut day = report_day(&events, now); - let mut week = report_week(&events, now); - let mut month = report_month(&events, now); - let heatmap = build_heatmap(&events, today); + // Agents that actually have data, in registry order. + let present_ids: HashSet<&str> = events.iter().map(|e| e.agent).collect(); + let present: Vec<&AgentDef> = AGENTS.iter().filter(|a| present_ids.contains(a.id)).collect(); - // "servers"/"skills" = how many the user has *installed* (global, constant - // across periods), not how many were called in the window. - let installed_servers = cfg.mcp_servers.len() as u64; - let installed_skills = cfg.skills.len() as u64; - for r in [&mut day, &mut week, &mut month] { - r.metrics.servers = installed_servers; - r.metrics.skills = installed_skills; - } + let scopes = if present.len() <= 1 { + // Single (or no) source → one scope, classic green UI, no chips. + let agent = present.first().map(|a| a.id).unwrap_or(AGENT_CLAUDE); + let mut scope = build_scope("all", "All", "", &events, PALETTE, agent, &cfg, now); + if agent == AGENT_CODEX { + scope.quota = codex_quota; + } + vec![scope] + } else { + // Aggregate scope first (default palette; models/slices merged below), + // then one accent-colored scope per agent. + let mut per_agent: Vec = Vec::new(); + for a in &present { + let filtered: Vec = events.iter().filter(|e| e.agent == a.id).map(clone_event).collect(); + let mut s = build_scope(a.id, a.label, a.color, &filtered, &a.palette, a.id, &cfg, now); + if a.id == AGENT_CODEX { + s.quota = codex_quota.clone(); + } + per_agent.push(s); + } + let mut all = build_scope("all", "All", "", &events, PALETTE, "", &cfg, now); + // All-scope model rows keep each agent's palette (rank within agent), and + // each period gains per-agent slices for the split bar + stacked chart. + for idx in 0..3usize { + let mut models: Vec = Vec::new(); + let mut slices: Vec = Vec::new(); + for (a, s) in present.iter().zip(per_agent.iter()) { + let p = match idx { + 0 => &s.day, + 1 => &s.week, + _ => &s.month, + }; + models.extend(p.models.iter().cloned()); + slices.push(AgentSlice { + id: a.id.to_string(), + label: a.label.to_string(), + color: a.color.to_string(), + tokens: p.metrics.total_tokens, + values: p.series.iter().map(|pt| pt.input + pt.cache + pt.output).collect(), + }); + } + models.sort_by(|x, y| y.tokens.partial_cmp(&x.tokens).unwrap_or(std::cmp::Ordering::Equal)); + let target = match idx { + 0 => &mut all.day, + 1 => &mut all.week, + _ => &mut all.month, + }; + target.models = models; + target.agents = slices; + } + let mut v = vec![all]; + v.extend(per_agent); + v + }; - // today's displayed tokens (M) for the tray + // today's displayed tokens (M) for the tray — across all agents let today_tokens: f64 = events .iter() .filter(|e| e.ts.date_naive() == today) @@ -121,12 +200,76 @@ pub fn build_dashboard() -> Dashboard { .sum(); Dashboard { + scopes, + today_tokens, + generated_at: now.to_rfc3339(), + } +} + +/// Cheap manual clone (Event holds Vecs; used only for per-agent +/// filtering during multi-agent builds). +fn clone_event(e: &Event) -> Event { + Event { + ts: e.ts, + session: e.session.clone(), + model: e.model.clone(), + input: e.input, + cache: e.cache, + output: e.output, + cost: e.cost, + priced: e.priced, + agent: e.agent, + mcp: e.mcp.clone(), + skills: e.skills.clone(), + } +} + +/// Build one scope (all three period reports + heatmap) over `events`. +/// `agent_scope` is the agent id the scope represents ("" for the aggregate) — +/// it decides which installed-server/skill counts the metrics carry. +#[allow(clippy::too_many_arguments)] +fn build_scope( + id: &str, + label: &str, + color: &str, + events: &[Event], + palette: &[&str], + agent_scope: &str, + cfg: &UserConfig, + now: DateTime, +) -> Scope { + let today = now.date_naive(); + let mut day = report_day(events, now, palette); + let mut week = report_week(events, now, palette); + let mut month = report_month(events, now, palette); + let heatmap = build_heatmap(events, today); + + // "servers"/"skills" = how many the user has *installed* (global, constant + // across periods), not how many were called in the window. Per agent: + // skills are a Claude-only concept, so a pure-Codex scope reports 0 and the + // UI hides the section. + let (installed_servers, installed_skills) = match agent_scope { + AGENT_CODEX => (cfg.codex_mcp_servers.len() as u64, 0u64), + AGENT_CLAUDE => (cfg.mcp_servers.len() as u64, cfg.skills.len() as u64), + _ => ( + (cfg.mcp_servers.len() + cfg.codex_mcp_servers.len()) as u64, + cfg.skills.len() as u64, + ), + }; + for r in [&mut day, &mut week, &mut month] { + r.metrics.servers = installed_servers; + r.metrics.skills = installed_skills; + } + + Scope { + id: id.to_string(), + label: label.to_string(), + color: color.to_string(), day, week, month, heatmap, - today_tokens, - generated_at: now.to_rfc3339(), + quota: None, } } @@ -145,7 +288,7 @@ fn compute_event(r: &RawEvent, cfg: &UserConfig, pricing: &Pricing) -> Event { let mcp = r .mcp .iter() - .filter(|s| cfg.is_user_mcp(s)) + .filter(|s| cfg.is_user_mcp(&r.agent, s)) .cloned() .collect(); let skills = r @@ -163,6 +306,7 @@ fn compute_event(r: &RawEvent, cfg: &UserConfig, pricing: &Pricing) -> Event { output: r.out_tok, cost: cost_opt.unwrap_or(0.0), priced: cost_opt.is_some(), + agent: agent_def(&r.agent).id, mcp, skills, } @@ -182,6 +326,7 @@ struct Agg { model_tok: HashMap, model_cost: HashMap, model_priced: HashMap, + model_agent: HashMap, mcp_counts: HashMap, skill_counts: HashMap, } @@ -195,8 +340,9 @@ impl Agg { if !e.session.is_empty() { self.sessions.insert(e.session.clone()); } - // Slash-command skill events carry no model (empty) — they're not LLM - // requests, so they must not inflate request counts or the model split. + // Slash-command skill / MCP-call-only events carry no model (empty) — + // they're not LLM requests, so they must not inflate request counts or + // the model split. if !e.model.is_empty() { self.requests += 1; // model totals keep all token types so shares sum to Total tokens @@ -204,6 +350,7 @@ impl Agg { *self.model_cost.entry(e.model.clone()).or_default() += e.cost; // a model is "priced" if any of its messages had a known price *self.model_priced.entry(e.model.clone()).or_default() |= e.priced; + self.model_agent.entry(e.model.clone()).or_insert(e.agent); } for s in &e.mcp { self.mcp_calls += 1; @@ -215,7 +362,7 @@ impl Agg { } } - fn models(&self) -> Vec { + fn models(&self, palette: &[&str]) -> Vec { let mut v: Vec<(String, f64, f64)> = self .model_tok .iter() @@ -230,8 +377,9 @@ impl Agg { vendor: vendor_of(&name).to_string(), tokens: (tok / 1e6 * 100.0).round() / 100.0, cost: (cost * 100.0).round() / 100.0, - color: if i < PALETTE.len() { PALETTE[i] } else { OVERFLOW_GRAY }.to_string(), + color: if i < palette.len() { palette[i] } else { OVERFLOW_GRAY }.to_string(), priced, + agent: self.model_agent.get(&name).copied().unwrap_or("").to_string(), name, } }) @@ -279,7 +427,7 @@ fn pct_delta(cur: f64, prev: f64) -> f64 { } // ── Day report: today, 24 hourly buckets ─────────────────────────── -fn report_day(events: &[Event], now: DateTime) -> PeriodReport { +fn report_day(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { let today = now.date_naive(); let yesterday = today - Duration::days(1); let mut agg = Agg::default(); @@ -332,16 +480,17 @@ fn report_day(events: &[Event], now: DateTime) -> PeriodReport { pct_delta(agg.cost, prev.cost), ), series, - models: agg.models(), + models: agg.models(palette), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, cost_trend: cost_b, + agents: Vec::new(), } } // ── Week report: current calendar week (Mon-Sun) vs previous week ──── -fn report_week(events: &[Event], now: DateTime) -> PeriodReport { +fn report_week(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { let today = now.date_naive(); // Monday of the current week (Mon=0 … Sun=6). let start = today - Duration::days(today.weekday().num_days_from_monday() as i64); @@ -399,16 +548,17 @@ fn report_week(events: &[Event], now: DateTime) -> PeriodReport { pct_delta(agg.cost, prev.cost), ), series, - models: agg.models(), + models: agg.models(palette), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, cost_trend: cost_b, + agents: Vec::new(), } } // ── Month report: current calendar month vs previous calendar month ── -fn report_month(events: &[Event], now: DateTime) -> PeriodReport { +fn report_month(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { use chrono::NaiveDate; let today = now.date_naive(); let (y, m) = (today.year(), today.month()); @@ -476,11 +626,12 @@ fn report_month(events: &[Event], now: DateTime) -> PeriodReport { pct_delta(agg.cost, prev.cost), ), series, - models: agg.models(), + models: agg.models(palette), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, cost_trend: cost_b, + agents: Vec::new(), } } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 168eefd..4792924 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -1,12 +1,17 @@ // Incremental event store. // // Ingestion (this file) is the only place that touches the JSONL logs. It -// parses each assistant message into a provider/config/price-independent -// RawEvent (just the facts), reads only newly-appended bytes of changed files -// (tracked by a per-file size/mtime/offset manifest), dedupes by message id, -// and persists everything to the cache dir. Aggregation (parser.rs) then works -// purely on these in-memory events — cheap, and recomputed per request because -// the Day/Week/Month windows are relative to "now". +// parses each log line into a provider/config/price-independent RawEvent +// (just the facts), reads only newly-appended bytes of changed files (tracked +// by a per-file manifest), dedupes by message id, and persists everything to +// the cache dir. Aggregation (parser.rs) then works purely on these in-memory +// events — cheap, and recomputed per request because the Day/Week/Month +// windows are relative to "now". +// +// Two sources are ingested, normalized to the same RawEvent shape: +// claude — ~/.claude/projects/**/*.jsonl (assistant messages) +// codex — ~/.codex/sessions/**/*.jsonl (token_count turn deltas) +use crate::model::Quota; use chrono::DateTime; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -15,28 +20,52 @@ use std::io::{Read, Seek, SeekFrom}; use std::path::PathBuf; use walkdir::WalkDir; +pub const AGENT_CLAUDE: &str = "claude"; +pub const AGENT_CODEX: &str = "codex"; + #[derive(Serialize, Deserialize, Clone)] pub struct RawEvent { pub ts_ms: i64, pub session: String, pub model: String, // raw model id (price lookup), normalized later for grouping - pub in_tok: f64, - pub cc: f64, // cache creation - pub cr: f64, // cache read + pub in_tok: f64, // uncached new input only (both agents) + pub cc: f64, // cache creation (claude only; codex has no such concept → 0) + pub cr: f64, // cache read (codex: cached_input_tokens, a subset of its raw input) pub out_tok: f64, pub mcp: Vec, // all mcp__ names called (unfiltered) pub skills: Vec, // all Skill input.skill ids called (unfiltered) - pub id: String, // message id (dedup) + pub id: String, // message id (dedup); "" = no cross-line dedup needed // Source log file (manifest key). Lets a truncated/rewritten file purge its // own stale events before being re-read, so re-ingestion stays idempotent. #[serde(default)] pub source: String, + // Owning agent id (AGENT_*). Default keeps old caches readable, though a + // version bump discards them anyway. + #[serde(default = "default_agent")] + pub agent: String, +} + +fn default_agent() -> String { + AGENT_CLAUDE.to_string() +} + +#[derive(Serialize, Deserialize, Clone, Default)] +struct FileState { + size: u64, + mtime_ms: i64, + offset: u64, // bytes already ingested + // Codex carry-over parse state: token_count lines don't repeat the model or + // session, so an incremental read resuming mid-file needs the last-seen + // values from the previous pass. Empty for claude files. + #[serde(default)] + model: String, + #[serde(default)] + session: String, } #[derive(Serialize, Deserialize, Default)] struct Manifest { - // path -> (size, mtime_ms, byte offset already ingested) - files: HashMap, + files: HashMap, } pub struct Store { @@ -47,6 +76,8 @@ pub struct Store { // count its token usage only once. index: HashMap, manifest: Manifest, + // Latest Codex rate-limit snapshot seen across all session files. + pub codex_quota: Option, } // Bump when the parsing/extraction logic changes in a way that requires @@ -56,7 +87,8 @@ pub struct Store { // v3: merge tool_use across lines sharing a message id (a thinking line + a // tool_use line were deduped, dropping the tool call). // v4: track a per-event source file (idempotent re-read of truncated logs). -const STORE_VERSION: u32 = 4; +// v5: multi-agent ingest (claude + codex), FileState manifest, quota snapshot. +const STORE_VERSION: u32 = 5; /// Atomically replace `path`'s contents: write a sibling temp file, then rename /// over the target (same-volume rename is atomic on Windows and Unix). Avoids @@ -67,10 +99,31 @@ fn write_atomic(path: &std::path::Path, data: &[u8]) -> std::io::Result<()> { fs::rename(&tmp, path) } -fn projects_dir() -> Option { +fn claude_dir() -> Option { Some(dirs::home_dir()?.join(".claude").join("projects")) } +/// ~/.codex/sessions, honoring the CODEX_HOME override the Codex CLI supports. +fn codex_dir() -> Option { + let home = std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .or_else(|| Some(dirs::home_dir()?.join(".codex")))?; + Some(home.join("sessions")) +} + +/// The log roots to ingest/watch: (agent id, directory). Missing directories +/// are simply skipped by the walker. +pub fn source_roots() -> Vec<(&'static str, PathBuf)> { + let mut v = Vec::new(); + if let Some(d) = claude_dir() { + v.push((AGENT_CLAUDE, d)); + } + if let Some(d) = codex_dir() { + v.push((AGENT_CODEX, d)); + } + v +} + fn cache_dir() -> Option { let d = dirs::cache_dir()?.join("tokenscope"); let _ = fs::create_dir_all(&d); @@ -82,6 +135,7 @@ impl Store { pub fn load() -> Self { let mut events: Vec = Vec::new(); let mut manifest = Manifest::default(); + let mut codex_quota = None; if let Some(dir) = cache_dir() { // If the cache was written by an older parser, discard it so ingest // does a full rescan and picks up newly-extracted facts. @@ -107,6 +161,11 @@ impl Store { events = e; manifest = m; } + // Quota is a best-effort side channel — a missing/corrupt file + // just means "no snapshot yet", never a rescan. + codex_quota = fs::read_to_string(dir.join("codex_quota.json")) + .ok() + .and_then(|t| serde_json::from_str::(&t).ok()); } } let index = events @@ -119,6 +178,7 @@ impl Store { events, index, manifest, + codex_quota, } } @@ -135,6 +195,11 @@ impl Store { if let Ok(t) = serde_json::to_string(&self.manifest) { let _ = write_atomic(&dir.join("offsets.json"), t.as_bytes()); } + if let Some(q) = &self.codex_quota { + if let Ok(t) = serde_json::to_string(q) { + let _ = write_atomic(&dir.join("codex_quota.json"), t.as_bytes()); + } + } let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); } } @@ -174,15 +239,23 @@ impl Store { removed } - /// Incrementally read only the new bytes of new/changed JSONL files. - /// Returns whether anything changed (new events or an updated file offset), - /// so the caller can skip a full cache rewrite when nothing moved. + /// Incrementally read only the new bytes of new/changed JSONL files across + /// all source roots. Returns whether anything changed (new events or an + /// updated file offset), so the caller can skip a full cache rewrite when + /// nothing moved. pub fn ingest(&mut self) -> bool { let mut dirty = false; - let Some(root) = projects_dir() else { - return false; - }; - for entry in WalkDir::new(&root) + for (agent, root) in source_roots() { + if self.ingest_root(agent, &root) { + dirty = true; + } + } + dirty + } + + fn ingest_root(&mut self, agent: &'static str, root: &PathBuf) -> bool { + let mut dirty = false; + for entry in WalkDir::new(root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map(|x| x == "jsonl").unwrap_or(false)) @@ -198,26 +271,26 @@ impl Store { .map(|d| d.as_millis() as i64) .unwrap_or(0); - let mut offset = match self.manifest.files.get(&key).copied() { - Some((psize, pmtime, poff)) => { - if psize == size && pmtime == mtime_ms { + let mut state = match self.manifest.files.get(&key).cloned() { + Some(prev) => { + if prev.size == size && prev.mtime_ms == mtime_ms { continue; // unchanged → skip } - if size < poff { + if size < prev.offset { // truncated / rewritten (e.g. log compaction): the bytes // we already ingested are gone, so purge this file's // events and re-read from the start, idempotently. self.purge_source(&key); - 0 + FileState::default() } else { - poff + prev } } - None => 0, + None => FileState::default(), }; let Ok(mut f) = fs::File::open(path) else { continue }; - if f.seek(SeekFrom::Start(offset)).is_err() { + if f.seek(SeekFrom::Start(state.offset)).is_err() { continue; } let mut buf = Vec::new(); @@ -230,13 +303,24 @@ impl Store { Some(i) => i + 1, None => 0, }; + // Codex session ids also live in the filename (rollout--); + // seed from it so events parsed before/without session_meta still + // group into a session. + if agent == AGENT_CODEX && state.session.is_empty() { + state.session = codex_session_from_filename(path); + } for line in buf[..process_until].split(|&b| b == b'\n') { if line.is_empty() { continue; } let Ok(s) = std::str::from_utf8(line) else { continue }; - if let Some(mut ev) = parse_line(s) { + let parsed = match agent { + AGENT_CODEX => self.parse_codex_line(s, &mut state), + _ => parse_claude_line(s), + }; + if let Some(mut ev) = parsed { ev.source = key.clone(); + ev.agent = agent.to_string(); if !ev.id.is_empty() { if let Some(&i) = self.index.get(&ev.id) { // Same message, another line: merge its tool calls @@ -251,16 +335,158 @@ impl Store { self.events.push(ev); } } - offset += process_until as u64; - self.manifest.files.insert(key, (size, mtime_ms, offset)); + state.offset += process_until as u64; + state.size = size; + state.mtime_ms = mtime_ms; + self.manifest.files.insert(key, state); dirty = true; } dirty } + + /// Parse one Codex rollout line. Most lines are conversation items we skip; + /// the ones that matter mutate the per-file parse state (model/session), + /// update the quota snapshot, or produce a RawEvent (token_count deltas and + /// MCP tool calls). + fn parse_codex_line(&mut self, line: &str, state: &mut FileState) -> Option { + let v: serde_json::Value = serde_json::from_str(line).ok()?; + let payload = v.get("payload")?; + match v.get("type")?.as_str()? { + "session_meta" => { + // `id` is this thread's own session id; `session_id` can be the + // parent for subagent threads. Prefer the file's own id. + if let Some(id) = payload + .get("id") + .and_then(|x| x.as_str()) + .or_else(|| payload.get("session_id").and_then(|x| x.as_str())) + { + state.session = id.to_string(); + } + None + } + "turn_context" => { + if let Some(m) = payload.get("model").and_then(|x| x.as_str()) { + state.model = m.to_string(); + } + None + } + "event_msg" => { + if payload.get("type")?.as_str()? != "token_count" { + return None; + } + let ts_ms = parse_ts(&v)?; + if let Some(rl) = payload.get("rate_limits") { + self.update_codex_quota(rl, ts_ms); + } + // Per-turn delta. `input_tokens` INCLUDES `cached_input_tokens` + // (unlike Claude, where cache_read is separate) — subtract so + // in_tok means "uncached new input" for both agents and + // in + cr + out reproduces Codex's own total. + let usage = payload.get("info")?.get("last_token_usage")?; + let g = |k: &str| usage.get(k).and_then(|x| x.as_f64()).unwrap_or(0.0); + let raw_in = g("input_tokens"); + let cached = g("cached_input_tokens").min(raw_in); + let out = g("output_tokens"); + if raw_in + out <= 0.0 { + return None; // rate-limit-only heartbeat, nothing to count + } + Some(RawEvent { + ts_ms, + session: state.session.clone(), + model: state.model.clone(), + in_tok: raw_in - cached, + cc: 0.0, + cr: cached, + out_tok: out, + mcp: Vec::new(), + skills: Vec::new(), + id: String::new(), // no message id; file purge keeps re-reads idempotent + source: String::new(), + agent: String::new(), + }) + } + "response_item" => { + // MCP tool calls: function_call items named mcp__[__]. + if payload.get("type")?.as_str()? != "function_call" { + return None; + } + let name = payload.get("name")?.as_str()?; + let rest = name.strip_prefix("mcp__")?; + let server = rest.split("__").next().unwrap_or("").to_string(); + if server.is_empty() { + return None; + } + Some(RawEvent { + ts_ms: parse_ts(&v)?, + session: state.session.clone(), + model: String::new(), // not an LLM request → no tokens/cost + in_tok: 0.0, + cc: 0.0, + cr: 0.0, + out_tok: 0.0, + mcp: vec![server], + skills: Vec::new(), + id: String::new(), + source: String::new(), + agent: String::new(), + }) + } + _ => None, + } + } + + /// Keep the newest rate-limit snapshot (files are walked in arbitrary order, + /// so compare timestamps rather than trusting encounter order). + fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64) { + if self.codex_quota.as_ref().map(|q| q.as_of_ms >= ts_ms).unwrap_or(false) { + return; + } + let win = |k: &str| rl.get(k); + let num = |w: Option<&serde_json::Value>, k: &str| { + w.and_then(|x| x.get(k)).and_then(|x| x.as_f64()).unwrap_or(0.0) + }; + let (p, s) = (win("primary"), win("secondary")); + // A snapshot with no windows at all carries no information — skip it. + if p.is_none() && s.is_none() { + return; + } + self.codex_quota = Some(Quota { + plan: rl + .get("plan_type") + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string(), + primary_pct: num(p, "used_percent"), + primary_minutes: num(p, "window_minutes") as u64, + primary_resets_at: num(p, "resets_at") as i64, + secondary_pct: num(s, "used_percent"), + secondary_minutes: num(s, "window_minutes") as u64, + secondary_resets_at: num(s, "resets_at") as i64, + as_of_ms: ts_ms, + }); + } } -/// Parse one JSONL line into a RawEvent (assistant messages only). -fn parse_line(line: &str) -> Option { +/// RFC3339 top-level `timestamp` → epoch ms. +fn parse_ts(v: &serde_json::Value) -> Option { + let ts = v.get("timestamp")?.as_str()?; + Some(DateTime::parse_from_rfc3339(ts).ok()?.timestamp_millis()) +} + +/// "rollout-2026-06-25T16-13-58-.jsonl" → "" (session id fallback). +fn codex_session_from_filename(path: &std::path::Path) -> String { + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); + // The uuid is everything after the timestamp: last 5 '-'-separated fields. + let parts: Vec<&str> = stem.split('-').collect(); + if parts.len() >= 5 { + parts[parts.len() - 5..].join("-") + } else { + stem.to_string() + } +} + +/// Parse one Claude JSONL line into a RawEvent (assistant messages only). +fn parse_claude_line(line: &str) -> Option { let v: serde_json::Value = serde_json::from_str(line).ok()?; match v.get("type")?.as_str()? { "assistant" => parse_assistant(&v), @@ -292,8 +518,7 @@ fn parse_user_command(v: &serde_json::Value) -> Option { if skill.is_empty() { return None; } - let ts = v.get("timestamp")?.as_str()?; - let ts_ms = DateTime::parse_from_rfc3339(ts).ok()?.timestamp_millis(); + let ts_ms = parse_ts(v)?; let session = v .get("sessionId") .and_then(|s| s.as_str()) @@ -316,6 +541,7 @@ fn parse_user_command(v: &serde_json::Value) -> Option { skills: vec![skill], id, source: String::new(), + agent: String::new(), }) } @@ -325,8 +551,7 @@ fn parse_assistant(v: &serde_json::Value) -> Option { if model == "" { return None; } - let ts = v.get("timestamp")?.as_str()?; - let ts_ms = DateTime::parse_from_rfc3339(ts).ok()?.timestamp_millis(); + let ts_ms = parse_ts(v)?; let session = v .get("sessionId") .and_then(|s| s.as_str()) @@ -382,5 +607,6 @@ fn parse_assistant(v: &serde_json::Value) -> Option { skills, id, source: String::new(), + agent: String::new(), }) } diff --git a/src/App.tsx b/src/App.tsx index e2d8982..4ca7823 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,8 +4,8 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { invoke } from "@tauri-apps/api/core"; import { domToPng } from "modern-screenshot"; import { - Dashboard, PeriodReport, ModelStat, Theme, TH, - fetchDashboard, fmtInt, fmtTokens, pct, + Dashboard, PeriodReport, ModelStat, Scope, Quota, Theme, TH, + fetchDashboard, fmtInt, fmtTokens, pct, themeForScope, } from "./data"; import { TokenGlyph, Segmented, BarChart, Sparkline, CostDonut, BarList, Heatmap, @@ -131,6 +131,91 @@ function SplitLegend({ t, inputM, outputM, cachedPct }: ); } +// Agent filter chips (All / Claude / Codex …). Rendered only when several +// sources have data; a single-source install never sees them. +function AgentChips({ scopes, value, theme, onSelect }: + { scopes: Scope[]; value: string; theme: Theme; onSelect: (id: string) => void }) { + const t = theme; + return ( +
+ {scopes.map((s) => { + const on = s.id === value; + return ( +
onSelect(s.id)} style={{ + display: "inline-flex", alignItems: "center", gap: 6, + font: `600 10.5px ${t.ui}`, letterSpacing: ".02em", padding: "4px 11px", + borderRadius: 20, cursor: "pointer", userSelect: "none", + color: on ? t.segOnText : t.segOffText, + background: on ? t.segOnBg : t.segBg, + border: `1px solid ${on ? t.segBorder : "transparent"}`, + boxShadow: on ? t.segOnShadow : "none", transition: "color .15s, background .15s", + }}> + {s.color && } + {s.label} +
+ ); + })} +
+ ); +} + +// Hero legend for the All scope: one entry per agent instead of Input/Output. +function AgentLegend({ t, slices, cachedPct }: + { t: Theme; slices: { label: string; color: string; tokens: number }[]; cachedPct: number }) { + return ( +
+ {slices.map((s) => ( + {s.label} {fmtTokens(s.tokens)} + ))} + {cachedPct}% cached +
+ ); +} + +// Codex rate-limit card: the two rolling windows (5h + weekly) straight from +// the session logs — data Claude doesn't expose. Bars turn amber near the cap. +function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { + const t = theme; + const now = Date.now(); + const stale = now - q.asOfMs > 60 * 60 * 1000; + const winLabel = (min: number) => + min === 10080 ? "Weekly" : min % 60 === 0 && min > 0 ? `${min / 60}h window` : `${min}m window`; + const resetsIn = (unixS: number) => { + const ms = unixS * 1000 - now; + if (ms <= 0) return "resetting…"; + const h = Math.floor(ms / 3.6e6), m = Math.round((ms % 3.6e6) / 6e4); + if (h >= 48) return `resets in ${Math.round(h / 24)}d`; + return h > 0 ? `resets in ${h}h ${m}m` : `resets in ${m}m`; + }; + const asOf = new Date(q.asOfMs).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { + const p = Math.max(0, Math.min(100, pctUsed)); + const col = p >= 80 ? "#e0795f" : t.accent; + return ( +
+ {label} +
+
+
+ = 80 ? col : t.text, flex: "0 0 34px", textAlign: "right" }}>{Math.round(p)}% + {sub} +
+ ); + }; + return ( +
+ + +
+ {q.plan && <>Plan: {q.plan}}{stale && · as of {asOf}} +
+
+ ); +} + const SectionRule = ({ t, m = "12px 0 10px" }: { t: Theme; m?: string }) => (
); @@ -190,7 +275,13 @@ function ScreenshotButton({ theme, busy, onClick }: { theme: Theme; busy: boolea } function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; openGen: number; active: boolean }) { - const t = TH[dark ? "dark" : "light"]; + // Agent scope filter. scopes[0] is always the aggregate; a stale selection + // (e.g. a source disappeared after a rescan) falls back to it. + const scopes = dash.scopes; + const [scopeId, setScopeId] = useState(scopes[0].id); + const scope = scopes.find((s) => s.id === scopeId) ?? scopes[0]; + // Filtering to one agent re-tints the whole panel with its accent. + const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); // Drag the popover by its body (Windows/Linux only — macOS uses the menu-bar // NSPanel and is gated out). A real OS window-drag begins only once the // pointer moves past a small threshold, so a plain click still clicks through @@ -198,11 +289,14 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const canDrag = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window && !navigator.userAgent.includes("Macintosh"); const dragRef = useRef<{ x: number; y: number } | null>(null); const [period, setPeriod] = useState<"Day" | "Week" | "Month">("Week"); - const P: PeriodReport = period === "Day" ? dash.day : period === "Month" ? dash.month : dash.week; + const P: PeriodReport = period === "Day" ? scope.day : period === "Month" ? scope.month : scope.week; const M = P.metrics; - // animated Total tokens: counts up from 0 on each open / period switch; - // held at 0 while the popover is hidden so it never flashes the final value. - const animTotal = useCountUp(M.totalTokens, `${period}:${openGen}`, active); + // Per-agent slices — non-empty only in the All scope with >=2 sources; they + // switch the hero bar + chart from Input/Output to a by-agent breakdown. + const slices = P.agents; + // animated Total tokens: counts up from 0 on each open / period / scope + // switch; held at 0 while the popover is hidden so it never flashes. + const animTotal = useCountUp(M.totalTokens, `${period}:${scope.id}:${openGen}`, active); const models = P.models; // Hide noise: 0% token-share rows, and $0 entries in the cost donut. // Show models whose share is at least 0.1% when rounded to 1 decimal; below @@ -317,6 +411,10 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
{/* scrolling body */}
+ {/* agent filter — only when several sources have data */} + {scopes.length > 1 && ( + + )} {/* hero */}
@@ -331,17 +429,25 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
${M.cost.toFixed(2)}
- {/* input(+cache) / output split — 2-colour; cache hits fold into input. + {/* hero split bar. All scope with several agents: one segment per agent + ("who is burning"); otherwise the classic input(+cache)/output split. When there's no usage the bar is just the empty track (no slivers). */}
- {M.totalTokens > 0 && <> + {M.totalTokens > 0 && (slices.length > 0 ? ( + slices.map((s) => ( +
+ )) + ) : <>
- } + )}
- - {/* bar chart */} - + {slices.length > 0 + ? + : } + {/* bar chart — stacked by agent in the All scope, In/Out elsewhere */} + 0 ? [...slices].reverse().map((s) => ({ color: s.color, values: s.values })) : undefined} /> {/* models */}
@@ -351,7 +457,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {/* cost donut */}
{costModels.length > 0 - ? + ? 1} /> :
} {unpricedModels.length > 0 && (
@@ -369,6 +475,14 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
+ {/* Codex quota — rate-limit windows read straight from the session logs */} + {scope.quota && ( + <> + +
+ + + )} {/* MCP — shown whenever the user has installed MCP servers */} {M.servers > 0 && ( <> @@ -378,7 +492,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {fmtInt(M.mcpCalls)} · {M.servers} servers
{P.mcp.length > 0 - ? + ? :
No MCP calls in this period
} )} @@ -391,14 +505,14 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {fmtInt(M.skillCalls)} · {M.skills} skills
{P.skills.length > 0 - ? + ? :
No skill calls in this period
} )} {/* heatmap */}
- + {/* footer note */}
Est. cost via models.dev / LiteLLM · estimate diff --git a/src/charts.tsx b/src/charts.tsx index 6e4fa9b..7653e14 100644 --- a/src/charts.tsx +++ b/src/charts.tsx @@ -34,23 +34,32 @@ export function Segmented({ value, items = ["Day", "Week", "Month"], theme, onSe ); } -export function BarChart({ data, theme, height = 96, accent, accentSoft, radius = 3 }: - { data: SeriesPoint[]; theme: Theme; height?: number; accent?: string; accentSoft?: string; radius?: number }) { +export function BarChart({ data, theme, height = 96, accent, accentSoft, radius = 3, segs }: + { data: SeriesPoint[]; theme: Theme; height?: number; accent?: string; accentSoft?: string; radius?: number; + // Optional custom stacking, top→bottom (e.g. per-agent in the All scope). + // Each entry paints one segment per bucket; values align with `data`. + segs?: { color: string; values: number[] }[] }) { const t = theme; accent = accent || t.accent; accentSoft = accentSoft || t.accentSoft; - const max = Math.max(...data.map((d) => d.input + d.cache + d.output), 1e-9); + // Default stacking: output on top, input(+cache) below — the classic view. + const stacks = segs ?? [ + { color: accentSoft, values: data.map((d) => d.output) }, + { color: accent, values: data.map((d) => d.input + d.cache) }, + ]; + const totals = data.map((_, i) => stacks.reduce((s, st) => s + (st.values[i] || 0), 0)); + const max = Math.max(...totals, 1e-9); const n = data.length; const gapPct = Math.max(0.8, Math.min(6, 32 / n)); const effRadius = n > 16 ? 1 : radius; - const [hi, setHi] = useState(null); + const [hi, setHi] = useState(-1); const [tip, setTip] = useState({ x: 0, y: 0 }); // position:fixed so the tooltip renders above the scrolling card (not clipped). // Anchor to the *visible bar top* (baseline − bar height), not the full-height // column top, so short bars don't push the tooltip up over the legend above. - const onBar = (d: SeriesPoint, e: React.MouseEvent) => { + const onBar = (i: number, e: React.MouseEvent) => { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); - const barPx = ((d.input + d.cache + d.output) / max) * height; - setHi(d); setTip({ x: r.left + r.width / 2, y: r.bottom - barPx }); + const barPx = (totals[i] / max) * height; + setHi(i); setTip({ x: r.left + r.width / 2, y: r.bottom - barPx }); }; return (
@@ -58,18 +67,20 @@ export function BarChart({ data, theme, height = 96, accent, accentSoft, radius {[0.25, 0.5, 0.75, 1].map((g, i) => (
))} - {data.map((d, i) => { - // stacked top→bottom: output · input(+cache) - const hO = (d.output / max) * height, hI = ((d.input + d.cache) / max) * height; - const empty = d.input + d.cache + d.output <= 0; - const on = hi === d; + {data.map((_, i) => { + const empty = totals[i] <= 0; + const on = hi === i; + // round the top of the first segment that actually shows + const firstVisible = stacks.findIndex((st) => (st.values[i] || 0) > 0); return (
onBar(d, e)} - onMouseLeave={empty ? undefined : () => setHi(null)} - style={{ flex: 1, alignSelf: "stretch", display: "flex", flexDirection: "column", justifyContent: "flex-end", position: "relative", zIndex: 1, cursor: "default", opacity: hi && !on && !empty ? 0.55 : 1, transition: "opacity .12s" }}> -
-
+ onMouseEnter={empty ? undefined : (e) => onBar(i, e)} + onMouseLeave={empty ? undefined : () => setHi(-1)} + style={{ flex: 1, alignSelf: "stretch", display: "flex", flexDirection: "column", justifyContent: "flex-end", position: "relative", zIndex: 1, cursor: "default", opacity: hi >= 0 && !on && !empty ? 0.55 : 1, transition: "opacity .12s" }}> + {stacks.map((st, si) => ( +
+ ))}
); })} @@ -79,7 +90,7 @@ export function BarChart({ data, theme, height = 96, accent, accentSoft, radius
{d.label}
))}
- {hi && ( + {hi >= 0 && (
- {(() => { const tot = hi.input + hi.cache + hi.output; return tot === 0 ? "No tokens" : fmtTokens(tot) + " tokens"; })()} + {totals[hi] === 0 ? "No tokens" : fmtTokens(totals[hi]) + " tokens"} - · {hi.full} + · {data[hi].full}
)}
@@ -126,14 +137,17 @@ export function Sparkline({ values, theme, width = 80, height = 24, accent, stro const DONUT_PALETTE = ["#1f9d63", "#34c27e", "#6ad0a0", "#a7e3c5", "#4b5a52"]; const DONUT_OVERFLOW = "#79817b"; -export function CostDonut({ models, theme, size = 104, thickness = 16 }: - { models: ModelStat[]; theme: Theme; size?: number; thickness?: number }) { +export function CostDonut({ models, theme, size = 104, thickness = 16, keepColors = false }: + { models: ModelStat[]; theme: Theme; size?: number; thickness?: number; + // true → keep each model's own (agent-tinted) color so wedges match the + // token-list dots; false → classic cost-rank green recoloring. + keepColors?: boolean }) { const t = theme; const [hi, setHi] = useState(-1); - // Rank by cost (desc) and recolor by that rank — usage from most to least. + // Rank by cost (desc); recolor by that rank unless colors are meaningful. const ranked = [...models] .sort((a, b) => b.cost - a.cost) - .map((m, i) => ({ ...m, color: i < DONUT_PALETTE.length ? DONUT_PALETTE[i] : DONUT_OVERFLOW })); + .map((m, i) => (keepColors ? m : { ...m, color: i < DONUT_PALETTE.length ? DONUT_PALETTE[i] : DONUT_OVERFLOW })); models = ranked; const total = models.reduce((s, m) => s + m.cost, 0) || 1e-9; const cx = size / 2, cy = size / 2; diff --git a/src/data.ts b/src/data.ts index f5f3086..50bf01c 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,21 +1,55 @@ import { invoke } from "@tauri-apps/api/core"; export interface SeriesPoint { label: string; full: string; input: number; cache: number; output: number } -export interface ModelStat { name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean } +export interface ModelStat { name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean; agent: string } export interface NamedCount { name: string; count: number } export interface Metrics { totalTokens: number; inputTokens: number; cacheTokens: number; outputTokens: number; cost: number; mcpCalls: number; skillCalls: number; requests: number; sessions: number; deltaTokens: number; deltaCost: number; servers: number; skills: number; } +// Per-agent share of a period (All scope only): `tokens` drives the hero +// split bar, `values` (aligned with series) drives the stacked chart. +export interface AgentSlice { id: string; label: string; color: string; tokens: number; values: number[] } export interface PeriodReport { metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; mcp: NamedCount[]; skills: NamedCount[]; reqTrend: number[]; costTrend: number[]; + agents: AgentSlice[]; } export interface HeatDay { date: string; tokens: number; level: number } -export interface Dashboard { +// Codex rate-limit snapshot (from the newest token_count event). +export interface Quota { + plan: string; + primaryPct: number; primaryMinutes: number; primaryResetsAt: number; + secondaryPct: number; secondaryMinutes: number; secondaryResetsAt: number; + asOfMs: number; +} +// One selectable dashboard view. A single data source yields exactly one scope +// (id "all", empty color → classic UI, no chips); several sources yield +// [All, agent, agent, …] and the header shows filter chips. +export interface Scope { + id: string; label: string; color: string; day: PeriodReport; week: PeriodReport; month: PeriodReport; - heatmap: HeatDay[]; todayTokens: number; generatedAt: string; + heatmap: HeatDay[]; quota: Quota | null; +} +export interface Dashboard { + scopes: Scope[]; todayTokens: number; generatedAt: string; +} + +// Per-agent accent pairs [accent, accentSoft], tuned per theme so light mode +// keeps enough contrast. Scope.color (backend) is the chart base; these drive +// the panel-wide accent switch when a single agent is filtered. +export const AGENT_ACCENTS: Record = { + claude: { dark: ["#d97757", "#eda380"], light: ["#c05b39", "#e8a184"] }, + codex: { dark: ["#10a37f", "#5ec9a8"], light: ["#0c8465", "#6fcdb0"] }, +}; + +/// Theme with the scope's agent accent applied (identity for the All scope). +export function themeForScope(base: Theme, scope: Scope, dark: boolean): Theme { + const acc = AGENT_ACCENTS[scope.id]; + if (!scope.color || !acc) return base; + const [accent, accentSoft] = acc[dark ? "dark" : "light"]; + return { ...base, accent, accentSoft }; } export async function fetchDashboard(): Promise { From 2daff164d414bd77f6be2c88aece69adeb37a8c5 Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 13:43:59 +0800 Subject: [PATCH 02/67] docs(readme)+release: multi-agent positioning, own release/tap pipeline, v1.1.0 - READMEs (en/zh): reposition as a unified dashboard for local AI coding agents (Claude Code + Codex), document the Codex data sources, quota card, agent chips, and cross-agent token normalization; point install links and the Homebrew cask at this repo's releases and tap (sunchj/tokenscope/tokenscope); credit the upstream remix. - release.yml: publish releases from this repo, update the SunChJ/homebrew-tokenscope tap, and skip the tap step quietly when HOMEBREW_TAP_TOKEN isn't configured instead of failing the job. - bump version to 1.1.0 (Codex support feature release). Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 18 ++++++++++++------ README-zh.md | 35 +++++++++++++++++++++-------------- README.md | 35 +++++++++++++++++++++-------------- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 4 ++-- src-tauri/tauri.conf.json | 2 +- 7 files changed, 59 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7939e09..6950dc7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,9 +80,9 @@ jobs: tagName: ${{ github.ref_name }} releaseName: "Tokenscope ${{ github.ref_name }}" releaseBody: | - Menu-bar / system-tray dashboard for Claude CLI token usage. + Menu-bar / system-tray dashboard for local AI coding agent token usage (Claude Code + Codex). - **macOS**: download `Tokenscope_*_universal.dmg` (Apple Silicon + Intel). Unsigned — right-click → **Open** on first launch, or install via `brew install --cask hdusy/tokenscope/tokenscope`. + **macOS**: download `Tokenscope_*_universal.dmg` (Apple Silicon + Intel). Unsigned — right-click → **Open** on first launch, or install via `brew install --cask sunchj/tokenscope/tokenscope`. **Windows**: download `Tokenscope_*_x64-setup.exe`. Unsigned — on first launch click **More info → Run anyway** to dismiss SmartScreen. # Publish immediately (not a draft): the asset's public download URL @@ -116,9 +116,15 @@ jobs: env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | + # No tap token configured → skip quietly (the release itself is done; + # the cask can be bumped by hand — see the tap repo's README). + if [ -z "$HOMEBREW_TAP_TOKEN" ]; then + echo "HOMEBREW_TAP_TOKEN not set; skipping tap update." + exit 0 + fi VERSION="${GITHUB_REF_NAME#v}" DMG_NAME="Tokenscope_${VERSION}_universal.dmg" - DOWNLOAD_URL="https://github.com/HduSy/tokenscope/releases/download/${GITHUB_REF_NAME}/${DMG_NAME}" + DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/${DMG_NAME}" # Wait for the release asset to be available, then compute sha256. # -f makes curl fail (non-zero) on a 404 so we keep retrying instead @@ -137,7 +143,7 @@ jobs: fi # Clone the tap repo and update the cask - git clone https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/HduSy/homebrew-tokenscope.git /tmp/homebrew-tap + git clone https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/SunChJ/homebrew-tokenscope.git /tmp/homebrew-tap cat > /tmp/homebrew-tap/Casks/tokenscope.rb <Tokenscope - MacOS menu-bar dashboard for Claude CLI token usage | Product Hunt -**macOS 菜单栏 / Windows 系统托盘工具**,展示 Claude CLI 的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**。 +**macOS 菜单栏 / Windows 系统托盘工具**,统一展示本机 **AI 编码 Agent**(Claude Code **与 Codex CLI**)的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**——所有 Agent 一个仪表盘。 + +> 本项目是 [HduSy/tokenscope](https://github.com/HduSy/tokenscope) 的 remix 版,扩展了多 Agent 支持(Codex 先行,更多 Agent 规划中——见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md))。 技术栈:**Tauri 2 + React + TypeScript**(前端)/ **Rust**(数据层)。 @@ -12,29 +14,34 @@ ## 它做什么 -- 菜单栏图标旁显示当日 Token 数(如 `⬡ 14.00M`) +- 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) - 点击打开面板:Day / Week / Month 切换 +- **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** +- **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 -- **只统计用户自己安装的 MCP / Skill**,过滤所有 Claude 内置工具与 Anthropic 自带 MCP +- **只统计用户自己安装的 MCP / Skill**,过滤所有内置工具与自带 MCP ## 数据来源(零侵入,只读) | 用途 | 路径 | |------|------| -| 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | -| 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | +| Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | +| Codex 会话日志(Token / 模型 / 配额) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | +| Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | +| Codex 用户 MCP 白名单 | `~/.codex/config.toml` → `[mcp_servers.*]` | | 用户 Skill 白名单 | `~/.claude/skills/` 目录 | -| 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 Claude CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | +| 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | ### 关键处理 -- 按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 +- Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 +- Codex:取 `token_count` 事件的每回合增量(`last_token_usage`),模型归属来自前置的 `turn_context`;Codex 的 `cached_input_tokens` 是其 `input_tokens` 的**子集**,已拆分成与 Claude 一致的独立 cache-read 口径——跨 Agent 的 token 数可直接对比 - token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 - 成本按四类 token 分别计价;模型带 `priced` 标记,**两源都查不到的模型只计 Token、UI 标注「暂无定价」** - 日志只有裸模型名、无厂商信息 → 第三方模型默认取官方厂商价(估算) -- 工具分类:`mcp____*` 且 server 在用户配置中 → MCP;Skill 调用(`Skill` 工具的 `input.skill`,或 `/skill` 斜杠命令)且在 skills 目录中 → Skill;其余忽略 +- 工具分类:`mcp____*` 且 server 在**所属 Agent** 的配置中 → MCP;Skill 调用(`Skill` 工具的 `input.skill`,或 `/skill` 斜杠命令)且在 skills 目录中 → Skill(仅 Claude);其余忽略 > 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 @@ -72,7 +79,7 @@ cost = input × price.input ### 方式一:Homebrew(推荐) ```bash -brew install --cask hdusy/tokenscope/tokenscope +brew install --cask sunchj/tokenscope/tokenscope ``` 安装后会自动清除隔离属性(cask 的 `postflight` 已内置 `xattr -cr`),**首次直接打开即可,不会弹「Apple 无法验证」**。 @@ -87,7 +94,7 @@ brew update && brew upgrade --cask tokenscope ### 方式二:下载 .dmg -1. 从 [Releases](https://github.com/HduSy/tokenscope/releases) 下载最新的 `Tokenscope_*_universal.dmg`(同时支持 Apple Silicon 与 Intel) +1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_universal.dmg`(同时支持 Apple Silicon 与 Intel) 2. 拖入「应用程序」 3. 因为是**未签名 / 未公证**构建,首次打开会被 Gatekeeper 拦截,二选一: - 右键 App →「打开」→ 再次确认「打开」,或 @@ -100,7 +107,7 @@ brew update && brew upgrade --cask tokenscope ### 方式三:Windows 安装 -1. 从 [Releases](https://github.com/HduSy/tokenscope/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` +1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` 2. 双击安装。因为是**未签名**构建,首次运行会被 SmartScreen 拦截 —— 点 **"更多信息" → "仍要运行"** 即可 3. 安装器按当前用户安装(无需管理员权限),并**自动注册开机自启** 4. 系统要求:**Windows 10 1803 及以上 / Windows 11**,需要 WebView2 运行时(Win 11 预装;Win 10 用户若没装,安装器会引导补装) @@ -143,10 +150,10 @@ src/ React 前端 charts.tsx 图表原语(柱状/甜甜圈/sparkline/热力图/分段控件) App.tsx 主面板 src-tauri/src/ - store.rs JSONL 增量摄取(按 message.id 去重 + 多行合并) - parser.rs 聚合(Day/Week/Month + 热力图) + store.rs 多源 JSONL 增量摄取(Claude + Codex 适配器) + parser.rs 按 Scope 聚合(All / 单 Agent;Day/Week/Month + 热力图) pricing.rs models.dev / LiteLLM 价格加载与计价 - config.rs 用户 MCP / Skill 白名单 + config.rs 用户 MCP / Skill 白名单(Claude + Codex) model.rs 返回给前端的数据结构 lib.rs Tauri 命令 + 菜单栏托盘 ``` diff --git a/README.md b/README.md index bd62b00..1c76a2f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Tokenscope - MacOS menu-bar dashboard for Claude CLI token usage | Product Hunt -A **menu-bar / system-tray app for macOS and Windows** that shows your Claude CLI **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown**. +A **menu-bar / system-tray app for macOS and Windows** that shows your **local AI coding agents'** (Claude Code **and Codex CLI**) **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown** — one unified dashboard for every agent on your machine. + +> This is a remix of [HduSy/tokenscope](https://github.com/HduSy/tokenscope), extended with multi-agent support (Codex first; more agents planned — see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)). Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). @@ -12,29 +14,34 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). ## What it does -- Shows today's token count next to the menu-bar icon (e.g. `⬡ 14.00M`) +- Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) - Click to open the panel: Day / Week / Month toggle +- **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions - Three breakdowns: **by model** / **by MCP call** / **by Skill call** +- **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap -- **Counts only the MCP servers / Skills you installed yourself** — all Claude built-in tools and Anthropic's bundled MCP servers are filtered out +- **Counts only the MCP servers / Skills you installed yourself** — all built-in tools and bundled MCP servers are filtered out ## Data sources (zero-intrusion, read-only) | Purpose | Path | |---------|------| -| Session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | -| User MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | +| Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | +| Codex session logs (tokens / model / rate limits) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | +| Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | +| Codex user MCP whitelist | `~/.codex/config.toml` → `[mcp_servers.*]` | | User Skill whitelist | `~/.claude/skills/` directory | -| Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching Claude CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | +| Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | ### Key processing -- Deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once +- Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once +- Codex: per-turn deltas from `token_count` events (`last_token_usage`), model attribution from the preceding `turn_context`; Codex's `cached_input_tokens` is a *subset* of its `input_tokens`, so it's split out to match Claude's separate cache-read accounting — token math is comparable across agents - Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins - Cost is priced per the four token types; each model carries a `priced` flag — **models not found in either source still count tokens but are labelled "no price" in the UI** - Logs contain only the bare model name (no vendor) → third-party models default to the official vendor price (an estimate) -- Tool classification: `mcp____*` where the server is in your config → MCP; a Skill call (the `Skill` tool's `input.skill`, or a `/skill` slash command) whose name is in your skills directory → Skill; everything else is ignored +- Tool classification: `mcp____*` where the server is in the owning agent's config → MCP; a Skill call (the `Skill` tool's `input.skill`, or a `/skill` slash command) whose name is in your skills directory → Skill (Claude only); everything else is ignored > Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". @@ -72,7 +79,7 @@ So a cache hit is **not** billed as normal input — it uses the dedicated (chea ### Option 1: Homebrew (recommended) ```bash -brew install --cask hdusy/tokenscope/tokenscope +brew install --cask sunchj/tokenscope/tokenscope ``` The cask's `postflight` strips the quarantine attribute (`xattr -cr`) automatically, so **it opens on first launch without the "Apple cannot verify" prompt**. @@ -87,7 +94,7 @@ brew update && brew upgrade --cask tokenscope ### Option 2: Download the .dmg -1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/HduSy/tokenscope/releases) (works on both Apple Silicon and Intel) +1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) (works on both Apple Silicon and Intel) 2. Drag it into Applications 3. Because the build is **unsigned / unnotarized**, Gatekeeper blocks the first launch — pick one: - Right-click the app → **Open** → confirm **Open** again, or @@ -100,7 +107,7 @@ brew update && brew upgrade --cask tokenscope ### Option 3: Install on Windows -1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/HduSy/tokenscope/releases) +1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 2. Double-click to install. Because the build is **unsigned**, Windows SmartScreen will warn on first run — click **More info → Run anyway** 3. The app installs per-user (no admin required) and registers itself for **launch at login** automatically 4. Requirements: **Windows 10 1803+ / Windows 11** with the WebView2 runtime (preinstalled on Windows 11; Windows 10 users without it will be prompted by the installer) @@ -143,10 +150,10 @@ src/ React frontend charts.tsx chart primitives (bars / donut / sparkline / heatmap / segmented control) App.tsx main panel src-tauri/src/ - store.rs incremental JSONL ingest (dedup by message.id + multi-line merge) - parser.rs aggregation (Day/Week/Month + heatmap) + store.rs incremental multi-source JSONL ingest (Claude + Codex adapters) + parser.rs aggregation into scopes (All / per-agent; Day/Week/Month + heatmap) pricing.rs models.dev / LiteLLM price loading and costing - config.rs user MCP / Skill whitelist + config.rs user MCP / Skill whitelists (Claude + Codex) model.rs data structures returned to the frontend lib.rs Tauri commands + menu-bar tray ``` diff --git a/package.json b/package.json index 4810235..5839c62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.0.2", + "version": "1.1.0", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1377cf3..88f9ccf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3974,7 +3974,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.0.2" +version = "1.1.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6086258..116049e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tokenscope" -version = "1.0.2" -description = "Claude CLI token usage dashboard" +version = "1.1.0" +description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a9cbcce..517b6aa 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.0.2", + "version": "1.1.0", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 06bce3cb104aadd313604c4a91bb2b10e8445a8d Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 14:40:11 +0800 Subject: [PATCH 03/67] feat(updater): in-app update check, download, and install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds self-updating via tauri-plugin-updater backed by GitHub Releases: - Backend: register updater + process plugins; updater endpoint is the release feed's latest.json, packages verified against the bundled minisign pubkey (bundle.createUpdaterArtifacts on). - Frontend: UpdateBanner in the panel — checks on launch and every 6h, shows "vX.Y.Z is available → Update", streams download progress, then offers a one-click Restart. A dismissed version stays hidden until the next one. All failure paths (offline, no latest.json yet, bad signature) stay silent and retry later. - CI: release workflow passes TAURI_SIGNING_PRIVATE_KEY(+_PASSWORD) to tauri-action so update packages are signed and latest.json is published; builds still succeed without the secrets (updater is then simply inert). - Version 1.2.0; README (en/zh) documents the update flow. Note: the updater keypair lives outside the repo (~/.tauri/tokenscope-updater.key{,.pub,.password} locally); losing the private key or password permanently breaks the update chain for shipped builds — back it up. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 6 + README-zh.md | 6 + README.md | 7 + package.json | 8 +- pnpm-lock.yaml | 38 ++++ src-tauri/Cargo.lock | 305 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 7 +- src-tauri/tauri.conf.json | 14 +- src/App.tsx | 107 ++++++++++ 11 files changed, 494 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6950dc7..413568f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,12 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # In-app updater: sign the update packages (minisign, NOT Apple + # signing) and let tauri-action publish latest.json alongside the + # assets. Without these secrets the build still succeeds — it just + # ships without update signatures, and in-app update stays silent. + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} # --- Code signing + notarization (optional, macOS only) --- # UNSIGNED BUILD BY DEFAULT. Do NOT uncomment these until the matching # secrets actually exist: tauri's bundler treats an empty diff --git a/README-zh.md b/README-zh.md index 8610086..34e4514 100644 --- a/README-zh.md +++ b/README-zh.md @@ -112,6 +112,12 @@ brew update && brew upgrade --cask tokenscope 3. 安装器按当前用户安装(无需管理员权限),并**自动注册开机自启** 4. 系统要求:**Windows 10 1803 及以上 / Windows 11**,需要 WebView2 运行时(Win 11 预装;Win 10 用户若没装,安装器会引导补装) +### 更新 + +自 v1.2.0 起,App 会自行检测新版本并在面板内提示(「v*x.y.z* is available → Update」), +点击即可下载安装、重启生效,无需手动下载。Homebrew 用户也可以继续 +`brew upgrade --cask tokenscope`,两条路径安装的是同一份构建。 + ### 首次启动后 - **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) diff --git a/README.md b/README.md index 1c76a2f..4ac32e1 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,13 @@ brew update && brew upgrade --cask tokenscope 3. The app installs per-user (no admin required) and registers itself for **launch at login** automatically 4. Requirements: **Windows 10 1803+ / Windows 11** with the WebView2 runtime (preinstalled on Windows 11; Windows 10 users without it will be prompted by the installer) +### Updating + +From v1.2.0 the app checks for new releases itself and shows an in-app banner +("v*x.y.z* is available → Update"); the update downloads, installs, and applies +on restart — no manual download needed. Homebrew users can keep using +`brew upgrade --cask tokenscope` as well; both paths install the same build. + ### After first launch - **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) diff --git a/package.json b/package.json index 5839c62..59621b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.1.0", + "version": "1.2.0", "private": true, "type": "module", "scripts": { @@ -11,6 +11,8 @@ }, "dependencies": { "@tauri-apps/api": "^2.1.1", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "modern-screenshot": "^4.7.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -24,6 +26,8 @@ "vite": "^5.4.11" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild"] + "onlyBuiltDependencies": [ + "esbuild" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76b24de..641307f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@tauri-apps/api': specifier: ^2.1.1 version: 2.11.0 + '@tauri-apps/plugin-process': + specifier: ^2.3.1 + version: 2.3.1 + '@tauri-apps/plugin-updater': + specifier: ^2.10.1 + version: 2.10.1 modern-screenshot: specifier: ^4.7.0 version: 4.7.0 @@ -316,66 +322,79 @@ packages: resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.1': resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.1': resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.1': resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.1': resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.1': resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.1': resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.1': resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.1': resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.1': resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.1': resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.1': resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.1': resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.61.1': resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} @@ -433,30 +452,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.11.2': resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.11.2': resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.11.2': resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.11.2': resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} @@ -481,6 +505,12 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} + + '@tauri-apps/plugin-updater@2.10.1': + resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1006,6 +1036,14 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + '@tauri-apps/plugin-process@2.3.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-updater@2.10.1': + dependencies: + '@tauri-apps/api': 2.11.0 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 88f9ccf..8c59fcd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -732,6 +741,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1637,6 +1657,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1912,6 +1947,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2137,6 +2202,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2420,6 +2491,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.12.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2435,6 +2507,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2507,6 +2591,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2523,6 +2613,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2935,15 +3039,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3012,6 +3121,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3021,6 +3142,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -3047,6 +3195,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3104,6 +3261,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.12.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -3324,6 +3504,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3525,7 +3721,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3558,6 +3754,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3581,7 +3788,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3738,6 +3945,16 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-single-instance" version = "2.4.2" @@ -3753,6 +3970,39 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.11.2" @@ -3763,7 +4013,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3786,7 +4036,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3974,7 +4224,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.1.0" +version = "1.2.0" dependencies = [ "base64 0.22.1", "chrono", @@ -3987,7 +4237,9 @@ dependencies = [ "tauri-nspanel", "tauri-plugin-autostart", "tauri-plugin-positioner", + "tauri-plugin-process", "tauri-plugin-single-instance", + "tauri-plugin-updater", "ureq", "walkdir", ] @@ -4006,6 +4258,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4641,6 +4903,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -5356,7 +5627,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5403,6 +5674,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.3" @@ -5547,6 +5828,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 116049e..b142703 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.1.0" +version = "1.2.0" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" @@ -33,6 +33,8 @@ walkdir = "2" # watch ~/.claude/projects so the dashboard refreshes within ~1s of a log write notify = "6" tauri-plugin-single-instance = "2" +tauri-plugin-updater = "2" +tauri-plugin-process = "2" [target.'cfg(target_os = "macos")'.dependencies] tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index bd6a919..f563fd1 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -15,6 +15,8 @@ "positioner:default", "autostart:allow-enable", "autostart:allow-disable", - "autostart:allow-is-enabled" + "autostart:allow-is-enabled", + "updater:default", + "process:allow-restart" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d3a2268..90beddc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -754,7 +754,12 @@ pub fn run() { .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, None, - )); + )) + // In-app updates: checks the GitHub release feed (latest.json) and + // installs signed update packages; process plugin provides the + // relaunch after install. Both driven from the frontend UpdateBanner. + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()); // Registers the WebviewPanelManager state used by `to_panel`/`get_webview_panel`. #[cfg(target_os = "macos")] { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 517b6aa..aac87e0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.1.0", + "version": "1.2.0", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", @@ -29,9 +29,21 @@ "csp": null } }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRGREQ5QzhCQjc0QUIxNTYKUldSV3NVcTNpNXpkVDJvWTFpMUx6WEZialVXSXdyeDNSZnM5ZnMzK29NTThPUk5CVDVtWG02SjYK", + "endpoints": [ + "https://github.com/SunChJ/tokenscope-remix/releases/latest/download/latest.json" + ], + "windows": { + "installMode": "passive" + } + } + }, "bundle": { "active": true, "targets": ["app", "dmg", "nsis"], + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.tsx b/src/App.tsx index 4ca7823..b000b86 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,8 @@ import { listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { invoke } from "@tauri-apps/api/core"; import { domToPng } from "modern-screenshot"; +import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; +import { relaunch } from "@tauri-apps/plugin-process"; import { Dashboard, PeriodReport, ModelStat, Scope, Quota, Theme, TH, fetchDashboard, fmtInt, fmtTokens, pct, themeForScope, @@ -131,6 +133,107 @@ function SplitLegend({ t, inputM, outputM, cachedPct }: ); } +// ── In-app updates ────────────────────────────────────────────── +// Poll the GitHub release feed (plugin-updater endpoint) on launch and every +// 6h; surface a slim banner when a newer signed build exists. Download + +// install happen in-app, then a relaunch finishes the update. A dismissed +// version stays hidden until the *next* version appears (localStorage). +type UpdateState = + | { phase: "idle" } + | { phase: "available"; update: Update } + | { phase: "downloading"; version: string; pct: number } + | { phase: "ready"; version: string } + | { phase: "error"; version: string }; + +function useUpdater(): [UpdateState, () => void, () => void] { + const [st, setSt] = useState({ phase: "idle" }); + const updRef = useRef(null); + useEffect(() => { + const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; + if (!inTauri) return; + let dead = false; + const probe = async () => { + try { + const u = await checkUpdate(); + if (dead || !u) return; + if (localStorage.getItem("tokenscope-skip-update") === u.version) return; + updRef.current = u; + setSt({ phase: "available", update: u }); + } catch { + // offline / rate-limited / no latest.json yet — stay quiet, retry later + } + }; + probe(); + const t = window.setInterval(probe, 6 * 60 * 60 * 1000); + return () => { dead = true; window.clearInterval(t); }; + }, []); + const install = async () => { + const u = updRef.current; + if (!u) return; + setSt({ phase: "downloading", version: u.version, pct: 0 }); + try { + let total = 0, got = 0; + await u.downloadAndInstall((e) => { + if (e.event === "Started") total = e.data.contentLength ?? 0; + else if (e.event === "Progress") { + got += e.data.chunkLength; + if (total > 0) setSt({ phase: "downloading", version: u.version, pct: Math.min(99, Math.round((got / total) * 100)) }); + } else if (e.event === "Finished") setSt({ phase: "ready", version: u.version }); + }); + setSt({ phase: "ready", version: u.version }); + } catch { + setSt({ phase: "error", version: u.version }); + } + }; + const dismiss = () => { + const u = updRef.current; + if (u) try { localStorage.setItem("tokenscope-skip-update", u.version); } catch {} + setSt({ phase: "idle" }); + }; + return [st, install, dismiss]; +} + +function UpdateBanner({ st, theme, onInstall, onDismiss }: + { st: UpdateState; theme: Theme; onInstall: () => void; onDismiss: () => void }) { + const t = theme; + if (st.phase === "idle") return null; + const Btn = ({ label, onClick }: { label: string; onClick: () => void }) => ( + + ); + return ( +
+ {st.phase === "available" && <> + + v{st.update.version} is available + + + + } + {st.phase === "downloading" && <> + Downloading v{st.version}… + {st.pct}% + } + {st.phase === "ready" && <> + v{st.version} installed + relaunch().catch(() => {})} /> + } + {st.phase === "error" && <> + Update failed — try again later + + } +
+ ); +} + // Agent filter chips (All / Claude / Codex …). Rendered only when several // sources have data; a single-source install never sees them. function AgentChips({ scopes, value, theme, onSelect }: @@ -282,6 +385,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const scope = scopes.find((s) => s.id === scopeId) ?? scopes[0]; // Filtering to one agent re-tints the whole panel with its accent. const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); + // In-app update lifecycle (idle → available → downloading → ready). + const [updSt, updInstall, updDismiss] = useUpdater(); // Drag the popover by its body (Windows/Linux only — macOS uses the menu-bar // NSPanel and is gated out). A real OS window-drag begins only once the // pointer moves past a small threshold, so a plain click still clicks through @@ -411,6 +516,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
{/* scrolling body */}
+ {/* in-app update prompt */} + {/* agent filter — only when several sources have data */} {scopes.length > 1 && ( From 136409f448e024404cfab303e27a4d750cdcdf18 Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 14:50:03 +0800 Subject: [PATCH 04/67] docs: stand on our own branding, move upstream credit to Acknowledgements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the upstream Product Hunt badge (it belongs to the original project's marketing) and the top-of-page remix blockquote. - Add a proper Acknowledgements (致谢) section at the end of both READMEs: credits @HduSy's tokenscope for the architecture and design this project inherits (MIT retained), and directs issues here rather than upstream. - Title the READMEs "Tokenscope Remix" to match the repo identity (the app/cask keep the Tokenscope product name). Repo metadata updated alongside (not in this diff): description now reflects the multi-agent scope, homepage points at our releases page instead of the upstream landing site, and topics were added. Co-Authored-By: Claude Fable 5 --- README-zh.md | 12 +++++++----- README.md | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README-zh.md b/README-zh.md index 34e4514..ff0999e 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,13 +1,9 @@ -# Tokenscope +# Tokenscope Remix [English](README.md) · **中文** -Tokenscope - MacOS menu-bar dashboard for Claude CLI token usage | Product Hunt - **macOS 菜单栏 / Windows 系统托盘工具**,统一展示本机 **AI 编码 Agent**(Claude Code **与 Codex CLI**)的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**——所有 Agent 一个仪表盘。 -> 本项目是 [HduSy/tokenscope](https://github.com/HduSy/tokenscope) 的 remix 版,扩展了多 Agent 支持(Codex 先行,更多 Agent 规划中——见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md))。 - 技术栈:**Tauri 2 + React + TypeScript**(前端)/ **Rust**(数据层)。 ![Tokenscope 面板(深色 / 浅色)](docs/screenshot.png) @@ -168,3 +164,9 @@ src-tauri/src/ 开发过程中遇到的典型 bug(现象、根因、解决办法)汇总在 [docs/BUGFIXES.md](docs/BUGFIXES.md)。 + +## 致谢 + +本项目源自 [@HduSy](https://github.com/HduSy) 的 [tokenscope](https://github.com/HduSy/tokenscope)——一个非常出色的 macOS 菜单栏 Claude CLI 用量仪表盘。本仓库继承的 Rust 数据摄取/聚合架构与面板设计均出自原作者之手,并沿用原项目的 MIT 协议,在此致以诚挚感谢!🙏 + +Tokenscope Remix 在其基础上扩展为多 Agent 仪表盘(Codex 已支持,更多 Agent 规划中——见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)),并独立维护:问题与需求请在本仓库提交,不要打扰上游。 diff --git a/README.md b/README.md index 4ac32e1..20a0e62 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,9 @@ -# Tokenscope +# Tokenscope Remix **English** · [中文](README-zh.md) -Tokenscope - MacOS menu-bar dashboard for Claude CLI token usage | Product Hunt - A **menu-bar / system-tray app for macOS and Windows** that shows your **local AI coding agents'** (Claude Code **and Codex CLI**) **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown** — one unified dashboard for every agent on your machine. -> This is a remix of [HduSy/tokenscope](https://github.com/HduSy/tokenscope), extended with multi-agent support (Codex first; more agents planned — see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)). - Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). ![Tokenscope panel (dark / light)](docs/screenshot.png) @@ -169,3 +165,9 @@ src-tauri/src/ Notable bugs found during development — symptom, root cause, and fix — are collected in [docs/BUGFIXES.md](docs/BUGFIXES.md). + +## Acknowledgements + +This project started as a remix of [tokenscope](https://github.com/HduSy/tokenscope) by [@HduSy](https://github.com/HduSy) — a beautifully built macOS menu-bar dashboard for Claude CLI usage. The clean Rust ingest/aggregation architecture and the panel design this project inherits are his work, and this repo remains under the original MIT license. Huge thanks! 🙏 + +Tokenscope Remix extends it into a multi-agent dashboard (Codex today, more planned — see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)) and is maintained independently: please file issues and feature requests here, not upstream. From ce3c2be841e549d6a104ca86aa1eaf667195675f Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 15:02:28 +0800 Subject: [PATCH 05/67] fix tray popover main-thread updates --- src-tauri/src/lib.rs | 50 ++++++++++++++++++++++++++++---------------- src/App.tsx | 7 +++++++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 90beddc..70dd4e7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -34,19 +34,25 @@ fn now_ms() -> i64 { .unwrap_or(0) } +fn update_tray_label(app: &tauri::AppHandle, label: String) { + let handle = app.clone(); + let _ = app.run_on_main_thread(move || { + if let Some(tray) = handle.tray_by_id("main") { + // macOS shows the label next to the menu-bar icon (set_title). + // Windows' taskbar tray has no equivalent, so mirror it into the + // tooltip there. Both APIs touch native tray state; on macOS that + // must happen on the main thread. + let _ = tray.set_title(Some(label.clone())); + let _ = tray.set_tooltip(Some(format!("Tokenscope · today {}", label))); + } + }); +} + /// Rebuild the dashboard (incremental), update the tray's token count, and push /// the fresh data to the UI so an open popover updates live. fn refresh(app: &tauri::AppHandle) { let dash = parser::build_dashboard(); - if let Some(tray) = app.tray_by_id("main") { - let label = fmt_tokens_m(dash.today_tokens); - // macOS shows the label next to the menu-bar icon (set_title). Windows' - // taskbar tray has no equivalent — set_title is a no-op there — so we - // surface the same number through the hover tooltip instead, the only - // text channel Shell_NotifyIcon exposes for a tray icon. - let _ = tray.set_title(Some(label.clone())); - let _ = tray.set_tooltip(Some(format!("Tokenscope · today {}", label))); - } + update_tray_label(app, fmt_tokens_m(dash.today_tokens)); check_milestones(app, &dash); let _ = app.emit("dashboard-updated", &dash); } @@ -645,13 +651,27 @@ fn watch_system_theme(app: &tauri::AppHandle) { /// Show the panel as a popover anchored under the tray icon, and focus it. /// Always reset the scroll to the top so it doesn't reopen mid-scroll. fn show_popover(app: &tauri::AppHandle) { + let handle = app.clone(); + let _ = app.run_on_main_thread(move || show_popover_inner(&handle)); +} + +fn show_popover_inner(app: &tauri::AppHandle) { // On macOS the window is an NSPanel — position it manually, then show() // (makes it key and orders it front, incl. over fullscreen Spaces). #[cfg(target_os = "macos")] { position_panel(app); - if let Ok(panel) = app.get_webview_panel("main") { - panel.show(); + match app.get_webview_panel("main") { + Ok(panel) => panel.show(), + Err(_) => { + // If the panel state is ever unavailable (for example after a + // plugin/setup ordering regression), still surface the window + // instead of making a tray click look like a no-op. + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } } } #[cfg(not(target_os = "macos"))] @@ -681,13 +701,7 @@ async fn get_dashboard(app: tauri::AppHandle) -> Dashboard { // Sync the tray count to this freshly-fetched value. The panel refetches the // instant it opens, while the tray otherwise only refreshes every 30s — so // without this the two could disagree for up to 30s during heavy usage. - if let Some(tray) = app.tray_by_id("main") { - let label = fmt_tokens_m(dash.today_tokens); - let _ = tray.set_title(Some(label.clone())); - // Mirror refresh(): keep the tooltip in sync for Windows, where the - // title isn't shown next to the icon. - let _ = tray.set_tooltip(Some(format!("Tokenscope · today {}", label))); - } + update_tray_label(&app, fmt_tokens_m(dash.today_tokens)); check_milestones(&app, &dash); dash } diff --git a/src/App.tsx b/src/App.tsx index b000b86..d4a7058 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -681,6 +681,10 @@ export default function App() { // failure must not pin the error page for the whole session — the next // successful fetch (focus refetch or the 30s background push) recovers it. const apply = (d: Dashboard) => { + if (!d.scopes?.length) { + setErr("dashboard returned no scopes"); + return; + } setDash(d); setErr(null); }; @@ -763,5 +767,8 @@ export default function App() {
); } + if (!dash.scopes.length) { + return
Failed to load: dashboard returned no scopes
; + } return ; } From 684c22770c8768c4176e91815c50d35a29f3b6da Mon Sep 17 00:00:00 2001 From: jingxing Date: Thu, 2 Jul 2026 15:16:25 +0800 Subject: [PATCH 06/67] improve release notes for packaged updates --- .github/workflows/release.yml | 58 ++++++++++++++++------------- src/App.tsx | 69 +++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 37 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 413568f..60822c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,38 @@ jobs: - name: Install frontend deps run: pnpm install + - name: Prepare release notes + id: release_notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gen=$(gh api --method POST \ + "repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="${{ github.ref_name }}" --jq '.body' || true) + + { + cat <<'BODY' + Menu-bar / system-tray dashboard for local AI coding agent token usage (Claude Code + Codex). + + ## Install + + **macOS**: download `Tokenscope_*_universal.dmg` (Apple Silicon + Intel). Unsigned — right-click → **Open** on first launch, or install via `brew install --cask sunchj/tokenscope/tokenscope`. + + **Windows**: download `Tokenscope_*_x64-setup.exe`. Unsigned — on first launch click **More info → Run anyway** to dismiss SmartScreen. + BODY + if [ -n "$gen" ]; then + printf '\n%s\n' "$gen" + else + printf '\n## What changed\n\n- Changes included since the previous release.\n' + fi + } > release-body.md + + { + echo "body<> "$GITHUB_OUTPUT" + - name: Build & publish release uses: tauri-apps/tauri-action@v0 env: @@ -85,12 +117,7 @@ jobs: with: tagName: ${{ github.ref_name }} releaseName: "Tokenscope ${{ github.ref_name }}" - releaseBody: | - Menu-bar / system-tray dashboard for local AI coding agent token usage (Claude Code + Codex). - - **macOS**: download `Tokenscope_*_universal.dmg` (Apple Silicon + Intel). Unsigned — right-click → **Open** on first launch, or install via `brew install --cask sunchj/tokenscope/tokenscope`. - - **Windows**: download `Tokenscope_*_x64-setup.exe`. Unsigned — on first launch click **More info → Run anyway** to dismiss SmartScreen. + releaseBody: ${{ steps.release_notes.outputs.body }} # Publish immediately (not a draft): the asset's public download URL # must be live for the Homebrew Cask step below to fetch it and for # `brew install` to work. @@ -98,25 +125,6 @@ jobs: prerelease: false args: --target ${{ matrix.target }} - # Append an auto-generated changelog (PRs/commits since the previous tag) - # to the release notes. Best-effort: never fail the release over notes. - # Only run once (on the macOS leg) — both jobs publish to the same release. - - name: Add changelog to release - if: matrix.os == 'macos-latest' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gen=$(gh api --method POST \ - "repos/${{ github.repository }}/releases/generate-notes" \ - -f tag_name="${{ github.ref_name }}" --jq '.body' || true) - cur=$(gh release view "${{ github.ref_name }}" --json body --jq '.body' || true) - { - printf '%s\n' "$cur" - if [ -n "$gen" ]; then printf '\n%s\n' "$gen"; fi - } > release-body.md - gh release edit "${{ github.ref_name }}" --notes-file release-body.md - - name: Update Homebrew Cask if: matrix.os == 'macos-latest' env: diff --git a/src/App.tsx b/src/App.tsx index d4a7058..e4f0410 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -193,10 +193,39 @@ function useUpdater(): [UpdateState, () => void, () => void] { return [st, install, dismiss]; } +function releaseNotesSummary(body?: string) { + if (!body) return null; + const lines = body + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => + line + .replace(/^[-*]\s+/, "") + .replace(/\*\*/g, "") + .replace(/`/g, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/\s+by @\S+.*$/, "") + .trim() + ) + .filter((line) => + line && + !line.startsWith("#") && + !/^install$/i.test(line) && + !/^what'?s changed$/i.test(line) && + !/^full changelog/i.test(line) && + !line.startsWith("Menu-bar / system-tray") + ); + if (!lines.length) return null; + const text = lines.slice(0, 2).join("; "); + return text.length > 180 ? `${text.slice(0, 177)}...` : text; +} + function UpdateBanner({ st, theme, onInstall, onDismiss }: { st: UpdateState; theme: Theme; onInstall: () => void; onDismiss: () => void }) { const t = theme; if (st.phase === "idle") return null; + const notes = st.phase === "available" ? releaseNotesSummary(st.update.body) : null; const Btn = ({ label, onClick }: { label: string; onClick: () => void }) => (
); From 519a6dd609d216a2ab9540a3a1f86c050a91a59b Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 9 Jul 2026 19:27:34 -0700 Subject: [PATCH 07/67] feat(skills): track Codex skill usage --- README-zh.md | 4 +- README.md | 4 +- src-tauri/src/config.rs | 55 ++++++++-- src-tauri/src/parser.rs | 18 ++-- src-tauri/src/store.rs | 216 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 265 insertions(+), 32 deletions(-) diff --git a/README-zh.md b/README-zh.md index ff0999e..8f99678 100644 --- a/README-zh.md +++ b/README-zh.md @@ -27,7 +27,7 @@ | Codex 会话日志(Token / 模型 / 配额) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex 用户 MCP 白名单 | `~/.codex/config.toml` → `[mcp_servers.*]` | -| 用户 Skill 白名单 | `~/.claude/skills/` 目录 | +| 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/` | | 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | ### 关键处理 @@ -37,7 +37,7 @@ - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 - 成本按四类 token 分别计价;模型带 `priced` 标记,**两源都查不到的模型只计 Token、UI 标注「暂无定价」** - 日志只有裸模型名、无厂商信息 → 第三方模型默认取官方厂商价(估算) -- 工具分类:`mcp____*` 且 server 在**所属 Agent** 的配置中 → MCP;Skill 调用(`Skill` 工具的 `input.skill`,或 `/skill` 斜杠命令)且在 skills 目录中 → Skill(仅 Claude);其余忽略 +- 工具分类:`mcp____*` 且 server 在**所属 Agent** 的配置中 → MCP;Claude 的 Skill 工具/斜杠命令及 Codex 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配 → Skill;其余忽略 > 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 diff --git a/README.md b/README.md index 20a0e62..a09f166 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). | Codex session logs (tokens / model / rate limits) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex user MCP whitelist | `~/.codex/config.toml` → `[mcp_servers.*]` | -| User Skill whitelist | `~/.claude/skills/` directory | +| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/` | | Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | ### Key processing @@ -37,7 +37,7 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins - Cost is priced per the four token types; each model carries a `priced` flag — **models not found in either source still count tokens but are labelled "no price" in the UI** - Logs contain only the bare model name (no vendor) → third-party models default to the official vendor price (an estimate) -- Tool classification: `mcp____*` where the server is in the owning agent's config → MCP; a Skill call (the `Skill` tool's `input.skill`, or a `/skill` slash command) whose name is in your skills directory → Skill (Claude only); everything else is ignored +- Tool classification: `mcp____*` where the server is in the owning agent's config → MCP; Claude Skill tool/slash-command calls and Codex `SKILL.md` reads are matched against that agent's user/project skill directories → Skill; everything else is ignored > Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 39a2573..5b76ed3 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -7,7 +7,8 @@ use std::path::{Path, PathBuf}; pub struct UserConfig { pub mcp_servers: HashSet, // claude, from ~/.claude.json pub codex_mcp_servers: HashSet, // codex, from ~/.codex/config.toml - pub skills: HashSet, + pub claude_skills: HashSet, + pub codex_skills: HashSet, } fn home() -> Option { @@ -43,12 +44,16 @@ fn mcps_from(json: Option<&serde_json::Value>) -> HashSet { } /// Add each subdirectory name of `dir` to the set (skills are folders). -fn scan_skill_dir(dir: &Path, set: &mut HashSet) { +fn scan_skill_dir(dir: &Path, set: &mut HashSet, include_hidden: bool) { if let Ok(entries) = fs::read_dir(dir) { for e in entries.flatten() { if e.path().is_dir() { if let Some(name) = e.file_name().to_str() { - set.insert(name.to_string()); + if (include_hidden || !name.starts_with('.')) + && e.path().join("SKILL.md").is_file() + { + set.insert(name.to_string()); + } } } } @@ -62,7 +67,36 @@ fn scan_skill_dir(dir: &Path, set: &mut HashSet) { fn load_user_skills() -> HashSet { let mut set = HashSet::new(); if let Some(h) = home() { - scan_skill_dir(&h.join(".claude").join("skills"), &mut set); + scan_skill_dir(&h.join(".claude").join("skills"), &mut set, true); + } + set +} + +fn codex_home() -> Option { + std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .or_else(|| home().map(|h| h.join(".codex"))) +} + +/// User Codex skills live in $CODEX_HOME/skills and ~/.agents/skills. Also +/// include project `.agents/skills` directories from session working dirs. +/// Hidden directories such as $CODEX_HOME/skills/.system are built-ins. +fn load_codex_skills(project_dirs: &[PathBuf]) -> HashSet { + let mut set = HashSet::new(); + let mut dirs = HashSet::new(); + if let Some(d) = codex_home() { + dirs.insert(d.join("skills")); + } + if let Some(h) = home() { + dirs.insert(h.join(".agents").join("skills")); + } + for project in project_dirs { + for dir in project.ancestors() { + dirs.insert(dir.join(".agents").join("skills")); + } + } + for dir in dirs { + scan_skill_dir(&dir, &mut set, false); } set } @@ -101,13 +135,14 @@ fn load_codex_mcps() -> HashSet { } impl UserConfig { - pub fn load() -> Self { + pub fn load(codex_project_dirs: &[PathBuf]) -> Self { // Parse ~/.claude.json a single time and derive the MCP whitelist from it. let json = read_user_config(); UserConfig { mcp_servers: mcps_from(json.as_ref()), codex_mcp_servers: load_codex_mcps(), - skills: load_user_skills(), + claude_skills: load_user_skills(), + codex_skills: load_codex_skills(codex_project_dirs), } } @@ -122,8 +157,12 @@ impl UserConfig { } /// A skill id (may be "plugin:skill") → strip plugin prefix, check dir. - pub fn is_user_skill(&self, skill: &str) -> bool { + pub fn is_user_skill(&self, agent: &str, skill: &str) -> bool { let key = skill.rsplit(':').next().unwrap_or(skill); - self.skills.contains(key) + if agent == crate::store::AGENT_CODEX { + self.codex_skills.contains(key) + } else { + self.claude_skills.contains(key) + } } } diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 33661a0..da77531 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -120,7 +120,7 @@ pub fn build_dashboard() -> Dashboard { let codex_quota = store.codex_quota.clone(); // 2. Aggregate: apply current config + prices, slice by current time. - let cfg = UserConfig::load(); + let cfg = UserConfig::load(&store.codex_project_dirs()); // Memoized price table (cheap clone); loaded/refreshed off-thread elsewhere // so neither parsing nor the network runs while we hold BUILD_LOCK. let pricing = Pricing::shared(); @@ -246,14 +246,18 @@ fn build_scope( // "servers"/"skills" = how many the user has *installed* (global, constant // across periods), not how many were called in the window. Per agent: - // skills are a Claude-only concept, so a pure-Codex scope reports 0 and the - // UI hides the section. let (installed_servers, installed_skills) = match agent_scope { - AGENT_CODEX => (cfg.codex_mcp_servers.len() as u64, 0u64), - AGENT_CLAUDE => (cfg.mcp_servers.len() as u64, cfg.skills.len() as u64), + AGENT_CODEX => ( + cfg.codex_mcp_servers.len() as u64, + cfg.codex_skills.len() as u64, + ), + AGENT_CLAUDE => ( + cfg.mcp_servers.len() as u64, + cfg.claude_skills.len() as u64, + ), _ => ( (cfg.mcp_servers.len() + cfg.codex_mcp_servers.len()) as u64, - cfg.skills.len() as u64, + (cfg.claude_skills.len() + cfg.codex_skills.len()) as u64, ), }; for r in [&mut day, &mut week, &mut month] { @@ -294,7 +298,7 @@ fn compute_event(r: &RawEvent, cfg: &UserConfig, pricing: &Pricing) -> Event { let skills = r .skills .iter() - .filter(|s| cfg.is_user_skill(s)) + .filter(|s| cfg.is_user_skill(&r.agent, s)) .map(|s| s.rsplit(':').next().unwrap_or(s).to_string()) .collect(); Event { diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 4792924..6394398 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -33,7 +33,7 @@ pub struct RawEvent { pub cr: f64, // cache read (codex: cached_input_tokens, a subset of its raw input) pub out_tok: f64, pub mcp: Vec, // all mcp__ names called (unfiltered) - pub skills: Vec, // all Skill input.skill ids called (unfiltered) + pub skills: Vec, // all detected Skill ids called (unfiltered) pub id: String, // message id (dedup); "" = no cross-line dedup needed // Source log file (manifest key). Lets a truncated/rewritten file purge its // own stale events before being re-read, so re-ingestion stays idempotent. @@ -61,6 +61,12 @@ struct FileState { model: String, #[serde(default)] session: String, + #[serde(default)] + cwd: String, + #[serde(default)] + turn_id: String, + #[serde(default)] + skills_seen: Vec, } #[derive(Serialize, Deserialize, Default)] @@ -88,7 +94,8 @@ pub struct Store { // tool_use line were deduped, dropping the tool call). // v4: track a per-event source file (idempotent re-read of truncated logs). // v5: multi-agent ingest (claude + codex), FileState manifest, quota snapshot. -const STORE_VERSION: u32 = 5; +// v6: extract Codex Skill calls and track project skill directories. +const STORE_VERSION: u32 = 6; /// Atomically replace `path`'s contents: write a sibling temp file, then rename /// over the target (same-volume rename is atomic on Windows and Unix). Avoids @@ -204,6 +211,21 @@ impl Store { } } + /// Working directories seen in Codex sessions. Config uses these to find + /// project-scoped `.agents/skills` without crawling the user's home dir. + pub fn codex_project_dirs(&self) -> Vec { + let mut dirs: Vec = self + .manifest + .files + .values() + .filter(|s| !s.cwd.is_empty()) + .map(|s| PathBuf::from(&s.cwd)) + .collect(); + dirs.sort(); + dirs.dedup(); + dirs + } + /// Rebuild the id→index map after the `events` vector is mutated wholesale /// (purge/prune shift positions, so partial updates aren't enough). fn rebuild_index(&mut self) { @@ -362,16 +384,35 @@ impl Store { { state.session = id.to_string(); } + if let Some(cwd) = payload.get("cwd").and_then(|x| x.as_str()) { + state.cwd = cwd.to_string(); + } None } "turn_context" => { if let Some(m) = payload.get("model").and_then(|x| x.as_str()) { state.model = m.to_string(); } + if let Some(cwd) = payload.get("cwd").and_then(|x| x.as_str()) { + state.cwd = cwd.to_string(); + } + if let Some(turn_id) = payload.get("turn_id").and_then(|x| x.as_str()) { + if state.turn_id != turn_id { + state.turn_id = turn_id.to_string(); + state.skills_seen.clear(); + } + } None } "event_msg" => { - if payload.get("type")?.as_str()? != "token_count" { + let event_type = payload.get("type")?.as_str()?; + // Older Codex logs may not carry turn_context.turn_id. A task + // boundary is still enough to reset per-turn Skill dedup. + if event_type == "task_started" { + state.skills_seen.clear(); + return None; + } + if event_type != "token_count" { return None; } let ts_ms = parse_ts(&v)?; @@ -406,18 +447,35 @@ impl Store { }) } "response_item" => { - // MCP tool calls: function_call items named mcp__[__]. - if payload.get("type")?.as_str()? != "function_call" { - return None; + let item_type = payload.get("type")?.as_str()?; + let name = payload.get("name").and_then(|x| x.as_str()).unwrap_or(""); + + // Classic Codex CLI shell call. `arguments` is JSON containing + // the command that reads a selected Skill's SKILL.md. + if item_type == "function_call" && name == "exec_command" { + let args = payload.get("arguments")?.as_str()?; + let cmd = serde_json::from_str::(args) + .ok() + .and_then(|a| a.get("cmd").and_then(|x| x.as_str()).map(str::to_owned)) + .unwrap_or_else(|| args.to_string()); + return codex_skill_event(&v, state, &cmd); } - let name = payload.get("name")?.as_str()?; - let rest = name.strip_prefix("mcp__")?; - let server = rest.split("__").next().unwrap_or("").to_string(); - if server.is_empty() { - return None; + + // Codex app wraps tool dispatch in a custom `exec` call. Its + // input still contains the concrete SKILL.md path. + if item_type == "custom_tool_call" && name == "exec" { + let input = payload.get("input")?.as_str()?; + return codex_skill_event(&v, state, input); } - Some(RawEvent { - ts_ms: parse_ts(&v)?, + + // MCP tool calls: function_call items named mcp__[__]. + let rest = (item_type == "function_call") + .then(|| name.strip_prefix("mcp__")) + .flatten()?; + let server = rest.split("__").next().unwrap_or("").to_string(); + let ts_ms = parse_ts(&v)?; + (!server.is_empty()).then(|| RawEvent { + ts_ms, session: state.session.clone(), model: String::new(), // not an LLM request → no tokens/cost in_tok: 0.0, @@ -473,6 +531,77 @@ fn parse_ts(v: &serde_json::Value) -> Option { Some(DateTime::parse_from_rfc3339(ts).ok()?.timestamp_millis()) } +/// Extract each directory name immediately above a referenced SKILL.md. The +/// config whitelist later rejects built-in/plugin paths and unknown names. +fn codex_skill_names(text: &str) -> Vec { + let mut found: Vec<(usize, String)> = Vec::new(); + for marker in ["/SKILL.md", "\\SKILL.md"] { + let mut offset = 0; + while let Some(rel) = text[offset..].find(marker) { + let end = offset + rel; + let before = &text[..end]; + let path_start = before + .rfind(|c: char| c.is_whitespace() || c == '\'' || c == '"') + .map(|i| i + 1) + .unwrap_or(0); + let path = &before[path_start..]; + let built_in = path.contains("/skills/.system/") + || path.contains("\\skills\\.system\\") + || path.contains("/plugins/cache/") + || path.contains("\\plugins\\cache\\"); + let start = path + .rfind(|c| c == '/' || c == '\\') + .map(|i| i + 1) + .unwrap_or(0); + let name = path[start..] + .trim_matches(|c: char| c.is_whitespace() || c == '\'' || c == '"'); + if !built_in && !name.is_empty() { + found.push((end, name.to_string())); + } + offset = end + marker.len(); + } + } + found.sort_by_key(|(pos, _)| *pos); + let mut names = Vec::new(); + for (_, name) in found { + if !names.contains(&name) { + names.push(name); + } + } + names +} + +/// One call per skill per Codex turn. Reading the same SKILL.md again to finish +/// a truncated/paged read is still the same invocation and must not inflate it. +fn codex_skill_event( + v: &serde_json::Value, + state: &mut FileState, + tool_input: &str, +) -> Option { + let skills: Vec = codex_skill_names(tool_input) + .into_iter() + .filter(|skill| !state.skills_seen.contains(skill)) + .collect(); + if skills.is_empty() { + return None; + } + state.skills_seen.extend(skills.iter().cloned()); + Some(RawEvent { + ts_ms: parse_ts(v)?, + session: state.session.clone(), + model: String::new(), + in_tok: 0.0, + cc: 0.0, + cr: 0.0, + out_tok: 0.0, + mcp: Vec::new(), + skills, + id: String::new(), + source: String::new(), + agent: String::new(), + }) +} + /// "rollout-2026-06-25T16-13-58-.jsonl" → "" (session id fallback). fn codex_session_from_filename(path: &std::path::Path) -> String { let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); @@ -610,3 +739,64 @@ fn parse_assistant(v: &serde_json::Value) -> Option { agent: String::new(), }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extracts_codex_skill_names_from_unix_and_windows_paths() { + let input = r#"sed ~/.codex/skills/find-skills/SKILL.md && type C:\Users\me\.agents\skills\release-notes\SKILL.md && cat ~/.codex/skills/.system/openai-docs/SKILL.md && cat ~/.codex/plugins/cache/bundled/skills/browser/SKILL.md"#; + assert_eq!( + codex_skill_names(input), + vec!["find-skills".to_string(), "release-notes".to_string()] + ); + } + + #[test] + fn counts_a_codex_skill_once_per_turn() { + let mut store = Store { + events: Vec::new(), + index: HashMap::new(), + manifest: Manifest::default(), + codex_quota: None, + }; + let mut state = FileState::default(); + let turn = |id: &str| { + format!( + r#"{{"timestamp":"2026-07-09T12:00:00Z","type":"turn_context","payload":{{"turn_id":"{id}","model":"gpt-5"}}}}"# + ) + }; + let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"custom_tool_call","name":"exec","input":"read ~/.codex/skills/find-skills/SKILL.md"}}"#; + + assert!(store.parse_codex_line(&turn("turn-1"), &mut state).is_none()); + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().skills, + vec!["find-skills"] + ); + assert!(store.parse_codex_line(call, &mut state).is_none()); + + assert!(store.parse_codex_line(&turn("turn-2"), &mut state).is_none()); + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().skills, + vec!["find-skills"] + ); + } + + #[test] + fn parses_classic_codex_exec_command_skill_call() { + let mut store = Store { + events: Vec::new(), + index: HashMap::new(), + manifest: Manifest::default(), + codex_quota: None, + }; + let mut state = FileState::default(); + let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"cat ~/.agents/skills/review/SKILL.md\"}"}}"#; + + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().skills, + vec!["review"] + ); + } +} From bb6cb2d4cfbb07311eef9b6f65aeb49fb3fbacb7 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 9 Jul 2026 19:30:02 -0700 Subject: [PATCH 08/67] feat(ui): filter total tokens by model --- src/App.tsx | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7d22204..5342be5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -441,18 +441,33 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const [period, setPeriod] = useState<"Day" | "Week" | "Month">("Week"); const P: PeriodReport = period === "Day" ? scope.day : period === "Month" ? scope.month : scope.week; const M = P.metrics; + const models = P.models; + const [totalModel, setTotalModel] = useState(""); + // The All scope can contain the same model once per agent. The selector is + // model-based, so combine those rows before calculating its Total and cost. + const modelTotals = Array.from(models.reduce((totals, model) => { + const prev = totals.get(model.name) ?? { name: model.name, tokens: 0, cost: 0 }; + prev.tokens += model.tokens; + prev.cost += model.cost; + totals.set(model.name, prev); + return totals; + }, new Map()).values()) + .filter((model) => model.tokens > 0) + .sort((a, b) => b.tokens - a.tokens); + const selectedModel = modelTotals.find((model) => model.name === totalModel); + const totalTokens = selectedModel?.tokens ?? M.totalTokens; + const totalCost = selectedModel?.cost ?? M.cost; // Per-agent slices — non-empty only in the All scope with >=2 sources; they // switch the hero bar + chart from Cached/New to a by-agent breakdown. const slices = P.agents; // animated Total tokens: counts up from 0 on each open / period / scope - // switch; held at 0 while the popover is hidden so it never flashes. - const animTotal = useCountUp(M.totalTokens, `${period}:${scope.id}:${openGen}`, active); + // / model switch; held at 0 while hidden so it never flashes. + const animTotal = useCountUp(totalTokens, `${period}:${scope.id}:${totalModel}:${openGen}`, active); // Explicit percentages avoid WebKit's incorrect flexGrow + flexBasis:0 sizing. const splitTot = M.inputTokens + M.cacheTokens + M.outputTokens; const cachePct = splitTot > 0 ? (M.cacheTokens / splitTot) * 100 : 0; const restPct = splitTot > 0 ? ((M.inputTokens + M.outputTokens) / splitTot) * 100 : 0; const agentTotal = slices.reduce((sum, s) => sum + s.tokens, 0); - const models = P.models; // Hide noise: 0% token-share rows, and $0 entries in the cost donut. // Show models whose share is at least 0.1% when rounded to 1 decimal; below // that it'd render a meaningless "0.0%" (a negligible token share). Such a @@ -575,15 +590,33 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {/* hero */}
-
Total tokens
+
+
Total tokens
+ +
{animTotal.toFixed(2)}M - {Math.round(M.deltaTokens) !== 0 && } + {!selectedModel && Math.round(M.deltaTokens) !== 0 && }
Est. cost
-
${M.cost.toFixed(2)}
+
${totalCost.toFixed(2)}
{/* All scope: one segment per agent. Single scope: cached vs new. */} From 37fc3de231429212e2e56cd45eb2b034513a7e30 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 9 Jul 2026 19:47:27 -0700 Subject: [PATCH 09/67] fix(mcp): track Codex tool calls --- README-zh.md | 4 +- README.md | 4 +- src-tauri/src/config.rs | 59 +++++++++++--- src-tauri/src/store.rs | 170 +++++++++++++++++++++++++++++++++------- 4 files changed, 193 insertions(+), 44 deletions(-) diff --git a/README-zh.md b/README-zh.md index 8f99678..c82dfc4 100644 --- a/README-zh.md +++ b/README-zh.md @@ -26,7 +26,7 @@ | Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | | Codex 会话日志(Token / 模型 / 配额) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | -| Codex 用户 MCP 白名单 | `~/.codex/config.toml` → `[mcp_servers.*]` | +| Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | | 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/` | | 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | @@ -37,7 +37,7 @@ - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 - 成本按四类 token 分别计价;模型带 `priced` 标记,**两源都查不到的模型只计 Token、UI 标注「暂无定价」** - 日志只有裸模型名、无厂商信息 → 第三方模型默认取官方厂商价(估算) -- 工具分类:`mcp____*` 且 server 在**所属 Agent** 的配置中 → MCP;Claude 的 Skill 工具/斜杠命令及 Codex 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配 → Skill;其余忽略 +- 工具分类:MCP 调用从直接的 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用中归属 server,再按**所属 Agent**配置过滤;Claude 的 Skill 工具/斜杠命令及 Codex 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配;其余忽略 > 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 diff --git a/README.md b/README.md index a09f166..f0eb8c0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). | Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | | Codex session logs (tokens / model / rate limits) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | -| Codex user MCP whitelist | `~/.codex/config.toml` → `[mcp_servers.*]` | +| Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | | User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/` | | Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | @@ -37,7 +37,7 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins - Cost is priced per the four token types; each model carries a `priced` flag — **models not found in either source still count tokens but are labelled "no price" in the UI** - Logs contain only the bare model name (no vendor) → third-party models default to the official vendor price (an estimate) -- Tool classification: `mcp____*` where the server is in the owning agent's config → MCP; Claude Skill tool/slash-command calls and Codex `SKILL.md` reads are matched against that agent's user/project skill directories → Skill; everything else is ignored +- Tool classification: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill tool/slash-command calls and Codex `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored > Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 5b76ed3..4f8ca4e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -108,18 +108,9 @@ fn norm_mcp(name: &str) -> String { name.replace('-', "_") } -/// MCP servers from ~/.codex/config.toml (honoring CODEX_HOME): every -/// `[mcp_servers.]` table header. A structural TOML parse isn't needed -/// for section names, so we scan headers instead of adding a toml dependency. -fn load_codex_mcps() -> HashSet { - let mut set = HashSet::new(); - let path = std::env::var_os("CODEX_HOME") - .map(PathBuf::from) - .or_else(|| home().map(|h| h.join(".codex"))) - .map(|d| d.join("config.toml")); - let Some(text) = path.and_then(|p| fs::read_to_string(p).ok()) else { - return set; - }; +/// Collect every `[mcp_servers.]` table header. A structural TOML parse +/// isn't needed for section names, so keep this dependency-free. +fn add_codex_mcps(text: &str, set: &mut HashSet) { for line in text.lines() { let line = line.trim(); // Match [mcp_servers.] and [mcp_servers.""], but not deeper @@ -131,6 +122,26 @@ fn load_codex_mcps() -> HashSet { set.insert(norm_mcp(name)); } } +} + +/// MCP servers from the global Codex config plus trusted project configs seen +/// in session working directories. +fn load_codex_mcps(project_dirs: &[PathBuf]) -> HashSet { + let mut set = HashSet::new(); + let mut paths = HashSet::new(); + if let Some(d) = codex_home() { + paths.insert(d.join("config.toml")); + } + for project in project_dirs { + for dir in project.ancestors() { + paths.insert(dir.join(".codex").join("config.toml")); + } + } + for path in paths { + if let Ok(text) = fs::read_to_string(path) { + add_codex_mcps(&text, &mut set); + } + } set } @@ -140,7 +151,7 @@ impl UserConfig { let json = read_user_config(); UserConfig { mcp_servers: mcps_from(json.as_ref()), - codex_mcp_servers: load_codex_mcps(), + codex_mcp_servers: load_codex_mcps(codex_project_dirs), claude_skills: load_user_skills(), codex_skills: load_codex_skills(codex_project_dirs), } @@ -166,3 +177,25 @@ impl UserConfig { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_and_normalizes_codex_mcp_server_names() { + let mut servers = HashSet::new(); + add_codex_mcps( + r#" + [mcp_servers.chrome-devtools] + [mcp_servers."node_repl"] + [mcp_servers.chrome-devtools.env] + "#, + &mut servers, + ); + assert_eq!( + servers, + HashSet::from(["chrome_devtools".into(), "node_repl".into()]) + ); + } +} diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 6394398..b1a4b7b 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -67,6 +67,11 @@ struct FileState { turn_id: String, #[serde(default)] skills_seen: Vec, + // Newer Codex logs emit tool_search_output groups, then call the selected + // tool by its short name (for example `js`). Persist the short-name mapping + // so incremental reads can still attribute later calls to their MCP server. + #[serde(default)] + mcp_tools: HashMap, } #[derive(Serialize, Deserialize, Default)] @@ -95,7 +100,8 @@ pub struct Store { // v4: track a per-event source file (idempotent re-read of truncated logs). // v5: multi-agent ingest (claude + codex), FileState manifest, quota snapshot. // v6: extract Codex Skill calls and track project skill directories. -const STORE_VERSION: u32 = 6; +// v7: extract Codex MCP calls from tool search and app custom-tool formats. +const STORE_VERSION: u32 = 7; /// Atomically replace `path`'s contents: write a sibling temp file, then rename /// over the target (same-volume rename is atomic on Windows and Unix). Avoids @@ -450,6 +456,13 @@ impl Store { let item_type = payload.get("type")?.as_str()?; let name = payload.get("name").and_then(|x| x.as_str()).unwrap_or(""); + // Newer Codex exposes lazily searched MCP tools under short + // function names. Remember which server owns each returned tool. + if item_type == "tool_search_output" { + remember_codex_mcp_tools(payload, state); + return None; + } + // Classic Codex CLI shell call. `arguments` is JSON containing // the command that reads a selected Skill's SKILL.md. if item_type == "function_call" && name == "exec_command" { @@ -458,23 +471,29 @@ impl Store { .ok() .and_then(|a| a.get("cmd").and_then(|x| x.as_str()).map(str::to_owned)) .unwrap_or_else(|| args.to_string()); - return codex_skill_event(&v, state, &cmd); + return codex_exec_event(&v, state, &cmd, false); } - // Codex app wraps tool dispatch in a custom `exec` call. Its - // input still contains the concrete SKILL.md path. + // Codex app wraps tool dispatch in a custom `exec` call. Its JS + // input contains concrete SKILL.md paths and tools.mcp__ calls. if item_type == "custom_tool_call" && name == "exec" { let input = payload.get("input")?.as_str()?; - return codex_skill_event(&v, state, input); + return codex_exec_event(&v, state, input, true); } - // MCP tool calls: function_call items named mcp__[__]. - let rest = (item_type == "function_call") - .then(|| name.strip_prefix("mcp__")) - .flatten()?; - let server = rest.split("__").next().unwrap_or("").to_string(); + if item_type != "function_call" { + return None; + } + // Old logs keep the mcp__server__tool name. Newer tool-search + // logs only keep the short tool name and need the mapping above. + let server = name + .strip_prefix("mcp__") + .and_then(|rest| rest.split("__").next()) + .filter(|server| !server.is_empty()) + .map(str::to_owned) + .or_else(|| state.mcp_tools.get(name).cloned())?; let ts_ms = parse_ts(&v)?; - (!server.is_empty()).then(|| RawEvent { + Some(RawEvent { ts_ms, session: state.session.clone(), model: String::new(), // not an LLM request → no tokens/cost @@ -531,6 +550,59 @@ fn parse_ts(v: &serde_json::Value) -> Option { Some(DateTime::parse_from_rfc3339(ts).ok()?.timestamp_millis()) } +/// Record short tool names returned by Codex's lazy tool search. A result group +/// named `mcp__node_repl__` with a child `js` means later `function_call: js` +/// events belong to the `node_repl` MCP server. +fn remember_codex_mcp_tools(payload: &serde_json::Value, state: &mut FileState) { + let Some(groups) = payload.get("tools").and_then(|x| x.as_array()) else { + return; + }; + for group in groups { + let Some(server) = group + .get("name") + .and_then(|x| x.as_str()) + .and_then(|name| name.strip_prefix("mcp__")) + .and_then(|rest| rest.split("__").next()) + .filter(|server| !server.is_empty()) + else { + continue; + }; + let Some(tools) = group.get("tools").and_then(|x| x.as_array()) else { + continue; + }; + for tool in tools { + if let Some(name) = tool.get("name").and_then(|x| x.as_str()) { + state.mcp_tools.insert(name.to_string(), server.to_string()); + } + } + } +} + +/// Extract every actual `tools.mcp__server__tool(...)` invocation from a Codex +/// app custom exec call. Repeated servers represent repeated tool calls. +fn codex_mcp_servers(text: &str) -> Vec { + const PREFIX: &str = "tools.mcp__"; + let mut servers = Vec::new(); + let mut offset = 0; + while let Some(rel) = text[offset..].find(PREFIX) { + let server_start = offset + rel + PREFIX.len(); + let rest = &text[server_start..]; + let Some(server_end) = rest.find("__") else { + break; + }; + let server = &rest[..server_end]; + let tool = &rest[server_end + 2..]; + let tool_end = tool + .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) + .unwrap_or(tool.len()); + if !server.is_empty() && tool_end > 0 && tool[tool_end..].trim_start().starts_with('(') { + servers.push(server.to_string()); + } + offset = server_start + server_end + 2 + tool_end; + } + servers +} + /// Extract each directory name immediately above a referenced SKILL.md. The /// config whitelist later rejects built-in/plugin paths and unknown names. fn codex_skill_names(text: &str) -> Vec { @@ -573,28 +645,35 @@ fn codex_skill_names(text: &str) -> Vec { /// One call per skill per Codex turn. Reading the same SKILL.md again to finish /// a truncated/paged read is still the same invocation and must not inflate it. -fn codex_skill_event( +fn codex_exec_event( v: &serde_json::Value, state: &mut FileState, tool_input: &str, + include_mcp: bool, ) -> Option { + let ts_ms = parse_ts(v)?; let skills: Vec = codex_skill_names(tool_input) .into_iter() .filter(|skill| !state.skills_seen.contains(skill)) .collect(); - if skills.is_empty() { + let mcp = if include_mcp { + codex_mcp_servers(tool_input) + } else { + Vec::new() + }; + if skills.is_empty() && mcp.is_empty() { return None; } state.skills_seen.extend(skills.iter().cloned()); Some(RawEvent { - ts_ms: parse_ts(v)?, + ts_ms, session: state.session.clone(), model: String::new(), in_tok: 0.0, cc: 0.0, cr: 0.0, out_tok: 0.0, - mcp: Vec::new(), + mcp, skills, id: String::new(), source: String::new(), @@ -744,6 +823,15 @@ fn parse_assistant(v: &serde_json::Value) -> Option { mod tests { use super::*; + fn empty_store() -> Store { + Store { + events: Vec::new(), + index: HashMap::new(), + manifest: Manifest::default(), + codex_quota: None, + } + } + #[test] fn extracts_codex_skill_names_from_unix_and_windows_paths() { let input = r#"sed ~/.codex/skills/find-skills/SKILL.md && type C:\Users\me\.agents\skills\release-notes\SKILL.md && cat ~/.codex/skills/.system/openai-docs/SKILL.md && cat ~/.codex/plugins/cache/bundled/skills/browser/SKILL.md"#; @@ -755,12 +843,7 @@ mod tests { #[test] fn counts_a_codex_skill_once_per_turn() { - let mut store = Store { - events: Vec::new(), - index: HashMap::new(), - manifest: Manifest::default(), - codex_quota: None, - }; + let mut store = empty_store(); let mut state = FileState::default(); let turn = |id: &str| { format!( @@ -785,12 +868,7 @@ mod tests { #[test] fn parses_classic_codex_exec_command_skill_call() { - let mut store = Store { - events: Vec::new(), - index: HashMap::new(), - manifest: Manifest::default(), - codex_quota: None, - }; + let mut store = empty_store(); let mut state = FileState::default(); let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"cat ~/.agents/skills/review/SKILL.md\"}"}}"#; @@ -799,4 +877,42 @@ mod tests { vec!["review"] ); } + + #[test] + fn maps_tool_search_results_to_short_mcp_calls() { + let mut store = empty_store(); + let mut state = FileState::default(); + let search = r#"{"timestamp":"2026-07-09T12:00:00Z","type":"response_item","payload":{"type":"tool_search_output","tools":[{"name":"mcp__node_repl__","tools":[{"name":"js"},{"name":"js_reset"}]}]}}"#; + let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"function_call","name":"js","arguments":"{}"}}"#; + + assert!(store.parse_codex_line(search, &mut state).is_none()); + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().mcp, + vec!["node_repl"] + ); + } + + #[test] + fn extracts_mcp_calls_from_codex_app_custom_exec() { + let mut store = empty_store(); + let mut state = FileState::default(); + let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"custom_tool_call","name":"exec","input":"await tools.mcp__node_repl__js({}); await tools.mcp__node_repl__js({}); const ref = tools.mcp__unused__inspect;"}}"#; + + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().mcp, + vec!["node_repl", "node_repl"] + ); + } + + #[test] + fn parses_prefixed_codex_mcp_call() { + let mut store = empty_store(); + let mut state = FileState::default(); + let call = r#"{"timestamp":"2026-07-09T12:00:01Z","type":"response_item","payload":{"type":"function_call","name":"mcp__chrome_devtools__click","arguments":"{}"}}"#; + + assert_eq!( + store.parse_codex_line(call, &mut state).unwrap().mcp, + vec!["chrome_devtools"] + ); + } } From 70288b76cb2242131b25c2f85136e5942b820da0 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Sun, 12 Jul 2026 23:45:59 -0700 Subject: [PATCH 10/67] fix: prevent input interruption and refresh pricing --- docs/REVIEW.md | 6 +- package.json | 2 +- public/confetti.html | 147 ----------------- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 333 ++------------------------------------ src-tauri/src/pricing.rs | 64 +++++++- src-tauri/tauri.conf.json | 3 +- 8 files changed, 80 insertions(+), 479 deletions(-) delete mode 100644 public/confetti.html diff --git a/docs/REVIEW.md b/docs/REVIEW.md index f8755c9..0ef8568 100644 --- a/docs/REVIEW.md +++ b/docs/REVIEW.md @@ -14,7 +14,7 @@ 2. 一切都经 `build_dashboard` 每 30s + 每次开 popover 触发,造成 O(全部历史) 的重复全量读写/解析(含主线程阻塞 IO 与锁内网络抓取); 3. 发布流水线脆弱。 -**跨平台/Windows 风险尤其突出**:当前 `feat/windows-support` 分支的 Critical 构建中断同时阻断 macOS `.dmg` 与 Windows NSIS 安装器;autostart 在 Windows 上写 `HKCU\...\Run` 且无法关闭;非原子写入与 AV/文件锁在 Windows 上更易触发缓存损坏;彩带依赖 WebView2 冷启动时序。 +**跨平台/Windows 风险尤其突出**:当前 `feat/windows-support` 分支的 Critical 构建中断同时阻断 macOS `.dmg` 与 Windows NSIS 安装器;autostart 在 Windows 上写 `HKCU\...\Run` 且无法关闭;非原子写入与 AV/文件锁在 Windows 上更易触发缓存损坏。 --- @@ -107,9 +107,7 @@ 9. **`fetch_cached` 持久化任何以 `{` 开头的 200 响应,可毒化缓存 24h**(`pricing.rs:70-79`)——结构校验通过后再覆盖缓存。 10. **Shell 插件与 `shell:allow-open` 能力被启用但从未使用**(`capabilities/default.json:16`、`lib.rs:550`、`Cargo.toml:19`)——纯负债,扩大攻击面,删除。 11. **`workflow_dispatch` 从非 tag ref 触发会产生畸形 release 并使 Homebrew 步骤失败**(`release.yml`)——加 `if: startsWith(github.ref, 'refs/tags/v')` 守卫。 -12. **Milestone 快照在状态锁外持久化,可能写入回退的旧 floor**(`lib.rs:134-155`)——`save_milestones` 移入持锁区并令 floor 单调。 -13. **请求趋势 sparkline 计入了请求指标排除的 slash-command 事件**(`parser.rs:288/350/420`)——给趋势自增加 `if !e.model.is_empty()` 守卫。 -14. **彩带尾部被截断:动画寿命长于固定 4200ms 隐藏定时器**(`confetti.html:135-141`、`lib.rs:255-256`)——经 IPC 发 `confetti-done` 再隐藏,或延迟提到 ~5500ms。 +12. **请求趋势 sparkline 计入了请求指标排除的 slash-command 事件**(`parser.rs:288/350/420`)——给趋势自增加 `if !e.model.is_empty()` 守卫。 --- diff --git a/package.json b/package.json index 59621b8..3866287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.2.0", + "version": "1.2.2", "private": true, "type": "module", "scripts": { diff --git a/public/confetti.html b/public/confetti.html deleted file mode 100644 index 3f7cf4a..0000000 --- a/public/confetti.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - Tokenscope Celebration - - - - - - - diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8c59fcd..7c698ad 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4224,7 +4224,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.2.0" +version = "1.2.2" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b142703..3a2da7b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.2.0" +version = "1.2.2" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 70dd4e7..3c2ce81 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,17 +5,17 @@ mod pricing; mod store; use model::Dashboard; -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; +use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; +#[cfg(not(target_os = "macos"))] +use tauri::WindowEvent; use tauri::{ menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Emitter, Manager, }; -#[cfg(not(target_os = "macos"))] -use tauri::WindowEvent; -use std::time::Duration; use tauri_plugin_autostart::ManagerExt; // Positioner is only used for the non-macOS fallback; macOS positions the // NSPanel manually (see position_panel). @@ -53,55 +53,11 @@ fn update_tray_label(app: &tauri::AppHandle, label: String) { fn refresh(app: &tauri::AppHandle) { let dash = parser::build_dashboard(); update_tray_label(app, fmt_tokens_m(dash.today_tokens)); - check_milestones(app, &dash); let _ = app.emit("dashboard-updated", &dash); } -/// Persisted 100M-token milestone snapshot. Stored in the app *data* dir so it -/// survives app restarts, reboots, and updates (which only replace the .app -/// bundle, never the data dir). The per-period ids let us tell a real crossing -/// from a period reset. -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct MilestoneState { - week_id: String, - week_floor: i64, - month_id: String, - month_floor: i64, -} - -/// 100M-token celebration tracking. `state` is the last persisted snapshot -/// (`None` only before the very first observation ever, so the first run -/// baselines without celebrating pre-existing usage). `active` guards against -/// overlapping celebrations. -struct Celebration { - state: std::sync::Mutex>, - active: AtomicBool, -} - -/// `~/Library/Application Support/tokenscope/milestones.json` (platform -/// equivalent elsewhere). Deliberately the data dir, not the Caches dir the -/// event store uses — Caches can be purged by the OS, milestones must not be. -fn milestones_path() -> Option { - let dir = dirs::data_dir()?.join("tokenscope"); - let _ = std::fs::create_dir_all(&dir); - Some(dir.join("milestones.json")) -} - -fn load_milestones() -> Option { - let t = std::fs::read_to_string(milestones_path()?).ok()?; - serde_json::from_str(&t).ok() -} - -fn save_milestones(m: &MilestoneState) { - if let Some(p) = milestones_path() { - if let Ok(t) = serde_json::to_string(m) { - let _ = std::fs::write(p, t); - } - } -} - // ── Launch-at-login preference ────────────────────────────────────── -// Persisted in the data dir (survives restarts/updates, like milestones). The +// Persisted in the data dir so it survives restarts and updates. The // on/off toggle lives in the tray's right-click menu; on startup we reconcile // the OS registration to this preference rather than force-enabling every // launch (which silently undid a user who had turned autostart off). @@ -146,202 +102,6 @@ fn reconcile_autostart(app: &tauri::AppHandle) -> bool { pref } -/// Current calendar-week and calendar-month identifiers, matching parser.rs's -/// period definitions (Monday-based week, calendar month), so a stored floor is -/// only ever compared within the same period. -fn period_ids() -> (String, String) { - use chrono::Datelike; - let d = chrono::Local::now().date_naive(); - let iso = d.iso_week(); - ( - format!("{}-W{:02}", iso.year(), iso.week()), - format!("{}-{:02}", d.year(), d.month()), - ) -} - -/// Decide whether to celebrate: fire if either period advanced to a higher -/// 100M floor *within the same period*. `None` (first ever observation) never -/// fires. A period-id mismatch means that period reset, so it re-baselines -/// silently rather than comparing floors. Returns a single bool, so a jump -/// across several boundaries — or week and month advancing together — is one -/// celebration. -fn milestone_fire(prev: Option<&MilestoneState>, cur: &MilestoneState) -> bool { - match prev { - None => false, - Some(p) => { - (p.week_id == cur.week_id && cur.week_floor > p.week_floor) - || (p.month_id == cur.month_id && cur.month_floor > p.month_floor) - } - } -} - -/// Observe the latest totals, persist the snapshot, and celebrate on a new -/// 100M-token milestone. We watch week ∪ month, not day: today is always within -/// both the current week and month, so a day crossing is already implied by the -/// month — but a calendar week can straddle a month boundary, so early in a -/// month the week total can lead the (freshly reset) month, hence both. Because -/// the snapshot is persisted, a crossing that happened while the app wasn't -/// running (it reads the logs Claude writes regardless) still catches up on the -/// next observation. -fn check_milestones(app: &tauri::AppHandle, dash: &Dashboard) { - let Some(state) = app.try_state::() else { - return; - }; - // The first scope is always the aggregate view (all agents combined). - let Some(primary) = dash.scopes.first() else { - return; - }; - // total_tokens is already in millions, so a 100M milestone is total / 100. - let (week_id, month_id) = period_ids(); - let cur = MilestoneState { - week_id, - week_floor: (primary.week.metrics.total_tokens / 100.0).floor() as i64, - month_id, - month_floor: (primary.month.metrics.total_tokens / 100.0).floor() as i64, - }; - - let mut g = state.state.lock().unwrap(); - let fire = milestone_fire(g.as_ref(), &cur); - // Keep the persisted floors monotonic within a period: a later observation - // with a lower total (a transient/partial read, or two observers racing) - // must not regress the stored floor and re-fire the celebration on restart. - let mut next = cur.clone(); - if let Some(prev) = g.as_ref() { - if prev.week_id == next.week_id && prev.week_floor > next.week_floor { - next.week_floor = prev.week_floor; - } - if prev.month_id == next.month_id && prev.month_floor > next.month_floor { - next.month_floor = prev.month_floor; - } - } - *g = Some(next.clone()); - // Persist while still holding the lock so two observers can't interleave and - // write a stale snapshot over a newer one. - save_milestones(&next); - drop(g); - if fire { - celebrate(app); - } -} - -/// Trigger the celebration overlay. Window/panel work must run on the main -/// thread (refresh() runs on a background thread), so hop there. -fn celebrate(app: &tauri::AppHandle) { - let handle = app.clone(); - let _ = app.run_on_main_thread(move || show_celebration(&handle)); -} - -/// Show (or reuse) a full-screen, click-through, non-activating overlay on the -/// primary monitor and run the confetti animation, then hide it after it plays. -/// Must be called on the main thread. -#[allow(deprecated)] // tauri_nspanel::cocoa (objc2 migration is upstream's) -fn show_celebration(app: &tauri::AppHandle) { - let Some(state) = app.try_state::() else { - return; - }; - // Skip if a celebration is already playing. - if state.active.swap(true, Ordering::SeqCst) { - return; - } - - let (pos, size) = match app.primary_monitor() { - Ok(Some(m)) => (*m.position(), *m.size()), - _ => { - state.active.store(false, Ordering::SeqCst); - return; - } - }; - - // Whether the confetti window was reused or freshly built — only used on - // macOS to decide whether to (re-)apply the NSPanel attributes. - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] - let existed = app.get_webview_window("confetti").is_some(); - let win = match app.get_webview_window("confetti") { - Some(w) => w, - None => { - match tauri::WebviewWindowBuilder::new( - app, - "confetti", - tauri::WebviewUrl::App("confetti.html".into()), - ) - .title("Tokenscope Celebration") - .inner_size(size.width as f64, size.height as f64) - .decorations(false) - .transparent(true) - .shadow(false) - .always_on_top(true) - .skip_taskbar(true) - .focused(false) - .resizable(false) - .visible(false) - .build() - { - Ok(w) => w, - Err(_) => { - state.active.store(false, Ordering::SeqCst); - return; - } - } - } - }; - - // Cover the whole primary monitor and let clicks pass through to the apps - // beneath — the celebration must never interrupt what the user is doing. - let _ = win.set_position(pos); - let _ = win.set_size(size); - let _ = win.set_ignore_cursor_events(true); - - #[cfg(target_os = "macos")] - { - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - #[allow(non_upper_case_globals)] - const NS_NONACTIVATING_PANEL: i32 = 1 << 7; - - // Convert to a non-activating panel once, so it can float over apps in - // native fullscreen without stealing focus (same approach as the main - // popover). On reuse the window is already a panel. - if !existed { - if let Ok(panel) = win.to_panel() { - panel.set_level(25); // NSMainMenuWindowLevel (24) + 1 - panel.set_style_mask(NS_NONACTIVATING_PANEL); - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary, - ); - } - } - let _ = win.eval("window.__burst&&window.__burst()"); - if let Ok(panel) = app.get_webview_panel("confetti") { - panel.show(); - } - } - #[cfg(not(target_os = "macos"))] - { - let _ = win.eval("window.__burst&&window.__burst()"); - let _ = win.show(); - } - - // Hide once the animation has played out (emission ~2.3s + fall/fade). - let app2 = app.clone(); - std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(4200)); - let app3 = app2.clone(); - let _ = app2.run_on_main_thread(move || { - #[cfg(target_os = "macos")] - if let Ok(panel) = app3.get_webview_panel("confetti") { - panel.order_out(None); - } - #[cfg(not(target_os = "macos"))] - if let Some(w) = app3.get_webview_window("confetti") { - let _ = w.hide(); - } - if let Some(st) = app3.try_state::() { - st.active.store(false, Ordering::SeqCst); - } - }); - }); -} - /// Last tray-icon rectangle (physical px: x, y, width, height), captured on tray /// click. Used to anchor the panel like tauri-plugin-positioner's /// TrayBottomCenter — but we can't use the positioner itself on a swizzled @@ -702,7 +462,6 @@ async fn get_dashboard(app: tauri::AppHandle) -> Dashboard { // instant it opens, while the tray otherwise only refreshes every 30s — so // without this the two could disagree for up to 30s during heavy usage. update_tray_label(&app, fmt_tokens_m(dash.today_tokens)); - check_milestones(&app, &dash); dash } @@ -796,14 +555,6 @@ pub fn run() { #[cfg(not(target_os = "macos"))] app.manage(DragGuard(AtomicI64::new(0))); - // 100M-token celebration tracking. Load the persisted snapshot so - // milestones survive restarts/reboots/updates; the first run ever - // (no file) baselines on first observation without celebrating. - app.manage(Celebration { - state: std::sync::Mutex::new(load_milestones()), - active: AtomicBool::new(false), - }); - // Reconcile launch-at-login with the user's saved preference. The // on/off toggle lives in the tray's right-click menu (built below); // we do NOT force-enable on every start, which would undo a manual @@ -1019,12 +770,15 @@ pub fn run() { // cold/stale cache) and refresh once a day. build_dashboard reads the // memoized copy, so neither JSON parsing nor the network ever runs // while BUILD_LOCK is held. - std::thread::spawn(|| { + let handle = app.handle().clone(); + std::thread::spawn(move || loop { pricing::Pricing::reload_shared(); - loop { - std::thread::sleep(Duration::from_secs(24 * 60 * 60)); - pricing::Pricing::reload_shared(); - } + // Rebuild immediately with the newly loaded prices. Otherwise + // an open panel can keep showing the startup built-in snapshot + // (and false "without pricing data" warnings) until the next + // unrelated 30-second refresh. + refresh(&handle); + std::thread::sleep(Duration::from_secs(24 * 60 * 60)); }); // Background refresh: keep the tray's token count current and push @@ -1084,64 +838,3 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } - -#[cfg(test)] -mod tests { - use super::*; - - fn ms(wk: &str, wf: i64, mo: &str, mf: i64) -> MilestoneState { - MilestoneState { - week_id: wk.into(), - week_floor: wf, - month_id: mo.into(), - month_floor: mf, - } - } - - #[test] - fn first_ever_observation_baselines_without_firing() { - // No prior snapshot → never celebrate pre-existing usage on first run. - assert!(!milestone_fire(None, &ms("2026-W24", 3, "2026-06", 3))); - } - - #[test] - fn no_change_does_not_fire() { - let prev = ms("2026-W24", 1, "2026-06", 3); - assert!(!milestone_fire(Some(&prev), &ms("2026-W24", 1, "2026-06", 3))); - } - - #[test] - fn month_crossing_fires() { - let prev = ms("2026-W24", 1, "2026-06", 3); - assert!(milestone_fire(Some(&prev), &ms("2026-W24", 1, "2026-06", 4))); - } - - #[test] - fn week_crossing_fires_even_when_month_flat() { - // Early in a month the week (straddling from the previous month) can lead. - let prev = ms("2026-W24", 0, "2026-06", 0); - assert!(milestone_fire(Some(&prev), &ms("2026-W24", 1, "2026-06", 0))); - } - - #[test] - fn multi_boundary_jump_is_a_single_fire() { - // 3 → 7 is still one celebration (fire is a bool, not a count). - let prev = ms("2026-W24", 1, "2026-06", 3); - assert!(milestone_fire(Some(&prev), &ms("2026-W24", 1, "2026-06", 7))); - } - - #[test] - fn new_month_rebaselines_silently() { - // Period id changed → that period reset; re-baseline, don't compare floors - // (so a new month opening below last month's floor never fires). - let prev = ms("2026-W24", 1, "2026-06", 3); - assert!(!milestone_fire(Some(&prev), &ms("2026-W27", 0, "2026-07", 0))); - } - - #[test] - fn new_week_does_not_fire_on_reset() { - let prev = ms("2026-W24", 2, "2026-06", 3); - // New week (id changed), month unchanged and flat → no fire. - assert!(!milestone_fire(Some(&prev), &ms("2026-W25", 0, "2026-06", 3))); - } -} diff --git a/src-tauri/src/pricing.rs b/src-tauri/src/pricing.rs index 8875620..b94ead4 100644 --- a/src-tauri/src/pricing.rs +++ b/src-tauri/src/pricing.rs @@ -109,7 +109,9 @@ fn fetch_cached(name: &str, url: &str, valid: impl Fn(&str) -> bool) -> Option bool) -> Option self.norm.get("gpt-5p3-codex"), + _ => None, + }) } /// Exact-or-normalized cost in USD. None = no pricing data for this model. @@ -296,3 +306,49 @@ impl Pricing { self.lookup(model).is_some() } } + +#[cfg(test)] +mod tests { + use super::Pricing; + + fn assert_cost(pricing: &Pricing, model: &str, expected: f64) { + let actual = pricing + .cost(model, 1_000_000.0, 1_000_000.0, 0.0, 1_000_000.0) + .unwrap(); + assert!((actual - expected).abs() < 1e-9, "{model}: {actual}"); + } + + #[test] + fn prices_recent_codex_models_from_litellm_raw() { + let mut pricing = Pricing { + exact: Default::default(), + norm: Default::default(), + }; + pricing.ingest_litellm( + r#"{ + "gpt-5.6-sol": { + "input_cost_per_token": 0.000005, + "cache_read_input_token_cost": 0.0000005, + "output_cost_per_token": 0.00003 + }, + "gpt-5.6-terra": { + "input_cost_per_token": 0.0000025, + "cache_read_input_token_cost": 0.00000025, + "output_cost_per_token": 0.000015 + }, + "gpt-5.3-codex": { + "input_cost_per_token": 0.00000175, + "cache_read_input_token_cost": 0.000000175, + "output_cost_per_token": 0.000014 + }, + "chatgpt/gpt-5.3-codex-spark": { + "litellm_provider": "chatgpt" + } + }"#, + ); + + assert_cost(&pricing, "gpt-5.6-sol", 35.5); + assert_cost(&pricing, "gpt-5.6-terra", 17.75); + assert_cost(&pricing, "gpt-5.3-codex-spark", 15.925); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index aac87e0..34ff828 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.2.0", + "version": "1.2.2", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", @@ -22,6 +22,7 @@ "shadow": false, "alwaysOnTop": true, "visible": false, + "focus": false, "skipTaskbar": true } ], From d5f6a0ca130c08a7448e4cdad27e65b266684fdd Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Sun, 12 Jul 2026 23:52:38 -0700 Subject: [PATCH 11/67] fix(ci): run release notes script with bash --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 898fa74..f3b6e33 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,7 @@ jobs: - name: Prepare release notes id: release_notes + shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | From 06bef993d79f7aa89899208374c729c4bd2c67cb Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 00:01:03 -0700 Subject: [PATCH 12/67] fix(ci): omit empty updater signing keys --- .github/workflows/release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3b6e33..6d7160c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,12 +92,10 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # In-app updater: sign the update packages (minisign, NOT Apple - # signing) and let tauri-action publish latest.json alongside the - # assets. Without these secrets the build still succeeds — it just - # ships without update signatures, and in-app update stays silent. - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # In-app updater signing is intentionally omitted until real + # TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) secrets are configured. + # Passing missing secrets as empty env vars makes tauri fail while + # decoding the key instead of producing an unsigned release. # --- Code signing + notarization (optional, macOS only) --- # UNSIGNED BUILD BY DEFAULT. Do NOT uncomment these until the matching # secrets actually exist: tauri's bundler treats an empty From 9303716cedfda1f786b14314a35dfd066d669ed2 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 00:10:39 -0700 Subject: [PATCH 13/67] fix(ci): build unsigned release artifacts --- .github/workflows/release.yml | 2 +- src-tauri/tauri.release.conf.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src-tauri/tauri.release.conf.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d7160c..326d128 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -122,7 +122,7 @@ jobs: # `brew install` to work. releaseDraft: false prerelease: false - args: --target ${{ matrix.target }} + args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json - name: Update Homebrew Cask if: matrix.os == 'macos-latest' diff --git a/src-tauri/tauri.release.conf.json b/src-tauri/tauri.release.conf.json new file mode 100644 index 0000000..55a9946 --- /dev/null +++ b/src-tauri/tauri.release.conf.json @@ -0,0 +1,5 @@ +{ + "bundle": { + "createUpdaterArtifacts": false + } +} From fbad451d2d1195ba327831215e5b62d6ec1ad72b Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 01:13:37 -0700 Subject: [PATCH 14/67] release: v1.3.0 --- .github/workflows/release.yml | 149 +++++++++----- README-zh.md | 19 +- README.md | 19 +- package.json | 6 +- scripts/version.mjs | 45 ++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 28 ++- src-tauri/src/model.rs | 16 ++ src-tauri/src/parser.rs | 377 ++++++++++++++++++++++++++++++---- src-tauri/src/store.rs | 21 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 228 ++++++++++++++++++-- src/data.ts | 23 ++- 14 files changed, 799 insertions(+), 138 deletions(-) create mode 100644 scripts/version.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 326d128..36abac7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,23 +1,98 @@ name: Release -# Push a version tag (e.g. v0.1.0) to build the app and publish a GitHub Release. +# A version bump on main creates its tag and publishes automatically. Pushing a +# matching v* tag remains supported for manual recovery/re-runs. on: push: + branches: + - main tags: - "v*" + paths: + - package.json + - src-tauri/Cargo.toml + - src-tauri/tauri.conf.json workflow_dispatch: + inputs: + tag: + description: Existing version tag to publish, for example v1.3.0 + required: true + type: string + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: + prepare: + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + publish: ${{ steps.version.outputs.publish }} + tag: ${{ steps.version.outputs.tag }} + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Validate version and prepare tag + id: version + shell: bash + run: | + set -euo pipefail + VERSION=$(node scripts/version.mjs) + TAG="v${VERSION}" + + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + if [ "${GITHUB_REF_NAME}" != "$TAG" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${VERSION}" + exit 1 + fi + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "${{ inputs.tag }}" != "$TAG" ]; then + echo "::error::Requested tag ${{ inputs.tag }} does not match app version ${VERSION}" + exit 1 + fi + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + echo "::error::Tag $TAG does not exist" + exit 1 + fi + else + BEFORE="${{ github.event.before }}" + PREVIOUS_PACKAGE=$(git show "${BEFORE}:package.json" 2>/dev/null || true) + PREVIOUS_VERSION="" + if [ -n "$PREVIOUS_PACKAGE" ]; then + PREVIOUS_VERSION=$(printf '%s' "$PREVIOUS_PACKAGE" | node -e \ + 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).version))') + fi + if [ "$VERSION" = "$PREVIOUS_VERSION" ]; then + echo "Version unchanged at $VERSION; nothing to publish." + echo "publish=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then + echo "::error::Tag $TAG already exists; choose a new version" + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "Tokenscope $TAG" + git push origin "$TAG" + fi + + echo "publish=true" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + release: - # Only build/publish for version tags. workflow_dispatch from a branch would - # otherwise set the version/tag to the branch name and publish a malformed - # release (and fail the Homebrew step on the resulting 404). - if: startsWith(github.ref, 'refs/tags/v') + needs: prepare + if: needs.prepare.outputs.publish == 'true' permissions: contents: write strategy: - # One platform's failure must not cancel the other — a broken Windows - # bundler shouldn't block the macOS .dmg from publishing (and vice versa). + # One platform's failure must not cancel the other. fail-fast: false matrix: include: @@ -28,6 +103,9 @@ jobs: target: x86_64-pc-windows-msvc rust-targets: x86_64-pc-windows-msvc runs-on: ${{ matrix.os }} + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} steps: - uses: actions/checkout@v6 @@ -53,7 +131,7 @@ jobs: workspaces: "./src-tauri -> target" - name: Install frontend deps - run: pnpm install + run: pnpm install --frozen-lockfile - name: Prepare release notes id: release_notes @@ -63,7 +141,7 @@ jobs: run: | gen=$(gh api --method POST \ "repos/${{ github.repository }}/releases/generate-notes" \ - -f tag_name="${{ github.ref_name }}" --jq '.body' || true) + -f tag_name="$RELEASE_TAG" --jq '.body' || true) { cat <<'BODY' @@ -88,38 +166,16 @@ jobs: echo "RELEASE_BODY" } >> "$GITHUB_OUTPUT" - - name: Build & publish release + - name: Build and publish release uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # In-app updater signing is intentionally omitted until real - # TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) secrets are configured. - # Passing missing secrets as empty env vars makes tauri fail while - # decoding the key instead of producing an unsigned release. - # --- Code signing + notarization (optional, macOS only) --- - # UNSIGNED BUILD BY DEFAULT. Do NOT uncomment these until the matching - # secrets actually exist: tauri's bundler treats an empty - # APPLE_CERTIFICATE env var as "a certificate is present" and then - # fails on `security import` ("SecKeychainItemImport ... not valid"). - # - # Once you have an Apple Developer ID, add these in repo - # Settings → Secrets and uncomment to get signed + notarized builds: - # - # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} # base64 of Developer ID .p12 - # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} # .p12 export password - # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} # "Developer ID Application: Name (TEAMID)" - # APPLE_ID: ${{ secrets.APPLE_ID }} # Apple ID email - # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} # app-specific password - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} # 10-char Team ID - # Windows builds are also unsigned — users get a SmartScreen warning - # on first launch, see README "Install on Windows". + # Add TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) plus platform signing + # secrets here when signed updater artifacts are enabled. with: - tagName: ${{ github.ref_name }} - releaseName: "Tokenscope ${{ github.ref_name }}" + tagName: ${{ env.RELEASE_TAG }} + releaseName: "Tokenscope ${{ env.RELEASE_TAG }}" releaseBody: ${{ steps.release_notes.outputs.body }} - # Publish immediately (not a draft): the asset's public download URL - # must be live for the Homebrew Cask step below to fetch it and for - # `brew install` to work. releaseDraft: false prerelease: false args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json @@ -129,19 +185,13 @@ jobs: env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | - # No tap token configured → skip quietly (the release itself is done; - # the cask can be bumped by hand — see the tap repo's README). if [ -z "$HOMEBREW_TAP_TOKEN" ]; then echo "HOMEBREW_TAP_TOKEN not set; skipping tap update." exit 0 fi - VERSION="${GITHUB_REF_NAME#v}" - DMG_NAME="Tokenscope_${VERSION}_universal.dmg" - DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/${DMG_NAME}" + DMG_NAME="Tokenscope_${RELEASE_VERSION}_universal.dmg" + DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${RELEASE_TAG}/${DMG_NAME}" - # Wait for the release asset to be available, then compute sha256. - # -f makes curl fail (non-zero) on a 404 so we keep retrying instead - # of hashing GitHub's error page and writing a bogus checksum. SHA256="" for i in $(seq 1 10); do if curl -fsSL "$DOWNLOAD_URL" -o /tmp/tokenscope.dmg; then @@ -152,14 +202,14 @@ jobs: sleep 10 done if [ -z "$SHA256" ]; then - echo "::error::Release asset $DMG_NAME never became available"; exit 1 + echo "::error::Release asset $DMG_NAME never became available" + exit 1 fi - # Clone the tap repo and update the cask git clone https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/SunChJ/homebrew-tokenscope.git /tmp/homebrew-tap cat > /tmp/homebrew-tap/Casks/tokenscope.rb <= :catalina" - app "Tokenscope.app" - # Unsigned/unnotarized build: strip the quarantine flag Homebrew - # adds so the app opens without the "Apple cannot verify" prompt. postflight do system_command "/usr/bin/xattr", args: ["-cr", "#{appdir}/Tokenscope.app"], @@ -185,5 +232,5 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Casks/tokenscope.rb - git diff --cached --quiet || git commit -m "Update cask to v${VERSION}" + git diff --cached --quiet || git commit -m "Update cask to ${RELEASE_TAG}" git push diff --git a/README-zh.md b/README-zh.md index c82dfc4..45eb9bf 100644 --- a/README-zh.md +++ b/README-zh.md @@ -11,7 +11,7 @@ ## 它做什么 - 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) -- 点击打开面板:Day / Week / Month 切换 +- 点击打开面板:Day / Week / Month 切换,以及按起止日期(含首尾日)的自定义区间筛选 - **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** @@ -144,6 +144,21 @@ pnpm tauri build # macOS 产出 .app / .dmg,Windows 产出 .exe (NSIS) 分发见 `PRD.md` §6.3(macOS 推荐 Homebrew Cask;`.dmg` / `.exe` 直接下载建议代码签名 + 公证)。 +## 发布 + +使用仓库内置脚本同步所有版本文件: + +```bash +pnpm version:set 1.3.1 +pnpm version:check +git commit -am "release: v1.3.1" +git push origin main +``` + +版本号变更推送到 `main` 后,GitHub Actions 会校验版本、创建 `vX.Y.Z` +标签,并自动构建和发布 macOS / Windows 安装包。发布失败时,也可以在 +Release workflow 中选择已有的同版本标签手工重跑。 + ## 结构 ``` @@ -153,7 +168,7 @@ src/ React 前端 App.tsx 主面板 src-tauri/src/ store.rs 多源 JSONL 增量摄取(Claude + Codex 适配器) - parser.rs 按 Scope 聚合(All / 单 Agent;Day/Week/Month + 热力图) + parser.rs 按 Scope 聚合(All / 单 Agent;预设周期 + 自定义区间 + 热力图) pricing.rs models.dev / LiteLLM 价格加载与计价 config.rs 用户 MCP / Skill 白名单(Claude + Codex) model.rs 返回给前端的数据结构 diff --git a/README.md b/README.md index f0eb8c0..df04ed2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). ## What it does - Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) -- Click to open the panel: Day / Week / Month toggle +- Click to open the panel: Day / Week / Month toggle plus an inclusive custom date-range filter - **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions - Three breakdowns: **by model** / **by MCP call** / **by Skill call** @@ -145,6 +145,21 @@ pnpm tauri build # outputs .app / .dmg on macOS, .exe (NSIS) on Windows to For distribution see `PRD.md` §6.3 (Homebrew Cask recommended on macOS; direct `.dmg` / `.exe` downloads benefit from code signing + notarization). +## Release + +Keep all app version files in sync with the bundled helper: + +```bash +pnpm version:set 1.3.1 +pnpm version:check +git commit -am "release: v1.3.1" +git push origin main +``` + +A version change pushed to `main` is validated by GitHub Actions, tagged as +`vX.Y.Z`, then built and published for macOS and Windows. A matching existing +tag can also be run manually from the Release workflow for recovery. + ## Structure ``` @@ -154,7 +169,7 @@ src/ React frontend App.tsx main panel src-tauri/src/ store.rs incremental multi-source JSONL ingest (Claude + Codex adapters) - parser.rs aggregation into scopes (All / per-agent; Day/Week/Month + heatmap) + parser.rs aggregation into scopes (All / per-agent; presets + custom ranges + heatmap) pricing.rs models.dev / LiteLLM price loading and costing config.rs user MCP / Skill whitelists (Claude + Codex) model.rs data structures returned to the frontend diff --git a/package.json b/package.json index 3866287..0034779 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "tokenscope", - "version": "1.2.2", + "version": "1.3.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "version:check": "node scripts/version.mjs", + "version:set": "node scripts/version.mjs" }, "dependencies": { "@tauri-apps/api": "^2.1.1", diff --git a/scripts/version.mjs b/scripts/version.mjs new file mode 100644 index 0000000..0c303cc --- /dev/null +++ b/scripts/version.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import fs from "node:fs"; + +const requested = process.argv[2]; +const semver = /^\d+\.\d+\.\d+$/; + +function readJson(path) { + return JSON.parse(fs.readFileSync(path, "utf8")); +} + +function replaceVersion(path, pattern, version) { + const source = fs.readFileSync(path, "utf8"); + if (!pattern.test(source)) throw new Error(`version field not found in ${path}`); + fs.writeFileSync(path, source.replace(pattern, (_match, before, after) => `${before}${version}${after}`)); +} + +if (requested) { + const version = requested.replace(/^v/, ""); + if (!semver.test(version)) throw new Error("version must use X.Y.Z"); + + replaceVersion("package.json", /^(\s*"version":\s*")[^"]+(".*)$/m, version); + replaceVersion("src-tauri/tauri.conf.json", /^(\s*"version":\s*")[^"]+(".*)$/m, version); + replaceVersion("src-tauri/Cargo.toml", /^(version = ")[^"]+(".*)$/m, version); + replaceVersion( + "src-tauri/Cargo.lock", + /(\[\[package\]\]\nname = "tokenscope"\nversion = ")[^"]+(".*)/, + version, + ); +} + +const versions = { + package: readJson("package.json").version, + tauri: readJson("src-tauri/tauri.conf.json").version, + cargo: fs.readFileSync("src-tauri/Cargo.toml", "utf8").match(/^version = "([^"]+)"/m)?.[1], + lock: fs.readFileSync("src-tauri/Cargo.lock", "utf8") + .match(/\[\[package\]\]\nname = "tokenscope"\nversion = "([^"]+)"/)?.[1], +}; +const unique = new Set(Object.values(versions)); +if (unique.size !== 1 || unique.has(undefined)) { + throw new Error(`version mismatch: ${JSON.stringify(versions)}`); +} +if (!semver.test(versions.package)) throw new Error("version must use X.Y.Z"); + +process.stdout.write(versions.package); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7c698ad..d7ed550 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4224,7 +4224,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.2.2" +version = "1.3.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3a2da7b..d5c3666 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.2.2" +version = "1.3.0" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c2ce81..ba74d2f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,7 +4,7 @@ mod parser; mod pricing; mod store; -use model::Dashboard; +use model::{Dashboard, RangeDashboard}; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -465,6 +465,25 @@ async fn get_dashboard(app: tauri::AppHandle) -> Dashboard { dash } +#[tauri::command] +async fn get_range_dashboard( + start_date: String, + end_date: String, +) -> Result { + let start = chrono::NaiveDate::parse_from_str(&start_date, "%Y-%m-%d") + .map_err(|_| "invalid start date".to_string())?; + let end = chrono::NaiveDate::parse_from_str(&end_date, "%Y-%m-%d") + .map_err(|_| "invalid end date".to_string())?; + match tauri::async_runtime::spawn_blocking(move || { + parser::build_range_dashboard(start, end) + }) + .await + { + Ok(result) => result, + Err(error) => Err(format!("failed to build date range: {error}")), + } +} + /// Save a full-panel screenshot (a `data:image/png;base64,...` URL captured in /// the webview) to the user's Desktop as `Tokenscope at
); @@ -422,6 +422,113 @@ function ScreenshotButton({ theme, busy, onClick }: { theme: Theme; busy: boolea ); } +function localIso(date: Date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function formatRange(range: DateRange, year = true) { + const options: Intl.DateTimeFormatOptions = { + month: "short", + day: "numeric", + ...(year ? { year: "numeric" } : {}), + }; + const format = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString("en-US", options); + return range.startDate === range.endDate + ? format(range.startDate) + : `${format(range.startDate)} – ${format(range.endDate)}`; +} + +function tokenAmount(valueM: number, targetM: number) { + if (targetM >= 1) return { value: valueM.toFixed(2), unit: "M" }; + if (targetM >= 0.001) { + const valueK = valueM * 1000; + return { value: valueK.toFixed(targetM < 0.01 ? 1 : 0), unit: "K" }; + } + return { value: Math.round(valueM * 1_000_000).toLocaleString("en-US"), unit: "tokens" }; +} + +function RangeFilter({ theme, dark, open, active, draft, max, busy, error, onToggle, onDraft, onApply, onClear }: + { theme: Theme; dark: boolean; open: boolean; active: boolean; draft: DateRange; max: string; busy: boolean; + error: string; onToggle: () => void; onDraft: (range: DateRange) => void; onApply: () => void; onClear: () => void }) { + const t = theme; + const root = useRef(null); + useEffect(() => { + if (!open) return; + const close = (event: MouseEvent) => { + if (!root.current?.contains(event.target as Node)) onToggle(); + }; + const escape = (event: KeyboardEvent) => { + if (event.key === "Escape") onToggle(); + }; + document.addEventListener("mousedown", close); + document.addEventListener("keydown", escape); + return () => { + document.removeEventListener("mousedown", close); + document.removeEventListener("keydown", escape); + }; + }, [open, onToggle]); + + const inputStyle: React.CSSProperties = { + width: 142, height: 30, borderRadius: 7, outline: "none", + border: `1px solid ${t.segBorder}`, background: t.segBg, color: t.text, + padding: "3px 7px", font: `500 10.5px ${t.mono}`, colorScheme: dark ? "dark" : "light", + }; + return ( +
+ + {open && ( +
{ event.preventDefault(); onApply(); }} style={{ + position: "absolute", top: 34, right: -68, zIndex: 40, width: 342, + padding: 11, borderRadius: 10, background: t.card, + border: `1px solid ${t.segBorder}`, boxShadow: "0 12px 32px rgba(0,0,0,0.28)", + }}> +
+ onDraft({ ...draft, startDate: event.target.value })} + style={inputStyle} /> + to + onDraft({ ...draft, endDate: event.target.value })} + style={inputStyle} /> +
+
+ + {error || "Dates are inclusive"} + + {active && } + +
+
+ )} +
+ ); +} + function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; openGen: number; active: boolean }) { // Agent scope filter. scopes[0] is always the aggregate; a stale selection // (e.g. a source disappeared after a rescan) falls back to it. @@ -439,7 +546,75 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const canDrag = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window && !navigator.userAgent.includes("Macintosh"); const dragRef = useRef<{ x: number; y: number } | null>(null); const [period, setPeriod] = useState<"Day" | "Week" | "Month">("Week"); - const P: PeriodReport = period === "Day" ? scope.day : period === "Month" ? scope.month : scope.week; + const today = localIso(new Date()); + const [rangeOpen, setRangeOpen] = useState(false); + const [draftRange, setDraftRange] = useState(() => { + const now = new Date(); + return { startDate: localIso(new Date(now.getFullYear(), now.getMonth(), 1)), endDate: localIso(now) }; + }); + const [activeRange, setActiveRange] = useState(null); + const [rangeDash, setRangeDash] = useState(null); + const [rangeBusy, setRangeBusy] = useState(false); + const [rangeError, setRangeError] = useState(""); + const rangeRequest = useRef(0); + const rangeBaseGeneration = useRef(""); + + const clearDateRange = () => { + rangeRequest.current += 1; + setActiveRange(null); + setRangeDash(null); + setRangeBusy(false); + setRangeError(""); + setRangeOpen(false); + }; + const selectPeriod = (value: string) => { + clearDateRange(); + setPeriod(value as "Day" | "Week" | "Month"); + }; + const applyDateRange = async () => { + if (!draftRange.startDate || !draftRange.endDate || draftRange.startDate > draftRange.endDate) { + setRangeError("Choose a valid date range"); + return; + } + const request = ++rangeRequest.current; + setRangeBusy(true); + setRangeError(""); + try { + const result = await fetchRangeDashboard(draftRange); + if (request !== rangeRequest.current) return; + setRangeDash(result); + setActiveRange({ ...draftRange }); + rangeBaseGeneration.current = dash.generatedAt; + setRangeOpen(false); + } catch (error) { + if (request === rangeRequest.current) { + setRangeError(error instanceof Error ? error.message : String(error)); + } + } finally { + if (request === rangeRequest.current) setRangeBusy(false); + } + }; + + // A live dashboard push means today's selected range may have changed. Keep + // an active custom range fresh without replacing the last good report if a + // transient refresh fails. + useEffect(() => { + if (!active || !activeRange || rangeBusy || rangeBaseGeneration.current === dash.generatedAt) return; + rangeBaseGeneration.current = dash.generatedAt; + const request = ++rangeRequest.current; + fetchRangeDashboard(activeRange) + .then((result) => { + if (request === rangeRequest.current) setRangeDash(result); + }) + .catch(() => {}); + }, [active, activeRange?.startDate, activeRange?.endDate, dash.generatedAt, rangeBusy]); + + const presetReport: PeriodReport = period === "Day" ? scope.day : period === "Month" ? scope.month : scope.week; + const rangeReport = activeRange + ? rangeDash?.scopes.find((item) => item.id === scope.id)?.report + : undefined; + const P: PeriodReport = rangeReport ?? presetReport; + const viewKey = activeRange ? `${activeRange.startDate}:${activeRange.endDate}` : period; const M = P.metrics; const models = P.models; const [totalModel, setTotalModel] = useState(""); @@ -462,7 +637,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const slices = P.agents; // animated Total tokens: counts up from 0 on each open / period / scope // / model switch; held at 0 while hidden so it never flashes. - const animTotal = useCountUp(totalTokens, `${period}:${scope.id}:${totalModel}:${openGen}`, active); + const animTotal = useCountUp(totalTokens, `${viewKey}:${scope.id}:${totalModel}:${openGen}`, active); + const heroTotal = tokenAmount(animTotal, totalTokens); // Explicit percentages avoid WebKit's incorrect flexGrow + flexBasis:0 sizing. const splitTot = M.inputTokens + M.cacheTokens + M.outputTokens; const cachePct = splitTot > 0 ? (M.cacheTokens / splitTot) * 100 : 0; @@ -481,7 +657,9 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash const maxM = Math.max(...tokenModels.map((m) => m.tokens), 1e-9); // Per-row shares that sum to exactly 100.0% (largest-remainder over visible rows). const tokenShares = sharePcts(tokenModels.map((m) => m.tokens)); - const trendSub = { Day: "today 24h", Week: "this week", Month: "this month" }[period]; + const trendSub = activeRange + ? formatRange(activeRange, false) + : { Day: "today 24h", Week: "this week", Month: "this month" }[period]; // screenshot capture: rasterize the full panel card to a PNG and hand it to // the Rust `save_screenshot` command (browser preview falls back to a download). @@ -574,7 +752,12 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash Tokenscope
- setPeriod(v as any)} /> + + { setRangeOpen((value) => !value); setRangeError(""); }} + onDraft={(range) => { setDraftRange(range); setRangeError(""); }} + onApply={applyDateRange} onClear={clearDateRange} />
@@ -583,6 +766,23 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
{/* in-app update prompt */} + {activeRange && ( +
+ + + + {formatRange(activeRange)} + +
+ )} {/* agent filter — only when several sources have data */} {scopes.length > 1 && ( @@ -610,13 +810,13 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash
- {animTotal.toFixed(2)}M + {heroTotal.value}{heroTotal.unit} {!selectedModel && Math.round(M.deltaTokens) !== 0 && }
Est. cost
-
${totalCost.toFixed(2)}
+
{fmtMoney(totalCost)}
{/* All scope: one segment per agent. Single scope: cached vs new. */} @@ -659,7 +859,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash - +
@@ -680,7 +880,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {fmtInt(M.mcpCalls)} · {M.servers} servers {P.mcp.length > 0 - ? + ? :
No MCP calls in this period
} )} @@ -693,7 +893,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash {fmtInt(M.skillCalls)} · {M.skills} skills {P.skills.length > 0 - ? + ? :
No skill calls in this period
} )} diff --git a/src/data.ts b/src/data.ts index 50bf01c..b2d4a9a 100644 --- a/src/data.ts +++ b/src/data.ts @@ -35,6 +35,9 @@ export interface Scope { export interface Dashboard { scopes: Scope[]; todayTokens: number; generatedAt: string; } +export interface DateRange { startDate: string; endDate: string } +export interface RangeScope { id: string; report: PeriodReport } +export interface RangeDashboard extends DateRange { scopes: RangeScope[] } // Per-agent accent pairs [accent, accentSoft], tuned per theme so light mode // keeps enough contrast. Scope.color (backend) is the chart base; these drive @@ -62,20 +65,30 @@ export async function fetchDashboard(): Promise { return res.json(); } +export async function fetchRangeDashboard(range: DateRange): Promise { + const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; + if (!inTauri) throw new Error("custom date ranges require the desktop app"); + return invoke("get_range_dashboard", { + startDate: range.startDate, + endDate: range.endDate, + }); +} + // ── formatting helpers ────────────────────────────────────────── export const fmtTokens = (m: number) => { if (m >= 1) return m.toFixed(2) + "M"; - const k = m * 1000; - // one decimal for sub-1K totals (e.g. "0.4K"), but only when it rounds to a - // non-zero label — avoid a misleadingly precise "0.0K" for tiny values. - if (k >= 0.05 && k < 1) return k.toFixed(1) + "K"; - return Math.round(k) + "K"; + const tokens = m * 1_000_000; + if (tokens < 1000) return Math.round(tokens).toLocaleString("en-US"); + const k = tokens / 1000; + const digits = k < 100 ? 1 : 0; + return k.toFixed(digits).replace(/\.0$/, "") + "K"; }; export const fmtInt = (n: number) => n.toLocaleString("en-US"); export const pct = (part: number, whole: number) => (whole > 0 ? Math.round((part / whole) * 100) : 0); export function fmtMoney(v: number) { if (v >= 100000) return "$" + Math.round(v / 1000) + "K"; if (v >= 10000) return "$" + (v / 1000).toFixed(1) + "K"; + if (v > 0 && v < 0.01) return "$" + v.toFixed(4); return "$" + v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } From 9a19ab3ccddfa5891bb7e37c225d11e935a3b5db Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 01:38:57 -0700 Subject: [PATCH 15/67] release: v1.3.1 --- .github/workflows/release.yml | 14 ++++++++++++-- README-zh.md | 29 +++++++++++++++++++++-------- README.md | 31 ++++++++++++++++++++++--------- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src-tauri/tauri.release.conf.json | 2 +- src/App.tsx | 26 +++++++++++++++++++------- 9 files changed, 79 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36abac7..c11cf3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,6 +106,8 @@ jobs: env: RELEASE_TAG: ${{ needs.prepare.outputs.tag }} RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} steps: - uses: actions/checkout@v6 @@ -133,6 +135,14 @@ jobs: - name: Install frontend deps run: pnpm install --frozen-lockfile + - name: Validate updater signing + shell: bash + run: | + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for updater artifacts" + exit 1 + fi + - name: Prepare release notes id: release_notes shell: bash @@ -170,14 +180,14 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Add TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) plus platform signing - # secrets here when signed updater artifacts are enabled. with: tagName: ${{ env.RELEASE_TAG }} releaseName: "Tokenscope ${{ env.RELEASE_TAG }}" releaseBody: ${{ steps.release_notes.outputs.body }} releaseDraft: false prerelease: false + uploadUpdaterJson: true + updaterJsonPreferNsis: true args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json - name: Update Homebrew Cask diff --git a/README-zh.md b/README-zh.md index 45eb9bf..edd1ea0 100644 --- a/README-zh.md +++ b/README-zh.md @@ -8,6 +8,14 @@ ![Tokenscope 面板(深色 / 浅色)](docs/screenshot.png) +## macOS 快速安装 + +Homebrew 是推荐的安装和升级方式: + +```bash +brew install --cask sunchj/tokenscope/tokenscope +``` + ## 它做什么 - 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) @@ -72,7 +80,10 @@ cost = input × price.input ## 安装 -### 方式一:Homebrew(推荐) +### macOS:Homebrew(推荐) + +Homebrew 是 macOS 的主要安装方式:一条命令完成安装,自动处理 +Gatekeeper 隔离属性,后续升级也更简单可控。 ```bash brew install --cask sunchj/tokenscope/tokenscope @@ -88,7 +99,7 @@ brew install --cask sunchj/tokenscope/tokenscope brew update && brew upgrade --cask tokenscope ``` -### 方式二:下载 .dmg +### macOS:下载 .dmg(备用) 1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_universal.dmg`(同时支持 Apple Silicon 与 Intel) 2. 拖入「应用程序」 @@ -101,7 +112,7 @@ brew update && brew upgrade --cask tokenscope > 未签名是当前的已知限制。要彻底「双击直开」需 Apple Developer ID 签名 + 公证,见 `PRD.md` §6.4。 -### 方式三:Windows 安装 +### Windows 1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` 2. 双击安装。因为是**未签名**构建,首次运行会被 SmartScreen 拦截 —— 点 **"更多信息" → "仍要运行"** 即可 @@ -110,9 +121,9 @@ brew update && brew upgrade --cask tokenscope ### 更新 -自 v1.2.0 起,App 会自行检测新版本并在面板内提示(「v*x.y.z* is available → Update」), -点击即可下载安装、重启生效,无需手动下载。Homebrew 用户也可以继续 -`brew upgrade --cask tokenscope`,两条路径安装的是同一份构建。 +App 会在启动时及此后每小时检测新版本,并在面板内提示 +(「v*x.y.z* is available → Update」)。Homebrew 用户建议继续使用 +`brew upgrade --cask tokenscope`;直接下载安装的用户可使用 App 内更新。 ### 首次启动后 @@ -156,8 +167,10 @@ git push origin main ``` 版本号变更推送到 `main` 后,GitHub Actions 会校验版本、创建 `vX.Y.Z` -标签,并自动构建和发布 macOS / Windows 安装包。发布失败时,也可以在 -Release workflow 中选择已有的同版本标签手工重跑。 +标签,并自动构建和发布 macOS / Windows 安装包、签名更新包和 `latest.json`。 +发布失败时,也可以在 Release workflow 中选择已有的同版本标签手工重跑。 +发布流程需要配置 `TAURI_SIGNING_PRIVATE_KEY` 和 +`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets。 ## 结构 diff --git a/README.md b/README.md index df04ed2..a5ac835 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). ![Tokenscope panel (dark / light)](docs/screenshot.png) +## Quick install on macOS + +Homebrew is the recommended installation and upgrade path: + +```bash +brew install --cask sunchj/tokenscope/tokenscope +``` + ## What it does - Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) @@ -72,7 +80,11 @@ So a cache hit is **not** billed as normal input — it uses the dedicated (chea ## Install -### Option 1: Homebrew (recommended) +### macOS: Homebrew (recommended) + +Homebrew is the primary installation method for macOS. It provides one-command +installation, handles Gatekeeper quarantine automatically, and keeps upgrades +predictable. ```bash brew install --cask sunchj/tokenscope/tokenscope @@ -88,7 +100,7 @@ Upgrade: brew update && brew upgrade --cask tokenscope ``` -### Option 2: Download the .dmg +### macOS: Download the .dmg (fallback) 1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) (works on both Apple Silicon and Intel) 2. Drag it into Applications @@ -101,7 +113,7 @@ brew update && brew upgrade --cask tokenscope > Unsigned is a current known limitation. A true "double-click to open" experience requires Apple Developer ID signing + notarization — see `PRD.md` §6.4. -### Option 3: Install on Windows +### Windows 1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 2. Double-click to install. Because the build is **unsigned**, Windows SmartScreen will warn on first run — click **More info → Run anyway** @@ -110,10 +122,9 @@ brew update && brew upgrade --cask tokenscope ### Updating -From v1.2.0 the app checks for new releases itself and shows an in-app banner -("v*x.y.z* is available → Update"); the update downloads, installs, and applies -on restart — no manual download needed. Homebrew users can keep using -`brew upgrade --cask tokenscope` as well; both paths install the same build. +The app checks for new releases on launch and every hour, then shows an in-app +banner ("v*x.y.z* is available → Update"). Homebrew users should keep using +`brew upgrade --cask tokenscope`; direct-download users can update in the app. ### After first launch @@ -157,8 +168,10 @@ git push origin main ``` A version change pushed to `main` is validated by GitHub Actions, tagged as -`vX.Y.Z`, then built and published for macOS and Windows. A matching existing -tag can also be run manually from the Release workflow for recovery. +`vX.Y.Z`, then built and published for macOS and Windows together with signed +updater artifacts and `latest.json`. A matching existing tag can also be run +manually from the Release workflow for recovery. The release workflow requires +`TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets. ## Structure diff --git a/package.json b/package.json index 0034779..5e30ba4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.0", + "version": "1.3.1", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d7ed550..e9022ba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4224,7 +4224,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.0" +version = "1.3.1" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d5c3666..e003955 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.0" +version = "1.3.1" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a5eb7df..21f841c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.0", + "version": "1.3.1", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src-tauri/tauri.release.conf.json b/src-tauri/tauri.release.conf.json index 55a9946..d57cd00 100644 --- a/src-tauri/tauri.release.conf.json +++ b/src-tauri/tauri.release.conf.json @@ -1,5 +1,5 @@ { "bundle": { - "createUpdaterArtifacts": false + "createUpdaterArtifacts": true } } diff --git a/src/App.tsx b/src/App.tsx index d7147c2..cc52c1a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -135,7 +135,7 @@ function SplitLegend({ t, cacheM, restM, cachedPct }: // ── In-app updates ────────────────────────────────────────────── // Poll the GitHub release feed (plugin-updater endpoint) on launch and every -// 6h; surface a slim banner when a newer signed build exists. Download + +// hour; surface a slim banner when a newer signed build exists. Download + // install happen in-app, then a relaunch finishes the update. A dismissed // version stays hidden until the *next* version appears (localStorage). type UpdateState = @@ -152,7 +152,10 @@ function useUpdater(): [UpdateState, () => void, () => void] { const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; if (!inTauri) return; let dead = false; + let checking = false; const probe = async () => { + if (checking) return; + checking = true; try { const u = await checkUpdate(); if (dead || !u) return; @@ -160,11 +163,14 @@ function useUpdater(): [UpdateState, () => void, () => void] { updRef.current = u; setSt({ phase: "available", update: u }); } catch { - // offline / rate-limited / no latest.json yet — stay quiet, retry later + // Offline / rate-limited / no latest.json: fail quietly. There is no + // immediate retry; the next regular hourly check remains scheduled. + } finally { + checking = false; } }; - probe(); - const t = window.setInterval(probe, 6 * 60 * 60 * 1000); + void probe(); + const t = window.setInterval(() => { void probe(); }, 60 * 60 * 1000); return () => { dead = true; window.clearInterval(t); }; }, []); const install = async () => { @@ -193,6 +199,8 @@ function useUpdater(): [UpdateState, () => void, () => void] { return [st, install, dismiss]; } +type UpdaterController = ReturnType; + function releaseNotesSummary(body?: string) { if (!body) return null; const lines = body @@ -529,7 +537,9 @@ function RangeFilter({ theme, dark, open, active, draft, max, busy, error, onTog ); } -function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; openGen: number; active: boolean }) { +function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater }: + { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; + openGen: number; active: boolean; updater: UpdaterController }) { // Agent scope filter. scopes[0] is always the aggregate; a stale selection // (e.g. a source disappeared after a rescan) falls back to it. const scopes = dash.scopes; @@ -538,7 +548,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active }: { dash // Filtering to one agent re-tints the whole panel with its accent. const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); // In-app update lifecycle (idle → available → downloading → ready). - const [updSt, updInstall, updDismiss] = useUpdater(); + const [updSt, updInstall, updDismiss] = updater; // Drag the popover by its body (Windows/Linux only — macOS uses the menu-bar // NSPanel and is gated out). A real OS window-drag begins only once the // pointer moves past a small threshold, so a plain click still clicks through @@ -928,6 +938,7 @@ export default function App() { const [err, setErr] = useState(null); const [openGen, setOpenGen] = useState(0); const [focused, setFocused] = useState(true); // browser preview: always "focused" + const updater = useUpdater(); // Theme preference: explicit Dark / Light, or System (follows the OS // appearance live on both macOS and Windows via prefers-color-scheme). First // run defaults to System. @@ -1051,5 +1062,6 @@ export default function App() { if (!dash.scopes.length) { return
Failed to load: dashboard returned no scopes
; } - return ; + return ; } From 7603a1668994ad53eb7d346d6704f14d41dc9092 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 02:16:51 -0700 Subject: [PATCH 16/67] release: v1.3.2 --- README-zh.md | 15 +-- README.md | 16 +-- package.json | 2 +- src-tauri/Cargo.lock | 69 ++++++++++++- src-tauri/Cargo.toml | 3 +- src-tauri/src/lib.rs | 195 +++++++++++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 203 +++++++++++++++++++------------------- 8 files changed, 381 insertions(+), 124 deletions(-) diff --git a/README-zh.md b/README-zh.md index edd1ea0..c6d7466 100644 --- a/README-zh.md +++ b/README-zh.md @@ -121,15 +121,18 @@ brew update && brew upgrade --cask tokenscope ### 更新 -App 会在启动时及此后每小时检测新版本,并在面板内提示 -(「v*x.y.z* is available → Update」)。Homebrew 用户建议继续使用 -`brew upgrade --cask tokenscope`;直接下载安装的用户可使用 App 内更新。 +App 会在启动时及此后每小时检测新版本。当前版本和更新操作常驻于左上角 +**Tokenscope** 下方。Homebrew 用户建议继续使用 `brew upgrade --cask tokenscope`; +直接下载安装的用户可在 App 内完成更新和重启。右键托盘菜单中的 +**Check for Updates…** 可立即手动检查。 ### 首次启动后 - **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) - **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) -- 左键点击图标开/关面板,右键出菜单(Open / Refresh / Quit) +- 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) +- **Show Weekly Remaining** 可在菜单栏 Token 数后追加 Codex 每周剩余配额(如 `12.40M-81%`);默认关闭,选择会被记住 +- **Dashboard Shortcut** 可开启全局快捷键来唤起面板(macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);默认关闭,选择会被记住 - 已自动设置**登录自启**,无需手动配置 ## 开发 @@ -160,9 +163,9 @@ pnpm tauri build # macOS 产出 .app / .dmg,Windows 产出 .exe (NSIS) 使用仓库内置脚本同步所有版本文件: ```bash -pnpm version:set 1.3.1 +pnpm version:set 1.3.2 pnpm version:check -git commit -am "release: v1.3.1" +git commit -am "release: v1.3.2" git push origin main ``` diff --git a/README.md b/README.md index a5ac835..cecbe98 100644 --- a/README.md +++ b/README.md @@ -122,15 +122,19 @@ brew update && brew upgrade --cask tokenscope ### Updating -The app checks for new releases on launch and every hour, then shows an in-app -banner ("v*x.y.z* is available → Update"). Homebrew users should keep using -`brew upgrade --cask tokenscope`; direct-download users can update in the app. +The app checks for new releases on launch and every hour. Its version and update +actions appear directly below **Tokenscope** in the top-left corner. Homebrew +users should keep using `brew upgrade --cask tokenscope`; direct-download users +can update and restart from the app. Use **Check for Updates…** in the tray's +right-click menu to check immediately. ### After first launch - **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) - **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) -- Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Quit) +- Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) +- **Show Weekly Remaining** optionally appends Codex's remaining weekly quota to the menu-bar label (for example `12.40M-81%`); it is off by default and remembers your choice +- **Dashboard Shortcut** enables a global hotkey for opening the panel (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux); it is off by default and remembers your choice - **Launch-at-login is set up automatically** — no manual configuration needed ## Develop @@ -161,9 +165,9 @@ For distribution see `PRD.md` §6.3 (Homebrew Cask recommended on macOS; direct Keep all app version files in sync with the bundled helper: ```bash -pnpm version:set 1.3.1 +pnpm version:set 1.3.2 pnpm version:check -git commit -am "release: v1.3.1" +git commit -am "release: v1.3.2" git push origin main ``` diff --git a/package.json b/package.json index 5e30ba4..be8d3a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.1", + "version": "1.3.2", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e9022ba..ef586fc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1359,6 +1359,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1480,6 +1490,24 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "global-hotkey" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -3930,6 +3958,21 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-positioner" version = "2.3.2" @@ -4224,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.1" +version = "1.3.2" dependencies = [ "base64 0.22.1", "chrono", @@ -4236,6 +4279,7 @@ dependencies = [ "tauri-build", "tauri-nspanel", "tauri-plugin-autostart", + "tauri-plugin-global-shortcut", "tauri-plugin-positioner", "tauri-plugin-process", "tauri-plugin-single-instance", @@ -5674,6 +5718,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "xattr" version = "1.6.1" @@ -5684,6 +5745,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yoke" version = "0.8.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e003955..b4d7fec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.1" +version = "1.3.2" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" @@ -35,6 +35,7 @@ notify = "6" tauri-plugin-single-instance = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" +tauri-plugin-global-shortcut = "2" [target.'cfg(target_os = "macos")'.dependencies] tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ba74d2f..0f7fcff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,7 @@ use tauri::{ Emitter, Manager, }; use tauri_plugin_autostart::ManagerExt; +use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState}; // Positioner is only used for the non-macOS fallback; macOS positions the // NSPanel manually (see position_panel). #[cfg(not(target_os = "macos"))] @@ -34,7 +35,55 @@ fn now_ms() -> i64 { .unwrap_or(0) } -fn update_tray_label(app: &tauri::AppHandle, label: String) { +const DASHBOARD_SHORTCUT: &str = "CommandOrControl+Alt+T"; + +#[derive(Clone, Default, serde::Deserialize, serde::Serialize)] +#[serde(default)] +struct TrayPreferences { + show_weekly_remaining: bool, + dashboard_shortcut: bool, +} + +struct TrayPreferencesState(std::sync::Mutex); + +fn weekly_remaining_pct(dash: &Dashboard) -> Option { + let quota = dash.scopes.iter().find_map(|scope| scope.quota.as_ref())?; + weekly_remaining_from_quota(quota) +} + +fn weekly_remaining_from_quota(quota: &model::Quota) -> Option { + let used = if quota.primary_minutes == 7 * 24 * 60 { + quota.primary_pct + } else if quota.secondary_minutes == 7 * 24 * 60 { + quota.secondary_pct + } else { + return None; + }; + Some((100.0 - used).clamp(0.0, 100.0).round() as u8) +} + +fn tray_label(dash: &Dashboard, show_weekly_remaining: bool) -> String { + let mut label = fmt_tokens_m(dash.today_tokens); + if show_weekly_remaining { + if let Some(remaining) = weekly_remaining_pct(dash) { + label.push_str(&format!("-{remaining}%")); + } + } + label +} + +fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { + let show_weekly_remaining = app + .try_state::() + .map(|state| { + state + .0 + .lock() + .map(|prefs| prefs.show_weekly_remaining) + .unwrap_or(false) + }) + .unwrap_or(false); + let label = tray_label(dash, show_weekly_remaining); let handle = app.clone(); let _ = app.run_on_main_thread(move || { if let Some(tray) = handle.tray_by_id("main") { @@ -52,7 +101,7 @@ fn update_tray_label(app: &tauri::AppHandle, label: String) { /// the fresh data to the UI so an open popover updates live. fn refresh(app: &tauri::AppHandle) { let dash = parser::build_dashboard(); - update_tray_label(app, fmt_tokens_m(dash.today_tokens)); + update_tray_label(app, &dash); let _ = app.emit("dashboard-updated", &dash); } @@ -80,6 +129,30 @@ fn save_autostart_pref(on: bool) { } } +fn tray_preferences_path() -> Option { + let dir = dirs::data_dir()?.join("tokenscope"); + let _ = std::fs::create_dir_all(&dir); + Some(dir.join("tray.json")) +} + +fn load_tray_preferences() -> TrayPreferences { + let Some(path) = tray_preferences_path() else { + return TrayPreferences::default(); + }; + std::fs::read_to_string(path) + .ok() + .and_then(|text| serde_json::from_str(&text).ok()) + .unwrap_or_default() +} + +fn save_tray_preferences(preferences: &TrayPreferences) { + if let Some(path) = tray_preferences_path() { + if let Ok(text) = serde_json::to_string(preferences) { + let _ = std::fs::write(path, text); + } + } +} + /// Bring the OS launch-at-login registration in line with the saved preference, /// returning the effective preference (used to seed the menu checkbox). First /// run (no saved pref) defaults to on and records it; thereafter we honor the @@ -461,7 +534,7 @@ async fn get_dashboard(app: tauri::AppHandle) -> Dashboard { // Sync the tray count to this freshly-fetched value. The panel refetches the // instant it opens, while the tray otherwise only refreshes every 30s — so // without this the two could disagree for up to 30s during heavy usage. - update_tray_label(&app, fmt_tokens_m(dash.today_tokens)); + update_tray_label(&app, &dash); dash } @@ -543,6 +616,15 @@ pub fn run() { show_popover(app); })) .plugin(tauri_plugin_positioner::init()) + .plugin( + tauri_plugin_global_shortcut::Builder::new() + .with_handler(|app, _shortcut, event| { + if event.state == ShortcutState::Pressed { + show_popover(app); + } + }) + .build(), + ) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, None, @@ -584,6 +666,21 @@ pub fn run() { // we do NOT force-enable on every start, which would undo a manual // opt-out. `autostart_on` seeds the menu checkbox. let autostart_on = reconcile_autostart(app.handle()); + let mut tray_preferences = load_tray_preferences(); + if tray_preferences.dashboard_shortcut + && app + .global_shortcut() + .register(DASHBOARD_SHORTCUT) + .is_err() + { + tray_preferences.dashboard_shortcut = false; + save_tray_preferences(&tray_preferences); + } + let weekly_remaining_on = tray_preferences.show_weekly_remaining; + let dashboard_shortcut_on = tray_preferences.dashboard_shortcut; + app.manage(TrayPreferencesState(std::sync::Mutex::new( + tray_preferences, + ))); // Popover behaviour. On macOS, convert the window to a non-activating // NSPanel so it can float over apps in native fullscreen, and hide it @@ -685,10 +782,38 @@ pub fn run() { // Build the menu-bar tray: app glyph (template icon) + today's tokens. let dash = parser::build_dashboard(); - let label = fmt_tokens_m(dash.today_tokens); + let label = tray_label(&dash, weekly_remaining_on); let open_i = MenuItem::with_id(app, "open", "Open Tokenscope", true, None::<&str>)?; let refresh_i = MenuItem::with_id(app, "refresh", "Refresh", true, None::<&str>)?; + let check_updates_i = MenuItem::with_id( + app, + "check-updates", + "Check for Updates…", + true, + None::<&str>, + )?; + let weekly_remaining_i = CheckMenuItem::with_id( + app, + "weekly-remaining", + "Show Weekly Remaining", + true, + weekly_remaining_on, + None::<&str>, + )?; + let shortcut_label = if cfg!(target_os = "macos") { + "Dashboard Shortcut (⌥⌘T)" + } else { + "Dashboard Shortcut (Ctrl+Alt+T)" + }; + let dashboard_shortcut_i = CheckMenuItem::with_id( + app, + "dashboard-shortcut", + shortcut_label, + true, + dashboard_shortcut_on, + None::<&str>, + )?; // Launch-at-login toggle (a checkbox item). Seeded from the reconciled // preference; clicking it flips the OS registration and persists. let autostart_i = CheckMenuItem::with_id( @@ -705,7 +830,10 @@ pub fn run() { &[ &open_i, &refresh_i, + &check_updates_i, &PredefinedMenuItem::separator(app)?, + &weekly_remaining_i, + &dashboard_shortcut_i, &autostart_i, &PredefinedMenuItem::separator(app)?, &quit_i, @@ -775,6 +903,44 @@ pub fn run() { .on_menu_event(move |app, event| match event.id.as_ref() { "open" => show_popover(app), "refresh" => refresh(app), + "check-updates" => { + show_popover(app); + let _ = app.emit("check-for-updates", ()); + } + "weekly-remaining" => { + if let Some(state) = app.try_state::() { + let Some((enabled, preferences)) = state.0.lock().ok().map(|mut prefs| { + prefs.show_weekly_remaining = !prefs.show_weekly_remaining; + (prefs.show_weekly_remaining, prefs.clone()) + }) else { + return; + }; + let _ = weekly_remaining_i.set_checked(enabled); + save_tray_preferences(&preferences); + refresh(app); + } + } + "dashboard-shortcut" => { + if let Some(state) = app.try_state::() { + let enabled = state + .0 + .lock() + .map(|prefs| prefs.dashboard_shortcut) + .unwrap_or(false); + let changed = if enabled { + app.global_shortcut().unregister(DASHBOARD_SHORTCUT).is_ok() + } else { + app.global_shortcut().register(DASHBOARD_SHORTCUT).is_ok() + }; + if changed { + if let Ok(mut prefs) = state.0.lock() { + prefs.dashboard_shortcut = !enabled; + let _ = dashboard_shortcut_i.set_checked(!enabled); + save_tray_preferences(&prefs); + } + } + } + } "autostart" => { // Flip the OS registration, re-read the real state, mirror // it into the checkbox, and persist the user's choice. @@ -862,3 +1028,24 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn weekly_remaining_uses_seven_day_quota() { + let quota = model::Quota { + plan: "pro".into(), + primary_pct: 20.0, + primary_minutes: 300, + primary_resets_at: 0, + secondary_pct: 19.4, + secondary_minutes: 7 * 24 * 60, + secondary_resets_at: 0, + as_of_ms: 0, + }; + + assert_eq!(weekly_remaining_from_quota("a), Some(81)); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 21f841c..38fbf6f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.1", + "version": "1.3.2", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index cc52c1a..79144a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { invoke } from "@tauri-apps/api/core"; import { domToPng } from "modern-screenshot"; import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; +import appPackage from "../package.json"; import { Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, @@ -126,8 +127,10 @@ function SplitLegend({ t, cacheM, restM, cachedPct }: display: "flex", alignItems: "center", gap: 14, font: `500 10px ${t.mono}`, color: t.dim, marginBottom: 14, whiteSpace: "nowrap", overflow: "hidden", }}> - {compact ? "Cache" : "Cached"} {fmtTokens(cacheM)} - New {fmtTokens(restM)} + {compact ? "Cache" : "Cached"} {fmtTokens(cacheM)} + New {fmtTokens(restM)} {cachedPct}% cached ); @@ -135,43 +138,72 @@ function SplitLegend({ t, cacheM, restM, cachedPct }: // ── In-app updates ────────────────────────────────────────────── // Poll the GitHub release feed (plugin-updater endpoint) on launch and every -// hour; surface a slim banner when a newer signed build exists. Download + -// install happen in-app, then a relaunch finishes the update. A dismissed -// version stays hidden until the *next* version appears (localStorage). +// hour; keep version and update actions below the Tokenscope brand. Download +// and install happen in-app, then a relaunch finishes the update. A dismissed +// version stays hidden until the next version appears (localStorage). type UpdateState = - | { phase: "idle" } + | { phase: "checking" } + | { phase: "current" } + | { phase: "check-failed" } + | { phase: "skipped"; version: string } | { phase: "available"; update: Update } | { phase: "downloading"; version: string; pct: number } | { phase: "ready"; version: string } - | { phase: "error"; version: string }; + | { phase: "install-failed"; version: string }; function useUpdater(): [UpdateState, () => void, () => void] { - const [st, setSt] = useState({ phase: "idle" }); + const [st, setSt] = useState(() => + typeof window !== "undefined" && "__TAURI_INTERNALS__" in window + ? { phase: "checking" } + : { phase: "current" } + ); const updRef = useRef(null); useEffect(() => { const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; if (!inTauri) return; let dead = false; let checking = false; - const probe = async () => { + let unlisten: (() => void) | null = null; + const probe = async (manual = false) => { if (checking) return; checking = true; + if (manual) setSt({ phase: "checking" }); try { const u = await checkUpdate(); - if (dead || !u) return; - if (localStorage.getItem("tokenscope-skip-update") === u.version) return; + if (dead) return; + if (!u) { + updRef.current = null; + setSt({ phase: "current" }); + return; + } + const skipped = localStorage.getItem("tokenscope-skip-update") === u.version; + if (skipped && !manual) { + updRef.current = null; + setSt({ phase: "skipped", version: u.version }); + return; + } + if (skipped) localStorage.removeItem("tokenscope-skip-update"); updRef.current = u; setSt({ phase: "available", update: u }); } catch { - // Offline / rate-limited / no latest.json: fail quietly. There is no - // immediate retry; the next regular hourly check remains scheduled. + // Offline / rate-limited / no latest.json: no automatic immediate + // retry; wait for the next hourly check or an explicit tray-menu check. + if (!dead) setSt({ phase: "check-failed" }); } finally { checking = false; } }; void probe(); const t = window.setInterval(() => { void probe(); }, 60 * 60 * 1000); - return () => { dead = true; window.clearInterval(t); }; + listen("check-for-updates", () => { void probe(true); }).then((stop) => { + if (dead) stop(); + else unlisten = stop; + }); + return () => { + dead = true; + window.clearInterval(t); + unlisten?.(); + }; }, []); const install = async () => { const u = updRef.current; @@ -188,101 +220,53 @@ function useUpdater(): [UpdateState, () => void, () => void] { }); setSt({ phase: "ready", version: u.version }); } catch { - setSt({ phase: "error", version: u.version }); + setSt({ phase: "install-failed", version: u.version }); } }; const dismiss = () => { const u = updRef.current; if (u) try { localStorage.setItem("tokenscope-skip-update", u.version); } catch {} - setSt({ phase: "idle" }); + setSt(u ? { phase: "skipped", version: u.version } : { phase: "current" }); }; return [st, install, dismiss]; } type UpdaterController = ReturnType; -function releaseNotesSummary(body?: string) { - if (!body) return null; - const lines = body - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => - line - .replace(/^[-*]\s+/, "") - .replace(/\*\*/g, "") - .replace(/`/g, "") - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") - .replace(/\s+by @\S+.*$/, "") - .trim() - ) - .filter((line) => - line && - !line.startsWith("#") && - !/^install$/i.test(line) && - !/^what'?s changed$/i.test(line) && - !/^full changelog/i.test(line) && - !line.startsWith("Menu-bar / system-tray") - ); - if (!lines.length) return null; - const text = lines.slice(0, 2).join("; "); - return text.length > 180 ? `${text.slice(0, 177)}...` : text; -} - -function UpdateBanner({ st, theme, onInstall, onDismiss }: +function VersionUpdate({ st, theme, onInstall, onDismiss }: { st: UpdateState; theme: Theme; onInstall: () => void; onDismiss: () => void }) { const t = theme; - if (st.phase === "idle") return null; - const notes = st.phase === "available" ? releaseNotesSummary(st.update.body) : null; - const Btn = ({ label, onClick }: { label: string; onClick: () => void }) => ( - ); + let status: React.ReactNode; + let title: string | undefined; + if (st.phase === "checking") status = "Checking…"; + else if (st.phase === "current") status = "Latest"; + else if (st.phase === "check-failed") status = "Check failed"; + else if (st.phase === "skipped") { + status = "Skipped"; + title = `Update v${st.version} was skipped`; + } else if (st.phase === "available") { + status = <> + ; + title = [`v${st.update.version} available`, st.update.body].filter(Boolean).join("\n\n"); + } else if (st.phase === "downloading") status = `${st.pct}%`; + else if (st.phase === "ready") { + status = relaunch().catch(() => {})} />; + } else { + status = <>Failed + ; + } return ( -
- {st.phase === "available" && <> -
- - v{st.update.version} is available - - - -
- {notes && ( -
- What's changed: {notes} -
- )} - } - {st.phase === "downloading" && <> -
- Downloading v{st.version}… - {st.pct}% -
- } - {st.phase === "ready" && <> -
- v{st.version} installed - relaunch().catch(() => {})} /> -
- } - {st.phase === "error" && <> -
- Update failed — try again later - -
- } + v{appPackage.version}·{status}
); } @@ -306,7 +290,8 @@ function AgentChips({ scopes, value, theme, onSelect }: border: `1px solid ${on ? t.segBorder : "transparent"}`, boxShadow: on ? t.segOnShadow : "none", transition: "color .15s, background .15s", }}> - {s.color && } + {s.color && } {s.label} ); @@ -324,7 +309,8 @@ function AgentLegend({ t, slices, cachedPct }: font: `500 10px ${t.mono}`, color: t.dim, marginBottom: 14, whiteSpace: "nowrap", overflow: "hidden", }}> {slices.map((s) => ( - {s.label} {fmtTokens(s.tokens)} + {s.label} {fmtTokens(s.tokens)} ))} {cachedPct}% cached @@ -547,7 +533,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const scope = scopes.find((s) => s.id === scopeId) ?? scopes[0]; // Filtering to one agent re-tints the whole panel with its accent. const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); - // In-app update lifecycle (idle → available → downloading → ready). + // In-app update lifecycle (checking/current → available → downloading → ready). const [updSt, updInstall, updDismiss] = updater; // Drag the popover by its body (Windows/Linux only — macOS uses the menu-bar // NSPanel and is gated out). A real OS window-drag begins only once the @@ -687,15 +673,22 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater if (!el) { showToast("Nothing to capture", false); return; } setShotBusy(true); try { + await document.fonts.ready; + el.classList.add("ts-no-transition"); + await new Promise((resolve) => requestAnimationFrame(() => resolve())); // explicit width/height = full scrollable content, not just the viewport; - // filter drops the capture button itself (and its in-flight spinner) so - // the saved image is a clean dashboard, not a shot of the button. + // Keep the capture button's layout slot but hide its cloned pixels. Removing + // the node would reflow the header and make the PNG differ from the panel. const dataUrl = await domToPng(el, { scale: 2, backgroundColor: dark ? "#1f2226" : "#ffffff", width: el.scrollWidth, height: el.scrollHeight, - filter: (n) => !(n instanceof HTMLElement && n.getAttribute("aria-label") === "save screenshot"), + onCloneEachNode: (node) => { + if (node instanceof HTMLElement && node.getAttribute("aria-label") === "save screenshot") { + node.style.visibility = "hidden"; + } + }, }); const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; if (inTauri) { @@ -713,6 +706,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater } catch { showToast("Screenshot failed", false); } finally { + el.classList.remove("ts-no-transition"); setShotBusy(false); } }; @@ -752,14 +746,17 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater {/* sticky header — stays put while the body scrolls */}
-
- - Tokenscope +
+
+ + Tokenscope +
+
@@ -774,8 +771,6 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater
{/* scrolling body */}
- {/* in-app update prompt */} - {activeRange && (
Date: Mon, 13 Jul 2026 02:27:20 -0700 Subject: [PATCH 17/67] release: v1.3.3 --- README-zh.md | 6 +- README.md | 6 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 219 +++++++++++++++++++++++++++++++++++--- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 95 ++++++++++++++++- 8 files changed, 307 insertions(+), 27 deletions(-) diff --git a/README-zh.md b/README-zh.md index c6d7466..caf25dc 100644 --- a/README-zh.md +++ b/README-zh.md @@ -132,7 +132,7 @@ App 会在启动时及此后每小时检测新版本。当前版本和更新操 - **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) - 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) - **Show Weekly Remaining** 可在菜单栏 Token 数后追加 Codex 每周剩余配额(如 `12.40M-81%`);默认关闭,选择会被记住 -- **Dashboard Shortcut** 可开启全局快捷键来唤起面板(macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);默认关闭,选择会被记住 +- **Dashboard Shortcut** 可用全局快捷键切换面板的显示/隐藏(默认 macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);通过 **Change Dashboard Shortcut…** 可直接录入自定义组合键。默认关闭,选择会被记住 - 已自动设置**登录自启**,无需手动配置 ## 开发 @@ -163,9 +163,9 @@ pnpm tauri build # macOS 产出 .app / .dmg,Windows 产出 .exe (NSIS) 使用仓库内置脚本同步所有版本文件: ```bash -pnpm version:set 1.3.2 +pnpm version:set 1.3.3 pnpm version:check -git commit -am "release: v1.3.2" +git commit -am "release: v1.3.3" git push origin main ``` diff --git a/README.md b/README.md index cecbe98..074c199 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ right-click menu to check immediately. - **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) - Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) - **Show Weekly Remaining** optionally appends Codex's remaining weekly quota to the menu-bar label (for example `12.40M-81%`); it is off by default and remembers your choice -- **Dashboard Shortcut** enables a global hotkey for opening the panel (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux); it is off by default and remembers your choice +- **Dashboard Shortcut** toggles the panel with a global hotkey (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux by default); use **Change Dashboard Shortcut…** to record your own combination. It is off by default and remembers your choice - **Launch-at-login is set up automatically** — no manual configuration needed ## Develop @@ -165,9 +165,9 @@ For distribution see `PRD.md` §6.3 (Homebrew Cask recommended on macOS; direct Keep all app version files in sync with the bundled helper: ```bash -pnpm version:set 1.3.2 +pnpm version:set 1.3.3 pnpm version:check -git commit -am "release: v1.3.2" +git commit -am "release: v1.3.3" git push origin main ``` diff --git a/package.json b/package.json index be8d3a2..02ebff1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.2", + "version": "1.3.3", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ef586fc..bf552ad 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.2" +version = "1.3.3" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b4d7fec..973b8f8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.2" +version = "1.3.3" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0f7fcff..0fc1cf9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,7 +17,7 @@ use tauri::{ Emitter, Manager, }; use tauri_plugin_autostart::ManagerExt; -use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState}; +use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; // Positioner is only used for the non-macOS fallback; macOS positions the // NSPanel manually (see position_panel). #[cfg(not(target_os = "macos"))] @@ -37,14 +37,64 @@ fn now_ms() -> i64 { const DASHBOARD_SHORTCUT: &str = "CommandOrControl+Alt+T"; -#[derive(Clone, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(default)] struct TrayPreferences { show_weekly_remaining: bool, dashboard_shortcut: bool, + dashboard_shortcut_key: String, +} + +impl Default for TrayPreferences { + fn default() -> Self { + Self { + show_weekly_remaining: false, + dashboard_shortcut: false, + dashboard_shortcut_key: DASHBOARD_SHORTCUT.to_string(), + } + } } struct TrayPreferencesState(std::sync::Mutex); +struct DashboardShortcutMenuState(CheckMenuItem); + +fn shortcut_label(shortcut: &str) -> String { + let macos = cfg!(target_os = "macos"); + let mut parts = Vec::new(); + for part in shortcut.split('+') { + let (order, label) = match part.to_ascii_uppercase().as_str() { + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + if macos { + (4, "⌘") + } else { + (1, "Ctrl") + } + } + "COMMAND" | "CMD" | "SUPER" => (4, if macos { "⌘" } else { "Win" }), + "CONTROL" | "CTRL" => (1, if macos { "⌃" } else { "Ctrl" }), + "OPTION" | "ALT" => (2, if macos { "⌥" } else { "Alt" }), + "SHIFT" => (3, if macos { "⇧" } else { "Shift" }), + _ => ( + 5, + part.strip_prefix("Key") + .or_else(|| part.strip_prefix("Digit")) + .unwrap_or(part), + ), + }; + parts.push((order, label)); + } + parts.sort_by_key(|part| part.0); + let labels = parts.into_iter().map(|part| part.1).collect::>(); + if macos { + labels.join("") + } else { + labels.join("+") + } +} + +fn dashboard_shortcut_menu_label(shortcut: &str) -> String { + format!("Dashboard Shortcut ({})", shortcut_label(shortcut)) +} fn weekly_remaining_pct(dash: &Dashboard) -> Option { let quota = dash.scopes.iter().find_map(|scope| scope.quota.as_ref())?; @@ -488,6 +538,47 @@ fn show_popover(app: &tauri::AppHandle) { let _ = app.run_on_main_thread(move || show_popover_inner(&handle)); } +fn toggle_popover(app: &tauri::AppHandle) { + let handle = app.clone(); + let _ = app.run_on_main_thread(move || { + #[cfg(target_os = "macos")] + { + let visible = handle + .get_webview_panel("main") + .map(|panel| panel.is_visible()) + .unwrap_or_else(|_| { + handle + .get_webview_window("main") + .and_then(|window| window.is_visible().ok()) + .unwrap_or(false) + }); + if visible { + if let Ok(panel) = handle.get_webview_panel("main") { + panel.order_out(None); + } else if let Some(window) = handle.get_webview_window("main") { + let _ = window.hide(); + } + } else { + show_popover_inner(&handle); + } + } + #[cfg(not(target_os = "macos"))] + { + let visible = handle + .get_webview_window("main") + .and_then(|window| window.is_visible().ok()) + .unwrap_or(false); + if visible { + if let Some(window) = handle.get_webview_window("main") { + let _ = window.hide(); + } + } else { + show_popover_inner(&handle); + } + } + }); +} + fn show_popover_inner(app: &tauri::AppHandle) { // On macOS the window is an NSPanel — position it manually, then show() // (makes it key and orders it front, incl. over fullscreen Spaces). @@ -522,6 +613,58 @@ fn show_popover_inner(app: &tauri::AppHandle) { } } +#[tauri::command] +fn set_dashboard_shortcut(app: tauri::AppHandle, shortcut: String) -> Result<(), String> { + let shortcut = shortcut.trim().to_string(); + shortcut + .parse::() + .map_err(|_| "invalid shortcut".to_string())?; + + let state = app + .try_state::() + .ok_or_else(|| "shortcut state unavailable".to_string())?; + let (was_enabled, previous) = state + .0 + .lock() + .map(|preferences| { + ( + preferences.dashboard_shortcut, + preferences.dashboard_shortcut_key.clone(), + ) + }) + .map_err(|_| "shortcut state unavailable".to_string())?; + + if was_enabled && previous != shortcut { + app.global_shortcut() + .unregister(previous.as_str()) + .map_err(|error| format!("could not replace shortcut: {error}"))?; + } + if !was_enabled || previous != shortcut { + if let Err(error) = app.global_shortcut().register(shortcut.as_str()) { + if was_enabled && previous != shortcut { + let _ = app.global_shortcut().register(previous.as_str()); + } + return Err(format!("shortcut unavailable: {error}")); + } + } + + let preferences = state + .0 + .lock() + .map(|mut preferences| { + preferences.dashboard_shortcut = true; + preferences.dashboard_shortcut_key = shortcut.clone(); + preferences.clone() + }) + .map_err(|_| "shortcut state unavailable".to_string())?; + save_tray_preferences(&preferences); + if let Some(menu) = app.try_state::() { + let _ = menu.0.set_checked(true); + let _ = menu.0.set_text(dashboard_shortcut_menu_label(&shortcut)); + } + Ok(()) +} + #[tauri::command] async fn get_dashboard(app: tauri::AppHandle) -> Dashboard { // build_dashboard does blocking IO (reads/writes the cache, parses logs) and @@ -620,7 +763,7 @@ pub fn run() { tauri_plugin_global_shortcut::Builder::new() .with_handler(|app, _shortcut, event| { if event.state == ShortcutState::Pressed { - show_popover(app); + toggle_popover(app); } }) .build(), @@ -645,7 +788,8 @@ pub fn run() { get_dashboard, get_range_dashboard, save_screenshot, - begin_drag + begin_drag, + set_dashboard_shortcut ]) .setup(move |app| { // Menu-bar–only app: no Dock icon, runs in the background. @@ -670,7 +814,7 @@ pub fn run() { if tray_preferences.dashboard_shortcut && app .global_shortcut() - .register(DASHBOARD_SHORTCUT) + .register(tray_preferences.dashboard_shortcut_key.as_str()) .is_err() { tray_preferences.dashboard_shortcut = false; @@ -678,6 +822,7 @@ pub fn run() { } let weekly_remaining_on = tray_preferences.show_weekly_remaining; let dashboard_shortcut_on = tray_preferences.dashboard_shortcut; + let dashboard_shortcut_key = tray_preferences.dashboard_shortcut_key.clone(); app.manage(TrayPreferencesState(std::sync::Mutex::new( tray_preferences, ))); @@ -801,19 +946,22 @@ pub fn run() { weekly_remaining_on, None::<&str>, )?; - let shortcut_label = if cfg!(target_os = "macos") { - "Dashboard Shortcut (⌥⌘T)" - } else { - "Dashboard Shortcut (Ctrl+Alt+T)" - }; let dashboard_shortcut_i = CheckMenuItem::with_id( app, "dashboard-shortcut", - shortcut_label, + dashboard_shortcut_menu_label(&dashboard_shortcut_key), true, dashboard_shortcut_on, None::<&str>, )?; + app.manage(DashboardShortcutMenuState(dashboard_shortcut_i.clone())); + let change_dashboard_shortcut_i = MenuItem::with_id( + app, + "change-dashboard-shortcut", + "Change Dashboard Shortcut…", + true, + None::<&str>, + )?; // Launch-at-login toggle (a checkbox item). Seeded from the reconciled // preference; clicking it flips the OS registration and persists. let autostart_i = CheckMenuItem::with_id( @@ -834,6 +982,7 @@ pub fn run() { &PredefinedMenuItem::separator(app)?, &weekly_remaining_i, &dashboard_shortcut_i, + &change_dashboard_shortcut_i, &autostart_i, &PredefinedMenuItem::separator(app)?, &quit_i, @@ -922,15 +1071,20 @@ pub fn run() { } "dashboard-shortcut" => { if let Some(state) = app.try_state::() { - let enabled = state + let (enabled, shortcut) = state .0 .lock() - .map(|prefs| prefs.dashboard_shortcut) - .unwrap_or(false); + .map(|prefs| { + ( + prefs.dashboard_shortcut, + prefs.dashboard_shortcut_key.clone(), + ) + }) + .unwrap_or((false, DASHBOARD_SHORTCUT.to_string())); let changed = if enabled { - app.global_shortcut().unregister(DASHBOARD_SHORTCUT).is_ok() + app.global_shortcut().unregister(shortcut.as_str()).is_ok() } else { - app.global_shortcut().register(DASHBOARD_SHORTCUT).is_ok() + app.global_shortcut().register(shortcut.as_str()).is_ok() }; if changed { if let Ok(mut prefs) = state.0.lock() { @@ -941,6 +1095,20 @@ pub fn run() { } } } + "change-dashboard-shortcut" => { + let shortcut = app + .try_state::() + .and_then(|state| { + state + .0 + .lock() + .ok() + .map(|prefs| prefs.dashboard_shortcut_key.clone()) + }) + .unwrap_or_else(|| DASHBOARD_SHORTCUT.to_string()); + show_popover(app); + let _ = app.emit("configure-dashboard-shortcut", shortcut); + } "autostart" => { // Flip the OS registration, re-read the real state, mirror // it into the checkbox, and persist the user's choice. @@ -1048,4 +1216,23 @@ mod tests { assert_eq!(weekly_remaining_from_quota("a), Some(81)); } + + #[test] + fn old_tray_preferences_get_the_default_shortcut() { + let preferences: TrayPreferences = + serde_json::from_str(r#"{"show_weekly_remaining":true,"dashboard_shortcut":true}"#) + .unwrap(); + + assert_eq!(preferences.dashboard_shortcut_key, DASHBOARD_SHORTCUT); + assert!(DASHBOARD_SHORTCUT.parse::().is_ok()); + assert!("Command+Alt+KeyT".parse::().is_ok()); + assert_eq!( + shortcut_label(DASHBOARD_SHORTCUT), + if cfg!(target_os = "macos") { + "⌥⌘T" + } else { + "Ctrl+Alt+T" + } + ); + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 38fbf6f..300d8d9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.2", + "version": "1.3.3", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index 79144a8..8864d7c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { invoke } from "@tauri-apps/api/core"; @@ -271,6 +271,84 @@ function VersionUpdate({ st, theme, onInstall, onDismiss }: ); } +function formatShortcut(shortcut: string) { + const macos = typeof navigator !== "undefined" && navigator.userAgent.includes("Macintosh"); + const parts = shortcut.split("+").map((part) => { + const key = part.toUpperCase(); + if (["COMMANDORCONTROL", "COMMANDORCTRL", "CMDORCTRL", "CMDORCONTROL"].includes(key)) return { order: macos ? 4 : 1, label: macos ? "⌘" : "Ctrl" }; + if (["COMMAND", "CMD", "SUPER"].includes(key)) return { order: 4, label: macos ? "⌘" : "Win" }; + if (["CONTROL", "CTRL"].includes(key)) return { order: 1, label: macos ? "⌃" : "Ctrl" }; + if (["OPTION", "ALT"].includes(key)) return { order: 2, label: macos ? "⌥" : "Alt" }; + if (key === "SHIFT") return { order: 3, label: macos ? "⇧" : "Shift" }; + return { order: 5, label: part.replace(/^Key/, "").replace(/^Digit/, "") }; + }); + const labels = parts.sort((a, b) => a.order - b.order).map((part) => part.label); + return macos ? labels.join("") : labels.join("+"); +} + +function shortcutFromKeyEvent(event: ReactKeyboardEvent) { + if (["Meta", "Control", "Alt", "Shift"].includes(event.key)) return null; + if (!event.metaKey && !event.ctrlKey && !event.altKey) return ""; + if (!event.code || event.code === "Unidentified") return ""; + const modifiers = [ + event.metaKey ? "Command" : "", + event.ctrlKey ? "Control" : "", + event.altKey ? "Alt" : "", + event.shiftKey ? "Shift" : "", + ].filter(Boolean); + return [...modifiers, event.code].join("+"); +} + +function ShortcutEditor({ current, theme, dark, onClose, onSaved }: + { current: string; theme: Theme; dark: boolean; onClose: () => void; onSaved: () => void }) { + const ref = useRef(null); + const [error, setError] = useState(""); + const [saving, setSaving] = useState(false); + useEffect(() => { ref.current?.focus(); }, []); + + const record = (event: ReactKeyboardEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (event.key === "Escape") { onClose(); return; } + const shortcut = shortcutFromKeyEvent(event); + if (shortcut === null) return; + if (!shortcut) { + setError("Include Command, Control, or Alt"); + return; + } + setSaving(true); + setError(""); + invoke("set_dashboard_shortcut", { shortcut }) + .then(onSaved) + .catch(() => { setSaving(false); setError("That shortcut is unavailable"); }); + }; + + return ( +
+
event.stopPropagation()} style={{ + width: 250, padding: "18px 20px", borderRadius: 12, outline: "none", textAlign: "center", + background: dark ? "#292d31" : "#ffffff", border: `1px solid ${theme.gridLine}`, + boxShadow: "0 18px 50px rgba(0,0,0,0.38)", color: theme.text, + }}> +
Dashboard Shortcut
+
{formatShortcut(current)}
+
+ {saving ? "Saving…" : "Press a new shortcut · Esc to cancel"} +
+ {error &&
{error}
} +
+
+ ); +} + // Agent filter chips (All / Claude / Codex …). Rendered only when several // sources have data; a single-source install never sees them. function AgentChips({ scopes, value, theme, onSelect }: @@ -535,6 +613,16 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); // In-app update lifecycle (checking/current → available → downloading → ready). const [updSt, updInstall, updDismiss] = updater; + const [shortcutEditor, setShortcutEditor] = useState(null); + useEffect(() => { + const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; + if (!inTauri) return; + let dead = false; + let unlisten: (() => void) | null = null; + listen("configure-dashboard-shortcut", (event) => setShortcutEditor(event.payload)) + .then((stop) => { if (dead) stop(); else unlisten = stop; }); + return () => { dead = true; unlisten?.(); }; + }, []); // Drag the popover by its body (Windows/Linux only — macOS uses the menu-bar // NSPanel and is gated out). A real OS window-drag begins only once the // pointer moves past a small threshold, so a plain click still clicks through @@ -924,6 +1012,11 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater {toast.msg}
)} + {shortcutEditor !== null && ( + setShortcutEditor(null)} + onSaved={() => { setShortcutEditor(null); showToast("Shortcut enabled", true); }} /> + )}
); } From 3d7ec4255e63ee4c35bedad195e93dcaa7c360a2 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 03:02:30 -0700 Subject: [PATCH 18/67] release: v1.3.4 --- README-zh.md | 4 ++-- README.md | 4 ++-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README-zh.md b/README-zh.md index caf25dc..abde739 100644 --- a/README-zh.md +++ b/README-zh.md @@ -163,9 +163,9 @@ pnpm tauri build # macOS 产出 .app / .dmg,Windows 产出 .exe (NSIS) 使用仓库内置脚本同步所有版本文件: ```bash -pnpm version:set 1.3.3 +pnpm version:set 1.3.4 pnpm version:check -git commit -am "release: v1.3.3" +git commit -am "release: v1.3.4" git push origin main ``` diff --git a/README.md b/README.md index 074c199..90793ad 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ For distribution see `PRD.md` §6.3 (Homebrew Cask recommended on macOS; direct Keep all app version files in sync with the bundled helper: ```bash -pnpm version:set 1.3.3 +pnpm version:set 1.3.4 pnpm version:check -git commit -am "release: v1.3.3" +git commit -am "release: v1.3.4" git push origin main ``` diff --git a/package.json b/package.json index 02ebff1..0733ef3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.3", + "version": "1.3.4", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bf552ad..52222db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.3" +version = "1.3.4" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 973b8f8..870d544 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.3" +version = "1.3.4" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 300d8d9..c5230ce 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.3", + "version": "1.3.4", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 6aaba7215274b2a7bda2527711dbeed08785f750 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 19:25:35 -0700 Subject: [PATCH 19/67] release: v1.3.5 --- README-zh.md | 2 +- README.md | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 173 +++++++++++++++++++++++++++++--------- src-tauri/src/model.rs | 2 + src-tauri/src/parser.rs | 4 + src-tauri/src/store.rs | 154 ++++++++++++++++++++++++++++++--- src-tauri/tauri.conf.json | 2 +- src/data.ts | 2 +- 11 files changed, 288 insertions(+), 59 deletions(-) diff --git a/README-zh.md b/README-zh.md index abde739..89c75e6 100644 --- a/README-zh.md +++ b/README-zh.md @@ -131,7 +131,7 @@ App 会在启动时及此后每小时检测新版本。当前版本和更新操 - **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) - **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) - 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) -- **Show Weekly Remaining** 可在菜单栏 Token 数后追加 Codex 每周剩余配额(如 `12.40M-81%`);默认关闭,选择会被记住 +- **Weekly Remaining** 可配置菜单栏 Token 数后的周剩余额度:**Off**、仅 **Codex**(如 `12.40M-76%`),或 **Codex + Spark**(`codex_bengalfox`,显示为 `12.40M-C76%-S93%`);默认关闭,选择会被记住 - **Dashboard Shortcut** 可用全局快捷键切换面板的显示/隐藏(默认 macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);通过 **Change Dashboard Shortcut…** 可直接录入自定义组合键。默认关闭,选择会被记住 - 已自动设置**登录自启**,无需手动配置 diff --git a/README.md b/README.md index 90793ad..9f1c3f8 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ right-click menu to check immediately. - **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) - **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) - Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) -- **Show Weekly Remaining** optionally appends Codex's remaining weekly quota to the menu-bar label (for example `12.40M-81%`); it is off by default and remembers your choice +- **Weekly Remaining** controls the quota shown beside the menu-bar token count: **Off**, **Codex** (for example `12.40M-76%`), or **Codex + Spark** (`codex_bengalfox`, shown as `12.40M-C76%-S93%`). It is off by default and remembers your choice - **Dashboard Shortcut** toggles the panel with a global hotkey (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux by default); use **Change Dashboard Shortcut…** to record your own combination. It is off by default and remembers your choice - **Launch-at-login is set up automatically** — no manual configuration needed diff --git a/package.json b/package.json index 0733ef3..b49c496 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.4", + "version": "1.3.5", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 52222db..96f504c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.4" +version = "1.3.5" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 870d544..a3ac4a1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.4" +version = "1.3.5" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0fc1cf9..daa04a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,7 +12,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(not(target_os = "macos"))] use tauri::WindowEvent; use tauri::{ - menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, + menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Emitter, Manager, }; @@ -37,10 +37,19 @@ fn now_ms() -> i64 { const DASHBOARD_SHORTCUT: &str = "CommandOrControl+Alt+T"; +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +enum WeeklyQuotaDisplay { + #[default] + Off, + Codex, + CodexAndSpark, +} + #[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(default)] struct TrayPreferences { - show_weekly_remaining: bool, + weekly_quota_display: WeeklyQuotaDisplay, dashboard_shortcut: bool, dashboard_shortcut_key: String, } @@ -48,7 +57,7 @@ struct TrayPreferences { impl Default for TrayPreferences { fn default() -> Self { Self { - show_weekly_remaining: false, + weekly_quota_display: WeeklyQuotaDisplay::Off, dashboard_shortcut: false, dashboard_shortcut_key: DASHBOARD_SHORTCUT.to_string(), } @@ -96,11 +105,6 @@ fn dashboard_shortcut_menu_label(shortcut: &str) -> String { format!("Dashboard Shortcut ({})", shortcut_label(shortcut)) } -fn weekly_remaining_pct(dash: &Dashboard) -> Option { - let quota = dash.scopes.iter().find_map(|scope| scope.quota.as_ref())?; - weekly_remaining_from_quota(quota) -} - fn weekly_remaining_from_quota(quota: &model::Quota) -> Option { let used = if quota.primary_minutes == 7 * 24 * 60 { quota.primary_pct @@ -112,28 +116,58 @@ fn weekly_remaining_from_quota(quota: &model::Quota) -> Option { Some((100.0 - used).clamp(0.0, 100.0).round() as u8) } -fn tray_label(dash: &Dashboard, show_weekly_remaining: bool) -> String { - let mut label = fmt_tokens_m(dash.today_tokens); - if show_weekly_remaining { - if let Some(remaining) = weekly_remaining_pct(dash) { - label.push_str(&format!("-{remaining}%")); +fn dashboard_quotas(dash: &Dashboard) -> (Option<&model::Quota>, Option<&model::Quota>) { + dash.scopes + .iter() + .find(|scope| scope.quota.is_some() || scope.spark_quota.is_some()) + .map(|scope| (scope.quota.as_ref(), scope.spark_quota.as_ref())) + .unwrap_or((None, None)) +} + +fn weekly_label_suffix( + display: WeeklyQuotaDisplay, + codex: Option<&model::Quota>, + spark: Option<&model::Quota>, +) -> String { + let mut suffix = String::new(); + match display { + WeeklyQuotaDisplay::Off => {} + WeeklyQuotaDisplay::Codex => { + if let Some(remaining) = codex.and_then(weekly_remaining_from_quota) { + suffix.push_str(&format!("-{remaining}%")); + } + } + WeeklyQuotaDisplay::CodexAndSpark => { + if let Some(remaining) = codex.and_then(weekly_remaining_from_quota) { + suffix.push_str(&format!("-C{remaining}%")); + } + if let Some(remaining) = spark.and_then(weekly_remaining_from_quota) { + suffix.push_str(&format!("-S{remaining}%")); + } } } + suffix +} + +fn tray_label(dash: &Dashboard, display: WeeklyQuotaDisplay) -> String { + let mut label = fmt_tokens_m(dash.today_tokens); + let (codex, spark) = dashboard_quotas(dash); + label.push_str(&weekly_label_suffix(display, codex, spark)); label } fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { - let show_weekly_remaining = app + let display = app .try_state::() .map(|state| { state .0 .lock() - .map(|prefs| prefs.show_weekly_remaining) - .unwrap_or(false) + .map(|prefs| prefs.weekly_quota_display) + .unwrap_or_default() }) - .unwrap_or(false); - let label = tray_label(dash, show_weekly_remaining); + .unwrap_or_default(); + let label = tray_label(dash, display); let handle = app.clone(); let _ = app.run_on_main_thread(move || { if let Some(tray) = handle.tray_by_id("main") { @@ -189,10 +223,23 @@ fn load_tray_preferences() -> TrayPreferences { let Some(path) = tray_preferences_path() else { return TrayPreferences::default(); }; - std::fs::read_to_string(path) + let Some(text) = std::fs::read_to_string(path).ok() else { + return TrayPreferences::default(); + }; + parse_tray_preferences(&text) +} + +fn parse_tray_preferences(text: &str) -> TrayPreferences { + let mut preferences: TrayPreferences = serde_json::from_str(text).unwrap_or_default(); + let legacy_weekly_on = serde_json::from_str::(text) .ok() - .and_then(|text| serde_json::from_str(&text).ok()) - .unwrap_or_default() + .filter(|value| value.get("weekly_quota_display").is_none()) + .and_then(|value| value.get("show_weekly_remaining")?.as_bool()) + .unwrap_or(false); + if legacy_weekly_on { + preferences.weekly_quota_display = WeeklyQuotaDisplay::Codex; + } + preferences } fn save_tray_preferences(preferences: &TrayPreferences) { @@ -820,7 +867,7 @@ pub fn run() { tray_preferences.dashboard_shortcut = false; save_tray_preferences(&tray_preferences); } - let weekly_remaining_on = tray_preferences.show_weekly_remaining; + let weekly_quota_display = tray_preferences.weekly_quota_display; let dashboard_shortcut_on = tray_preferences.dashboard_shortcut; let dashboard_shortcut_key = tray_preferences.dashboard_shortcut_key.clone(); app.manage(TrayPreferencesState(std::sync::Mutex::new( @@ -927,7 +974,7 @@ pub fn run() { // Build the menu-bar tray: app glyph (template icon) + today's tokens. let dash = parser::build_dashboard(); - let label = tray_label(&dash, weekly_remaining_on); + let label = tray_label(&dash, weekly_quota_display); let open_i = MenuItem::with_id(app, "open", "Open Tokenscope", true, None::<&str>)?; let refresh_i = MenuItem::with_id(app, "refresh", "Refresh", true, None::<&str>)?; @@ -938,14 +985,36 @@ pub fn run() { true, None::<&str>, )?; - let weekly_remaining_i = CheckMenuItem::with_id( + let weekly_off_i = CheckMenuItem::with_id( + app, + "weekly-off", + "Off", + true, + weekly_quota_display == WeeklyQuotaDisplay::Off, + None::<&str>, + )?; + let weekly_codex_i = CheckMenuItem::with_id( app, - "weekly-remaining", - "Show Weekly Remaining", + "weekly-codex", + "Codex", true, - weekly_remaining_on, + weekly_quota_display == WeeklyQuotaDisplay::Codex, None::<&str>, )?; + let weekly_codex_spark_i = CheckMenuItem::with_id( + app, + "weekly-codex-spark", + "Codex + Spark", + true, + weekly_quota_display == WeeklyQuotaDisplay::CodexAndSpark, + None::<&str>, + )?; + let weekly_menu = Submenu::with_items( + app, + "Weekly Remaining", + true, + &[&weekly_off_i, &weekly_codex_i, &weekly_codex_spark_i], + )?; let dashboard_shortcut_i = CheckMenuItem::with_id( app, "dashboard-shortcut", @@ -980,7 +1049,7 @@ pub fn run() { &refresh_i, &check_updates_i, &PredefinedMenuItem::separator(app)?, - &weekly_remaining_i, + &weekly_menu, &dashboard_shortcut_i, &change_dashboard_shortcut_i, &autostart_i, @@ -1056,15 +1125,23 @@ pub fn run() { show_popover(app); let _ = app.emit("check-for-updates", ()); } - "weekly-remaining" => { + "weekly-off" | "weekly-codex" | "weekly-codex-spark" => { + let display = match event.id.as_ref() { + "weekly-codex" => WeeklyQuotaDisplay::Codex, + "weekly-codex-spark" => WeeklyQuotaDisplay::CodexAndSpark, + _ => WeeklyQuotaDisplay::Off, + }; if let Some(state) = app.try_state::() { - let Some((enabled, preferences)) = state.0.lock().ok().map(|mut prefs| { - prefs.show_weekly_remaining = !prefs.show_weekly_remaining; - (prefs.show_weekly_remaining, prefs.clone()) + let Some(preferences) = state.0.lock().ok().map(|mut prefs| { + prefs.weekly_quota_display = display; + prefs.clone() }) else { return; }; - let _ = weekly_remaining_i.set_checked(enabled); + let _ = weekly_off_i.set_checked(display == WeeklyQuotaDisplay::Off); + let _ = weekly_codex_i.set_checked(display == WeeklyQuotaDisplay::Codex); + let _ = weekly_codex_spark_i + .set_checked(display == WeeklyQuotaDisplay::CodexAndSpark); save_tray_preferences(&preferences); refresh(app); } @@ -1203,7 +1280,7 @@ mod tests { #[test] fn weekly_remaining_uses_seven_day_quota() { - let quota = model::Quota { + let codex = model::Quota { plan: "pro".into(), primary_pct: 20.0, primary_minutes: 300, @@ -1213,16 +1290,34 @@ mod tests { secondary_resets_at: 0, as_of_ms: 0, }; + let spark = model::Quota { + primary_pct: 7.0, + primary_minutes: 7 * 24 * 60, + ..codex.clone() + }; - assert_eq!(weekly_remaining_from_quota("a), Some(81)); + assert_eq!(weekly_remaining_from_quota(&codex), Some(81)); + assert_eq!( + weekly_label_suffix(WeeklyQuotaDisplay::Codex, Some(&codex), Some(&spark)), + "-81%" + ); + assert_eq!( + weekly_label_suffix( + WeeklyQuotaDisplay::CodexAndSpark, + Some(&codex), + Some(&spark) + ), + "-C81%-S93%" + ); } #[test] - fn old_tray_preferences_get_the_default_shortcut() { - let preferences: TrayPreferences = - serde_json::from_str(r#"{"show_weekly_remaining":true,"dashboard_shortcut":true}"#) - .unwrap(); + fn old_tray_preferences_are_migrated() { + let preferences = parse_tray_preferences( + r#"{"show_weekly_remaining":true,"dashboard_shortcut":true}"#, + ); + assert_eq!(preferences.weekly_quota_display, WeeklyQuotaDisplay::Codex); assert_eq!(preferences.dashboard_shortcut_key, DASHBOARD_SHORTCUT); assert!(DASHBOARD_SHORTCUT.parse::().is_ok()); assert!("Command+Alt+KeyT".parse::().is_ok()); diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 780de65..599b4ca 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -122,6 +122,8 @@ pub struct Scope { pub month: PeriodReport, pub heatmap: Vec, pub quota: Option, // Codex only + #[serde(rename = "sparkQuota")] + pub spark_quota: Option, // codex_bengalfox only } #[derive(Debug, Clone, Serialize)] diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 473ebe1..92b9798 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -111,6 +111,7 @@ pub fn build_dashboard() -> Dashboard { store.save(); } let codex_quota = store.codex_quota.clone(); + let codex_spark_quota = store.codex_spark_quota.clone(); // 2. Aggregate: apply current config + prices, slice by current time. let cfg = UserConfig::load(&store.codex_project_dirs()); @@ -145,6 +146,7 @@ pub fn build_dashboard() -> Dashboard { let mut scope = build_scope("all", "All", "", &events, PALETTE, agent, &cfg, now); if agent == AGENT_CODEX { scope.quota = codex_quota; + scope.spark_quota = codex_spark_quota; } vec![scope] } else { @@ -156,6 +158,7 @@ pub fn build_dashboard() -> Dashboard { let mut s = build_scope(a.id, a.label, a.color, &filtered, &a.palette, a.id, &cfg, now); if a.id == AGENT_CODEX { s.quota = codex_quota.clone(); + s.spark_quota = codex_spark_quota.clone(); } per_agent.push(s); } @@ -367,6 +370,7 @@ fn build_scope( month, heatmap, quota: None, + spark_quota: None, } } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index faca111..41310a0 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -87,8 +87,9 @@ pub struct Store { // count its token usage only once. index: HashMap, manifest: Manifest, - // Latest Codex rate-limit snapshot seen across all session files. + // Latest general and model-specific Codex rate-limit snapshots. pub codex_quota: Option, + pub codex_spark_quota: Option, } // Bump when the parsing/extraction logic changes in a way that requires @@ -103,6 +104,10 @@ pub struct Store { // v7: extract Codex MCP calls from tool search and app custom-tool formats. // v8: retain complete history for custom-range tracking and settlement. const STORE_VERSION: u32 = 8; +const QUOTA_CACHE_VERSION: u32 = 1; +// One-time quota migration: at most 32 × 64 KiB = 2 MiB of log content. +const RECENT_QUOTA_FILES: usize = 32; +const QUOTA_TAIL_BYTES: u64 = 64 * 1024; /// Atomically replace `path`'s contents: write a sibling temp file, then rename /// over the target (same-volume rename is atomic on Windows and Unix). Avoids @@ -150,10 +155,13 @@ impl Store { let mut events: Vec = Vec::new(); let mut manifest = Manifest::default(); let mut codex_quota = None; + let mut codex_spark_quota = None; + let mut version_ok = false; + let mut quota_cache_ok = false; if let Some(dir) = cache_dir() { // If the cache was written by an older parser, discard it so ingest // does a full rescan and picks up newly-extracted facts. - let version_ok = fs::read_to_string(dir.join("version")) + version_ok = fs::read_to_string(dir.join("version")) .ok() .and_then(|s| s.trim().parse::().ok()) == Some(STORE_VERSION); @@ -175,11 +183,19 @@ impl Store { events = e; manifest = m; } - // Quota is a best-effort side channel — a missing/corrupt file - // just means "no snapshot yet", never a rescan. - codex_quota = fs::read_to_string(dir.join("codex_quota.json")) + quota_cache_ok = fs::read_to_string(dir.join("quota_version")) .ok() - .and_then(|t| serde_json::from_str::(&t).ok()); + .and_then(|s| s.trim().parse::().ok()) + == Some(QUOTA_CACHE_VERSION); + if quota_cache_ok { + codex_quota = fs::read_to_string(dir.join("codex_quota.json")) + .ok() + .and_then(|t| serde_json::from_str::(&t).ok()); + codex_spark_quota = + fs::read_to_string(dir.join("codex_bengalfox_quota.json")) + .ok() + .and_then(|t| serde_json::from_str::(&t).ok()); + } } } let index = events @@ -188,12 +204,20 @@ impl Store { .filter(|(_, e)| !e.id.is_empty()) .map(|(i, e)| (e.id.clone(), i)) .collect(); - Store { + let mut store = Store { events, index, manifest, codex_quota, + codex_spark_quota, + }; + if version_ok && !quota_cache_ok { + store.rebuild_codex_quotas(); + if let Some(dir) = cache_dir() { + store.save_quota_cache(&dir); + } } + store } pub fn save(&self) { @@ -209,12 +233,89 @@ impl Store { if let Ok(t) = serde_json::to_string(&self.manifest) { let _ = write_atomic(&dir.join("offsets.json"), t.as_bytes()); } - if let Some(q) = &self.codex_quota { - if let Ok(t) = serde_json::to_string(q) { - let _ = write_atomic(&dir.join("codex_quota.json"), t.as_bytes()); + self.save_quota_cache(&dir); + let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); + } + } + + fn save_quota_cache(&self, dir: &std::path::Path) { + let save = |name: &str, quota: &Option| match quota { + Some(quota) => { + if let Ok(text) = serde_json::to_string(quota) { + let _ = write_atomic(&dir.join(name), text.as_bytes()); + } + } + None => { + let _ = fs::remove_file(dir.join(name)); + } + }; + save("codex_quota.json", &self.codex_quota); + save("codex_bengalfox_quota.json", &self.codex_spark_quota); + let _ = write_atomic( + &dir.join("quota_version"), + QUOTA_CACHE_VERSION.to_string().as_bytes(), + ); + } + + /// One-time migration from the old single-quota cache. Read only the tails + /// of recently active Codex logs; token_count snapshots occur frequently, + /// so this separates current quota buckets without rescanning usage history. + fn rebuild_codex_quotas(&mut self) { + self.codex_quota = None; + self.codex_spark_quota = None; + let Some(root) = codex_dir() else { + return; + }; + let mut files: Vec<_> = WalkDir::new(root) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .path() + .extension() + .map(|x| x == "jsonl") + .unwrap_or(false) + }) + .filter_map(|entry| { + let modified = entry.metadata().ok()?.modified().ok()?; + Some((modified, entry.into_path())) + }) + .collect(); + files.sort_by(|a, b| b.0.cmp(&a.0)); + + for (_, path) in files.into_iter().take(RECENT_QUOTA_FILES) { + let Ok(mut file) = fs::File::open(path) else { + continue; + }; + let Ok(size) = file.metadata().map(|meta| meta.len()) else { + continue; + }; + let start = size.saturating_sub(QUOTA_TAIL_BYTES); + if file.seek(SeekFrom::Start(start)).is_err() { + continue; + } + let mut bytes = Vec::with_capacity((size - start) as usize); + if file.read_to_end(&mut bytes).is_err() { + continue; + } + let skip = if start == 0 { + 0 + } else { + bytes + .iter() + .position(|byte| *byte == b'\n') + .map(|i| i + 1) + .unwrap_or(bytes.len()) + }; + let mut state = FileState::default(); + for line in bytes[skip..].split(|byte| *byte == b'\n') { + let Ok(text) = std::str::from_utf8(line) else { + continue; + }; + if text.contains("\"rate_limits\"") { + let _ = self.parse_codex_line(text, &mut state); } } - let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); } } @@ -502,7 +603,12 @@ impl Store { /// Keep the newest rate-limit snapshot (files are walked in arbitrary order, /// so compare timestamps rather than trusting encounter order). fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64) { - if self.codex_quota.as_ref().map(|q| q.as_of_ms >= ts_ms).unwrap_or(false) { + let slot = match rl.get("limit_id").and_then(|value| value.as_str()) { + None | Some("codex") => &mut self.codex_quota, + Some("codex_bengalfox") => &mut self.codex_spark_quota, + _ => return, + }; + if slot.as_ref().map(|q| q.as_of_ms >= ts_ms).unwrap_or(false) { return; } let win = |k: &str| rl.get(k); @@ -514,7 +620,7 @@ impl Store { if p.is_none() && s.is_none() { return; } - self.codex_quota = Some(Quota { + *slot = Some(Quota { plan: rl .get("plan_type") .and_then(|x| x.as_str()) @@ -816,9 +922,31 @@ mod tests { index: HashMap::new(), manifest: Manifest::default(), codex_quota: None, + codex_spark_quota: None, } } + #[test] + fn keeps_general_and_spark_quotas_separate() { + let mut store = empty_store(); + let snapshot = |limit_id: &str, used_percent: f64| { + serde_json::json!({ + "limit_id": limit_id, + "primary": { + "used_percent": used_percent, + "window_minutes": 10080, + "resets_at": 123 + } + }) + }; + + store.update_codex_quota(&snapshot("codex", 24.0), 100); + store.update_codex_quota(&snapshot("codex_bengalfox", 7.0), 200); + + assert_eq!(store.codex_quota.unwrap().primary_pct, 24.0); + assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 7.0); + } + #[test] fn extracts_codex_skill_names_from_unix_and_windows_paths() { let input = r#"sed ~/.codex/skills/find-skills/SKILL.md && type C:\Users\me\.agents\skills\release-notes\SKILL.md && cat ~/.codex/skills/.system/openai-docs/SKILL.md && cat ~/.codex/plugins/cache/bundled/skills/browser/SKILL.md"#; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c5230ce..0b0a4f0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.4", + "version": "1.3.5", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/data.ts b/src/data.ts index b2d4a9a..1dfd44e 100644 --- a/src/data.ts +++ b/src/data.ts @@ -30,7 +30,7 @@ export interface Quota { export interface Scope { id: string; label: string; color: string; day: PeriodReport; week: PeriodReport; month: PeriodReport; - heatmap: HeatDay[]; quota: Quota | null; + heatmap: HeatDay[]; quota: Quota | null; sparkQuota?: Quota | null; } export interface Dashboard { scopes: Scope[]; todayTokens: number; generatedAt: string; From b26be8b81905c0ae93b89fbe21d47fdacede9b4e Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 19:42:54 -0700 Subject: [PATCH 20/67] feat: add project settlement exports --- README-zh.md | 1 + README.md | 1 + src-tauri/src/lib.rs | 27 +++++++ src-tauri/src/model.rs | 14 ++++ src-tauri/src/parser.rs | 77 ++++++++++++++++++- src-tauri/src/store.rs | 159 +++++++++++++++++++++++++++++++++++++++- src/App.tsx | 77 ++++++++++++++++++- src/data.ts | 3 +- 8 files changed, 353 insertions(+), 6 deletions(-) diff --git a/README-zh.md b/README-zh.md index 89c75e6..5c2ca4b 100644 --- a/README-zh.md +++ b/README-zh.md @@ -22,6 +22,7 @@ brew install --cask sunchj/tokenscope/tokenscope - 点击打开面板:Day / Week / Month 切换,以及按起止日期(含首尾日)的自定义区间筛选 - **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions +- 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** - **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 diff --git a/README.md b/README.md index 9f1c3f8..d5f8913 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ brew install --cask sunchj/tokenscope/tokenscope - Click to open the panel: Day / Week / Month toggle plus an inclusive custom date-range filter - **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions +- Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content - Three breakdowns: **by model** / **by MCP call** / **by Skill call** - **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index daa04a4..1371927 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -770,6 +770,32 @@ fn save_screenshot(data_url: String) -> Result { Ok(path.to_string_lossy().into_owned()) } +/// Save a project settlement export to Desktop. The frontend supplies only a +/// compact generated CSV; raw JSONL content never leaves the Rust data layer. +#[tauri::command] +fn save_project_export(csv: String, label: String) -> Result { + if csv.len() > 5 * 1024 * 1024 { + return Err("export is too large".to_string()); + } + let label: String = label + .chars() + .filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ' ' | '-' | '_')) + .take(48) + .collect(); + let dir = dirs::desktop_dir() + .ok_or_else(|| "could not resolve the Desktop directory".to_string())?; + let stamp = chrono::Local::now().format("%Y-%m-%d at %H.%M.%S"); + let name = if label.trim().is_empty() { + format!("Tokenscope Projects {stamp}.csv") + } else { + format!("Tokenscope {} {stamp}.csv", label.trim()) + }; + let path = dir.join(name); + std::fs::write(&path, csv.as_bytes()) + .map_err(|error| format!("failed to write export: {error}"))?; + Ok(path.to_string_lossy().into_owned()) +} + /// For CLI/example validation against real logs. pub fn dashboard_json() -> String { serde_json::to_string_pretty(&parser::build_dashboard()).unwrap_or_default() @@ -835,6 +861,7 @@ pub fn run() { get_dashboard, get_range_dashboard, save_screenshot, + save_project_export, begin_drag, set_dashboard_shortcut ]) diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 599b4ca..56ddecc 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -29,6 +29,19 @@ pub struct NamedCount { pub count: u64, } +/// Settlement summary for one local project/repository in the selected period. +/// The stable id is a hash of the local project root; raw paths never cross the +/// Tauri boundary. +#[derive(Debug, Clone, Serialize)] +pub struct ProjectStat { + pub id: String, + pub name: String, + pub tokens: f64, + pub cost: f64, + pub requests: u64, + pub sessions: u64, +} + #[derive(Debug, Clone, Serialize, Default)] pub struct Metrics { #[serde(rename = "totalTokens")] @@ -70,6 +83,7 @@ pub struct PeriodReport { pub metrics: Metrics, pub series: Vec, pub models: Vec, + pub projects: Vec, pub mcp: Vec, pub skills: Vec, #[serde(rename = "reqTrend")] diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 92b9798..dda9169 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -27,6 +27,8 @@ struct Event { cost: f64, // USD (differentiated by token type), 0 if unknown model priced: bool, // whether a price was found for this model agent: &'static str, // owning agent id (interned via agent_def) + project_id: String, + project_name: String, mcp: Vec, // user-installed server names called in this msg skills: Vec, // user-installed skill names called in this msg } @@ -126,7 +128,7 @@ pub fn build_dashboard() -> Dashboard { .events .iter() .filter(|raw| raw.ts_ms >= report_cutoff) - .map(|r| compute_event(r, &cfg, &pricing)) + .map(|r| compute_event(r, store.project_for(r), &cfg, &pricing)) .collect(); let today = now.date_naive(); @@ -245,7 +247,7 @@ pub fn build_range_dashboard(start: NaiveDate, end: NaiveDate) -> Result= previous_start && date <= end }) - .map(|raw| compute_event(raw, &cfg, &pricing)) + .map(|raw| compute_event(raw, store.project_for(raw), &cfg, &pricing)) .collect(); let present: Vec<&AgentDef> = AGENTS .iter() @@ -332,6 +334,8 @@ fn clone_event(e: &Event) -> Event { cost: e.cost, priced: e.priced, agent: e.agent, + project_id: e.project_id.clone(), + project_name: e.project_name.clone(), mcp: e.mcp.clone(), skills: e.skills.clone(), } @@ -392,7 +396,12 @@ fn set_installed_counts(report: &mut PeriodReport, agent_scope: &str, cfg: &User /// Derive a computed Event from a stored RawEvent, applying the *current* user /// config (MCP/Skill whitelist) and prices. This is why these aren't baked into /// the store: installing an MCP or a price refresh applies retroactively. -fn compute_event(r: &RawEvent, cfg: &UserConfig, pricing: &Pricing) -> Event { +fn compute_event( + r: &RawEvent, + project: Option<&crate::store::ProjectRef>, + cfg: &UserConfig, + pricing: &Pricing, +) -> Event { let ts = DateTime::from_timestamp_millis(r.ts_ms) .unwrap_or_default() .with_timezone(&Local); @@ -423,6 +432,10 @@ fn compute_event(r: &RawEvent, cfg: &UserConfig, pricing: &Pricing) -> Event { cost: cost_opt.unwrap_or(0.0), priced: cost_opt.is_some(), agent: agent_def(&r.agent).id, + project_id: project.map(|project| project.id.clone()).unwrap_or_default(), + project_name: project + .map(|project| project.name.clone()) + .unwrap_or_else(|| "Other".to_string()), mcp, skills, } @@ -443,10 +456,20 @@ struct Agg { model_cost: HashMap, model_priced: HashMap, model_agent: HashMap, + projects: HashMap, mcp_counts: HashMap, skill_counts: HashMap, } +#[derive(Default)] +struct ProjectAgg { + name: String, + tokens: f64, + cost: f64, + requests: u64, + sessions: HashSet, +} + impl Agg { fn add(&mut self, e: &Event) { self.input += e.input; @@ -468,6 +491,18 @@ impl Agg { *self.model_priced.entry(e.model.clone()).or_default() |= e.priced; self.model_agent.entry(e.model.clone()).or_insert(e.agent); } + if !e.project_id.is_empty() { + let project = self.projects.entry(e.project_id.clone()).or_default(); + project.name = e.project_name.clone(); + project.tokens += e.input + e.cache + e.output; + project.cost += e.cost; + if !e.model.is_empty() { + project.requests += 1; + } + if !e.session.is_empty() { + project.sessions.insert(e.session.clone()); + } + } for s in &e.mcp { self.mcp_calls += 1; *self.mcp_counts.entry(s.clone()).or_default() += 1; @@ -514,6 +549,32 @@ impl Agg { v } + fn projects(&self) -> Vec { + let mut projects: Vec = self + .projects + .iter() + .map(|(id, project)| ProjectStat { + id: id.clone(), + name: project.name.clone(), + tokens: project.tokens / 1e6, + cost: (project.cost * 1_000_000.0).round() / 1_000_000.0, + requests: project.requests, + sessions: project.sessions.len() as u64, + }) + .collect(); + projects.sort_by(|a, b| { + b.cost + .partial_cmp(&a.cost) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| { + b.tokens + .partial_cmp(&a.tokens) + .unwrap_or(std::cmp::Ordering::Equal) + }) + }); + projects + } + fn metrics(&self, delta_tokens: f64, delta_cost: f64) -> Metrics { Metrics { total_tokens: (self.input + self.cache + self.output) / 1e6, @@ -597,6 +658,7 @@ fn report_day(events: &[Event], now: DateTime, palette: &[&str]) -> Perio ), series, models: agg.models(palette), + projects: agg.projects(), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -665,6 +727,7 @@ fn report_week(events: &[Event], now: DateTime, palette: &[&str]) -> Peri ), series, models: agg.models(palette), + projects: agg.projects(), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -742,6 +805,7 @@ fn report_month(events: &[Event], now: DateTime, palette: &[&str]) -> Per ), series, models: agg.models(palette), + projects: agg.projects(), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -858,6 +922,7 @@ fn report_range( ), series, models: agg.models(palette), + projects: agg.projects(), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: request_buckets, @@ -936,6 +1001,8 @@ mod tests { cost: tokens / 1e6, priced: true, agent: AGENT_CLAUDE, + project_id: "project-test".to_string(), + project_name: "Test project".to_string(), mcp: Vec::new(), skills: Vec::new(), } @@ -959,6 +1026,10 @@ mod tests { assert_eq!(report.metrics.total_tokens, 3.0); assert_eq!(report.metrics.requests, 2); assert_eq!(report.metrics.delta_tokens, 200.0); + assert_eq!(report.projects.len(), 1); + assert_eq!(report.projects[0].name, "Test project"); + assert_eq!(report.projects[0].tokens, 3.0); + assert_eq!(report.projects[0].requests, 2); assert_eq!(report.series.len(), 2); assert_eq!(report.series[0].input, 1.0); assert_eq!(report.series[1].input, 2.0); diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 41310a0..e5e5146 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -17,12 +17,18 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::io::{Read, Seek, SeekFrom}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use walkdir::WalkDir; pub const AGENT_CLAUDE: &str = "claude"; pub const AGENT_CODEX: &str = "codex"; +#[derive(Serialize, Deserialize, Clone)] +pub struct ProjectRef { + pub id: String, + pub name: String, +} + #[derive(Serialize, Deserialize, Clone)] pub struct RawEvent { pub ts_ms: i64, @@ -90,6 +96,7 @@ pub struct Store { // Latest general and model-specific Codex rate-limit snapshots. pub codex_quota: Option, pub codex_spark_quota: Option, + projects_by_source: HashMap, } // Bump when the parsing/extraction logic changes in a way that requires @@ -105,6 +112,7 @@ pub struct Store { // v8: retain complete history for custom-range tracking and settlement. const STORE_VERSION: u32 = 8; const QUOTA_CACHE_VERSION: u32 = 1; +const PROJECT_CACHE_VERSION: u32 = 1; // One-time quota migration: at most 32 × 64 KiB = 2 MiB of log content. const RECENT_QUOTA_FILES: usize = 32; const QUOTA_TAIL_BYTES: u64 = 64 * 1024; @@ -149,6 +157,74 @@ fn cache_dir() -> Option { Some(d) } +fn project_group(source: &str, agent: &str, state: Option<&FileState>) -> String { + if agent == AGENT_CLAUDE { + return Path::new(source) + .parent() + .unwrap_or_else(|| Path::new(source)) + .to_string_lossy() + .to_string(); + } + state + .filter(|state| !state.cwd.is_empty()) + .map(|state| state.cwd.clone()) + .unwrap_or_else(|| source.to_string()) +} + +fn read_cwd_prefix(path: &Path) -> Option { + let file = fs::File::open(path).ok()?; + let mut bytes = Vec::with_capacity(64 * 1024); + file.take(64 * 1024).read_to_end(&mut bytes).ok()?; + for line in bytes.split(|byte| *byte == b'\n') { + let Ok(value) = serde_json::from_slice::(line) else { + continue; + }; + let cwd = value + .get("cwd") + .or_else(|| value.get("payload").and_then(|payload| payload.get("cwd"))) + .and_then(|cwd| cwd.as_str()); + if let Some(cwd) = cwd.filter(|cwd| !cwd.is_empty()) { + return Some(cwd.to_string()); + } + } + None +} + +fn project_root(cwd: &str) -> PathBuf { + let path = PathBuf::from(cwd); + for ancestor in path.ancestors() { + if ancestor.join(".git").exists() { + return ancestor.to_path_buf(); + } + } + path +} + +fn stable_project_id(key: &str) -> String { + // FNV-1a: tiny, deterministic across platforms and Rust releases. + let mut hash = 0xcbf29ce484222325u64; + for byte in key.as_bytes() { + hash ^= *byte as u64; + hash = hash.wrapping_mul(0x100000001b3); + } + format!("project-{hash:016x}") +} + +fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { + let root = cwd.map(project_root).unwrap_or_else(|| PathBuf::from(fallback)); + let key = root.to_string_lossy(); + let name = root + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| !name.is_empty()) + .unwrap_or("Other") + .to_string(); + ProjectRef { + id: stable_project_id(&key), + name, + } +} + impl Store { /// Load persisted events + offset manifest (empty on first run). pub fn load() -> Self { @@ -156,6 +232,7 @@ impl Store { let mut manifest = Manifest::default(); let mut codex_quota = None; let mut codex_spark_quota = None; + let mut projects_by_source = HashMap::new(); let mut version_ok = false; let mut quota_cache_ok = false; if let Some(dir) = cache_dir() { @@ -196,6 +273,16 @@ impl Store { .ok() .and_then(|t| serde_json::from_str::(&t).ok()); } + let project_cache_ok = fs::read_to_string(dir.join("project_version")) + .ok() + .and_then(|text| text.trim().parse::().ok()) + == Some(PROJECT_CACHE_VERSION); + if project_cache_ok { + projects_by_source = fs::read_to_string(dir.join("projects.json")) + .ok() + .and_then(|text| serde_json::from_str(&text).ok()) + .unwrap_or_default(); + } } } let index = events @@ -210,6 +297,7 @@ impl Store { manifest, codex_quota, codex_spark_quota, + projects_by_source, }; if version_ok && !quota_cache_ok { store.rebuild_codex_quotas(); @@ -217,6 +305,11 @@ impl Store { store.save_quota_cache(&dir); } } + if version_ok && store.refresh_projects() { + if let Some(dir) = cache_dir() { + store.save_project_cache(&dir); + } + } store } @@ -234,10 +327,21 @@ impl Store { let _ = write_atomic(&dir.join("offsets.json"), t.as_bytes()); } self.save_quota_cache(&dir); + self.save_project_cache(&dir); let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); } } + fn save_project_cache(&self, dir: &Path) { + if let Ok(text) = serde_json::to_string(&self.projects_by_source) { + let _ = write_atomic(&dir.join("projects.json"), text.as_bytes()); + } + let _ = write_atomic( + &dir.join("project_version"), + PROJECT_CACHE_VERSION.to_string().as_bytes(), + ); + } + fn save_quota_cache(&self, dir: &std::path::Path) { let save = |name: &str, quota: &Option| match quota { Some(quota) => { @@ -334,6 +438,55 @@ impl Store { dirs } + pub fn project_for(&self, event: &RawEvent) -> Option<&ProjectRef> { + self.projects_by_source.get(&event.source) + } + + /// Resolve each source file to a stable project id and short display name. + /// Existing Codex manifests already retain cwd. For old Claude caches, read + /// at most one 64 KiB prefix per project directory, then persist the mapping + /// so subsequent launches do no project-discovery IO. + fn refresh_projects(&mut self) -> bool { + let sources: HashMap = self + .events + .iter() + .filter(|event| !event.source.is_empty()) + .map(|event| (event.source.clone(), event.agent.clone())) + .collect(); + let mut resolved_groups: HashMap = HashMap::new(); + + for (source, agent) in &sources { + if let Some(project) = self.projects_by_source.get(source) { + resolved_groups + .entry(project_group(source, agent, self.manifest.files.get(source))) + .or_insert_with(|| project.clone()); + } + } + + let mut dirty = false; + for (source, agent) in sources { + if self.projects_by_source.contains_key(&source) { + continue; + } + let state = self.manifest.files.get(&source); + let group = project_group(&source, &agent, state); + let project = if let Some(project) = resolved_groups.get(&group) { + project.clone() + } else { + let cwd = state + .filter(|state| !state.cwd.is_empty()) + .map(|state| state.cwd.clone()) + .or_else(|| read_cwd_prefix(Path::new(&source))); + let project = project_ref(cwd.as_deref(), &group); + resolved_groups.insert(group, project.clone()); + project + }; + self.projects_by_source.insert(source, project); + dirty = true; + } + dirty + } + /// Rebuild the id→index map after the `events` vector is mutated wholesale /// (purge/prune shift positions, so partial updates aren't enough). fn rebuild_index(&mut self) { @@ -366,6 +519,9 @@ impl Store { dirty = true; } } + if self.refresh_projects() { + dirty = true; + } dirty } @@ -923,6 +1079,7 @@ mod tests { manifest: Manifest::default(), codex_quota: None, codex_spark_quota: None, + projects_by_source: HashMap::new(), } } diff --git a/src/App.tsx b/src/App.tsx index 8864d7c..67a7743 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, Scope, Quota, Theme, TH, + Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, ProjectStat, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -395,6 +395,44 @@ function AgentLegend({ t, slices, cachedPct }: ); } +function ProjectSettlement({ projects, theme, onExport }: + { projects: ProjectStat[]; theme: Theme; onExport: (projects: ProjectStat[]) => void }) { + const t = theme; + const [selected, setSelected] = useState(""); + useEffect(() => { + if (selected && !projects.some((project) => project.id === selected)) setSelected(""); + }, [projects, selected]); + const rows = selected ? projects.filter((project) => project.id === selected) : projects; + return ( +
+
+ + + +
+ {rows.map((project) => ( +
+ {project.name} + {fmtMoney(project.cost)} + {fmtInt(project.requests)} requests · {fmtInt(project.sessions)} sessions + {fmtTokens(project.tokens)} +
+ ))} +
+ ); +} + // Codex rate-limit card: the two rolling windows (5h + weekly) straight from // the session logs — data Claude doesn't expose. Bars turn amber near the cap. function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { @@ -701,6 +739,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const viewKey = activeRange ? `${activeRange.startDate}:${activeRange.endDate}` : period; const M = P.metrics; const models = P.models; + const projects = P.projects ?? []; const [totalModel, setTotalModel] = useState(""); // The All scope can contain the same model once per agent. The selector is // model-based, so combine those rows before calculating its Total and cost. @@ -755,6 +794,36 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater setToast({ msg, ok }); toastTimer.current = window.setTimeout(() => setToast(null), 1800); }; + const exportProjects = async (rows: ProjectStat[]) => { + if (!rows.length) { showToast("No project usage to export", false); return; } + const csvCell = (value: string | number) => `"${String(value).replace(/"/g, '""')}"`; + const periodLabel = activeRange ? formatRange(activeRange) : period; + const csv = "\uFEFF" + [ + ["Scope", "Period", "Project", "Tokens", "Estimated cost USD", "Requests", "Sessions"], + ...rows.map((project) => [ + scope.label, periodLabel, project.name, Math.round(project.tokens * 1_000_000), + project.cost.toFixed(6), project.requests, project.sessions, + ]), + ].map((row) => row.map(csvCell).join(",")).join("\r\n"); + const label = `${scope.label} ${activeRange ? `${activeRange.startDate} to ${activeRange.endDate}` : period}`; + try { + const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; + if (inTauri) { + await invoke("save_project_export", { csv, label }); + showToast("Project CSV saved to Desktop", true); + } else { + const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" })); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "tokenscope-projects.csv"; + anchor.click(); + URL.revokeObjectURL(url); + showToast("Project CSV downloaded", true); + } + } catch { + showToast("Project export failed", false); + } + }; const captureScreenshot = async () => { if (shotBusy) return; const el = document.querySelector(".om-scroll"); @@ -946,6 +1015,12 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater {unpricedModels.map((m) => m.name).join(", ")}
)} + {projects.length > 0 && ( + <> + + + + )} {/* footer stats */}
diff --git a/src/data.ts b/src/data.ts index 1dfd44e..d151274 100644 --- a/src/data.ts +++ b/src/data.ts @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; export interface SeriesPoint { label: string; full: string; input: number; cache: number; output: number } export interface ModelStat { name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean; agent: string } export interface NamedCount { name: string; count: number } +export interface ProjectStat { id: string; name: string; tokens: number; cost: number; requests: number; sessions: number } export interface Metrics { totalTokens: number; inputTokens: number; cacheTokens: number; outputTokens: number; cost: number; mcpCalls: number; skillCalls: number; requests: number; sessions: number; @@ -12,7 +13,7 @@ export interface Metrics { // split bar, `values` (aligned with series) drives the stacked chart. export interface AgentSlice { id: string; label: string; color: string; tokens: number; values: number[] } export interface PeriodReport { - metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; + metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; projects: ProjectStat[]; mcp: NamedCount[]; skills: NamedCount[]; reqTrend: number[]; costTrend: number[]; agents: AgentSlice[]; } From fbd569ce49c26e7b4480b2ca14f5c4f6d262569f Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 19:52:00 -0700 Subject: [PATCH 21/67] feat: track failed and aborted work --- README-zh.md | 1 + README.md | 1 + src-tauri/src/model.rs | 18 ++ src-tauri/src/parser.rs | 214 +++++++++++++++++++-- src-tauri/src/store.rs | 409 ++++++++++++++++++++++++++++++++++++++-- src/App.tsx | 38 +++- src/data.ts | 7 +- 7 files changed, 659 insertions(+), 29 deletions(-) diff --git a/README-zh.md b/README-zh.md index 5c2ca4b..a53d4dc 100644 --- a/README-zh.md +++ b/README-zh.md @@ -23,6 +23,7 @@ brew install --cask sunchj/tokenscope/tokenscope - **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 +- 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** - **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 diff --git a/README.md b/README.md index d5f8913..035a04f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ brew install --cask sunchj/tokenscope/tokenscope - **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content +- Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward - Three breakdowns: **by model** / **by MCP call** / **by Skill call** - **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 56ddecc..ea6a96a 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -42,6 +42,21 @@ pub struct ProjectStat { pub sessions: u64, } +#[derive(Debug, Clone, Serialize, Default)] +pub struct ReliabilityStats { + #[serde(rename = "completedTurns")] + pub completed_turns: u64, + #[serde(rename = "abortedTurns")] + pub aborted_turns: u64, + #[serde(rename = "toolErrors")] + pub tool_errors: u64, + pub denials: u64, + #[serde(rename = "wastedTokens")] + pub wasted_tokens: f64, + #[serde(rename = "wastedCost")] + pub wasted_cost: f64, +} + #[derive(Debug, Clone, Serialize, Default)] pub struct Metrics { #[serde(rename = "totalTokens")] @@ -84,6 +99,7 @@ pub struct PeriodReport { pub series: Vec, pub models: Vec, pub projects: Vec, + pub reliability: ReliabilityStats, pub mcp: Vec, pub skills: Vec, #[serde(rename = "reqTrend")] @@ -147,6 +163,8 @@ pub struct Dashboard { pub today_tokens: f64, // M tokens across all agents, for the tray label #[serde(rename = "generatedAt")] pub generated_at: String, + #[serde(rename = "telemetrySinceMs")] + pub telemetry_since_ms: i64, } /// One scope's aggregation for an inclusive, user-selected date range. diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index dda9169..50fb648 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -33,6 +33,20 @@ struct Event { skills: Vec, // user-installed skill names called in this msg } +#[derive(Clone)] +struct TurnEvent { + ts: DateTime, + agent: &'static str, + outcome: String, + input: f64, + cache_creation: f64, + cache_read: f64, + output: f64, + cost: f64, + tool_errors: u64, + denials: u64, +} + /// Static per-agent identity: label + accent + a 5-step chart palette (rank /// shades of the accent, ending in a muted overflow tone). pub struct AgentDef { @@ -114,6 +128,7 @@ pub fn build_dashboard() -> Dashboard { } let codex_quota = store.codex_quota.clone(); let codex_spark_quota = store.codex_spark_quota.clone(); + let telemetry_since_ms = store.telemetry_since_ms; // 2. Aggregate: apply current config + prices, slice by current time. let cfg = UserConfig::load(&store.codex_project_dirs()); @@ -130,6 +145,12 @@ pub fn build_dashboard() -> Dashboard { .filter(|raw| raw.ts_ms >= report_cutoff) .map(|r| compute_event(r, store.project_for(r), &cfg, &pricing)) .collect(); + let turns: Vec = store + .turns + .iter() + .filter(|turn| turn.ts_ms >= report_cutoff) + .map(|turn| compute_turn(turn, &pricing)) + .collect(); let today = now.date_naive(); @@ -145,7 +166,9 @@ pub fn build_dashboard() -> Dashboard { let scopes = if present.len() <= 1 { // Single (or no) source → one scope, classic green UI, no chips. let agent = present.first().map(|a| a.id).unwrap_or(AGENT_CLAUDE); - let mut scope = build_scope("all", "All", "", &events, PALETTE, agent, &cfg, now); + let mut scope = build_scope( + "all", "All", "", &events, &turns, PALETTE, agent, &cfg, now, + ); if agent == AGENT_CODEX { scope.quota = codex_quota; scope.spark_quota = codex_spark_quota; @@ -157,14 +180,29 @@ pub fn build_dashboard() -> Dashboard { let mut per_agent: Vec = Vec::new(); for a in &present { let filtered: Vec = events.iter().filter(|e| e.agent == a.id).map(clone_event).collect(); - let mut s = build_scope(a.id, a.label, a.color, &filtered, &a.palette, a.id, &cfg, now); + let filtered_turns: Vec = turns + .iter() + .filter(|turn| turn.agent == a.id) + .cloned() + .collect(); + let mut s = build_scope( + a.id, + a.label, + a.color, + &filtered, + &filtered_turns, + &a.palette, + a.id, + &cfg, + now, + ); if a.id == AGENT_CODEX { s.quota = codex_quota.clone(); s.spark_quota = codex_spark_quota.clone(); } per_agent.push(s); } - let mut all = build_scope("all", "All", "", &events, PALETTE, "", &cfg, now); + let mut all = build_scope("all", "All", "", &events, &turns, PALETTE, "", &cfg, now); // All-scope model rows keep each agent's palette (rank within agent), and // each period gains per-agent slices for the split bar + stacked chart. for idx in 0..3usize { @@ -210,6 +248,7 @@ pub fn build_dashboard() -> Dashboard { scopes, today_tokens, generated_at: now.to_rfc3339(), + telemetry_since_ms, } } @@ -249,6 +288,18 @@ pub fn build_range_dashboard(start: NaiveDate, end: NaiveDate) -> Result = store + .turns + .iter() + .filter(|turn| { + let Some(timestamp) = DateTime::from_timestamp_millis(turn.ts_ms) else { + return false; + }; + let date = timestamp.with_timezone(&Local).date_naive(); + date >= previous_start && date <= end + }) + .map(|turn| compute_turn(turn, &pricing)) + .collect(); let present: Vec<&AgentDef> = AGENTS .iter() .filter(|agent| present_ids.contains(agent.id)) @@ -256,7 +307,7 @@ pub fn build_range_dashboard(start: NaiveDate, end: NaiveDate) -> Result Result = turns + .iter() + .filter(|turn| turn.agent == agent.id) + .cloned() + .collect(); + let mut report = report_range( + &filtered, + &filtered_turns, + start, + end, + &agent.palette, + ); set_installed_counts(&mut report, agent.id, &cfg); per_agent.push(RangeScope { id: agent.id.to_string(), @@ -278,7 +340,7 @@ pub fn build_range_dashboard(start: NaiveDate, end: NaiveDate) -> Result, ) -> Scope { let today = now.date_naive(); - let mut day = report_day(events, now, palette); - let mut week = report_week(events, now, palette); - let mut month = report_month(events, now, palette); + let mut day = report_day(events, turns, now, palette); + let mut week = report_week(events, turns, now, palette); + let mut month = report_month(events, turns, now, palette); let heatmap = build_heatmap(events, today); for r in [&mut day, &mut week, &mut month] { @@ -441,6 +504,67 @@ fn compute_event( } } +fn compute_turn(turn: &crate::store::TurnTelemetry, pricing: &Pricing) -> TurnEvent { + let ts = DateTime::from_timestamp_millis(turn.ts_ms) + .unwrap_or_default() + .with_timezone(&Local); + let cost = pricing + .cost( + &turn.model, + turn.input_tokens, + turn.output_tokens, + turn.cache_creation_tokens, + turn.cache_read_tokens, + ) + .or_else(|| { + pricing.cost( + &normalize_model(&turn.model), + turn.input_tokens, + turn.output_tokens, + turn.cache_creation_tokens, + turn.cache_read_tokens, + ) + }) + .unwrap_or(0.0); + TurnEvent { + ts, + agent: agent_def(&turn.agent).id, + outcome: turn.outcome.clone(), + input: turn.input_tokens, + cache_creation: turn.cache_creation_tokens, + cache_read: turn.cache_read_tokens, + output: turn.output_tokens, + cost, + tool_errors: turn.tool_errors, + denials: turn.denials, + } +} + +fn reliability(turns: &[TurnEvent], start: NaiveDate, end: NaiveDate) -> ReliabilityStats { + let mut stats = ReliabilityStats::default(); + for turn in turns { + let date = turn.ts.date_naive(); + if date < start || date > end { + continue; + } + match turn.outcome.as_str() { + "completed" => stats.completed_turns += 1, + "aborted" => { + stats.aborted_turns += 1; + stats.wasted_tokens += + turn.input + turn.cache_creation + turn.cache_read + turn.output; + stats.wasted_cost += turn.cost; + } + _ => {} + } + stats.tool_errors += turn.tool_errors; + stats.denials += turn.denials; + } + stats.wasted_tokens = (stats.wasted_tokens / 1e6 * 1_000_000.0).round() / 1_000_000.0; + stats.wasted_cost = (stats.wasted_cost * 1_000_000.0).round() / 1_000_000.0; + stats +} + // ── aggregation helpers ──────────────────────────────────────────── #[derive(Default)] struct Agg { @@ -604,7 +728,12 @@ fn pct_delta(cur: f64, prev: f64) -> f64 { } // ── Day report: today, 24 hourly buckets ─────────────────────────── -fn report_day(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { +fn report_day( + events: &[Event], + turns: &[TurnEvent], + now: DateTime, + palette: &[&str], +) -> PeriodReport { let today = now.date_naive(); let yesterday = today - Duration::days(1); let mut agg = Agg::default(); @@ -659,6 +788,7 @@ fn report_day(events: &[Event], now: DateTime, palette: &[&str]) -> Perio series, models: agg.models(palette), projects: agg.projects(), + reliability: reliability(turns, today, today), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -668,7 +798,12 @@ fn report_day(events: &[Event], now: DateTime, palette: &[&str]) -> Perio } // ── Week report: current calendar week (Mon-Sun) vs previous week ──── -fn report_week(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { +fn report_week( + events: &[Event], + turns: &[TurnEvent], + now: DateTime, + palette: &[&str], +) -> PeriodReport { let today = now.date_naive(); // Monday of the current week (Mon=0 … Sun=6). let start = today - Duration::days(today.weekday().num_days_from_monday() as i64); @@ -728,6 +863,7 @@ fn report_week(events: &[Event], now: DateTime, palette: &[&str]) -> Peri series, models: agg.models(palette), projects: agg.projects(), + reliability: reliability(turns, start, next_start - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -737,7 +873,12 @@ fn report_week(events: &[Event], now: DateTime, palette: &[&str]) -> Peri } // ── Month report: current calendar month vs previous calendar month ── -fn report_month(events: &[Event], now: DateTime, palette: &[&str]) -> PeriodReport { +fn report_month( + events: &[Event], + turns: &[TurnEvent], + now: DateTime, + palette: &[&str], +) -> PeriodReport { let today = now.date_naive(); let (y, m) = (today.year(), today.month()); let cur_first = NaiveDate::from_ymd_opt(y, m, 1).unwrap(); @@ -806,6 +947,7 @@ fn report_month(events: &[Event], now: DateTime, palette: &[&str]) -> Per series, models: agg.models(palette), projects: agg.projects(), + reliability: reliability(turns, cur_first, next_first - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -817,6 +959,7 @@ fn report_month(events: &[Event], now: DateTime, palette: &[&str]) -> Per // ── Custom report: inclusive dates vs the preceding equal-length range ──── fn report_range( events: &[Event], + turns: &[TurnEvent], start: NaiveDate, end: NaiveDate, palette: &[&str], @@ -923,6 +1066,7 @@ fn report_range( series, models: agg.models(palette), projects: agg.projects(), + reliability: reliability(turns, start, end), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: request_buckets, @@ -1018,6 +1162,7 @@ mod tests { ]; let report = report_range( &events, + &[], NaiveDate::from_ymd_opt(2026, 7, 2).unwrap(), NaiveDate::from_ymd_opt(2026, 7, 3).unwrap(), PALETTE, @@ -1038,12 +1183,13 @@ mod tests { #[test] fn custom_single_day_uses_hours_and_long_ranges_cap_chart_buckets() { let day = NaiveDate::from_ymd_opt(2026, 7, 13).unwrap(); - let hourly = report_range(&[event(2026, 7, 13, 23, 500.0)], day, day, PALETTE); + let hourly = report_range(&[event(2026, 7, 13, 23, 500.0)], &[], day, day, PALETTE); assert_eq!(hourly.series.len(), 24); assert_eq!(hourly.metrics.total_tokens, 0.0005); assert_eq!(hourly.series[23].input, 0.0005); let long = report_range( + &[], &[], NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(), NaiveDate::from_ymd_opt(2026, 4, 10).unwrap(), @@ -1051,4 +1197,46 @@ mod tests { ); assert!(long.series.len() <= 48); } + + #[test] + fn reliability_counts_aborted_waste_and_tool_issues() { + let ts = Local + .with_ymd_and_hms(2026, 7, 13, 12, 0, 0) + .single() + .unwrap(); + let turns = vec![ + TurnEvent { + ts, + agent: AGENT_CODEX, + outcome: "aborted".to_string(), + input: 500_000.0, + cache_creation: 0.0, + cache_read: 400_000.0, + output: 100_000.0, + cost: 1.25, + tool_errors: 2, + denials: 1, + }, + TurnEvent { + ts, + agent: AGENT_CODEX, + outcome: "completed".to_string(), + input: 0.0, + cache_creation: 0.0, + cache_read: 0.0, + output: 0.0, + cost: 0.0, + tool_errors: 0, + denials: 0, + }, + ]; + let stats = reliability(&turns, ts.date_naive(), ts.date_naive()); + + assert_eq!(stats.completed_turns, 1); + assert_eq!(stats.aborted_turns, 1); + assert_eq!(stats.tool_errors, 2); + assert_eq!(stats.denials, 1); + assert_eq!(stats.wasted_tokens, 1.0); + assert_eq!(stats.wasted_cost, 1.25); + } } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index e5e5146..7f21088 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -29,6 +29,40 @@ pub struct ProjectRef { pub name: String, } +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(default)] +pub struct TurnTelemetry { + pub ts_ms: i64, + pub source: String, + pub agent: String, + pub session: String, + pub turn_id: String, + pub model: String, + pub outcome: String, + pub abort_reason: String, + pub input_tokens: f64, + pub cache_creation_tokens: f64, + pub cache_read_tokens: f64, + pub output_tokens: f64, + pub reasoning_tokens: f64, + pub duration_ms: u64, + pub ttft_ms: u64, + pub tool_errors: u64, + pub denials: u64, + pub compactions: u64, + pub context_tokens: f64, + pub context_window: f64, + // Claude can repeat one assistant message across several JSONL lines. + // Persist ids so incremental restarts never count its usage twice. + usage_ids: Vec, +} + +#[derive(Serialize, Deserialize, Default)] +struct TelemetryCache { + since_ms: i64, + turns: Vec, +} + #[derive(Serialize, Deserialize, Clone)] pub struct RawEvent { pub ts_ms: i64, @@ -78,6 +112,9 @@ struct FileState { // so incremental reads can still attribute later calls to their MCP server. #[serde(default)] mcp_tools: HashMap, + // Runtime-only source key used while appending telemetry records. + #[serde(skip)] + source: String, } #[derive(Serialize, Deserialize, Default)] @@ -97,6 +134,9 @@ pub struct Store { pub codex_quota: Option, pub codex_spark_quota: Option, projects_by_source: HashMap, + pub telemetry_since_ms: i64, + pub turns: Vec, + telemetry_index: HashMap, } // Bump when the parsing/extraction logic changes in a way that requires @@ -113,6 +153,7 @@ pub struct Store { const STORE_VERSION: u32 = 8; const QUOTA_CACHE_VERSION: u32 = 1; const PROJECT_CACHE_VERSION: u32 = 1; +const TELEMETRY_CACHE_VERSION: u32 = 1; // One-time quota migration: at most 32 × 64 KiB = 2 MiB of log content. const RECENT_QUOTA_FILES: usize = 32; const QUOTA_TAIL_BYTES: u64 = 64 * 1024; @@ -210,6 +251,10 @@ fn stable_project_id(key: &str) -> String { format!("project-{hash:016x}") } +fn telemetry_key(source: &str, turn_id: &str) -> String { + format!("{source}\0{turn_id}") +} + fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { let root = cwd.map(project_root).unwrap_or_else(|| PathBuf::from(fallback)); let key = root.to_string_lossy(); @@ -233,8 +278,11 @@ impl Store { let mut codex_quota = None; let mut codex_spark_quota = None; let mut projects_by_source = HashMap::new(); + let mut telemetry_since_ms = chrono::Utc::now().timestamp_millis(); + let mut turns = Vec::new(); let mut version_ok = false; let mut quota_cache_ok = false; + let mut telemetry_cache_ok = false; if let Some(dir) = cache_dir() { // If the cache was written by an older parser, discard it so ingest // does a full rescan and picks up newly-extracted facts. @@ -283,6 +331,20 @@ impl Store { .and_then(|text| serde_json::from_str(&text).ok()) .unwrap_or_default(); } + let telemetry_version_ok = fs::read_to_string(dir.join("telemetry_version")) + .ok() + .and_then(|text| text.trim().parse::().ok()) + == Some(TELEMETRY_CACHE_VERSION); + if telemetry_version_ok { + if let Some(cache) = fs::read_to_string(dir.join("telemetry.json")) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + { + telemetry_since_ms = cache.since_ms; + turns = cache.turns; + telemetry_cache_ok = true; + } + } } } let index = events @@ -291,6 +353,11 @@ impl Store { .filter(|(_, e)| !e.id.is_empty()) .map(|(i, e)| (e.id.clone(), i)) .collect(); + let telemetry_index = turns + .iter() + .enumerate() + .map(|(index, turn)| (telemetry_key(&turn.source, &turn.turn_id), index)) + .collect(); let mut store = Store { events, index, @@ -298,6 +365,9 @@ impl Store { codex_quota, codex_spark_quota, projects_by_source, + telemetry_since_ms, + turns, + telemetry_index, }; if version_ok && !quota_cache_ok { store.rebuild_codex_quotas(); @@ -310,6 +380,11 @@ impl Store { store.save_project_cache(&dir); } } + if version_ok && !telemetry_cache_ok { + if let Some(dir) = cache_dir() { + store.save_telemetry_cache(&dir); + } + } store } @@ -328,10 +403,25 @@ impl Store { } self.save_quota_cache(&dir); self.save_project_cache(&dir); + self.save_telemetry_cache(&dir); let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); } } + fn save_telemetry_cache(&self, dir: &Path) { + let cache = TelemetryCache { + since_ms: self.telemetry_since_ms, + turns: self.turns.clone(), + }; + if let Ok(text) = serde_json::to_string(&cache) { + let _ = write_atomic(&dir.join("telemetry.json"), text.as_bytes()); + } + let _ = write_atomic( + &dir.join("telemetry_version"), + TELEMETRY_CACHE_VERSION.to_string().as_bytes(), + ); + } + fn save_project_cache(&self, dir: &Path) { if let Ok(text) = serde_json::to_string(&self.projects_by_source) { let _ = write_atomic(&dir.join("projects.json"), text.as_bytes()); @@ -442,6 +532,48 @@ impl Store { self.projects_by_source.get(&event.source) } + fn turn_mut( + &mut self, + state: &FileState, + agent: &str, + ts_ms: i64, + ) -> Option<&mut TurnTelemetry> { + if state.source.is_empty() || state.turn_id.is_empty() { + return None; + } + if ts_ms > 0 && (self.telemetry_since_ms == 0 || ts_ms < self.telemetry_since_ms) { + self.telemetry_since_ms = ts_ms; + } + let key = telemetry_key(&state.source, &state.turn_id); + let index = if let Some(index) = self.telemetry_index.get(&key) { + *index + } else { + let index = self.turns.len(); + self.turns.push(TurnTelemetry { + ts_ms, + source: state.source.clone(), + agent: agent.to_string(), + session: state.session.clone(), + turn_id: state.turn_id.clone(), + model: state.model.clone(), + ..TurnTelemetry::default() + }); + self.telemetry_index.insert(key, index); + index + }; + let turn = &mut self.turns[index]; + if turn.ts_ms == 0 || (ts_ms > 0 && ts_ms < turn.ts_ms) { + turn.ts_ms = ts_ms; + } + if turn.session.is_empty() { + turn.session = state.session.clone(); + } + if !state.model.is_empty() { + turn.model = state.model.clone(); + } + Some(turn) + } + /// Resolve each source file to a stable project id and short display name. /// Existing Codex manifests already retain cwd. For old Claude caches, read /// at most one 64 KiB prefix per project directory, then persist the mapping @@ -499,6 +631,15 @@ impl Store { .collect(); } + fn rebuild_telemetry_index(&mut self) { + self.telemetry_index = self + .turns + .iter() + .enumerate() + .map(|(index, turn)| (telemetry_key(&turn.source, &turn.turn_id), index)) + .collect(); + } + /// Drop every event that came from `key`, then rebuild the index. Used before /// re-reading a truncated/rewritten file so re-ingestion is idempotent /// (otherwise the cross-line tool_use merge re-appends calls and id-less @@ -506,6 +647,8 @@ impl Store { fn purge_source(&mut self, key: &str) { self.events.retain(|e| e.source != key); self.rebuild_index(); + self.turns.retain(|turn| turn.source != key); + self.rebuild_telemetry_index(); } /// Incrementally read only the new bytes of new/changed JSONL files across @@ -560,6 +703,7 @@ impl Store { } None => FileState::default(), }; + state.source = key.clone(); let Ok(mut f) = fs::File::open(path) else { continue }; if f.seek(SeekFrom::Start(state.offset)).is_err() { @@ -588,7 +732,7 @@ impl Store { let Ok(s) = std::str::from_utf8(line) else { continue }; let parsed = match agent { AGENT_CODEX => self.parse_codex_line(s, &mut state), - _ => parse_claude_line(s), + _ => self.parse_claude_line(s, &mut state), }; if let Some(mut ev) = parsed { ev.source = key.clone(); @@ -656,16 +800,71 @@ impl Store { } "event_msg" => { let event_type = payload.get("type")?.as_str()?; - // Older Codex logs may not carry turn_context.turn_id. A task - // boundary is still enough to reset per-turn Skill dedup. - if event_type == "task_started" { - state.skills_seen.clear(); - return None; + let ts_ms = parse_ts(&v)?; + if let Some(turn_id) = payload.get("turn_id").and_then(|value| value.as_str()) { + if state.turn_id != turn_id { + state.turn_id = turn_id.to_string(); + state.skills_seen.clear(); + } } - if event_type != "token_count" { - return None; + match event_type { + "task_started" => { + state.skills_seen.clear(); + let context_window = payload + .get("model_context_window") + .and_then(|value| value.as_f64()) + .unwrap_or(0.0); + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.context_window = turn.context_window.max(context_window); + } + return None; + } + "task_complete" => { + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.outcome = "completed".to_string(); + turn.duration_ms = payload + .get("duration_ms") + .and_then(|value| value.as_u64()) + .unwrap_or(0); + turn.ttft_ms = payload + .get("time_to_first_token_ms") + .and_then(|value| value.as_u64()) + .unwrap_or(0); + } + return None; + } + "turn_aborted" => { + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.outcome = "aborted".to_string(); + turn.abort_reason = payload + .get("reason") + .and_then(|value| value.as_str()) + .unwrap_or("") + .to_string(); + turn.duration_ms = payload + .get("duration_ms") + .and_then(|value| value.as_u64()) + .unwrap_or(0); + } + return None; + } + "context_compacted" => { + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.compactions += 1; + } + return None; + } + "patch_apply_end" => { + if payload.get("success").and_then(|value| value.as_bool()) == Some(false) { + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.tool_errors += 1; + } + } + return None; + } + "token_count" => {} + _ => return None, } - let ts_ms = parse_ts(&v)?; if let Some(rl) = payload.get("rate_limits") { self.update_codex_quota(rl, ts_ms); } @@ -678,9 +877,23 @@ impl Store { let raw_in = g("input_tokens"); let cached = g("cached_input_tokens").min(raw_in); let out = g("output_tokens"); + let reasoning = g("reasoning_output_tokens").min(out); if raw_in + out <= 0.0 { return None; // rate-limit-only heartbeat, nothing to count } + let context_window = payload + .get("info") + .and_then(|info| info.get("model_context_window")) + .and_then(|value| value.as_f64()) + .unwrap_or(0.0); + if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { + turn.input_tokens += raw_in - cached; + turn.cache_read_tokens += cached; + turn.output_tokens += out; + turn.reasoning_tokens += reasoning; + turn.context_tokens = turn.context_tokens.max(raw_in); + turn.context_window = turn.context_window.max(context_window); + } Some(RawEvent { ts_ms, session: state.session.clone(), @@ -756,6 +969,118 @@ impl Store { } } + fn parse_claude_line(&mut self, line: &str, state: &mut FileState) -> Option { + let value: serde_json::Value = serde_json::from_str(line).ok()?; + let ts_ms = parse_ts(&value).unwrap_or(0); + if let Some(session) = value.get("sessionId").and_then(|item| item.as_str()) { + state.session = session.to_string(); + } + if let Some(cwd) = value.get("cwd").and_then(|item| item.as_str()) { + state.cwd = cwd.to_string(); + } + + match value.get("type").and_then(|item| item.as_str()) { + Some("user") => { + if value.get("interruptedMessageId").is_some() { + if let Some(turn) = self.turn_mut(state, AGENT_CLAUDE, ts_ms) { + turn.outcome = "aborted".to_string(); + turn.abort_reason = "interrupted".to_string(); + } + } + let is_tool_result = value.get("sourceToolUseID").is_some() + || value.get("sourceToolAssistantUUID").is_some(); + if !is_tool_result && value.get("isMeta").and_then(|item| item.as_bool()) != Some(true) + { + if let Some(turn_id) = value + .get("promptId") + .or_else(|| value.get("uuid")) + .and_then(|item| item.as_str()) + { + state.turn_id = turn_id.to_string(); + } + } + let tool_errors = value + .get("message") + .and_then(|message| message.get("content")) + .and_then(|content| content.as_array()) + .map(|content| { + content + .iter() + .filter(|block| { + block.get("type").and_then(|item| item.as_str()) + == Some("tool_result") + && block.get("is_error").and_then(|item| item.as_bool()) + == Some(true) + }) + .count() as u64 + }) + .unwrap_or(0); + let denied = u64::from(value.get("toolDenialKind").is_some()); + if tool_errors > 0 || denied > 0 || !state.turn_id.is_empty() { + if let Some(turn) = self.turn_mut(state, AGENT_CLAUDE, ts_ms) { + turn.tool_errors += tool_errors; + turn.denials += denied; + } + } + } + Some("assistant") => { + let message = value.get("message").unwrap_or(&serde_json::Value::Null); + if state.turn_id.is_empty() { + if let Some(turn_id) = value + .get("requestId") + .or_else(|| message.get("id")) + .and_then(|item| item.as_str()) + { + state.turn_id = turn_id.to_string(); + } + } + let usage_id = message + .get("id") + .and_then(|item| item.as_str()) + .unwrap_or(""); + if let Some(turn) = self.turn_mut(state, AGENT_CLAUDE, ts_ms) { + if let Some(model) = message.get("model").and_then(|item| item.as_str()) { + turn.model = model.to_string(); + } + if !usage_id.is_empty() && !turn.usage_ids.iter().any(|id| id == usage_id) { + let usage = message.get("usage").unwrap_or(&serde_json::Value::Null); + let number = |key: &str| { + usage + .get(key) + .and_then(|item| item.as_f64()) + .unwrap_or(0.0) + }; + turn.input_tokens += number("input_tokens"); + turn.cache_creation_tokens += number("cache_creation_input_tokens"); + turn.cache_read_tokens += number("cache_read_input_tokens"); + turn.output_tokens += number("output_tokens"); + turn.usage_ids.push(usage_id.to_string()); + } + } + } + Some("system") => match value.get("subtype").and_then(|item| item.as_str()) { + Some("turn_duration") => { + if let Some(turn) = self.turn_mut(state, AGENT_CLAUDE, ts_ms) { + turn.outcome = "completed".to_string(); + turn.duration_ms = value + .get("durationMs") + .and_then(|item| item.as_u64()) + .unwrap_or(0); + } + } + Some("compact_boundary") => { + if let Some(turn) = self.turn_mut(state, AGENT_CLAUDE, ts_ms) { + turn.compactions += 1; + } + } + _ => {} + }, + _ => {} + } + + parse_claude_value(&value) + } + /// Keep the newest rate-limit snapshot (files are walked in arbitrary order, /// so compare timestamps rather than trusting encounter order). fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64) { @@ -942,15 +1267,14 @@ fn codex_session_from_filename(path: &std::path::Path) -> String { } } -/// Parse one Claude JSONL line into a RawEvent (assistant messages only). -fn parse_claude_line(line: &str) -> Option { - let v: serde_json::Value = serde_json::from_str(line).ok()?; +/// Parse one Claude JSON value into a RawEvent (assistant messages only). +fn parse_claude_value(v: &serde_json::Value) -> Option { match v.get("type")?.as_str()? { - "assistant" => parse_assistant(&v), + "assistant" => parse_assistant(v), // Skills invoked via slash command (e.g. `/find-skills`) are logged as a // user message with a tag, NOT as a Skill tool_use, so // they need a separate path or they'd never be counted. - "user" => parse_user_command(&v), + "user" => parse_user_command(v), _ => None, } } @@ -1080,6 +1404,9 @@ mod tests { codex_quota: None, codex_spark_quota: None, projects_by_source: HashMap::new(), + telemetry_since_ms: 0, + turns: Vec::new(), + telemetry_index: HashMap::new(), } } @@ -1104,6 +1431,60 @@ mod tests { assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 7.0); } + #[test] + fn records_codex_aborted_turn_and_wasted_tokens() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/codex.jsonl".to_string(), + session: "session-1".to_string(), + model: "gpt-5".to_string(), + ..FileState::default() + }; + let started = r#"{"timestamp":"2026-07-13T12:00:00Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1","model_context_window":200000}}"#; + let usage = r#"{"timestamp":"2026-07-13T12:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"model_context_window":200000,"last_token_usage":{"input_tokens":1000,"cached_input_tokens":800,"output_tokens":100,"reasoning_output_tokens":40}}}}"#; + let aborted = r#"{"timestamp":"2026-07-13T12:00:02Z","type":"event_msg","payload":{"type":"turn_aborted","turn_id":"turn-1","duration_ms":2000,"reason":"interrupted"}}"#; + + assert!(store.parse_codex_line(started, &mut state).is_none()); + assert!(store.parse_codex_line(usage, &mut state).is_some()); + assert!(store.parse_codex_line(aborted, &mut state).is_none()); + + let turn = &store.turns[0]; + assert_eq!(turn.outcome, "aborted"); + assert_eq!(turn.input_tokens, 200.0); + assert_eq!(turn.cache_read_tokens, 800.0); + assert_eq!(turn.output_tokens, 100.0); + assert_eq!(turn.reasoning_tokens, 40.0); + assert_eq!(turn.duration_ms, 2000); + } + + #[test] + fn deduplicates_claude_turn_usage_and_counts_tool_errors() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/claude.jsonl".to_string(), + ..FileState::default() + }; + let user = r#"{"timestamp":"2026-07-13T12:00:00Z","type":"user","sessionId":"session-1","promptId":"prompt-1","message":{"content":"hi"}}"#; + let assistant = r#"{"timestamp":"2026-07-13T12:00:01Z","type":"assistant","sessionId":"session-1","message":{"id":"message-1","model":"claude-sonnet-4","content":[],"usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":30,"output_tokens":40}}}"#; + let error = r#"{"timestamp":"2026-07-13T12:00:02Z","type":"user","sessionId":"session-1","sourceToolUseID":"tool-1","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","is_error":true,"content":"failed"}]}}"#; + let duration = r#"{"timestamp":"2026-07-13T12:00:03Z","type":"system","sessionId":"session-1","subtype":"turn_duration","durationMs":3000}"#; + + let _ = store.parse_claude_line(user, &mut state); + let _ = store.parse_claude_line(assistant, &mut state); + let _ = store.parse_claude_line(assistant, &mut state); + let _ = store.parse_claude_line(error, &mut state); + let _ = store.parse_claude_line(duration, &mut state); + + let turn = &store.turns[0]; + assert_eq!(turn.outcome, "completed"); + assert_eq!(turn.input_tokens, 10.0); + assert_eq!(turn.cache_creation_tokens, 20.0); + assert_eq!(turn.cache_read_tokens, 30.0); + assert_eq!(turn.output_tokens, 40.0); + assert_eq!(turn.tool_errors, 1); + assert_eq!(turn.duration_ms, 3000); + } + #[test] fn extracts_codex_skill_names_from_unix_and_windows_paths() { let input = r#"sed ~/.codex/skills/find-skills/SKILL.md && type C:\Users\me\.agents\skills\release-notes\SKILL.md && cat ~/.codex/skills/.system/openai-docs/SKILL.md && cat ~/.codex/plugins/cache/bundled/skills/browser/SKILL.md"#; diff --git a/src/App.tsx b/src/App.tsx index 67a7743..a2608db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, ProjectStat, Scope, Quota, Theme, TH, + Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -433,6 +433,36 @@ function ProjectSettlement({ projects, theme, onExport }: ); } +function ReliabilitySection({ stats, sinceMs, theme }: + { stats: ReliabilityStats; sinceMs: number; theme: Theme }) { + const t = theme; + const turns = stats.completedTurns + stats.abortedTurns; + const success = turns > 0 ? Math.round((stats.completedTurns / turns) * 100) : 0; + const since = sinceMs > 0 + ? new Date(sinceMs).toLocaleDateString("en-US", { month: "short", day: "numeric" }) + : "this version"; + const item = (label: string, value: string, sub: string) => ( +
+
{label}
+
{value}
+
{sub}
+
+ ); + return ( +
+
+ + tracked since {since} +
+
+ {item("Success", turns > 0 ? `${success}%` : "—", `${fmtInt(turns)} turns`)} + {item("Aborted", fmtInt(stats.abortedTurns), stats.wastedCost > 0 ? `${fmtMoney(stats.wastedCost)} wasted` : `${fmtTokens(stats.wastedTokens)} wasted`)} + {item("Tool issues", fmtInt(stats.toolErrors + stats.denials), `${fmtInt(stats.toolErrors)} errors · ${fmtInt(stats.denials)} denied`)} +
+
+ ); +} + // Codex rate-limit card: the two rolling windows (5h + weekly) straight from // the session logs — data Claude doesn't expose. Bars turn amber near the cap. function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { @@ -740,6 +770,10 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const M = P.metrics; const models = P.models; const projects = P.projects ?? []; + const reliability = P.reliability ?? { + completedTurns: 0, abortedTurns: 0, toolErrors: 0, denials: 0, + wastedTokens: 0, wastedCost: 0, + }; const [totalModel, setTotalModel] = useState(""); // The All scope can contain the same model once per agent. The selector is // model-based, so combine those rows before calculating its Total and cost. @@ -1021,6 +1055,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater )} + + {/* footer stats */}
diff --git a/src/data.ts b/src/data.ts index d151274..b74dcbc 100644 --- a/src/data.ts +++ b/src/data.ts @@ -4,6 +4,10 @@ export interface SeriesPoint { label: string; full: string; input: number; cache export interface ModelStat { name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean; agent: string } export interface NamedCount { name: string; count: number } export interface ProjectStat { id: string; name: string; tokens: number; cost: number; requests: number; sessions: number } +export interface ReliabilityStats { + completedTurns: number; abortedTurns: number; toolErrors: number; denials: number; + wastedTokens: number; wastedCost: number; +} export interface Metrics { totalTokens: number; inputTokens: number; cacheTokens: number; outputTokens: number; cost: number; mcpCalls: number; skillCalls: number; requests: number; sessions: number; @@ -14,6 +18,7 @@ export interface Metrics { export interface AgentSlice { id: string; label: string; color: string; tokens: number; values: number[] } export interface PeriodReport { metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; projects: ProjectStat[]; + reliability: ReliabilityStats; mcp: NamedCount[]; skills: NamedCount[]; reqTrend: number[]; costTrend: number[]; agents: AgentSlice[]; } @@ -34,7 +39,7 @@ export interface Scope { heatmap: HeatDay[]; quota: Quota | null; sparkQuota?: Quota | null; } export interface Dashboard { - scopes: Scope[]; todayTokens: number; generatedAt: string; + scopes: Scope[]; todayTokens: number; generatedAt: string; telemetrySinceMs?: number; } export interface DateRange { startDate: string; endDate: string } export interface RangeScope { id: string; report: PeriodReport } From b7250eb84e524ccf37a20aefb90e2944f1a500a0 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 19:53:57 -0700 Subject: [PATCH 22/67] feat: add turn response performance --- README-zh.md | 1 + README.md | 1 + src-tauri/src/model.rs | 15 +++++++++++ src-tauri/src/parser.rs | 59 +++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 40 +++++++++++++++++++++++++++- src/data.ts | 6 ++++- 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/README-zh.md b/README-zh.md index a53d4dc..bff5bf6 100644 --- a/README-zh.md +++ b/README-zh.md @@ -24,6 +24,7 @@ brew install --cask sunchj/tokenscope/tokenscope - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 - 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 +- 响应性能:展示 Turn 耗时中位数、P95,以及 Codex 首 Token 时间;复用同一条增量 telemetry 数据流 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** - **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 diff --git a/README.md b/README.md index 035a04f..9b83421 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ brew install --cask sunchj/tokenscope/tokenscope - Metrics: total tokens (input/output), estimated cost, requests / sessions - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content - Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward +- Response performance: median and P95 turn duration plus Codex time-to-first-token, using the same incremental telemetry stream - Three breakdowns: **by model** / **by MCP call** / **by Skill call** - **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index ea6a96a..214e3db 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -57,6 +57,20 @@ pub struct ReliabilityStats { pub wasted_cost: f64, } +#[derive(Debug, Clone, Serialize, Default)] +pub struct PerformanceStats { + #[serde(rename = "trackedTurns")] + pub tracked_turns: u64, + #[serde(rename = "medianDurationMs")] + pub median_duration_ms: u64, + #[serde(rename = "p95DurationMs")] + pub p95_duration_ms: u64, + #[serde(rename = "medianTtftMs")] + pub median_ttft_ms: u64, + #[serde(rename = "p95TtftMs")] + pub p95_ttft_ms: u64, +} + #[derive(Debug, Clone, Serialize, Default)] pub struct Metrics { #[serde(rename = "totalTokens")] @@ -100,6 +114,7 @@ pub struct PeriodReport { pub models: Vec, pub projects: Vec, pub reliability: ReliabilityStats, + pub performance: PerformanceStats, pub mcp: Vec, pub skills: Vec, #[serde(rename = "reqTrend")] diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 50fb648..0267d24 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -45,6 +45,8 @@ struct TurnEvent { cost: f64, tool_errors: u64, denials: u64, + duration_ms: u64, + ttft_ms: u64, } /// Static per-agent identity: label + accent + a 5-step chart palette (rank @@ -537,6 +539,8 @@ fn compute_turn(turn: &crate::store::TurnTelemetry, pricing: &Pricing) -> TurnEv cost, tool_errors: turn.tool_errors, denials: turn.denials, + duration_ms: turn.duration_ms, + ttft_ms: turn.ttft_ms, } } @@ -565,6 +569,46 @@ fn reliability(turns: &[TurnEvent], start: NaiveDate, end: NaiveDate) -> Reliabi stats } +fn percentile(values: &mut [u64], percent: usize) -> u64 { + if values.is_empty() { + return 0; + } + values.sort_unstable(); + let rank = (values.len() * percent).div_ceil(100).max(1); + values[rank - 1] +} + +fn performance(turns: &[TurnEvent], start: NaiveDate, end: NaiveDate) -> PerformanceStats { + let mut durations: Vec = turns + .iter() + .filter(|turn| { + let date = turn.ts.date_naive(); + date >= start && date <= end && turn.duration_ms > 0 + }) + .map(|turn| turn.duration_ms) + .collect(); + let mut ttfts: Vec = turns + .iter() + .filter(|turn| { + let date = turn.ts.date_naive(); + date >= start && date <= end && turn.ttft_ms > 0 + }) + .map(|turn| turn.ttft_ms) + .collect(); + let tracked_turns = durations.len() as u64; + let median_duration_ms = percentile(&mut durations, 50); + let p95_duration_ms = percentile(&mut durations, 95); + let median_ttft_ms = percentile(&mut ttfts, 50); + let p95_ttft_ms = percentile(&mut ttfts, 95); + PerformanceStats { + tracked_turns, + median_duration_ms, + p95_duration_ms, + median_ttft_ms, + p95_ttft_ms, + } +} + // ── aggregation helpers ──────────────────────────────────────────── #[derive(Default)] struct Agg { @@ -789,6 +833,7 @@ fn report_day( models: agg.models(palette), projects: agg.projects(), reliability: reliability(turns, today, today), + performance: performance(turns, today, today), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -864,6 +909,7 @@ fn report_week( models: agg.models(palette), projects: agg.projects(), reliability: reliability(turns, start, next_start - Duration::days(1)), + performance: performance(turns, start, next_start - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -948,6 +994,7 @@ fn report_month( models: agg.models(palette), projects: agg.projects(), reliability: reliability(turns, cur_first, next_first - Duration::days(1)), + performance: performance(turns, cur_first, next_first - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -1067,6 +1114,7 @@ fn report_range( models: agg.models(palette), projects: agg.projects(), reliability: reliability(turns, start, end), + performance: performance(turns, start, end), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: request_buckets, @@ -1216,6 +1264,8 @@ mod tests { cost: 1.25, tool_errors: 2, denials: 1, + duration_ms: 1_000, + ttft_ms: 100, }, TurnEvent { ts, @@ -1228,6 +1278,8 @@ mod tests { cost: 0.0, tool_errors: 0, denials: 0, + duration_ms: 3_000, + ttft_ms: 300, }, ]; let stats = reliability(&turns, ts.date_naive(), ts.date_naive()); @@ -1238,5 +1290,12 @@ mod tests { assert_eq!(stats.denials, 1); assert_eq!(stats.wasted_tokens, 1.0); assert_eq!(stats.wasted_cost, 1.25); + + let perf = performance(&turns, ts.date_naive(), ts.date_naive()); + assert_eq!(perf.tracked_turns, 2); + assert_eq!(perf.median_duration_ms, 1_000); + assert_eq!(perf.p95_duration_ms, 3_000); + assert_eq!(perf.median_ttft_ms, 100); + assert_eq!(perf.p95_ttft_ms, 300); } } diff --git a/src/App.tsx b/src/App.tsx index a2608db..8acfae2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, + Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, PerformanceStats, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -463,6 +463,39 @@ function ReliabilitySection({ stats, sinceMs, theme }: ); } +function formatDuration(ms: number) { + if (ms <= 0) return "—"; + if (ms < 1000) return `${Math.round(ms)}ms`; + if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`; + const minutes = Math.floor(ms / 60_000); + const seconds = Math.round((ms % 60_000) / 1000); + return minutes < 60 ? `${minutes}m ${seconds}s` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`; +} + +function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: Theme }) { + const t = theme; + const item = (label: string, value: string, sub: string) => ( +
+
{label}
+
{value}
+
{sub}
+
+ ); + return ( +
+
+ + {fmtInt(stats.trackedTurns)} timed turns +
+
+ {item("Median", formatDuration(stats.medianDurationMs), "turn duration")} + {item("P95", formatDuration(stats.p95DurationMs), "slow tail")} + {item("First token", formatDuration(stats.medianTtftMs), stats.p95TtftMs > 0 ? `P95 ${formatDuration(stats.p95TtftMs)}` : "Codex only")} +
+
+ ); +} + // Codex rate-limit card: the two rolling windows (5h + weekly) straight from // the session logs — data Claude doesn't expose. Bars turn amber near the cap. function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { @@ -774,6 +807,9 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater completedTurns: 0, abortedTurns: 0, toolErrors: 0, denials: 0, wastedTokens: 0, wastedCost: 0, }; + const performance = P.performance ?? { + trackedTurns: 0, medianDurationMs: 0, p95DurationMs: 0, medianTtftMs: 0, p95TtftMs: 0, + }; const [totalModel, setTotalModel] = useState(""); // The All scope can contain the same model once per agent. The selector is // model-based, so combine those rows before calculating its Total and cost. @@ -1057,6 +1093,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater )} + + {/* footer stats */}
diff --git a/src/data.ts b/src/data.ts index b74dcbc..140543c 100644 --- a/src/data.ts +++ b/src/data.ts @@ -8,6 +8,10 @@ export interface ReliabilityStats { completedTurns: number; abortedTurns: number; toolErrors: number; denials: number; wastedTokens: number; wastedCost: number; } +export interface PerformanceStats { + trackedTurns: number; medianDurationMs: number; p95DurationMs: number; + medianTtftMs: number; p95TtftMs: number; +} export interface Metrics { totalTokens: number; inputTokens: number; cacheTokens: number; outputTokens: number; cost: number; mcpCalls: number; skillCalls: number; requests: number; sessions: number; @@ -18,7 +22,7 @@ export interface Metrics { export interface AgentSlice { id: string; label: string; color: string; tokens: number; values: number[] } export interface PeriodReport { metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; projects: ProjectStat[]; - reliability: ReliabilityStats; + reliability: ReliabilityStats; performance: PerformanceStats; mcp: NamedCount[]; skills: NamedCount[]; reqTrend: number[]; costTrend: number[]; agents: AgentSlice[]; } From 0379d57aa8bbcbd8bcb488beed503af3b151546f Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 19:57:47 -0700 Subject: [PATCH 23/67] feat: add context and quota trends --- README-zh.md | 2 + README.md | 2 + src-tauri/src/model.rs | 30 ++++++++++ src-tauri/src/parser.rs | 119 ++++++++++++++++++++++++++++++++++++++++ src-tauri/src/store.rs | 79 ++++++++++++++++++++++++-- src/App.tsx | 51 ++++++++++++++++- src/data.ts | 8 ++- 7 files changed, 281 insertions(+), 10 deletions(-) diff --git a/README-zh.md b/README-zh.md index bff5bf6..acaa04f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -25,6 +25,8 @@ brew install --cask sunchj/tokenscope/tokenscope - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 - 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 - 响应性能:展示 Turn 耗时中位数、P95,以及 Codex 首 Token 时间;复用同一条增量 telemetry 数据流 +- Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数与 Codex reasoning Token 占比 +- 周额度趋势:对 Codex 快照下采样,展示当前重置周期的每日消耗速度与预计触顶时间 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** - **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 diff --git a/README.md b/README.md index 9b83421..32887a1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ brew install --cask sunchj/tokenscope/tokenscope - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content - Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward - Response performance: median and P95 turn duration plus Codex time-to-first-token, using the same incremental telemetry stream +- Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and Codex reasoning-token share +- Weekly quota trend: downsampled Codex snapshots with daily burn rate and time-to-cap projection for the current reset cycle - Three breakdowns: **by model** / **by MCP call** / **by Skill call** - **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 214e3db..e42d187 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -71,6 +71,31 @@ pub struct PerformanceStats { pub p95_ttft_ms: u64, } +#[derive(Debug, Clone, Serialize, Default)] +pub struct ContextStats { + #[serde(rename = "trackedTurns")] + pub tracked_turns: u64, + #[serde(rename = "medianPct")] + pub median_pct: f64, + #[serde(rename = "peakPct")] + pub peak_pct: f64, + #[serde(rename = "nearLimitTurns")] + pub near_limit_turns: u64, + pub compactions: u64, + #[serde(rename = "reasoningTokens")] + pub reasoning_tokens: f64, + #[serde(rename = "reasoningPct")] + pub reasoning_pct: f64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct QuotaTrendPoint { + #[serde(rename = "tsMs")] + pub ts_ms: i64, + #[serde(rename = "usedPct")] + pub used_pct: f64, +} + #[derive(Debug, Clone, Serialize, Default)] pub struct Metrics { #[serde(rename = "totalTokens")] @@ -115,6 +140,7 @@ pub struct PeriodReport { pub projects: Vec, pub reliability: ReliabilityStats, pub performance: PerformanceStats, + pub context: ContextStats, pub mcp: Vec, pub skills: Vec, #[serde(rename = "reqTrend")] @@ -169,6 +195,10 @@ pub struct Scope { pub quota: Option, // Codex only #[serde(rename = "sparkQuota")] pub spark_quota: Option, // codex_bengalfox only + #[serde(rename = "quotaTrend")] + pub quota_trend: Vec, + #[serde(rename = "sparkQuotaTrend")] + pub spark_quota_trend: Vec, } #[derive(Debug, Clone, Serialize)] diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 0267d24..26a356b 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -47,6 +47,10 @@ struct TurnEvent { denials: u64, duration_ms: u64, ttft_ms: u64, + reasoning: f64, + context_tokens: f64, + context_window: f64, + compactions: u64, } /// Static per-agent identity: label + accent + a 5-step chart palette (rank @@ -118,6 +122,48 @@ fn vendor_of(model: &str) -> &'static str { } } +fn quota_trend( + history: &[crate::store::QuotaHistoryPoint], + limit_id: &str, + current: Option<&Quota>, +) -> Vec { + let Some(current) = current else { + return Vec::new(); + }; + let (used_pct, resets_at) = if current.primary_minutes == 7 * 24 * 60 { + (current.primary_pct, current.primary_resets_at) + } else if current.secondary_minutes == 7 * 24 * 60 { + (current.secondary_pct, current.secondary_resets_at) + } else { + return Vec::new(); + }; + let mut points: Vec = history + .iter() + .filter(|point| point.limit_id == limit_id && point.resets_at == resets_at) + .map(|point| QuotaTrendPoint { + ts_ms: point.ts_ms, + used_pct: point.used_pct, + }) + .collect(); + points.sort_by_key(|point| point.ts_ms); + if points.last().map(|point| point.ts_ms) != Some(current.as_of_ms) { + points.push(QuotaTrendPoint { + ts_ms: current.as_of_ms, + used_pct, + }); + } + const MAX_POINTS: usize = 48; + if points.len() <= MAX_POINTS { + return points; + } + (0..MAX_POINTS) + .map(|index| { + let source = index * (points.len() - 1) / (MAX_POINTS - 1); + points[source].clone() + }) + .collect() +} + pub fn build_dashboard() -> Dashboard { let _guard = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); @@ -130,6 +176,12 @@ pub fn build_dashboard() -> Dashboard { } let codex_quota = store.codex_quota.clone(); let codex_spark_quota = store.codex_spark_quota.clone(); + let codex_quota_trend = quota_trend(&store.quota_history, "codex", codex_quota.as_ref()); + let codex_spark_quota_trend = quota_trend( + &store.quota_history, + "codex_bengalfox", + codex_spark_quota.as_ref(), + ); let telemetry_since_ms = store.telemetry_since_ms; // 2. Aggregate: apply current config + prices, slice by current time. @@ -174,6 +226,8 @@ pub fn build_dashboard() -> Dashboard { if agent == AGENT_CODEX { scope.quota = codex_quota; scope.spark_quota = codex_spark_quota; + scope.quota_trend = codex_quota_trend; + scope.spark_quota_trend = codex_spark_quota_trend; } vec![scope] } else { @@ -201,6 +255,8 @@ pub fn build_dashboard() -> Dashboard { if a.id == AGENT_CODEX { s.quota = codex_quota.clone(); s.spark_quota = codex_spark_quota.clone(); + s.quota_trend = codex_quota_trend.clone(); + s.spark_quota_trend = codex_spark_quota_trend.clone(); } per_agent.push(s); } @@ -440,6 +496,8 @@ fn build_scope( heatmap, quota: None, spark_quota: None, + quota_trend: Vec::new(), + spark_quota_trend: Vec::new(), } } @@ -541,6 +599,10 @@ fn compute_turn(turn: &crate::store::TurnTelemetry, pricing: &Pricing) -> TurnEv denials: turn.denials, duration_ms: turn.duration_ms, ttft_ms: turn.ttft_ms, + reasoning: turn.reasoning_tokens, + context_tokens: turn.context_tokens, + context_window: turn.context_window, + compactions: turn.compactions, } } @@ -609,6 +671,43 @@ fn performance(turns: &[TurnEvent], start: NaiveDate, end: NaiveDate) -> Perform } } +fn context_stats(turns: &[TurnEvent], start: NaiveDate, end: NaiveDate) -> ContextStats { + let selected: Vec<&TurnEvent> = turns + .iter() + .filter(|turn| { + let date = turn.ts.date_naive(); + date >= start && date <= end + }) + .collect(); + let mut pressure: Vec = selected + .iter() + .filter(|turn| turn.context_window > 0.0 && turn.context_tokens > 0.0) + .map(|turn| (turn.context_tokens / turn.context_window * 100.0).clamp(0.0, 100.0)) + .collect(); + pressure.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let median = if pressure.is_empty() { + 0.0 + } else { + pressure[(pressure.len() - 1) / 2] + }; + let peak = pressure.last().copied().unwrap_or(0.0); + let reasoning: f64 = selected.iter().map(|turn| turn.reasoning).sum(); + let output: f64 = selected.iter().map(|turn| turn.output).sum(); + ContextStats { + tracked_turns: pressure.len() as u64, + median_pct: (median * 10.0).round() / 10.0, + peak_pct: (peak * 10.0).round() / 10.0, + near_limit_turns: pressure.iter().filter(|value| **value >= 80.0).count() as u64, + compactions: selected.iter().map(|turn| turn.compactions).sum(), + reasoning_tokens: (reasoning / 1e6 * 1_000_000.0).round() / 1_000_000.0, + reasoning_pct: if output > 0.0 { + (reasoning / output * 1000.0).round() / 10.0 + } else { + 0.0 + }, + } +} + // ── aggregation helpers ──────────────────────────────────────────── #[derive(Default)] struct Agg { @@ -834,6 +933,7 @@ fn report_day( projects: agg.projects(), reliability: reliability(turns, today, today), performance: performance(turns, today, today), + context: context_stats(turns, today, today), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -910,6 +1010,7 @@ fn report_week( projects: agg.projects(), reliability: reliability(turns, start, next_start - Duration::days(1)), performance: performance(turns, start, next_start - Duration::days(1)), + context: context_stats(turns, start, next_start - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -995,6 +1096,7 @@ fn report_month( projects: agg.projects(), reliability: reliability(turns, cur_first, next_first - Duration::days(1)), performance: performance(turns, cur_first, next_first - Duration::days(1)), + context: context_stats(turns, cur_first, next_first - Duration::days(1)), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: req_b, @@ -1115,6 +1217,7 @@ fn report_range( projects: agg.projects(), reliability: reliability(turns, start, end), performance: performance(turns, start, end), + context: context_stats(turns, start, end), mcp: Agg::named(&agg.mcp_counts), skills: Agg::named(&agg.skill_counts), req_trend: request_buckets, @@ -1266,6 +1369,10 @@ mod tests { denials: 1, duration_ms: 1_000, ttft_ms: 100, + reasoning: 50_000.0, + context_tokens: 160_000.0, + context_window: 200_000.0, + compactions: 1, }, TurnEvent { ts, @@ -1280,6 +1387,10 @@ mod tests { denials: 0, duration_ms: 3_000, ttft_ms: 300, + reasoning: 0.0, + context_tokens: 100_000.0, + context_window: 200_000.0, + compactions: 0, }, ]; let stats = reliability(&turns, ts.date_naive(), ts.date_naive()); @@ -1297,5 +1408,13 @@ mod tests { assert_eq!(perf.p95_duration_ms, 3_000); assert_eq!(perf.median_ttft_ms, 100); assert_eq!(perf.p95_ttft_ms, 300); + + let context = context_stats(&turns, ts.date_naive(), ts.date_naive()); + assert_eq!(context.tracked_turns, 2); + assert_eq!(context.median_pct, 50.0); + assert_eq!(context.peak_pct, 80.0); + assert_eq!(context.near_limit_turns, 1); + assert_eq!(context.compactions, 1); + assert_eq!(context.reasoning_tokens, 0.05); } } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 7f21088..b9543bb 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -57,10 +57,20 @@ pub struct TurnTelemetry { usage_ids: Vec, } +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct QuotaHistoryPoint { + pub ts_ms: i64, + pub limit_id: String, + pub used_pct: f64, + pub resets_at: i64, +} + #[derive(Serialize, Deserialize, Default)] struct TelemetryCache { since_ms: i64, turns: Vec, + #[serde(default)] + quota_history: Vec, } #[derive(Serialize, Deserialize, Clone)] @@ -136,6 +146,7 @@ pub struct Store { projects_by_source: HashMap, pub telemetry_since_ms: i64, pub turns: Vec, + pub quota_history: Vec, telemetry_index: HashMap, } @@ -280,6 +291,7 @@ impl Store { let mut projects_by_source = HashMap::new(); let mut telemetry_since_ms = chrono::Utc::now().timestamp_millis(); let mut turns = Vec::new(); + let mut quota_history = Vec::new(); let mut version_ok = false; let mut quota_cache_ok = false; let mut telemetry_cache_ok = false; @@ -342,6 +354,7 @@ impl Store { { telemetry_since_ms = cache.since_ms; turns = cache.turns; + quota_history = cache.quota_history; telemetry_cache_ok = true; } } @@ -367,6 +380,7 @@ impl Store { projects_by_source, telemetry_since_ms, turns, + quota_history, telemetry_index, }; if version_ok && !quota_cache_ok { @@ -412,6 +426,7 @@ impl Store { let cache = TelemetryCache { since_ms: self.telemetry_since_ms, turns: self.turns.clone(), + quota_history: self.quota_history.clone(), }; if let Ok(text) = serde_json::to_string(&cache) { let _ = write_atomic(&dir.join("telemetry.json"), text.as_bytes()); @@ -1084,12 +1099,17 @@ impl Store { /// Keep the newest rate-limit snapshot (files are walked in arbitrary order, /// so compare timestamps rather than trusting encounter order). fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64) { - let slot = match rl.get("limit_id").and_then(|value| value.as_str()) { - None | Some("codex") => &mut self.codex_quota, - Some("codex_bengalfox") => &mut self.codex_spark_quota, + let limit_id = match rl.get("limit_id").and_then(|value| value.as_str()) { + None | Some("codex") => "codex", + Some("codex_bengalfox") => "codex_bengalfox", _ => return, }; - if slot.as_ref().map(|q| q.as_of_ms >= ts_ms).unwrap_or(false) { + let previous = if limit_id == "codex" { + self.codex_quota.as_ref() + } else { + self.codex_spark_quota.as_ref() + }; + if previous.map(|quota| quota.as_of_ms >= ts_ms).unwrap_or(false) { return; } let win = |k: &str| rl.get(k); @@ -1101,7 +1121,7 @@ impl Store { if p.is_none() && s.is_none() { return; } - *slot = Some(Quota { + let quota = Quota { plan: rl .get("plan_type") .and_then(|x| x.as_str()) @@ -1114,7 +1134,51 @@ impl Store { secondary_minutes: num(s, "window_minutes") as u64, secondary_resets_at: num(s, "resets_at") as i64, as_of_ms: ts_ms, + }; + self.record_quota_history(limit_id, "a); + if limit_id == "codex" { + self.codex_quota = Some(quota); + } else { + self.codex_spark_quota = Some(quota); + } + } + + fn record_quota_history(&mut self, limit_id: &str, quota: &Quota) { + let weekly = if quota.primary_minutes == 7 * 24 * 60 { + Some((quota.primary_pct, quota.primary_resets_at)) + } else if quota.secondary_minutes == 7 * 24 * 60 { + Some((quota.secondary_pct, quota.secondary_resets_at)) + } else { + None + }; + let Some((used_pct, resets_at)) = weekly else { + return; + }; + let last = self + .quota_history + .iter() + .rev() + .find(|point| point.limit_id == limit_id); + if let Some(last) = last { + if quota.as_of_ms <= last.ts_ms { + return; + } + let same_value = (used_pct - last.used_pct).abs() < 0.1; + let recent = quota.as_of_ms - last.ts_ms < 15 * 60 * 1000; + if resets_at == last.resets_at && same_value && recent { + return; + } + } + self.quota_history.push(QuotaHistoryPoint { + ts_ms: quota.as_of_ms, + limit_id: limit_id.to_string(), + used_pct, + resets_at, }); + if self.quota_history.len() % 256 == 0 { + let cutoff = quota.as_of_ms - 180 * 24 * 60 * 60 * 1000; + self.quota_history.retain(|point| point.ts_ms >= cutoff); + } } } @@ -1406,6 +1470,7 @@ mod tests { projects_by_source: HashMap::new(), telemetry_since_ms: 0, turns: Vec::new(), + quota_history: Vec::new(), telemetry_index: HashMap::new(), } } @@ -1426,9 +1491,11 @@ mod tests { store.update_codex_quota(&snapshot("codex", 24.0), 100); store.update_codex_quota(&snapshot("codex_bengalfox", 7.0), 200); + store.update_codex_quota(&snapshot("codex", 24.05), 300); - assert_eq!(store.codex_quota.unwrap().primary_pct, 24.0); + assert_eq!(store.codex_quota.unwrap().primary_pct, 24.05); assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 7.0); + assert_eq!(store.quota_history.len(), 2); } #[test] diff --git a/src/App.tsx b/src/App.tsx index 8acfae2..b538ec6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - Dashboard, DateRange, PeriodReport, RangeDashboard, ModelStat, PerformanceStats, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, + ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, PerformanceStats, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -496,9 +496,34 @@ function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: ); } +function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) { + const t = theme; + const item = (label: string, value: string, sub: string, warn = false) => ( +
+
{label}
+
{value}
+
{sub}
+
+ ); + return ( +
+
+ + {fmtInt(stats.trackedTurns)} measured turns +
+
+ {item("Median", stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", "context")} + {item("Peak", stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80)} + {item("Compacted", fmtInt(stats.compactions), "times")} + {item("Reasoning", stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : "Codex only")} +
+
+ ); +} + // Codex rate-limit card: the two rolling windows (5h + weekly) straight from // the session logs — data Claude doesn't expose. Bars turn amber near the cap. -function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { +function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; theme: Theme }) { const t = theme; const now = Date.now(); const stale = now - q.asOfMs > 60 * 60 * 1000; @@ -512,6 +537,14 @@ function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) { return h > 0 ? `resets in ${h}h ${m}m` : `resets in ${m}m`; }; const asOf = new Date(q.asOfMs).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + const quotaValues = trend.map((point) => point.usedPct); + const first = trend[0], last = trend[trend.length - 1]; + const elapsedHours = first && last ? (last.tsMs - first.tsMs) / 3.6e6 : 0; + const burnPerDay = elapsedHours > 0 ? ((last.usedPct - first.usedPct) / elapsedHours) * 24 : 0; + const hoursToCap = burnPerDay > 0 && last ? ((100 - last.usedPct) / burnPerDay) * 24 : 0; + const horizon = hoursToCap > 0 + ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h to cap` : `${(hoursToCap / 24).toFixed(1)}d to cap` + : "stable"; const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { const p = Math.max(0, Math.min(100, pctUsed)); const col = p >= 80 ? "#e0795f" : t.accent; @@ -530,6 +563,12 @@ function QuotaCard({ q, theme }: { q: Quota; theme: Theme }) {
+
+ + + {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%/day · {horizon} : "Collecting weekly trend"} + +
{q.plan && <>Plan: {q.plan}}{stale && · as of {asOf}}
@@ -810,6 +849,10 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const performance = P.performance ?? { trackedTurns: 0, medianDurationMs: 0, p95DurationMs: 0, medianTtftMs: 0, p95TtftMs: 0, }; + const context = P.context ?? { + trackedTurns: 0, medianPct: 0, peakPct: 0, nearLimitTurns: 0, + compactions: 0, reasoningTokens: 0, reasoningPct: 0, + }; const [totalModel, setTotalModel] = useState(""); // The All scope can contain the same model once per agent. The selector is // model-based, so combine those rows before calculating its Total and cost. @@ -1095,6 +1138,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater + + {/* footer stats */}
@@ -1110,7 +1155,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater <>
- + )} {/* MCP — shown whenever the user has installed MCP servers */} diff --git a/src/data.ts b/src/data.ts index 140543c..a37dd40 100644 --- a/src/data.ts +++ b/src/data.ts @@ -12,6 +12,11 @@ export interface PerformanceStats { trackedTurns: number; medianDurationMs: number; p95DurationMs: number; medianTtftMs: number; p95TtftMs: number; } +export interface ContextStats { + trackedTurns: number; medianPct: number; peakPct: number; nearLimitTurns: number; + compactions: number; reasoningTokens: number; reasoningPct: number; +} +export interface QuotaTrendPoint { tsMs: number; usedPct: number } export interface Metrics { totalTokens: number; inputTokens: number; cacheTokens: number; outputTokens: number; cost: number; mcpCalls: number; skillCalls: number; requests: number; sessions: number; @@ -22,7 +27,7 @@ export interface Metrics { export interface AgentSlice { id: string; label: string; color: string; tokens: number; values: number[] } export interface PeriodReport { metrics: Metrics; series: SeriesPoint[]; models: ModelStat[]; projects: ProjectStat[]; - reliability: ReliabilityStats; performance: PerformanceStats; + reliability: ReliabilityStats; performance: PerformanceStats; context: ContextStats; mcp: NamedCount[]; skills: NamedCount[]; reqTrend: number[]; costTrend: number[]; agents: AgentSlice[]; } @@ -41,6 +46,7 @@ export interface Scope { id: string; label: string; color: string; day: PeriodReport; week: PeriodReport; month: PeriodReport; heatmap: HeatDay[]; quota: Quota | null; sparkQuota?: Quota | null; + quotaTrend?: QuotaTrendPoint[]; sparkQuotaTrend?: QuotaTrendPoint[]; } export interface Dashboard { scopes: Scope[]; todayTokens: number; generatedAt: string; telemetrySinceMs?: number; From a7e943b83e10ce8141ea61b270a897b2a0118752 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 20:00:08 -0700 Subject: [PATCH 24/67] chore: keep Rust lint-clean --- src-tauri/src/lib.rs | 2 +- src-tauri/src/parser.rs | 6 +++--- src-tauri/src/store.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1371927..5b3b6a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -530,7 +530,7 @@ fn system_is_dark() -> bool { let defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; let key: id = msg_send![ class!(NSString), - stringWithUTF8String: b"AppleInterfaceStyle\0".as_ptr() as *const std::os::raw::c_char + stringWithUTF8String: c"AppleInterfaceStyle".as_ptr() ]; let val: id = msg_send![defaults, stringForKey: key]; if val == nil { diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 26a356b..f5dbeea 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -812,7 +812,7 @@ impl Agg { count: *c, }) .collect(); - v.sort_by(|a, b| b.count.cmp(&a.count)); + v.sort_by_key(|item| std::cmp::Reverse(item.count)); v } @@ -957,7 +957,7 @@ fn report_week( let mut agg = Agg::default(); let mut prev = Agg::default(); - let mut buckets = vec![(0.0f64, 0.0f64, 0.0f64); 7]; + let mut buckets = [(0.0f64, 0.0f64, 0.0f64); 7]; let mut req_b = vec![0.0f64; 7]; let mut cost_b = vec![0.0f64; 7]; @@ -1068,7 +1068,7 @@ fn report_month( let series = (0..days_in_month) .map(|i| { let dn = (i + 1) as u32; - let label = if i == 0 || dn % 5 == 0 { + let label = if i == 0 || dn.is_multiple_of(5) { dn.to_string() } else { String::new() diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index b9543bb..a68b383 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -490,7 +490,7 @@ impl Store { Some((modified, entry.into_path())) }) .collect(); - files.sort_by(|a, b| b.0.cmp(&a.0)); + files.sort_by_key(|item| std::cmp::Reverse(item.0)); for (_, path) in files.into_iter().take(RECENT_QUOTA_FILES) { let Ok(mut file) = fs::File::open(path) else { @@ -1175,7 +1175,7 @@ impl Store { used_pct, resets_at, }); - if self.quota_history.len() % 256 == 0 { + if self.quota_history.len().is_multiple_of(256) { let cutoff = quota.as_of_ms - 180 * 24 * 60 * 60 * 1000; self.quota_history.retain(|point| point.ts_ms >= cutoff); } @@ -1260,7 +1260,7 @@ fn codex_skill_names(text: &str) -> Vec { || path.contains("/plugins/cache/") || path.contains("\\plugins\\cache\\"); let start = path - .rfind(|c| c == '/' || c == '\\') + .rfind(['/', '\\']) .map(|i| i + 1) .unwrap_or(0); let name = path[start..] From df8263bf98d53a17c2fff28c8ac398431dc272ed Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 20:17:05 -0700 Subject: [PATCH 25/67] feat: add collapsible sections and localization --- src-tauri/src/lib.rs | 267 +++++++++++++++++++++++++++++---- src/App.tsx | 341 +++++++++++++++++++++++++++---------------- src/charts.tsx | 66 +++++---- src/data.ts | 4 +- src/i18n.tsx | 310 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 805 insertions(+), 183 deletions(-) create mode 100644 src/i18n.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b3b6a4..318b100 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,12 +46,38 @@ enum WeeklyQuotaDisplay { CodexAndSpark, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "lowercase")] +enum AppLanguage { + #[default] + En, + Zh, +} + +impl AppLanguage { + fn parse(value: &str) -> Option { + match value { + "en" => Some(Self::En), + "zh" => Some(Self::Zh), + _ => None, + } + } + + fn as_str(self) -> &'static str { + match self { + Self::En => "en", + Self::Zh => "zh", + } + } +} + #[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(default)] struct TrayPreferences { weekly_quota_display: WeeklyQuotaDisplay, dashboard_shortcut: bool, dashboard_shortcut_key: String, + language: AppLanguage, } impl Default for TrayPreferences { @@ -60,12 +86,82 @@ impl Default for TrayPreferences { weekly_quota_display: WeeklyQuotaDisplay::Off, dashboard_shortcut: false, dashboard_shortcut_key: DASHBOARD_SHORTCUT.to_string(), + language: AppLanguage::En, } } } struct TrayPreferencesState(std::sync::Mutex); -struct DashboardShortcutMenuState(CheckMenuItem); +struct TrayMenuState { + open: MenuItem, + refresh: MenuItem, + check_updates: MenuItem, + weekly: Submenu, + weekly_off: CheckMenuItem, + dashboard_shortcut: CheckMenuItem, + change_dashboard_shortcut: MenuItem, + autostart: CheckMenuItem, + language: Submenu, + language_en: CheckMenuItem, + language_zh: CheckMenuItem, + quit: MenuItem, +} + +struct TrayCopy { + open: &'static str, + refresh: &'static str, + check_updates: &'static str, + weekly: &'static str, + off: &'static str, + dashboard_shortcut: &'static str, + change_dashboard_shortcut: &'static str, + autostart: &'static str, + language: &'static str, + english: &'static str, + chinese: &'static str, + quit: &'static str, + today: &'static str, + ready: &'static str, +} + +fn tray_copy(language: AppLanguage) -> &'static TrayCopy { + static EN: TrayCopy = TrayCopy { + open: "Open Tokenscope", + refresh: "Refresh", + check_updates: "Check for Updates…", + weekly: "Weekly Remaining", + off: "Off", + dashboard_shortcut: "Dashboard Shortcut", + change_dashboard_shortcut: "Change Dashboard Shortcut…", + autostart: "Launch at Login", + language: "Language", + english: "English", + chinese: "Simplified Chinese", + quit: "Quit", + today: "today", + ready: "Ready", + }; + static ZH: TrayCopy = TrayCopy { + open: "打开 Tokenscope", + refresh: "刷新", + check_updates: "检查更新…", + weekly: "周剩余额度", + off: "关闭", + dashboard_shortcut: "Dashboard 快捷键", + change_dashboard_shortcut: "修改 Dashboard 快捷键…", + autostart: "登录时启动", + language: "语言", + english: "English", + chinese: "简体中文", + quit: "退出", + today: "今日", + ready: "就绪", + }; + match language { + AppLanguage::En => &EN, + AppLanguage::Zh => &ZH, + } +} fn shortcut_label(shortcut: &str) -> String { let macos = cfg!(target_os = "macos"); @@ -101,8 +197,34 @@ fn shortcut_label(shortcut: &str) -> String { } } -fn dashboard_shortcut_menu_label(shortcut: &str) -> String { - format!("Dashboard Shortcut ({})", shortcut_label(shortcut)) +fn dashboard_shortcut_menu_label(shortcut: &str, language: AppLanguage) -> String { + format!( + "{} ({})", + tray_copy(language).dashboard_shortcut, + shortcut_label(shortcut) + ) +} + +fn apply_tray_language(menu: &TrayMenuState, language: AppLanguage, shortcut: &str) { + let copy = tray_copy(language); + let _ = menu.open.set_text(copy.open); + let _ = menu.refresh.set_text(copy.refresh); + let _ = menu.check_updates.set_text(copy.check_updates); + let _ = menu.weekly.set_text(copy.weekly); + let _ = menu.weekly_off.set_text(copy.off); + let _ = menu + .dashboard_shortcut + .set_text(dashboard_shortcut_menu_label(shortcut, language)); + let _ = menu + .change_dashboard_shortcut + .set_text(copy.change_dashboard_shortcut); + let _ = menu.autostart.set_text(copy.autostart); + let _ = menu.language.set_text(copy.language); + let _ = menu.language_en.set_text(copy.english); + let _ = menu.language_zh.set_text(copy.chinese); + let _ = menu.language_en.set_checked(language == AppLanguage::En); + let _ = menu.language_zh.set_checked(language == AppLanguage::Zh); + let _ = menu.quit.set_text(copy.quit); } fn weekly_remaining_from_quota(quota: &model::Quota) -> Option { @@ -149,25 +271,26 @@ fn weekly_label_suffix( suffix } -fn tray_label(dash: &Dashboard, display: WeeklyQuotaDisplay) -> String { - let mut label = fmt_tokens_m(dash.today_tokens); +fn tray_label(dash: &Dashboard, display: WeeklyQuotaDisplay, language: AppLanguage) -> String { + let mut label = fmt_tokens_m(dash.today_tokens, language); let (codex, spark) = dashboard_quotas(dash); label.push_str(&weekly_label_suffix(display, codex, spark)); label } fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { - let display = app + let (display, language) = app .try_state::() .map(|state| { state .0 .lock() - .map(|prefs| prefs.weekly_quota_display) + .map(|prefs| (prefs.weekly_quota_display, prefs.language)) .unwrap_or_default() }) .unwrap_or_default(); - let label = tray_label(dash, display); + let label = tray_label(dash, display, language); + let today = tray_copy(language).today; let handle = app.clone(); let _ = app.run_on_main_thread(move || { if let Some(tray) = handle.tray_by_id("main") { @@ -176,7 +299,7 @@ fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { // tooltip there. Both APIs touch native tray state; on macOS that // must happen on the main thread. let _ = tray.set_title(Some(label.clone())); - let _ = tray.set_tooltip(Some(format!("Tokenscope · today {}", label))); + let _ = tray.set_tooltip(Some(format!("Tokenscope · {today} {label}"))); } }); } @@ -705,10 +828,51 @@ fn set_dashboard_shortcut(app: tauri::AppHandle, shortcut: String) -> Result<(), }) .map_err(|_| "shortcut state unavailable".to_string())?; save_tray_preferences(&preferences); - if let Some(menu) = app.try_state::() { - let _ = menu.0.set_checked(true); - let _ = menu.0.set_text(dashboard_shortcut_menu_label(&shortcut)); + if let Some(menu) = app.try_state::() { + let _ = menu.dashboard_shortcut.set_checked(true); + let _ = menu + .dashboard_shortcut + .set_text(dashboard_shortcut_menu_label( + &shortcut, + preferences.language, + )); + } + Ok(()) +} + +#[tauri::command] +fn get_app_language(app: tauri::AppHandle) -> String { + app.try_state::() + .and_then(|state| { + state + .0 + .lock() + .ok() + .map(|prefs| prefs.language.as_str().to_string()) + }) + .unwrap_or_else(|| AppLanguage::En.as_str().to_string()) +} + +#[tauri::command] +fn set_app_language(app: tauri::AppHandle, language: String) -> Result<(), String> { + let language = + AppLanguage::parse(language.trim()).ok_or_else(|| "invalid language".to_string())?; + let state = app + .try_state::() + .ok_or_else(|| "language state unavailable".to_string())?; + let preferences = state + .0 + .lock() + .map(|mut preferences| { + preferences.language = language; + preferences.clone() + }) + .map_err(|_| "language state unavailable".to_string())?; + save_tray_preferences(&preferences); + if let Some(menu) = app.try_state::() { + apply_tray_language(&menu, language, &preferences.dashboard_shortcut_key); } + let _ = app.emit("language-changed", language.as_str()); Ok(()) } @@ -801,7 +965,7 @@ pub fn dashboard_json() -> String { serde_json::to_string_pretty(&parser::build_dashboard()).unwrap_or_default() } -fn fmt_tokens_m(m: f64) -> String { +fn fmt_tokens_m(m: f64, language: AppLanguage) -> String { if m >= 1.0 { format!("{:.2}M", m) } else { @@ -809,7 +973,7 @@ fn fmt_tokens_m(m: f64) -> String { // no usage yet (e.g. just past midnight) — "0K" reads like "OK", so // show a clearer idle label instead. if k <= 0 { - "Ready".to_string() + tray_copy(language).ready.to_string() } else { format!("{k}K") } @@ -863,7 +1027,9 @@ pub fn run() { save_screenshot, save_project_export, begin_drag, - set_dashboard_shortcut + set_dashboard_shortcut, + get_app_language, + set_app_language ]) .setup(move |app| { // Menu-bar–only app: no Dock icon, runs in the background. @@ -897,6 +1063,7 @@ pub fn run() { let weekly_quota_display = tray_preferences.weekly_quota_display; let dashboard_shortcut_on = tray_preferences.dashboard_shortcut; let dashboard_shortcut_key = tray_preferences.dashboard_shortcut_key.clone(); + let language = tray_preferences.language; app.manage(TrayPreferencesState(std::sync::Mutex::new( tray_preferences, ))); @@ -1001,21 +1168,22 @@ pub fn run() { // Build the menu-bar tray: app glyph (template icon) + today's tokens. let dash = parser::build_dashboard(); - let label = tray_label(&dash, weekly_quota_display); + let label = tray_label(&dash, weekly_quota_display, language); + let copy = tray_copy(language); - let open_i = MenuItem::with_id(app, "open", "Open Tokenscope", true, None::<&str>)?; - let refresh_i = MenuItem::with_id(app, "refresh", "Refresh", true, None::<&str>)?; + let open_i = MenuItem::with_id(app, "open", copy.open, true, None::<&str>)?; + let refresh_i = MenuItem::with_id(app, "refresh", copy.refresh, true, None::<&str>)?; let check_updates_i = MenuItem::with_id( app, "check-updates", - "Check for Updates…", + copy.check_updates, true, None::<&str>, )?; let weekly_off_i = CheckMenuItem::with_id( app, "weekly-off", - "Off", + copy.off, true, weekly_quota_display == WeeklyQuotaDisplay::Off, None::<&str>, @@ -1038,23 +1206,22 @@ pub fn run() { )?; let weekly_menu = Submenu::with_items( app, - "Weekly Remaining", + copy.weekly, true, &[&weekly_off_i, &weekly_codex_i, &weekly_codex_spark_i], )?; let dashboard_shortcut_i = CheckMenuItem::with_id( app, "dashboard-shortcut", - dashboard_shortcut_menu_label(&dashboard_shortcut_key), + dashboard_shortcut_menu_label(&dashboard_shortcut_key, language), true, dashboard_shortcut_on, None::<&str>, )?; - app.manage(DashboardShortcutMenuState(dashboard_shortcut_i.clone())); let change_dashboard_shortcut_i = MenuItem::with_id( app, "change-dashboard-shortcut", - "Change Dashboard Shortcut…", + copy.change_dashboard_shortcut, true, None::<&str>, )?; @@ -1063,12 +1230,34 @@ pub fn run() { let autostart_i = CheckMenuItem::with_id( app, "autostart", - "Launch at Login", + copy.autostart, true, autostart_on, None::<&str>, )?; - let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let language_en_i = CheckMenuItem::with_id( + app, + "language-en", + copy.english, + true, + language == AppLanguage::En, + None::<&str>, + )?; + let language_zh_i = CheckMenuItem::with_id( + app, + "language-zh", + copy.chinese, + true, + language == AppLanguage::Zh, + None::<&str>, + )?; + let language_menu = Submenu::with_items( + app, + copy.language, + true, + &[&language_en_i, &language_zh_i], + )?; + let quit_i = MenuItem::with_id(app, "quit", copy.quit, true, None::<&str>)?; let menu = Menu::with_items( app, &[ @@ -1080,17 +1269,32 @@ pub fn run() { &dashboard_shortcut_i, &change_dashboard_shortcut_i, &autostart_i, + &language_menu, &PredefinedMenuItem::separator(app)?, &quit_i, ], )?; + app.manage(TrayMenuState { + open: open_i.clone(), + refresh: refresh_i.clone(), + check_updates: check_updates_i.clone(), + weekly: weekly_menu.clone(), + weekly_off: weekly_off_i.clone(), + dashboard_shortcut: dashboard_shortcut_i.clone(), + change_dashboard_shortcut: change_dashboard_shortcut_i.clone(), + autostart: autostart_i.clone(), + language: language_menu.clone(), + language_en: language_en_i.clone(), + language_zh: language_zh_i.clone(), + quit: quit_i.clone(), + }); let lh_tray = last_hidden.clone(); let _tray = TrayIconBuilder::with_id("main") .icon(tauri::include_image!("icons/tray-icon.png")) .icon_as_template(false) .title(&label) - .tooltip(format!("Tokenscope · today {}", label)) + .tooltip(format!("Tokenscope · {} {}", copy.today, label)) .menu(&menu) .show_menu_on_left_click(false) // left = toggle panel, right = menu .on_tray_icon_event(move |tray, event| { @@ -1223,6 +1427,14 @@ pub fn run() { let _ = autostart_i.set_checked(now_on); save_autostart_pref(now_on); } + "language-en" | "language-zh" => { + let language = if event.id.as_ref() == "language-zh" { + "zh" + } else { + "en" + }; + let _ = set_app_language(app.clone(), language.to_string()); + } "quit" => app.exit(0), _ => {} }) @@ -1346,6 +1558,7 @@ mod tests { assert_eq!(preferences.weekly_quota_display, WeeklyQuotaDisplay::Codex); assert_eq!(preferences.dashboard_shortcut_key, DASHBOARD_SHORTCUT); + assert_eq!(preferences.language, AppLanguage::En); assert!(DASHBOARD_SHORTCUT.parse::().is_ok()); assert!("Command+Alt+KeyT".parse::().is_ok()); assert_eq!( diff --git a/src/App.tsx b/src/App.tsx index b538ec6..e68b2e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,9 @@ import { } from "./data"; import { TokenGlyph, Segmented, BarChart, Sparkline, CostDonut, BarList, Heatmap, + ListToggle, } from "./charts"; +import { I18nProvider, localeTag, TEXT, useI18n, type Locale } from "./i18n"; // Count up to `target`. Restarts from 0 whenever `resetKey` changes (popover // open / period switch); on a live value change it eases from the current @@ -49,11 +51,12 @@ function useCountUp(target: number, resetKey: string, active: boolean, duration } function Delta({ v, theme }: { v: number; theme: Theme }) { + const { text } = useI18n(); const up = v >= 0; // Usage/cost going up is "bad" → red; going down is "good" → green. const col = up ? "#e0795f" : theme.accent; return ( - {up ? "▲" : "▼"}{Math.abs(Math.round(v))}% @@ -113,6 +116,7 @@ function MiniStat({ label, value, sub, theme, accent, children }: // otherwise overflow. Mirrors the split bar above (dark = cached, light = rest). function SplitLegend({ t, cacheM, restM, cachedPct }: { t: Theme; cacheM: number; restM: number; cachedPct: number }) { + const { text } = useI18n(); const ref = useRef(null); const [compact, setCompact] = useState(false); const key = `${cacheM}|${restM}|${cachedPct}`; @@ -128,10 +132,10 @@ function SplitLegend({ t, cacheM, restM, cachedPct }: font: `500 10px ${t.mono}`, color: t.dim, marginBottom: 14, whiteSpace: "nowrap", overflow: "hidden", }}> {compact ? "Cache" : "Cached"} {fmtTokens(cacheM)} + marginRight: 5, verticalAlign: "-0.5px" }} />{compact ? text.cache : text.cached} {fmtTokens(cacheM)} New {fmtTokens(restM)} - {cachedPct}% cached + marginRight: 5, verticalAlign: "-0.5px" }} />{text.new} {fmtTokens(restM)} + {cachedPct}% {text.cachedLower}
); } @@ -236,6 +240,7 @@ type UpdaterController = ReturnType; function VersionUpdate({ st, theme, onInstall, onDismiss }: { st: UpdateState; theme: Theme; onInstall: () => void; onDismiss: () => void }) { const t = theme; + const { text } = useI18n(); const Action = ({ label, title, onClick }: { label: string; title?: string; onClick: () => void }) => (
- {rows.map((project) => ( + {shownRows.map((project) => (
{project.name} {fmtMoney(project.cost)} - {fmtInt(project.requests)} requests · {fmtInt(project.sessions)} sessions + {fmtInt(project.requests)} {text.requests} · {fmtInt(project.sessions)} {text.sessions} {fmtTokens(project.tokens)}
))} + setOpen((value) => !value)} /> +
+ ); +} + +function ModelList({ models, shares, max, theme }: + { models: ModelStat[]; shares: number[]; max: number; theme: Theme }) { + const [open, setOpen] = useState(false); + const shown = models.slice(0, open ? models.length : 3); + return ( +
+ {shown.map((model, index) => ( + + ))} + setOpen((value) => !value)} />
); } @@ -436,11 +462,12 @@ function ProjectSettlement({ projects, theme, onExport }: function ReliabilitySection({ stats, sinceMs, theme }: { stats: ReliabilityStats; sinceMs: number; theme: Theme }) { const t = theme; + const { locale, text } = useI18n(); const turns = stats.completedTurns + stats.abortedTurns; const success = turns > 0 ? Math.round((stats.completedTurns / turns) * 100) : 0; const since = sinceMs > 0 - ? new Date(sinceMs).toLocaleDateString("en-US", { month: "short", day: "numeric" }) - : "this version"; + ? new Date(sinceMs).toLocaleDateString(localeTag(locale), { month: "short", day: "numeric" }) + : text.thisVersion; const item = (label: string, value: string, sub: string) => (
{label}
@@ -451,13 +478,13 @@ function ReliabilitySection({ stats, sinceMs, theme }: return (
- - tracked since {since} + + {text.trackedSince} {since}
- {item("Success", turns > 0 ? `${success}%` : "—", `${fmtInt(turns)} turns`)} - {item("Aborted", fmtInt(stats.abortedTurns), stats.wastedCost > 0 ? `${fmtMoney(stats.wastedCost)} wasted` : `${fmtTokens(stats.wastedTokens)} wasted`)} - {item("Tool issues", fmtInt(stats.toolErrors + stats.denials), `${fmtInt(stats.toolErrors)} errors · ${fmtInt(stats.denials)} denied`)} + {item(text.success, turns > 0 ? `${success}%` : "—", `${fmtInt(turns)} ${text.turns}`)} + {item(text.aborted, fmtInt(stats.abortedTurns), stats.wastedCost > 0 ? `${fmtMoney(stats.wastedCost)} ${text.wasted}` : `${fmtTokens(stats.wastedTokens)} ${text.wasted}`)} + {item(text.toolIssues, fmtInt(stats.toolErrors + stats.denials), `${fmtInt(stats.toolErrors)} ${text.errors} · ${fmtInt(stats.denials)} ${text.denied}`)}
); @@ -474,6 +501,7 @@ function formatDuration(ms: number) { function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: Theme }) { const t = theme; + const { text } = useI18n(); const item = (label: string, value: string, sub: string) => (
{label}
@@ -484,13 +512,13 @@ function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: return (
- - {fmtInt(stats.trackedTurns)} timed turns + + {fmtInt(stats.trackedTurns)} {text.timedTurns}
- {item("Median", formatDuration(stats.medianDurationMs), "turn duration")} - {item("P95", formatDuration(stats.p95DurationMs), "slow tail")} - {item("First token", formatDuration(stats.medianTtftMs), stats.p95TtftMs > 0 ? `P95 ${formatDuration(stats.p95TtftMs)}` : "Codex only")} + {item(text.median, formatDuration(stats.medianDurationMs), text.turnDuration)} + {item(text.p95, formatDuration(stats.p95DurationMs), text.slowTail)} + {item(text.firstToken, formatDuration(stats.medianTtftMs), stats.p95TtftMs > 0 ? `P95 ${formatDuration(stats.p95TtftMs)}` : text.codexOnly)}
); @@ -498,25 +526,31 @@ function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) { const t = theme; - const item = (label: string, value: string, sub: string, warn = false) => ( -
+ const { text } = useI18n(); + const [open, setOpen] = useState(false); + const item = (key: string, label: string, value: string, sub: string, warn = false) => ( +
{label}
{value}
{sub}
); + const items = [ + item("median", text.median, stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", text.context), + item("peak", text.peak, stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80), + item("compacted", text.compacted, fmtInt(stats.compactions), text.times), + item("reasoning", text.reasoning, stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : text.codexOnly), + ]; return (
- - {fmtInt(stats.trackedTurns)} measured turns + + {fmtInt(stats.trackedTurns)} {text.measuredTurns}
-
- {item("Median", stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", "context")} - {item("Peak", stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80)} - {item("Compacted", fmtInt(stats.compactions), "times")} - {item("Reasoning", stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : "Codex only")} +
+ {items.slice(0, open ? items.length : 3)}
+ setOpen((value) => !value)} />
); } @@ -525,26 +559,27 @@ function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) // the session logs — data Claude doesn't expose. Bars turn amber near the cap. function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; theme: Theme }) { const t = theme; + const { locale, text } = useI18n(); const now = Date.now(); const stale = now - q.asOfMs > 60 * 60 * 1000; const winLabel = (min: number) => - min === 10080 ? "Weekly" : min % 60 === 0 && min > 0 ? `${min / 60}h window` : `${min}m window`; + min === 10080 ? text.weekly : min % 60 === 0 && min > 0 ? `${min / 60}h ${text.window}` : `${min}m ${text.window}`; const resetsIn = (unixS: number) => { const ms = unixS * 1000 - now; - if (ms <= 0) return "resetting…"; + if (ms <= 0) return text.resetting; const h = Math.floor(ms / 3.6e6), m = Math.round((ms % 3.6e6) / 6e4); - if (h >= 48) return `resets in ${Math.round(h / 24)}d`; - return h > 0 ? `resets in ${h}h ${m}m` : `resets in ${m}m`; + if (h >= 48) return `${text.resetsIn} ${Math.round(h / 24)}d`; + return h > 0 ? `${text.resetsIn} ${h}h ${m}m` : `${text.resetsIn} ${m}m`; }; - const asOf = new Date(q.asOfMs).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + const asOf = new Date(q.asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }); const quotaValues = trend.map((point) => point.usedPct); const first = trend[0], last = trend[trend.length - 1]; const elapsedHours = first && last ? (last.tsMs - first.tsMs) / 3.6e6 : 0; const burnPerDay = elapsedHours > 0 ? ((last.usedPct - first.usedPct) / elapsedHours) * 24 : 0; const hoursToCap = burnPerDay > 0 && last ? ((100 - last.usedPct) / burnPerDay) * 24 : 0; const horizon = hoursToCap > 0 - ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h to cap` : `${(hoursToCap / 24).toFixed(1)}d to cap` - : "stable"; + ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h ${text.toCap}` : `${(hoursToCap / 24).toFixed(1)}d ${text.toCap}` + : text.stable; const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { const p = Math.max(0, Math.min(100, pctUsed)); const col = p >= 80 ? "#e0795f" : t.accent; @@ -566,11 +601,11 @@ function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; th
- {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%/day · {horizon} : "Collecting weekly trend"} + {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%{text.perDay} · {horizon} : text.collectingWeeklyTrend}
- {q.plan && <>Plan: {q.plan}}{stale && · as of {asOf}} + {q.plan && <>{text.plan}: {q.plan}}{stale && · {text.asOf} {asOf}}
); @@ -585,10 +620,11 @@ const Label = ({ t, children }: { t: Theme; children: React.ReactNode }) => ( function ThemeToggle({ pref, theme, onCycle }: { pref: "dark" | "light" | "system"; theme: Theme; onCycle: () => void }) { const t = theme; + const { text } = useI18n(); // Single button cycling Dark → Light → System; the icon shows the current mode. - const label = pref === "system" ? "System" : pref === "dark" ? "Dark" : "Light"; + const label = pref === "system" ? text.system : pref === "dark" ? text.dark : text.light; return ( - + ); +} + function localIso(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); @@ -641,31 +692,32 @@ function localIso(date: Date) { return `${year}-${month}-${day}`; } -function formatRange(range: DateRange, year = true) { +function formatRange(range: DateRange, locale: Locale, year = true) { const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", ...(year ? { year: "numeric" } : {}), }; - const format = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString("en-US", options); + const format = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString(localeTag(locale), options); return range.startDate === range.endDate ? format(range.startDate) : `${format(range.startDate)} – ${format(range.endDate)}`; } -function tokenAmount(valueM: number, targetM: number) { +function tokenAmount(valueM: number, targetM: number, tokenUnit: string) { if (targetM >= 1) return { value: valueM.toFixed(2), unit: "M" }; if (targetM >= 0.001) { const valueK = valueM * 1000; return { value: valueK.toFixed(targetM < 0.01 ? 1 : 0), unit: "K" }; } - return { value: Math.round(valueM * 1_000_000).toLocaleString("en-US"), unit: "tokens" }; + return { value: Math.round(valueM * 1_000_000).toLocaleString("en-US"), unit: tokenUnit }; } function RangeFilter({ theme, dark, open, active, draft, max, busy, error, onToggle, onDraft, onApply, onClear }: { theme: Theme; dark: boolean; open: boolean; active: boolean; draft: DateRange; max: string; busy: boolean; error: string; onToggle: () => void; onDraft: (range: DateRange) => void; onApply: () => void; onClear: () => void }) { const t = theme; + const { text } = useI18n(); const root = useRef(null); useEffect(() => { if (!open) return; @@ -690,8 +742,8 @@ function RangeFilter({ theme, dark, open, active, draft, max, busy, error, onTog }; return (
- } + }}>{text.clear}} + }}>{busy ? text.loading : text.apply}
)} @@ -741,9 +793,10 @@ function RangeFilter({ theme, dark, open, active, draft, max, busy, error, onTog ); } -function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater }: +function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen, active, updater }: { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; - openGen: number; active: boolean; updater: UpdaterController }) { + onToggleLanguage: () => void; openGen: number; active: boolean; updater: UpdaterController }) { + const { locale, text } = useI18n(); // Agent scope filter. scopes[0] is always the aggregate; a stale selection // (e.g. a source disappeared after a rescan) falls back to it. const scopes = dash.scopes; @@ -797,7 +850,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater }; const applyDateRange = async () => { if (!draftRange.startDate || !draftRange.endDate || draftRange.startDate > draftRange.endDate) { - setRangeError("Choose a valid date range"); + setRangeError(text.chooseValidRange); return; } const request = ++rangeRequest.current; @@ -874,7 +927,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater // animated Total tokens: counts up from 0 on each open / period / scope // / model switch; held at 0 while hidden so it never flashes. const animTotal = useCountUp(totalTokens, `${viewKey}:${scope.id}:${totalModel}:${openGen}`, active); - const heroTotal = tokenAmount(animTotal, totalTokens); + const heroTotal = tokenAmount(animTotal, totalTokens, text.tokens); // Explicit percentages avoid WebKit's incorrect flexGrow + flexBasis:0 sizing. const splitTot = M.inputTokens + M.cacheTokens + M.outputTokens; const cachePct = splitTot > 0 ? (M.cacheTokens / splitTot) * 100 : 0; @@ -894,8 +947,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater // Per-row shares that sum to exactly 100.0% (largest-remainder over visible rows). const tokenShares = sharePcts(tokenModels.map((m) => m.tokens)); const trendSub = activeRange - ? formatRange(activeRange, false) - : { Day: "today 24h", Week: "this week", Month: "this month" }[period]; + ? formatRange(activeRange, locale, false) + : { Day: text.today24h, Week: text.thisWeek, Month: text.thisMonth }[period]; // screenshot capture: rasterize the full panel card to a PNG and hand it to // the Rust `save_screenshot` command (browser preview falls back to a download). @@ -908,13 +961,15 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater toastTimer.current = window.setTimeout(() => setToast(null), 1800); }; const exportProjects = async (rows: ProjectStat[]) => { - if (!rows.length) { showToast("No project usage to export", false); return; } + if (!rows.length) { showToast(text.noProjectUsage, false); return; } const csvCell = (value: string | number) => `"${String(value).replace(/"/g, '""')}"`; - const periodLabel = activeRange ? formatRange(activeRange) : period; + const periodLabel = activeRange + ? formatRange(activeRange, locale) + : { Day: text.day, Week: text.week, Month: text.month }[period]; const csv = "\uFEFF" + [ - ["Scope", "Period", "Project", "Tokens", "Estimated cost USD", "Requests", "Sessions"], + [text.scope, text.period, text.project, "Tokens", text.estimatedCostUsd, text.requestStats, text.sessions], ...rows.map((project) => [ - scope.label, periodLabel, project.name, Math.round(project.tokens * 1_000_000), + scope.id === "all" ? text.all : scope.label, periodLabel, project.name, Math.round(project.tokens * 1_000_000), project.cost.toFixed(6), project.requests, project.sessions, ]), ].map((row) => row.map(csvCell).join(",")).join("\r\n"); @@ -923,7 +978,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; if (inTauri) { await invoke("save_project_export", { csv, label }); - showToast("Project CSV saved to Desktop", true); + showToast(text.projectCsvSaved, true); } else { const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" })); const anchor = document.createElement("a"); @@ -931,16 +986,16 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater anchor.download = "tokenscope-projects.csv"; anchor.click(); URL.revokeObjectURL(url); - showToast("Project CSV downloaded", true); + showToast(text.projectCsvDownloaded, true); } } catch { - showToast("Project export failed", false); + showToast(text.projectExportFailed, false); } }; const captureScreenshot = async () => { if (shotBusy) return; const el = document.querySelector(".om-scroll"); - if (!el) { showToast("Nothing to capture", false); return; } + if (!el) { showToast(text.nothingToCapture, false); return; } setShotBusy(true); try { await document.fonts.ready; @@ -963,7 +1018,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater const inTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; if (inTauri) { await invoke("save_screenshot", { dataUrl }); - showToast("Saved to Desktop", true); + showToast(text.savedToDesktop, true); } else { const a = document.createElement("a"); a.href = dataUrl; @@ -971,10 +1026,10 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater document.body.appendChild(a); a.click(); a.remove(); - showToast("Downloaded", true); + showToast(text.downloaded, true); } } catch { - showToast("Screenshot failed", false); + showToast(text.screenshotFailed, false); } finally { el.classList.remove("ts-no-transition"); setShotBusy(false); @@ -1026,7 +1081,10 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater Tokenscope
- +
+ + +
@@ -1051,8 +1109,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater - {formatRange(activeRange)} - @@ -1066,11 +1124,11 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater
-
Total tokens
+
{text.totalTokens}
@@ -1090,7 +1148,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater
-
Est. cost
+
{text.estimatedCost}
{fmtMoney(totalCost)}
@@ -1113,25 +1171,25 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater segs={slices.length > 0 ? [...slices].reverse().map((s) => ({ color: s.color, values: s.values })) : undefined} /> {/* models */} -
- {tokenModels.length === 0 &&
No usage in this period
} - {tokenModels.map((m, i) => )} +
+ {tokenModels.length === 0 &&
{text.noUsagePeriod}
} + {/* cost donut */} -
+
{costModels.length > 0 - ? 1} /> + ? 1} /> :
} {unpricedModels.length > 0 && (
- {unpricedModels.length} model{unpricedModels.length > 1 ? "s" : ""} without pricing data (cost not counted):{" "} + {unpricedModels.length} {unpricedModels.length > 1 ? text.models : text.model} {text.withoutPricing}:{" "} {unpricedModels.map((m) => m.name).join(", ")}
)} {projects.length > 0 && ( <> - + )} @@ -1139,14 +1197,14 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater - + {/* footer stats */}
- + - +
@@ -1154,7 +1212,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater {scope.quota && ( <> -
+
)} @@ -1163,12 +1221,12 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater <>
- - {fmtInt(M.mcpCalls)} · {M.servers} servers + + {fmtInt(M.mcpCalls)} · {M.servers} {text.servers}
{P.mcp.length > 0 ? - :
No MCP calls in this period
} + :
{text.noMcpCalls}
} )} {/* Skill — shown whenever the user has installed skills */} @@ -1176,21 +1234,21 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater <>
- - {fmtInt(M.skillCalls)} · {M.skills} skills + + {fmtInt(M.skillCalls)} · {M.skills} {text.skills}
{P.skills.length > 0 ? - :
No skill calls in this period
} + :
{text.noSkillCalls}
} )} {/* heatmap */} -
+
{/* footer note */}
- Est. cost via models.dev / LiteLLM · estimate + {text.estimateNote}
{/* /scrolling body */}
@@ -1209,7 +1267,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, openGen, active, updater {shortcutEditor !== null && ( setShortcutEditor(null)} - onSaved={() => { setShortcutEditor(null); showToast("Shortcut enabled", true); }} /> + onSaved={() => { setShortcutEditor(null); showToast(text.shortcutEnabled, true); }} /> )}
); @@ -1221,6 +1279,21 @@ export default function App() { const [openGen, setOpenGen] = useState(0); const [focused, setFocused] = useState(true); // browser preview: always "focused" const updater = useUpdater(); + const [locale, setLocale] = useState(() => { + const saved = typeof localStorage !== "undefined" ? localStorage.getItem("tokenscope-language") : null; + return saved === "zh" ? "zh" : "en"; + }); + const applyLanguage = (next: Locale) => { + setLocale(next); + try { localStorage.setItem("tokenscope-language", next); } catch {} + }; + const toggleLanguage = () => { + const next = locale === "en" ? "zh" : "en"; + applyLanguage(next); + if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) { + invoke("set_app_language", { language: next }).catch(() => {}); + } + }; // Theme preference: explicit Dark / Light, or System (follows the OS // appearance live on both macOS and Windows via prefers-color-scheme). First // run defaults to System. @@ -1279,6 +1352,12 @@ export default function App() { // live updates pushed from the background refresh thread — swaps the data in // place (no Loading), so values update without any flicker. listen("dashboard-updated", (e) => apply(e.payload)).then(track); + invoke("get_app_language").then((value) => { + if (value === "en" || value === "zh") applyLanguage(value); + }).catch(() => {}); + listen("language-changed", (e) => { + if (e.payload === "en" || e.payload === "zh") applyLanguage(e.payload); + }).then(track); // System appearance pushed natively from Rust (macOS). The webview's // prefers-color-scheme is unreliable for our hidden, non-activating menu-bar // panel, so the native event is the source of truth for System mode there; @@ -1306,6 +1385,10 @@ export default function App() { document.body.style.background = "transparent"; }, [dark]); + useEffect(() => { + document.documentElement.lang = locale === "zh" ? "zh-CN" : "en"; + }, [locale]); + // Suppress per-property CSS transitions across a theme flip so the panel // repaints in the new theme in one step instead of cross-fading each color // (see .ts-no-transition in main.tsx). A background light→dark switch lands @@ -1329,21 +1412,23 @@ export default function App() { }, [dark]); const t = TH[dark ? "dark" : "light"]; + const text = TEXT[locale]; + let content: React.ReactNode; if (err) { - return
Failed to load: {err}
; - } - if (!dash) { - return ( + content =
{text.loadFailed}: {err}
; + } else if (!dash) { + content = (
Loading…
+ font: `500 12px ${t.mono}`, color: t.dim }}>{text.loading}
); + } else if (!dash.scopes.length) { + content =
{text.loadFailed}: {text.noScopes}
; + } else { + content = ; } - if (!dash.scopes.length) { - return
Failed to load: dashboard returned no scopes
; - } - return ; + return {content}; } diff --git a/src/charts.tsx b/src/charts.tsx index 7653e14..dabb76d 100644 --- a/src/charts.tsx +++ b/src/charts.tsx @@ -3,6 +3,7 @@ import { Theme, ModelStat, NamedCount, SeriesPoint, HeatDay, fmtInt, fmtMoney, fmtTokens, linePath, fmtHeatDate, } from "./data"; +import { localeTag, localizeSeriesText, useI18n } from "./i18n"; export function TokenGlyph({ color = "#1f9d63", size = 14 }: { color?: string; size?: number }) { return ( @@ -18,6 +19,8 @@ export function TokenGlyph({ color = "#1f9d63", size = 14 }: { color?: string; s export function Segmented({ value, items = ["Day", "Week", "Month"], theme, onSelect }: { value: string; items?: string[]; theme: Theme; onSelect?: (v: string) => void }) { const t = theme; + const { text } = useI18n(); + const labels: Record = { Day: text.day, Week: text.week, Month: text.month }; return (
{items.map((it) => { @@ -27,7 +30,7 @@ export function Segmented({ value, items = ["Day", "Week", "Month"], theme, onSe font: `600 11px ${t.ui}`, letterSpacing: ".02em", padding: "3px 11px", borderRadius: 5, cursor: "pointer", userSelect: "none", color: on ? t.segOnText : t.segOffText, background: on ? t.segOnBg : "transparent", boxShadow: on ? t.segOnShadow : "none", transition: "color .15s, background .15s", - }}>{it}
+ }}>{labels[it] ?? it} ); })} @@ -40,6 +43,7 @@ export function BarChart({ data, theme, height = 96, accent, accentSoft, radius // Each entry paints one segment per bucket; values align with `data`. segs?: { color: string; values: number[] }[] }) { const t = theme; + const { locale, text } = useI18n(); accent = accent || t.accent; accentSoft = accentSoft || t.accentSoft; // Default stacking: output on top, input(+cache) below — the classic view. const stacks = segs ?? [ @@ -87,7 +91,7 @@ export function BarChart({ data, theme, height = 96, accent, accentSoft, radius
{data.map((d, i) => ( -
{d.label}
+
{localizeSeriesText(d.label, locale)}
))}
{hi >= 0 && ( @@ -99,9 +103,9 @@ export function BarChart({ data, theme, height = 96, accent, accentSoft, radius font: `500 10px ${t.mono}`, whiteSpace: "nowrap", pointerEvents: "none", zIndex: 9999, boxShadow: "0 4px 14px rgba(0,0,0,0.35)" }}> - {totals[hi] === 0 ? "No tokens" : fmtTokens(totals[hi]) + " tokens"} + {totals[hi] === 0 ? text.noTokens : `${fmtTokens(totals[hi])} ${text.tokens}`} - · {data[hi].full} + · {localizeSeriesText(data[hi].full, locale)} )} @@ -137,18 +141,20 @@ export function Sparkline({ values, theme, width = 80, height = 24, accent, stro const DONUT_PALETTE = ["#1f9d63", "#34c27e", "#6ad0a0", "#a7e3c5", "#4b5a52"]; const DONUT_OVERFLOW = "#79817b"; -export function CostDonut({ models, theme, size = 104, thickness = 16, keepColors = false }: - { models: ModelStat[]; theme: Theme; size?: number; thickness?: number; +export function CostDonut({ models, theme, size = 104, thickness = 16, keepColors = false, limit = 3 }: + { models: ModelStat[]; theme: Theme; size?: number; thickness?: number; limit?: number; // true → keep each model's own (agent-tinted) color so wedges match the // token-list dots; false → classic cost-rank green recoloring. keepColors?: boolean }) { const t = theme; const [hi, setHi] = useState(-1); + const [open, setOpen] = useState(false); // Rank by cost (desc); recolor by that rank unless colors are meaningful. const ranked = [...models] .sort((a, b) => b.cost - a.cost) .map((m, i) => (keepColors ? m : { ...m, color: i < DONUT_PALETTE.length ? DONUT_PALETTE[i] : DONUT_OVERFLOW })); models = ranked; + const shownModels = models.slice(0, open ? models.length : limit); const total = models.reduce((s, m) => s + m.cost, 0) || 1e-9; const cx = size / 2, cy = size / 2; const rOut = (size - 2) / 2, rIn = rOut - thickness; @@ -204,7 +210,7 @@ export function CostDonut({ models, theme, size = 104, thickness = 16, keepColor
- {models.map((m, i) => ( + {shownModels.map((m, i) => (
setHi(i)} onMouseLeave={() => setHi(-1)} style={{ display: "flex", alignItems: "center", gap: 7, padding: "2.5px 0", opacity: hi === -1 || hi === i ? 1 : 0.45, transition: "opacity .14s", cursor: "default", userSelect: "none" }}> @@ -212,12 +218,29 @@ export function CostDonut({ models, theme, size = 104, thickness = 16, keepColor {fmtMoney(m.cost)}
))} + setOpen((value) => !value)} />
); } -export function BarList({ items, theme, accent, limit = 5 }: +export function ListToggle({ expanded, total, limit = 3, theme, onToggle }: + { expanded: boolean; total: number; limit?: number; theme: Theme; onToggle: () => void }) { + const { text } = useI18n(); + if (total <= limit) return null; + const remaining = total - limit; + return ( + + ); +} + +export function BarList({ items, theme, accent, limit = 3 }: { items: NamedCount[]; theme: Theme; accent?: string; limit?: number }) { const t = theme; accent = accent || t.accent; const [open, setOpen] = useState(false); @@ -227,7 +250,6 @@ export function BarList({ items, theme, accent, limit = 5 }: // as ModelRow's token bars, and gives a descending comparison ladder even when // usage is spread across many skills (count / total leaves every bar tiny). const max = items.reduce((m, i) => Math.max(m, i.count), 0) || 1; - const more = items.length - shown.length; return (
{shown.map((it, i) => ( @@ -242,18 +264,7 @@ export function BarList({ items, theme, accent, limit = 5 }: {fmtInt(it.count)}
))} - {more > 0 && ( -
setOpen(true)} style={{ font: `500 9.5px ${t.ui}`, color: t.faint, paddingTop: 4, cursor: "pointer", userSelect: "none" }} - onMouseEnter={(e) => (e.currentTarget.style.color = t.dim)} onMouseLeave={(e) => (e.currentTarget.style.color = t.faint)}> - +{more} more -
- )} - {open && items.length > limit && ( -
setOpen(false)} style={{ font: `500 9.5px ${t.ui}`, color: t.faint, paddingTop: 4, cursor: "pointer", userSelect: "none" }} - onMouseEnter={(e) => (e.currentTarget.style.color = t.dim)} onMouseLeave={(e) => (e.currentTarget.style.color = t.faint)}> - show less -
- )} + setOpen((value) => !value)} /> ); } @@ -267,6 +278,7 @@ function ramp(accent: string, lvl: number, gridLine: string, card: string) { export function Heatmap({ days, theme, accent, gap = 2 }: { days: HeatDay[]; theme: Theme; accent?: string; gap?: number }) { const t = theme; accent = accent || t.accent; + const { locale, text } = useI18n(); const [hi, setHi] = useState(null); const [tip, setTip] = useState({ x: 0, y: 0 }); const wrapRef = useRef(null); @@ -299,7 +311,9 @@ export function Heatmap({ days, theme, accent, gap = 2 }: } } }); - const MN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const MN = locale === "zh" + ? ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"] + : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const onCell = (d: HeatDay, e: React.MouseEvent) => { // viewport coords → tooltip uses position:fixed so it isn't clipped by the // scrolling card's overflow (renders on top of the panel). @@ -327,9 +341,9 @@ export function Heatmap({ days, theme, accent, gap = 2 }: ))}
- Less + {text.heatLess} {[0, 1, 2, 3, 4].map((l) => ())} - More + {text.heatMore}
{hi && (
- {hi.tokens === 0 ? "No calls" : fmtTokens(hi.tokens) + " tokens"} - · {fmtHeatDate(hi.date)} + {hi.tokens === 0 ? text.noCalls : `${fmtTokens(hi.tokens)} ${text.tokens}`} + · {fmtHeatDate(hi.date, localeTag(locale))}
)} diff --git a/src/data.ts b/src/data.ts index a37dd40..0663887 100644 --- a/src/data.ts +++ b/src/data.ts @@ -162,7 +162,7 @@ export const TH: Record<"dark" | "light", Theme> = { }, }; -export function fmtHeatDate(iso: string) { +export function fmtHeatDate(iso: string, locale = "en-US") { const d = new Date(iso + "T00:00:00"); - return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); + return d.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric" }); } diff --git a/src/i18n.tsx b/src/i18n.tsx new file mode 100644 index 0000000..991a6a5 --- /dev/null +++ b/src/i18n.tsx @@ -0,0 +1,310 @@ +import { createContext, useContext, type ReactNode } from "react"; + +export type Locale = "en" | "zh"; + +const en = { + all: "All", + day: "Day", + week: "Week", + month: "Month", + tokens: "tokens", + requests: "requests", + sessions: "sessions", + showMore: "Show more", + showLess: "Show less", + comparedPrevious: "Compared with the previous period", + cache: "Cache", + cached: "Cached", + new: "New", + cachedLower: "cached", + checking: "Checking…", + latest: "Latest", + checkFailed: "Check failed", + skipped: "Skipped", + update: "Update", + restart: "Restart", + failed: "Failed", + dismiss: "Dismiss", + updateSkipped: "was skipped", + updateTo: "Update to", + skipVersion: "Skip this version", + available: "available", + restartInto: "Restart into", + shortcutModifier: "Include Command, Control, or Alt", + shortcutUnavailable: "That shortcut is unavailable", + shortcutDialog: "Change dashboard shortcut", + dashboardShortcut: "Dashboard Shortcut", + saving: "Saving…", + shortcutHint: "Press a new shortcut · Esc to cancel", + shortcutEnabled: "Shortcut enabled", + projectSettlement: "Project settlement", + projectFilter: "Project settlement filter", + allProjects: "All projects", + exportProjectCsv: "Export project settlement as CSV", + reliability: "Reliability", + trackedSince: "tracked since", + thisVersion: "this version", + success: "Success", + aborted: "Aborted", + toolIssues: "Tool issues", + turns: "turns", + wasted: "wasted", + errors: "errors", + denied: "denied", + responsePerformance: "Response performance", + timedTurns: "timed turns", + median: "Median", + turnDuration: "turn duration", + p95: "P95", + slowTail: "slow tail", + firstToken: "First token", + codexOnly: "Codex only", + contextHealth: "Context health", + measuredTurns: "measured turns", + context: "context", + peak: "Peak", + compacted: "Compacted", + times: "times", + reasoning: "Reasoning", + weekly: "Weekly", + window: "window", + resetting: "resetting…", + resetsIn: "resets in", + stable: "stable", + toCap: "to cap", + collectingWeeklyTrend: "Collecting weekly trend", + perDay: "/day", + plan: "Plan", + asOf: "as of", + system: "System", + dark: "Dark", + light: "Light", + theme: "Theme", + clickToChange: "click to change", + saveScreenshot: "Save screenshot to Desktop", + changeDateRange: "Change date range", + filterDateRange: "Filter by date range", + startDate: "Start date", + endDate: "End date", + to: "to", + datesInclusive: "Dates are inclusive", + clear: "Clear", + loading: "Loading…", + apply: "Apply", + chooseValidRange: "Choose a valid date range", + today24h: "today 24h", + thisWeek: "this week", + thisMonth: "this month", + noProjectUsage: "No project usage to export", + projectCsvSaved: "Project CSV saved to Desktop", + projectCsvDownloaded: "Project CSV downloaded", + projectExportFailed: "Project export failed", + nothingToCapture: "Nothing to capture", + savedToDesktop: "Saved to Desktop", + downloaded: "Downloaded", + screenshotFailed: "Screenshot failed", + totalTokens: "Total tokens", + totalTokensModel: "Total tokens model", + allModels: "All models", + estimatedCost: "Est. cost", + tokensByModel: "Tokens by model", + noUsagePeriod: "No usage in this period", + costByModel: "Cost by model", + model: "model", + models: "models", + withoutPricing: "without pricing data (cost not counted)", + requestStats: "Requests", + costTrend: "Cost trend", + codexQuota: "Codex quota", + mcpCalls: "MCP calls", + servers: "servers", + noMcpCalls: "No MCP calls in this period", + skillCalls: "Skill calls", + skills: "skills", + noSkillCalls: "No skill calls in this period", + dailyActivity: "Daily activity", + estimateNote: "Est. cost via models.dev / LiteLLM · estimate", + scope: "Scope", + period: "Period", + project: "Project", + estimatedCostUsd: "Estimated cost USD", + noTokens: "No tokens", + noCalls: "No calls", + heatLess: "Less", + heatMore: "More", + loadFailed: "Failed to load", + noScopes: "dashboard returned no scopes", + switchLanguage: "Switch to Chinese", +} as const; + +export type Copy = Record; + +const zh: Copy = { + all: "全部", + day: "日", + week: "周", + month: "月", + tokens: "tokens", + requests: "次请求", + sessions: "个会话", + showMore: "展开更多", + showLess: "收起", + comparedPrevious: "与上一周期相比", + cache: "缓存", + cached: "已缓存", + new: "新增", + cachedLower: "已缓存", + checking: "检查中…", + latest: "已是最新", + checkFailed: "检查失败", + skipped: "已跳过", + update: "更新", + restart: "重启", + failed: "失败", + dismiss: "关闭", + updateSkipped: "已被跳过", + updateTo: "更新至", + skipVersion: "跳过此版本", + available: "可更新", + restartInto: "重启并进入", + shortcutModifier: "请包含 Command、Control 或 Alt", + shortcutUnavailable: "该快捷键不可用", + shortcutDialog: "修改 dashboard 快捷键", + dashboardShortcut: "Dashboard 快捷键", + saving: "保存中…", + shortcutHint: "按下新的快捷键 · Esc 取消", + shortcutEnabled: "快捷键已启用", + projectSettlement: "项目结算", + projectFilter: "项目结算筛选", + allProjects: "全部项目", + exportProjectCsv: "导出项目结算 CSV", + reliability: "可靠性", + trackedSince: "统计始于", + thisVersion: "当前版本", + success: "成功率", + aborted: "已中止", + toolIssues: "工具问题", + turns: "轮对话", + wasted: "浪费", + errors: "错误", + denied: "拒绝", + responsePerformance: "响应性能", + timedTurns: "轮计时对话", + median: "中位数", + turnDuration: "对话耗时", + p95: "P95", + slowTail: "慢请求", + firstToken: "首 token", + codexOnly: "仅 Codex", + contextHealth: "上下文健康度", + measuredTurns: "轮已测量对话", + context: "上下文", + peak: "峰值", + compacted: "压缩", + times: "次", + reasoning: "推理", + weekly: "每周", + window: "窗口", + resetting: "重置中…", + resetsIn: "距重置", + stable: "稳定", + toCap: "后触顶", + collectingWeeklyTrend: "正在收集周趋势", + perDay: "/天", + plan: "计划", + asOf: "数据截至", + system: "跟随系统", + dark: "深色", + light: "浅色", + theme: "主题", + clickToChange: "点击切换", + saveScreenshot: "保存截图到桌面", + changeDateRange: "修改日期范围", + filterDateRange: "按日期范围筛选", + startDate: "开始日期", + endDate: "结束日期", + to: "至", + datesInclusive: "包含起止日期", + clear: "清除", + loading: "加载中…", + apply: "应用", + chooseValidRange: "请选择有效的日期范围", + today24h: "今天 24 小时", + thisWeek: "本周", + thisMonth: "本月", + noProjectUsage: "没有可导出的项目用量", + projectCsvSaved: "项目 CSV 已保存到桌面", + projectCsvDownloaded: "项目 CSV 已下载", + projectExportFailed: "项目导出失败", + nothingToCapture: "没有可截图的内容", + savedToDesktop: "已保存到桌面", + downloaded: "已下载", + screenshotFailed: "截图失败", + totalTokens: "总 tokens", + totalTokensModel: "总 tokens 模型", + allModels: "全部模型", + estimatedCost: "预估费用", + tokensByModel: "各模型 tokens", + noUsagePeriod: "此周期暂无用量", + costByModel: "各模型费用", + model: "个模型", + models: "个模型", + withoutPricing: "缺少价格数据(未计入费用)", + requestStats: "请求数", + costTrend: "费用趋势", + codexQuota: "Codex 额度", + mcpCalls: "MCP 调用", + servers: "个服务", + noMcpCalls: "此周期暂无 MCP 调用", + skillCalls: "Skill 调用", + skills: "个 Skills", + noSkillCalls: "此周期暂无 Skill 调用", + dailyActivity: "每日活跃度", + estimateNote: "费用由 models.dev / LiteLLM 估算", + scope: "来源", + period: "周期", + project: "项目", + estimatedCostUsd: "预估费用(USD)", + noTokens: "无 tokens", + noCalls: "无调用", + heatLess: "少", + heatMore: "多", + loadFailed: "加载失败", + noScopes: "dashboard 未返回数据来源", + switchLanguage: "Switch to English", +}; + +export const TEXT: Record = { en, zh }; + +const I18nContext = createContext({ locale: "en" as Locale, text: en as Copy }); + +export function I18nProvider({ locale, children }: { locale: Locale; children: ReactNode }) { + return {children}; +} + +export function useI18n() { + return useContext(I18nContext); +} + +export const localeTag = (locale: Locale) => locale === "zh" ? "zh-CN" : "en-US"; + +const weekdays: Record = { + Mon: "周一", Tue: "周二", Wed: "周三", Thu: "周四", Fri: "周五", Sat: "周六", Sun: "周日", +}; +const months: Record = { + Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, + Jul: 7, Aug: 8, Sep: 9, Oct: 10, Nov: 11, Dec: 12, +}; + +export function localizeSeriesText(value: string, locale: Locale) { + if (locale === "en" || !value) return value; + return value.split(" – ").map((part) => { + const weekdayDate = part.match(/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2})$/); + if (weekdayDate) return `${months[weekdayDate[2]]}月${weekdayDate[3]}日 ${weekdays[weekdayDate[1]]}`; + const fullDate = part.match(/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2}), (\d{4})( \d{2}:\d{2})?$/); + if (fullDate) return `${fullDate[3]}年${months[fullDate[1]]}月${fullDate[2]}日${fullDate[4] ?? ""}`; + const monthDate = part.match(/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2})$/); + if (monthDate) return `${months[monthDate[1]]}月${monthDate[2]}日`; + return weekdays[part] ?? part; + }).join(" – "); +} From 5989ea835a27403ea0ab83df60f83ce0b42eb68f Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 20:17:48 -0700 Subject: [PATCH 26/67] release: v1.3.6 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b49c496..37de0a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.5", + "version": "1.3.6", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 96f504c..ee093f0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.5" +version = "1.3.6" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a3ac4a1..d8f322b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.5" +version = "1.3.6" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0b0a4f0..995cb0d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.5", + "version": "1.3.6", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From e750145fd1cbf83c5808646013096b88c1de577c Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 20:33:30 -0700 Subject: [PATCH 27/67] feat: simplify dashboard status and quota --- README-zh.md | 3 +- README.md | 3 +- docs/DESIGN-multi-agent.md | 7 +- src/App.tsx | 157 ++++++++++++++++--------------------- src/i18n.tsx | 20 +---- 5 files changed, 76 insertions(+), 114 deletions(-) diff --git a/README-zh.md b/README-zh.md index acaa04f..a445a46 100644 --- a/README-zh.md +++ b/README-zh.md @@ -24,11 +24,10 @@ brew install --cask sunchj/tokenscope/tokenscope - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 - 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 -- 响应性能:展示 Turn 耗时中位数、P95,以及 Codex 首 Token 时间;复用同一条增量 telemetry 数据流 - Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数与 Codex reasoning Token 占比 - 周额度趋势:对 Codex 快照下采样,展示当前重置周期的每日消耗速度与预计触顶时间 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** -- **Codex 配额卡**:直接从 Codex 自己的日志读取 5 小时窗口与周窗口的已用百分比、plan、重置倒计时 +- **Codex 配额卡**:直接从 Codex 自己的日志读取当前周额度的已用百分比、plan 与重置倒计时 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 - **只统计用户自己安装的 MCP / Skill**,过滤所有内置工具与自带 MCP diff --git a/README.md b/README.md index 32887a1..3d255c5 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,10 @@ brew install --cask sunchj/tokenscope/tokenscope - Metrics: total tokens (input/output), estimated cost, requests / sessions - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content - Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward -- Response performance: median and P95 turn duration plus Codex time-to-first-token, using the same incremental telemetry stream - Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and Codex reasoning-token share - Weekly quota trend: downsampled Codex snapshots with daily burn rate and time-to-cap projection for the current reset cycle - Three breakdowns: **by model** / **by MCP call** / **by Skill call** -- **Codex quota card**: the 5-hour and weekly rate-limit windows (used %, plan, reset countdown) read straight from Codex's own logs +- **Codex quota card**: the active weekly rate-limit window (used %, plan, reset countdown) read straight from Codex's own logs - Cost donut (hover for a single model), year-long activity heatmap - **Counts only the MCP servers / Skills you installed yourself** — all built-in tools and bundled MCP servers are filtered out diff --git a/docs/DESIGN-multi-agent.md b/docs/DESIGN-multi-agent.md index 1bda36c..3e4ce1b 100644 --- a/docs/DESIGN-multi-agent.md +++ b/docs/DESIGN-multi-agent.md @@ -79,7 +79,7 @@ trait UsageSource { | `session_meta` | 会话元信息 | `session_id`、`cwd`、`cli_version`、`model_provider`、`source.subagent`(子代理标记)、`parent_thread_id` | | `turn_context` | 回合上下文 | `turn_id`、`model`(如 `gpt-5.5`)、`cwd`、`reasoning_effort` | | `event_msg / token_count` | **Token 用量** | `info.last_token_usage`(本回合增量)与 `info.total_token_usage`(会话累计):`input_tokens` / `cached_input_tokens` / `output_tokens` / `reasoning_output_tokens` | -| `event_msg / token_count` | **配额** | `rate_limits.primary`(5h 窗口 used_percent / resets_at)、`rate_limits.secondary`(周窗口)、`plan_type` | +| `event_msg / token_count` | **配额** | `rate_limits.primary` / `rate_limits.secondary` 的有效窗口与 `plan_type`;历史记录可能仍含已停用的 5h 窗口 | | `response_item / function_call` | 工具调用 | MCP / 自定义工具调用统计 | **解析要点**: @@ -160,13 +160,12 @@ Gemini CLI、Cursor CLI、OpenCode、Copilot CLI……只要实现 UsageSource ``` ┌ CODEX QUOTA ────────────────────────────┐ -│ 5h window ████████░░░░░░ 62% │ │ Weekly ███░░░░░░░░░░░ 23% │ -│ Plan: Pro resets in 2h 14m │ +│ Plan: Pro resets in 5d │ └─────────────────────────────────────────┘ ``` - 取**最新一条** `token_count.rate_limits`;数据超过 1 小时未更新时置灰并标注「as of HH:MM」。≥80% 时进度条转警示色,可选系统通知(后期)。 + 取**最新一条** `token_count.rate_limits`;展示有效窗口,隐藏官方已停用的 5h 窗口及缺失字段形成的空窗口。数据超过 1 小时未更新时置灰并标注「as of HH:MM」。≥80% 时进度条转警示色,可选系统通知(后期)。 - Codex 视图中 output 条内再细分 **reasoning tokens**(浅色纹理段),hover 显示占比。 ### 3.5 各区块在不同过滤态下的行为 diff --git a/src/App.tsx b/src/App.tsx index e68b2e8..0b4bfa6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, PerformanceStats, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, + ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -142,13 +142,16 @@ function SplitLegend({ t, cacheM, restM, cachedPct }: // ── In-app updates ────────────────────────────────────────────── // Poll the GitHub release feed (plugin-updater endpoint) on launch and every -// hour; keep version and update actions below the Tokenscope brand. Download +// hour; keep actionable update states below the Tokenscope brand. Download // and install happen in-app, then a relaunch finishes the update. A dismissed // version stays hidden until the next version appears (localStorage). +type UpdateFeedbackState = { + phase: "checking" | "current" | "check-failed"; + manual: boolean; +}; + type UpdateState = - | { phase: "checking" } - | { phase: "current" } - | { phase: "check-failed" } + | UpdateFeedbackState | { phase: "skipped"; version: string } | { phase: "available"; update: Update } | { phase: "downloading"; version: string; pct: number } @@ -158,8 +161,8 @@ type UpdateState = function useUpdater(): [UpdateState, () => void, () => void] { const [st, setSt] = useState(() => typeof window !== "undefined" && "__TAURI_INTERNALS__" in window - ? { phase: "checking" } - : { phase: "current" } + ? { phase: "checking", manual: false } + : { phase: "current", manual: false } ); const updRef = useRef(null); useEffect(() => { @@ -168,16 +171,30 @@ function useUpdater(): [UpdateState, () => void, () => void] { let dead = false; let checking = false; let unlisten: (() => void) | null = null; + let feedbackTimer = 0; + const settle = (phase: "current" | "check-failed", manual: boolean) => { + const next: UpdateFeedbackState = { phase, manual }; + setSt(next); + window.clearTimeout(feedbackTimer); + if (manual) { + feedbackTimer = window.setTimeout(() => { + if (!dead) setSt((current) => current === next ? { ...next, manual: false } : current); + }, 2500); + } + }; const probe = async (manual = false) => { if (checking) return; checking = true; - if (manual) setSt({ phase: "checking" }); + if (manual) { + window.clearTimeout(feedbackTimer); + setSt({ phase: "checking", manual: true }); + } try { const u = await checkUpdate(); if (dead) return; if (!u) { updRef.current = null; - setSt({ phase: "current" }); + settle("current", manual); return; } const skipped = localStorage.getItem("tokenscope-skip-update") === u.version; @@ -192,7 +209,7 @@ function useUpdater(): [UpdateState, () => void, () => void] { } catch { // Offline / rate-limited / no latest.json: no automatic immediate // retry; wait for the next hourly check or an explicit tray-menu check. - if (!dead) setSt({ phase: "check-failed" }); + if (!dead) settle("check-failed", manual); } finally { checking = false; } @@ -206,6 +223,7 @@ function useUpdater(): [UpdateState, () => void, () => void] { return () => { dead = true; window.clearInterval(t); + window.clearTimeout(feedbackTimer); unlisten?.(); }; }, []); @@ -230,14 +248,14 @@ function useUpdater(): [UpdateState, () => void, () => void] { const dismiss = () => { const u = updRef.current; if (u) try { localStorage.setItem("tokenscope-skip-update", u.version); } catch {} - setSt(u ? { phase: "skipped", version: u.version } : { phase: "current" }); + setSt(u ? { phase: "skipped", version: u.version } : { phase: "current", manual: false }); }; return [st, install, dismiss]; } type UpdaterController = ReturnType; -function VersionUpdate({ st, theme, onInstall, onDismiss }: +function UpdateNotice({ st, theme, onInstall, onDismiss }: { st: UpdateState; theme: Theme; onInstall: () => void; onDismiss: () => void }) { const t = theme; const { text } = useI18n(); @@ -249,29 +267,26 @@ function VersionUpdate({ st, theme, onInstall, onDismiss }: ); let status: React.ReactNode; let title: string | undefined; - if (st.phase === "checking") status = text.checking; - else if (st.phase === "current") status = text.latest; - else if (st.phase === "check-failed") status = text.checkFailed; - else if (st.phase === "skipped") { - status = text.skipped; - title = `${text.update} v${st.version} ${text.updateSkipped}`; - } else if (st.phase === "available") { - status = <> + if (st.phase === "checking" && st.manual) status = text.checking; + else if (st.phase === "current" && st.manual) status = text.latest; + else if (st.phase === "check-failed" && st.manual) status = {text.checkFailed}; + else if (st.phase === "available") { + status = <> ; title = [`v${st.update.version} ${text.available}`, st.update.body].filter(Boolean).join("\n\n"); - } else if (st.phase === "downloading") status = `${st.pct}%`; + } else if (st.phase === "downloading") status = `v${st.version} · ${st.pct}%`; else if (st.phase === "ready") { status = relaunch().catch(() => {})} />; - } else { + } else if (st.phase === "install-failed") { status = <>{text.failed} ; - } + } else return null; return (
- v{appPackage.version}·{status} + {status}
); } @@ -490,73 +505,35 @@ function ReliabilitySection({ stats, sinceMs, theme }: ); } -function formatDuration(ms: number) { - if (ms <= 0) return "—"; - if (ms < 1000) return `${Math.round(ms)}ms`; - if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`; - const minutes = Math.floor(ms / 60_000); - const seconds = Math.round((ms % 60_000) / 1000); - return minutes < 60 ? `${minutes}m ${seconds}s` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`; -} - -function PerformanceSection({ stats, theme }: { stats: PerformanceStats; theme: Theme }) { - const t = theme; - const { text } = useI18n(); - const item = (label: string, value: string, sub: string) => ( -
-
{label}
-
{value}
-
{sub}
-
- ); - return ( -
-
- - {fmtInt(stats.trackedTurns)} {text.timedTurns} -
-
- {item(text.median, formatDuration(stats.medianDurationMs), text.turnDuration)} - {item(text.p95, formatDuration(stats.p95DurationMs), text.slowTail)} - {item(text.firstToken, formatDuration(stats.medianTtftMs), stats.p95TtftMs > 0 ? `P95 ${formatDuration(stats.p95TtftMs)}` : text.codexOnly)} -
-
- ); -} - function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) { const t = theme; const { text } = useI18n(); - const [open, setOpen] = useState(false); - const item = (key: string, label: string, value: string, sub: string, warn = false) => ( -
+ const item = (label: string, value: string, sub: string, warn = false) => ( +
{label}
{value}
{sub}
); - const items = [ - item("median", text.median, stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", text.context), - item("peak", text.peak, stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80), - item("compacted", text.compacted, fmtInt(stats.compactions), text.times), - item("reasoning", text.reasoning, stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : text.codexOnly), - ]; return (
{fmtInt(stats.trackedTurns)} {text.measuredTurns}
-
- {items.slice(0, open ? items.length : 3)} +
+ {item(text.median, stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", text.context)} + {item(text.peak, stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80)} + {item(text.compacted, fmtInt(stats.compactions), text.times)} + {item(text.reasoning, stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : text.codexOnly)}
- setOpen((value) => !value)} />
); } -// Codex rate-limit card: the two rolling windows (5h + weekly) straight from -// the session logs — data Claude doesn't expose. Bars turn amber near the cap. +// Codex rate-limit card straight from the session logs. Keep collecting every +// window for compatibility, but do not render the retired 5h limit or empty +// windows omitted by newer Codex clients. Bars turn amber near the cap. function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; theme: Theme }) { const t = theme; const { locale, text } = useI18n(); @@ -580,6 +557,10 @@ function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; th const horizon = hoursToCap > 0 ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h ${text.toCap}` : `${(hoursToCap / 24).toFixed(1)}d ${text.toCap}` : text.stable; + const windows = [ + { id: "primary", minutes: q.primaryMinutes, pct: q.primaryPct, resetsAt: q.primaryResetsAt }, + { id: "secondary", minutes: q.secondaryMinutes, pct: q.secondaryPct, resetsAt: q.secondaryResetsAt }, + ].filter((window) => window.minutes > 0 && window.minutes !== 5 * 60); const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { const p = Math.max(0, Math.min(100, pctUsed)); const col = p >= 80 ? "#e0795f" : t.accent; @@ -596,8 +577,9 @@ function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; th }; return (
- - + {windows.map((window) => ( + + ))}
@@ -676,10 +658,11 @@ function LanguageToggle({ locale, theme, onToggle }: const { text } = useI18n(); return ( ); @@ -899,9 +882,6 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen completedTurns: 0, abortedTurns: 0, toolErrors: 0, denials: 0, wastedTokens: 0, wastedCost: 0, }; - const performance = P.performance ?? { - trackedTurns: 0, medianDurationMs: 0, p95DurationMs: 0, medianTtftMs: 0, p95TtftMs: 0, - }; const context = P.context ?? { trackedTurns: 0, medianPct: 0, peakPct: 0, nearLimitTurns: 0, compactions: 0, reasoningTokens: 0, reasoningPct: 0, @@ -1081,9 +1061,12 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen Tokenscope
-
- - +
+
+ v{appPackage.version} + +
+
@@ -1195,9 +1178,7 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen - - - + {/* footer stats */}
diff --git a/src/i18n.tsx b/src/i18n.tsx index 991a6a5..415fdd7 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -18,14 +18,12 @@ const en = { new: "New", cachedLower: "cached", checking: "Checking…", - latest: "Latest", + latest: "Up to date", checkFailed: "Check failed", - skipped: "Skipped", update: "Update", restart: "Restart", failed: "Failed", dismiss: "Dismiss", - updateSkipped: "was skipped", updateTo: "Update to", skipVersion: "Skip this version", available: "available", @@ -51,13 +49,7 @@ const en = { wasted: "wasted", errors: "errors", denied: "denied", - responsePerformance: "Response performance", - timedTurns: "timed turns", median: "Median", - turnDuration: "turn duration", - p95: "P95", - slowTail: "slow tail", - firstToken: "First token", codexOnly: "Codex only", contextHealth: "Context health", measuredTurns: "measured turns", @@ -155,14 +147,12 @@ const zh: Copy = { new: "新增", cachedLower: "已缓存", checking: "检查中…", - latest: "已是最新", + latest: "已是最新版本", checkFailed: "检查失败", - skipped: "已跳过", update: "更新", restart: "重启", failed: "失败", dismiss: "关闭", - updateSkipped: "已被跳过", updateTo: "更新至", skipVersion: "跳过此版本", available: "可更新", @@ -188,13 +178,7 @@ const zh: Copy = { wasted: "浪费", errors: "错误", denied: "拒绝", - responsePerformance: "响应性能", - timedTurns: "轮计时对话", median: "中位数", - turnDuration: "对话耗时", - p95: "P95", - slowTail: "慢请求", - firstToken: "首 token", codexOnly: "仅 Codex", contextHealth: "上下文健康度", measuredTurns: "轮已测量对话", From 92b5d01ab734bd2111985c3ad1da1d315d72775e Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 20:33:41 -0700 Subject: [PATCH 28/67] release: v1.3.7 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 37de0a5..a83c8db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.6", + "version": "1.3.7", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ee093f0..66b730b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.6" +version = "1.3.7" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d8f322b..0a95058 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.6" +version = "1.3.7" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 995cb0d..b54176c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.6", + "version": "1.3.7", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From dcfde4ec02b949f4c01f2c14215d78b63e3e60e1 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 22:21:15 -0700 Subject: [PATCH 29/67] fix(ui): move Codex quota below model costs --- src/App.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0b4bfa6..08b5cda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1169,6 +1169,14 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen {unpricedModels.map((m) => m.name).join(", ")}
)} + {/* Codex quota — rate-limit windows read straight from the session logs */} + {scope.quota && ( + <> + +
+ + + )} {projects.length > 0 && ( <> @@ -1189,14 +1197,6 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen
- {/* Codex quota — rate-limit windows read straight from the session logs */} - {scope.quota && ( - <> - -
- - - )} {/* MCP — shown whenever the user has installed MCP servers */} {M.servers > 0 && ( <> From cd4b2035a69580aa74f3f298cbb8ea5f55549605 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 13 Jul 2026 22:21:37 -0700 Subject: [PATCH 30/67] release: v1.3.8 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a83c8db..f5ed37b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.7", + "version": "1.3.8", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 66b730b..a110e35 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.7" +version = "1.3.8" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0a95058..82307c5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.7" +version = "1.3.8" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b54176c..7492f89 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.7", + "version": "1.3.8", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 27da20ea25a2ac625b08b44c20f13d4e33795e9a Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:06:22 -0700 Subject: [PATCH 31/67] fix(pricing): don't let a cacheless reseller shadow first-party prices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit models.dev lists the same model under many providers (the first-party vendor plus resellers / gateways / clouds). Ingestion is first-writer-wins over providers iterated in key order (serde_json has no preserve_order, so that is alphabetical), and the previous ordering only pushed bare ids ahead of "vendor/model" ones — so among the bare entries the alphabetically first provider won. For claude-fable-5 that is `abacus`, whose entry is not a cheaper price but an incomplete record: identical input/output to Anthropic's, with cache_read/cache_write simply absent, so both defaulted to 0. Claude Code usage is ~98% cache-read tokens, so pricing cache at $0 charged for only ~2% of the volume — measured 4.3x low over a month of real logs ($328 vs $1409). `claude-sonnet-5` was worse still: the shadowing entry also carried a 50% higher input price than the official one. Order entries best-first instead: first-party vendor, then entries that actually carry cache pricing, then bare ids. is_first_party() covers the five original vendors plus GLM/Qwen/Kimi/MiniMax official providers (both the international and -cn keys; *-coding-plan / *-token-plan keys are excluded since plan rates aren't the pay-as-you-go API price). Ported from upstream HduSy/tokenscope 3a30273 and 0987a4c. Co-Authored-By: Claude Opus 5 (1M context) --- src-tauri/src/pricing.rs | 139 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/pricing.rs b/src-tauri/src/pricing.rs index b94ead4..daa0bee 100644 --- a/src-tauri/src/pricing.rs +++ b/src-tauri/src/pricing.rs @@ -54,6 +54,43 @@ fn bare(s: &str) -> &str { s.rsplit('/').next().unwrap_or(s) } +/// Whether `provider` is `id`'s first-party vendor, as opposed to a reseller, +/// gateway, or cloud that re-lists the same model (often with a markup, or with +/// cache-token pricing omitted). Lets the authoritative price win regardless of +/// the order models.dev happens to iterate its providers in. Unknown vendors +/// return false and fall back to the completeness/bare-id ordering. +/// +/// Vendors with both an international and a China provider key list both; +/// subscription-plan keys (`*-coding-plan`, `*-token-plan`) are deliberately +/// excluded — plan rates aren't the pay-as-you-go API price. When both keys +/// carry the same bare id, the stable sort keeps models.dev's key order, so the +/// alphabetically-first (international, USD) entry wins the tie. +fn is_first_party(provider: &str, id: &str) -> bool { + let l = id.to_lowercase(); + let vendors: &[&str] = if l.contains("claude") { + &["anthropic"] + } else if l.contains("gpt") || l.starts_with("o1") || l.starts_with("o3") { + &["openai"] + } else if l.contains("gemini") { + &["google"] + } else if l.contains("deepseek") { + &["deepseek"] + } else if l.contains("grok") { + &["xai"] + } else if l.contains("glm") { + &["zai", "zhipuai"] + } else if l.contains("qwen") { + &["alibaba", "alibaba-cn"] + } else if l.contains("kimi") { + &["moonshotai", "moonshotai-cn"] + } else if l.contains("minimax") { + &["minimax", "minimax-cn"] + } else { + return false; + }; + vendors.contains(&provider) +} + fn cache_dir() -> Option { let dir = dirs::cache_dir()?.join("tokenscope"); let _ = fs::create_dir_all(&dir); @@ -206,9 +243,9 @@ impl Pricing { fn ingest_modelsdev(&mut self, text: &str) { let Ok(json) = serde_json::from_str::(text) else { return }; let Some(root) = json.as_object() else { return }; - // gather (id, price); bare ids (no '/') first so official-vendor prices win - let mut entries: Vec<(String, ModelPrice)> = Vec::new(); - for prov in root.values() { + // gather (provider, id, price) + let mut entries: Vec<(&str, String, ModelPrice)> = Vec::new(); + for (prov_name, prov) in root { let Some(models) = prov.get("models").and_then(|m| m.as_object()) else { continue }; for (id, m) in models { let Some(c) = m.get("cost").and_then(|c| c.as_object()) else { continue }; @@ -219,11 +256,22 @@ impl Pricing { cache_create: g("cache_write") / 1e6, cache_read: g("cache_read") / 1e6, }; - entries.push((id.clone(), price)); + entries.push((prov_name.as_str(), id.clone(), price)); } } - entries.sort_by_key(|(id, _)| id.contains('/')); // false(0)=bare first - for (id, price) in entries { + // insert() is first-writer-wins, so order entries best-first. models.dev + // lists the same model under many providers (the first-party vendor plus + // resellers / gateways / clouds), and some reseller entries omit + // cache-token pricing entirely. Prefer, in order: the model's first-party + // vendor; then entries that actually carry cache pricing (so a reseller + // that omits it can't zero it out — Claude usage is mostly cache reads, so + // dropping cache pricing undercounts cost several-fold); then bare ids over + // "vendor/model" duplicates. + entries.sort_by_key(|(prov, id, price)| { + let has_cache = price.cache_create > 0.0 || price.cache_read > 0.0; + (!is_first_party(prov, id), !has_cache, id.contains('/')) + }); + for (_, id, price) in entries { self.insert(&id, price); } } @@ -309,7 +357,84 @@ impl Pricing { #[cfg(test)] mod tests { - use super::Pricing; + use super::{is_first_party, Pricing}; + + fn empty() -> Pricing { + Pricing { + exact: Default::default(), + norm: Default::default(), + } + } + + fn approx(a: f64, b: f64) -> bool { + (a - b).abs() <= b.abs() * 1e-9 + 1e-18 + } + + // A reseller that omits cache-token pricing and sorts before the first-party + // vendor (models.dev iterates providers in key order) must not shadow the + // official entry — otherwise cache tokens, which dominate Claude usage, are + // priced at zero and cost is undercounted several-fold. + #[test] + fn first_party_entry_wins_over_cacheless_reseller() { + let json = r#"{ + "abacus": { "models": { "claude-x": { "cost": { "input": 5, "output": 25 } } } }, + "anthropic": { "models": { "claude-x": { "cost": { "input": 5, "output": 25, "cache_write": 6.25, "cache_read": 0.5 } } } } + }"#; + let mut p = empty(); + p.ingest_modelsdev(json); + let price = p.lookup("claude-x").expect("claude-x should be priced"); + assert!(approx(price.input, 5e-6)); + assert!(approx(price.output, 25e-6)); + assert!(approx(price.cache_create, 6.25e-6)); + assert!(approx(price.cache_read, 0.5e-6)); + } + + // Same shape as the abacus case, but for a Chinese vendor: the cacheless + // reseller sorts first alphabetically yet must not shadow the official + // zhipuai entry. + #[test] + fn cn_vendor_entry_wins_over_cacheless_reseller() { + let json = r#"{ + "abacus": { "models": { "glm-x": { "cost": { "input": 1, "output": 3.2 } } } }, + "zhipuai": { "models": { "glm-x": { "cost": { "input": 1, "output": 3.2, "cache_write": 1.25, "cache_read": 0.2 } } } } + }"#; + let mut p = empty(); + p.ingest_modelsdev(json); + let price = p.lookup("glm-x").expect("glm-x should be priced"); + assert!(approx(price.cache_create, 1.25e-6)); + assert!(approx(price.cache_read, 0.2e-6)); + } + + // Both international and -cn keys are first-party; subscription-plan keys + // and resellers are not. + #[test] + fn first_party_vendor_mapping() { + assert!(is_first_party("zai", "glm-5")); + assert!(is_first_party("zhipuai", "GLM-5.1")); + assert!(!is_first_party("zai-coding-plan", "glm-5")); + assert!(is_first_party("alibaba", "qwen3-max")); + assert!(is_first_party("alibaba-cn", "qwen3-max")); + assert!(!is_first_party("alibaba-coding-plan", "qwen3-max")); + assert!(is_first_party("moonshotai", "kimi-k2-thinking")); + assert!(is_first_party("moonshotai-cn", "kimi-k2-thinking")); + assert!(is_first_party("minimax", "MiniMax-M2.5")); + assert!(is_first_party("minimax-cn", "MiniMax-M2.5")); + assert!(!is_first_party("abacus", "MiniMax-M2.5")); + } + + // With no first-party match, a complete price still beats a cache-less one. + #[test] + fn cache_bearing_entry_wins_when_no_first_party() { + let json = r#"{ + "aaa": { "models": { "acme-1": { "cost": { "input": 2, "output": 4 } } } }, + "bbb": { "models": { "acme-1": { "cost": { "input": 2, "output": 4, "cache_write": 2.5, "cache_read": 0.2 } } } } + }"#; + let mut p = empty(); + p.ingest_modelsdev(json); + let price = p.lookup("acme-1").expect("acme-1 should be priced"); + assert!(approx(price.cache_read, 0.2e-6)); + assert!(approx(price.cache_create, 2.5e-6)); + } fn assert_cost(pricing: &Pricing, model: &str, expected: f64) { let actual = pricing From b40963d11a9738d9df4f0aca3cd2d4dc09bfb388 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:06:22 -0700 Subject: [PATCH 32/67] fix(store): drop the replayed parent history in a forked Codex thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex forks a subagent/resumed thread by replaying the parent thread's entire history — token_count, task_started/complete, tool calls, even the parent's session_meta — into the head of the child's rollout file, all restamped with the fork instant. Codex events carry no message id, so nothing deduped them: every replayed turn was counted a second time. Measured over real logs: 7234 replayed usage events across 116 forked files, inflating tokens by 8.5% (day) / 4.7% (week) / 5.7% (month). All 7234 were verified to have an exact usage match still present in their parent file, so dropping them loses nothing. The replayed session_meta also clobbered the child's own session id and cwd, mis-attributing its sessions and projects. The window is unambiguous: across 131 fresh July sessions there are zero token_count lines before the first turn_context, versus 7234 in forked ones. So a file whose session_meta names a forked_from_id/parent_thread_id skips everything until its own first turn_context. Those events also had an empty model (turn_context sets it), so they were counted in Total tokens but excluded from requests, the model breakdown and cost — the model rows did not sum to the total. Fixed by the same change. STORE_VERSION 8 -> 9 forces the one-time rescan that rebuilds the cache. Co-Authored-By: Claude Opus 5 (1M context) --- src-tauri/src/store.rs | 99 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index a68b383..4e3f094 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -122,6 +122,20 @@ struct FileState { // so incremental reads can still attribute later calls to their MCP server. #[serde(default)] mcp_tools: HashMap, + // Codex forks a subagent/resumed thread by replaying the parent thread's + // entire history — token_count, task_started/complete, tool calls, even the + // parent's session_meta — into the head of the child's rollout file, all + // restamped with the fork instant. Those lines are not new activity: the + // parent file still holds them, and Codex events carry no message id to + // dedupe on, so ingesting them double-counts tokens (measured ~5% of a + // month) and duplicates turn telemetry. Skip everything until the child's + // first `turn_context`, which is where its own turns begin. + #[serde(default)] + replaying: bool, + // Whether this file's own `session_meta` (its first line) has been read. + // Replayed parent metas follow it and must not overwrite session/cwd. + #[serde(default)] + meta_seen: bool, // Runtime-only source key used while appending telemetry records. #[serde(skip)] source: String, @@ -161,7 +175,9 @@ pub struct Store { // v6: extract Codex Skill calls and track project skill directories. // v7: extract Codex MCP calls from tool search and app custom-tool formats. // v8: retain complete history for custom-range tracking and settlement. -const STORE_VERSION: u32 = 8; +// v9: drop the replayed parent history at the head of a forked Codex thread +// (it was double-counting tokens and turn telemetry). +const STORE_VERSION: u32 = 9; const QUOTA_CACHE_VERSION: u32 = 1; const PROJECT_CACHE_VERSION: u32 = 1; const TELEMETRY_CACHE_VERSION: u32 = 1; @@ -782,8 +798,24 @@ impl Store { fn parse_codex_line(&mut self, line: &str, state: &mut FileState) -> Option { let v: serde_json::Value = serde_json::from_str(line).ok()?; let payload = v.get("payload")?; - match v.get("type")?.as_str()? { + let line_type = v.get("type")?.as_str()?; + // A forked thread replays its parent's history before its own first + // turn_context. Drop that whole window (see FileState::replaying). + if state.replaying && line_type != "turn_context" { + return None; + } + match line_type { "session_meta" => { + // Only the file's own meta (its first line) counts; the replayed + // parent metas that follow would clobber session/cwd. + if state.meta_seen { + return None; + } + state.meta_seen = true; + // A fork/resume names the thread it continues; everything up to + // this file's first turn_context is that thread's replayed log. + state.replaying = payload.get("forked_from_id").is_some() + || payload.get("parent_thread_id").is_some(); // `id` is this thread's own session id; `session_id` can be the // parent for subagent threads. Prefer the file's own id. if let Some(id) = payload @@ -799,6 +831,8 @@ impl Store { None } "turn_context" => { + // The child's own turns start here; the replay window is over. + state.replaying = false; if let Some(m) = payload.get("model").and_then(|x| x.as_str()) { state.model = m.to_string(); } @@ -1498,6 +1532,67 @@ mod tests { assert_eq!(store.quota_history.len(), 2); } + // A forked/subagent thread replays the parent's whole history — usage, turn + // telemetry, tool calls, even the parent's session_meta — at the head of its + // rollout file, restamped with the fork instant. The parent file still holds + // all of it and Codex events have no message id to dedupe on, so counting + // the replay would double-count. Everything before the child's first + // turn_context must be dropped. + #[test] + fn skips_replayed_parent_history_in_a_forked_codex_thread() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/child.jsonl".to_string(), + ..FileState::default() + }; + let own_meta = r#"{"timestamp":"2026-07-24T02:30:43Z","type":"session_meta","payload":{"id":"child-1","forked_from_id":"parent-1","parent_thread_id":"parent-1","cwd":"/work/child"}}"#; + let replayed_meta = r#"{"timestamp":"2026-07-24T02:30:44Z","type":"session_meta","payload":{"id":"parent-1","cwd":"/work/parent"}}"#; + let replayed_started = r#"{"timestamp":"2026-07-24T02:30:44Z","type":"event_msg","payload":{"type":"task_started","turn_id":"old-turn","model_context_window":200000}}"#; + let replayed_usage = r#"{"timestamp":"2026-07-24T02:30:44Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":116643,"cached_input_tokens":114432,"output_tokens":592}}}}"#; + let replayed_mcp = r#"{"timestamp":"2026-07-24T02:30:44Z","type":"response_item","payload":{"type":"function_call","name":"mcp__server__tool"}}"#; + let own_context = r#"{"timestamp":"2026-07-24T02:31:00Z","type":"turn_context","payload":{"model":"gpt-5.6-sol","turn_id":"new-turn","cwd":"/work/child"}}"#; + let own_usage = r#"{"timestamp":"2026-07-24T02:31:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":50}}}}"#; + + assert!(store.parse_codex_line(own_meta, &mut state).is_none()); + assert!(store.parse_codex_line(replayed_meta, &mut state).is_none()); + assert!(store.parse_codex_line(replayed_started, &mut state).is_none()); + assert!(store.parse_codex_line(replayed_usage, &mut state).is_none()); + assert!(store.parse_codex_line(replayed_mcp, &mut state).is_none()); + // The replayed parent meta must not have hijacked the child's identity. + assert_eq!(state.session, "child-1"); + assert_eq!(state.cwd, "/work/child"); + // Nothing before the first turn_context may produce turn telemetry. + assert!(store.turns.is_empty()); + + // The child's first turn_context ends the replay window; its own turns + // count normally from there. + assert!(store.parse_codex_line(own_context, &mut state).is_none()); + let event = store + .parse_codex_line(own_usage, &mut state) + .expect("the child's own usage still counts"); + assert_eq!(event.in_tok, 400.0); + assert_eq!(event.cr, 100.0); + assert_eq!(event.out_tok, 50.0); + assert_eq!(event.model, "gpt-5.6-sol"); + } + + // A fresh (non-forked) session has no replay window, so its very first + // events count even before any turn_context. + #[test] + fn keeps_all_events_in_a_fresh_codex_session() { + let mut store = empty_store(); + let mut state = FileState::default(); + let meta = r#"{"timestamp":"2026-07-24T02:30:43Z","type":"session_meta","payload":{"id":"fresh-1","cwd":"/work"}}"#; + let usage = r#"{"timestamp":"2026-07-24T02:30:44Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":300,"cached_input_tokens":100,"output_tokens":20}}}}"#; + + assert!(store.parse_codex_line(meta, &mut state).is_none()); + let event = store + .parse_codex_line(usage, &mut state) + .expect("a fresh session's usage counts from the start"); + assert_eq!(event.in_tok, 200.0); + assert_eq!(event.session, "fresh-1"); + } + #[test] fn records_codex_aborted_turn_and_wasted_tokens() { let mut store = empty_store(); From 0c944cf7ae983260793d5e5c4bb5bdfde89c9aa5 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:06:22 -0700 Subject: [PATCH 33/67] feat: disable webview context menu in production WKWebView (macOS) and WebView2 (Windows) show a built-in right-click menu (Reload / Back / Inspect). Reload re-mounts the whole panel and freezes it mid-load, so suppress the menu in production via a contextmenu preventDefault; dev keeps it for Inspect Element. Cherry-picked from upstream HduSy/tokenscope 4f6048a. Co-Authored-By: Claude Opus 5 (1M context) --- src/main.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main.tsx b/src/main.tsx index af591f6..1bf8059 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,4 @@ +/// import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; @@ -17,6 +18,14 @@ style.textContent = ` `; document.head.appendChild(style); +// WKWebView (macOS) and WebView2 (Windows) both surface a built-in context +// menu on right-click (Reload / Back / Inspect). Reload is dangerous here — it +// re-mounts the whole panel UI and freezes it mid-load — so suppress the menu +// in production. Dev keeps it for Inspect Element. +if (import.meta.env.PROD) { + window.addEventListener("contextmenu", (e) => e.preventDefault()); +} + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( From 26d6ebaa2c528f2387480213a95e4975b9caffd0 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:06:22 -0700 Subject: [PATCH 34/67] feat(pricing): force-refresh prices via tray Refresh, ETag-conditional + 30s throttle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tray Refresh item now also kicks off a throttled background price refresh: force=true bypasses the 24h cache, fetches models.dev + LiteLLM with If-None-Match (304 skips the ~5MB download and re-parse, 200 writes), then pushes dashboard-updated so an open panel re-prices live — all off-thread, no UI freeze. Pricing::load returns None when nothing changed so the table isn't re-parsed. Throttled to one per 30s via compare_exchange (fixed window) so rapid clicks can't race on the cache. Ported from upstream HduSy/tokenscope 824e644, keeping this fork's extra cache hardening: upstream validates only the freshly fetched body, while we also gate the fresh-cache read, the 304 read-back and the stale fallback through valid(), so a corrupt or truncated cache falls through to the bundled snapshot instead of being served as a price table. Co-Authored-By: Claude Opus 5 (1M context) --- src-tauri/src/lib.rs | 47 ++++++++++++++++-- src-tauri/src/pricing.rs | 104 ++++++++++++++++++++++++++++----------- 2 files changed, 120 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 318b100..9df4de5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -312,6 +312,43 @@ fn refresh(app: &tauri::AppHandle) { let _ = app.emit("dashboard-updated", &dash); } +/// Cooldown for manual force-refreshes (the tray "Refresh" item). Price tables +/// change at most a few times a day, so back-to-back clicks inside this window +/// coalesce into one fetch. +const FORCE_COOLDOWN_MS: i64 = 30_000; +static LAST_FORCE_MS: AtomicI64 = AtomicI64::new(0); + +/// Off-thread, silent price-table refresh (models.dev + LiteLLM) bypassing the +/// 24h cache, folded into the tray's "Refresh" item. Returns immediately; once +/// the new table is swapped in, refresh() pushes dashboard-updated so an open +/// panel re-prices live, same silent path as the 30s background poll (no +/// loading state, no UI feedback). Throttled to one per FORCE_COOLDOWN_MS via +/// compare_exchange (fixed window, not sliding) so rapid clicks can't spawn +/// concurrent fetches racing on the cache. +fn refresh_pricing_bg(app: &tauri::AppHandle) { + let now = now_ms(); + loop { + let prev = LAST_FORCE_MS.load(Ordering::Relaxed); + if now - prev < FORCE_COOLDOWN_MS { + return; + } + match LAST_FORCE_MS.compare_exchange(prev, now, Ordering::Relaxed, Ordering::Relaxed) { + Ok(_) => break, + Err(_) => continue, + } + } + let handle = app.clone(); + std::thread::spawn(move || { + pricing::Pricing::reload_shared(true); + refresh(&handle); + }); +} + +#[tauri::command] +fn refresh_pricing(app: tauri::AppHandle) { + refresh_pricing_bg(&app); +} + // ── Launch-at-login preference ────────────────────────────────────── // Persisted in the data dir so it survives restarts and updates. The // on/off toggle lives in the tray's right-click menu; on startup we reconcile @@ -1029,7 +1066,8 @@ pub fn run() { begin_drag, set_dashboard_shortcut, get_app_language, - set_app_language + set_app_language, + refresh_pricing ]) .setup(move |app| { // Menu-bar–only app: no Dock icon, runs in the background. @@ -1351,7 +1389,10 @@ pub fn run() { }) .on_menu_event(move |app, event| match event.id.as_ref() { "open" => show_popover(app), - "refresh" => refresh(app), + "refresh" => { + refresh(app); + refresh_pricing_bg(app); + } "check-updates" => { show_popover(app); let _ = app.emit("check-for-updates", ()); @@ -1446,7 +1487,7 @@ pub fn run() { // while BUILD_LOCK is held. let handle = app.handle().clone(); std::thread::spawn(move || loop { - pricing::Pricing::reload_shared(); + pricing::Pricing::reload_shared(false); // Rebuild immediately with the newly loaded prices. Otherwise // an open panel can keep showing the startup built-in snapshot // (and false "without pricing data" warnings) until the next diff --git a/src-tauri/src/pricing.rs b/src-tauri/src/pricing.rs index daa0bee..40c8a50 100644 --- a/src-tauri/src/pricing.rs +++ b/src-tauri/src/pricing.rs @@ -130,54 +130,99 @@ fn valid_litellm(text: &str) -> bool { } /// Read a fresh (<24h) cache for `name`, else fetch `url` & cache it, else fall -/// back to any stale cache. Returns the raw JSON text. `valid` gates what gets -/// written to the cache: a 200 carrying a JSON error envelope (CDN/proxy/rate -/// limit) would otherwise poison the cache for 24h with zero usable prices, so -/// we only persist a body that actually parses as a price table — and keep the -/// previous good cache otherwise. -fn fetch_cached(name: &str, url: &str, valid: impl Fn(&str) -> bool) -> Option { - let path = cache_dir()?.join(format!("{name}.json")); - if let Ok(meta) = fs::metadata(&path) { - let fresh = meta - .modified() - .ok() - .and_then(|m| SystemTime::now().duration_since(m).ok()) - .map(|age| age < MAX_AGE) - .unwrap_or(false); - if fresh { - if let Ok(t) = fs::read_to_string(&path) { - if valid(&t) { - return Some(t); +/// back to any stale cache. Returns `(text, changed)`: `changed` is true only +/// when a 200 actually fetched new content (a 304, a fresh local cache, or a +/// stale fallback all leave it false) — the caller uses it to skip re-parsing +/// ~5MB of JSON when nothing moved. `force` skips the freshness check and +/// re-fetches unconditionally (only the tray "Refresh" item passes true; the +/// 24h background poll passes false). `valid` gates both what gets written to +/// the cache and what we're willing to read back: a 200 carrying a JSON error +/// envelope (CDN/proxy/rate limit) would otherwise poison the cache for 24h +/// with zero usable prices, so we only persist — and only return — a body that +/// actually parses as a price table, keeping the previous good cache otherwise. +/// Fetches are conditional GETs: we send the last ETag as If-None-Match, so an +/// unchanged table gets a 304 with no body — skipping the ~3MB download and the +/// write. +fn fetch_cached( + name: &str, + url: &str, + valid: impl Fn(&str) -> bool, + force: bool, +) -> (Option, bool) { + let Some(dir) = cache_dir() else { + return (None, false); + }; + let path = dir.join(format!("{name}.json")); + let etag_path = dir.join(format!("{name}.etag")); + if !force { + if let Ok(meta) = fs::metadata(&path) { + let fresh = meta + .modified() + .ok() + .and_then(|m| SystemTime::now().duration_since(m).ok()) + .map(|age| age < MAX_AGE) + .unwrap_or(false); + if fresh { + if let Ok(t) = fs::read_to_string(&path) { + if valid(&t) { + return (Some(t), false); + } } } } } - // fetch fresh — only overwrite the cache if the body validates as a table - if let Ok(resp) = ureq::get(url).timeout(Duration::from_secs(10)).call() { + // Conditional GET: send the cached ETag so the CDN can answer 304 (no body) + // when the table hasn't changed. Only send an ETag when we still have the + // body it describes — an orphan .etag (e.g. user deleted the .json) would + // otherwise draw a 304 for a body we no longer have. + let cached_etag = fs::read_to_string(&etag_path) + .ok() + .filter(|_| fs::metadata(&path).is_ok()); + let mut req = ureq::get(url).timeout(Duration::from_secs(10)); + if let Some(e) = cached_etag.as_deref() { + req = req.set("If-None-Match", e); + } + if let Ok(resp) = req.call() { + if resp.status() == 304 { + return (fs::read_to_string(&path).ok().filter(|t| valid(t)), false); + } + let new_etag = resp.header("etag").map(str::to_string); if let Ok(text) = resp.into_string() { if valid(&text) { let _ = fs::write(&path, &text); - return Some(text); + if let Some(etag) = new_etag { + let _ = fs::write(&etag_path, etag); + } + return (Some(text), true); } } } // Last successful raw response is the offline price source. Never return a // corrupt/partial cache: let the bundled snapshot take over in that case. - fs::read_to_string(&path).ok().filter(|t| valid(t)) + (fs::read_to_string(&path).ok().filter(|t| valid(t)), false) } impl Pricing { - pub fn load() -> Self { + pub fn load(force: bool) -> Option { + let (md, md_changed) = fetch_cached("modelsdev", MODELSDEV_URL, valid_modelsdev, force); + let (ll, ll_changed) = fetch_cached("litellm", LITELLM_URL, valid_litellm, force); + // Nothing moved (both 304 / fresh cache / stale fallback) and we already + // have a table loaded — skip the ~5MB re-parse and keep the current one. + // First-ever load still parses (PRICING is unset) so a table exists + // before the first build_dashboard runs. + if PRICING.get().is_some() && !md_changed && !ll_changed { + return None; + } let mut p = Pricing { exact: HashMap::new(), norm: HashMap::new(), }; // 1. models.dev — primary (inserted first, so it wins on conflict) - if let Some(text) = fetch_cached("modelsdev", MODELSDEV_URL, valid_modelsdev) { + if let Some(text) = md { p.ingest_modelsdev(&text); } // 2. LiteLLM — fills gaps models.dev doesn't cover - if let Some(text) = fetch_cached("litellm", LITELLM_URL, valid_litellm) { + if let Some(text) = ll { p.ingest_litellm(&text); } // 3. bundled LiteLLM snapshot — offline fallback for anything the live @@ -185,7 +230,7 @@ impl Pricing { p.ingest_litellm(LITELLM_SNAPSHOT); // 4. built-in backstop (a handful of core models, last resort) p.ingest_builtin(); - p + Some(p) } /// Just the built-in snapshot — no disk, no network. Returned by `shared()` @@ -215,8 +260,11 @@ impl Pricing { /// Load the full table (cache read + network on cold/stale cache) and swap it /// into the shared cell. MUST run on a background thread — never the main /// thread or a BUILD_LOCK holder — since the fetch can block up to ~20s. - pub fn reload_shared() { - let p = Arc::new(Pricing::load()); + pub fn reload_shared(force: bool) { + let Some(p) = Pricing::load(force) else { + return; // nothing changed — keep the current table, no re-parse, no swap + }; + let p = Arc::new(p); match PRICING.get() { Some(lock) => { if let Ok(mut g) = lock.write() { From 74e316fa7440f3e565af83c40a68f252f047c273 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:09:04 -0700 Subject: [PATCH 35/67] docs: log the pricing and Codex replay bugs, and the replay caveat Records both data-accuracy fixes in the bug log with symptom / cause / fix, and writes down the one assumption the replay fix rests on: it is a window skip, not content-level dedup, so it needs the parent rollout file to still be on disk. Existing installs are safe, but a fresh install after Codex has rotated a parent away would silently lose that history. Issues are disabled on the repo, so this is the caveat's home rather than a tracker entry. Renumbers the later entries, which are sequential. Co-Authored-By: Claude Opus 5 (1M context) --- docs/BUGFIXES.md | 82 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/docs/BUGFIXES.md b/docs/BUGFIXES.md index b5ba390..ba62988 100644 --- a/docs/BUGFIXES.md +++ b/docs/BUGFIXES.md @@ -50,9 +50,65 @@ fix. Newest first. Useful as a reference for similar issues. --- +## Data accuracy (cost & token totals) + +### 4. Cache tokens priced at $0, undercounting Claude cost ~4× + +- **Symptom**: Claude's monthly cost read far too low ($328 where the real + API-equivalent figure was $1409), while Codex looked correct. The error came + and went depending on which model was in use. +- **Cause**: models.dev lists the same model under many providers (first-party + vendor plus resellers / gateways / clouds). `ingest_modelsdev` is + first-writer-wins over providers iterated in key order — and `serde_json` + has no `preserve_order`, so that order is **alphabetical**. The previous sort + only pushed bare ids ahead of `vendor/model` ones, so among bare entries the + alphabetically first provider won. For `claude-fable-5` that is `abacus`, + whose entry is not a cheaper price but an **incomplete record**: identical + `input`/`output` to Anthropic's, with `cache_read`/`cache_write` simply + absent, so both defaulted to `0`. Claude Code usage is ~98% cache-read + tokens, so this charged for only ~2% of the volume. `claude-sonnet-5` was + worse — the shadowing entry also carried a 50% higher input price. +- **Fix**: Added `is_first_party()` and ordered entries best-first in + `pricing.rs`: the model's first-party vendor, then entries that actually + carry cache pricing, then bare ids over `vendor/model` duplicates. Ported + from upstream `3a30273` / `0987a4c`. + +### 5. Forked Codex threads double-counted their parent's history + +- **Symptom**: Codex token totals ran high — measured 8.5% (day), 4.7% (week), + 5.7% (month) — and the model breakdown didn't sum to Total tokens. Some + events were attributed to the wrong session and project. +- **Cause**: Codex forks a subagent/resumed thread by replaying the parent + thread's **entire history** — `token_count`, `task_started`/`task_complete`, + tool calls, even the parent's `session_meta` — into the head of the child's + rollout file, all restamped with the fork instant. Claude events dedupe by + `message.id`, but **Codex events carry no id**, so nothing caught it and + every replayed turn was counted a second time. The replayed `session_meta` + also clobbered the child's own session id and cwd. Because `state.model` is + only set by `turn_context`, the replayed events also had an empty model, so + they landed in Total tokens but were excluded from requests, the model + breakdown, and cost. +- **Fix**: `FileState` gained `replaying`/`meta_seen`; a file whose + `session_meta` names a `forked_from_id`/`parent_thread_id` skips **every** + line until its own first `turn_context`. The window is unambiguous: across + 131 fresh sessions there were zero `token_count` lines before the first + `turn_context`, versus 7234 in forked ones — and all 7234 were verified to + still have an exact usage match in a parent file, so nothing is lost. + Bumped `STORE_VERSION` to 9 for a one-time rescan. +- **Known limitation**: this is a *window skip*, not content-level dedup, so it + assumes the parent rollout file is still on disk. Existing installs are safe + (the event cache retains what it already ingested), but Codex rotates + `~/.codex/sessions`, so a **fresh install after a parent was rotated away + would silently lose that history**. A content-level key (session id + + original timestamp + usage tuple) would close this, but the replay restamps + timestamps, so the original would have to be recoverable first. Left as-is: + correct for every case observed, and strictly simpler. + +--- + ## Release & distribution (CI) -### 4. Release CI failed: empty Apple signing env var +### 6. Release CI failed: empty Apple signing env var - **Symptom**: The `v0.1.1` build failed at the bundle step with `security: SecKeychainItemImport: ... parameters ... not valid` / @@ -65,7 +121,7 @@ fix. Newest first. Useful as a reference for similar issues. - **Fix**: Commented out the Apple signing/notarization env in `release.yml` until the real secrets exist. The build now does ad-hoc signing, like local. -### 5. GitHub Release had no .dmg / .app — it was a draft +### 7. GitHub Release had no .dmg / .app — it was a draft - **Symptom**: "The release has no artifacts." - **Cause**: `releaseDraft: true` — the build *did* succeed and attach the @@ -76,7 +132,7 @@ fix. Newest first. Useful as a reference for similar issues. - **Fix**: Set `releaseDraft: false` so each tag publishes immediately and the asset URL is live for the Homebrew step and `brew install`. -### 6. Homebrew Cask step would hash a 404 page +### 8. Homebrew Cask step would hash a 404 page - **Symptom**: Latent — the cask `sha256` could be computed from an error page. - **Cause**: The cask step fetched the asset with `curl -sL` (no `-f`), so a 404 @@ -85,7 +141,7 @@ fix. Newest first. Useful as a reference for similar issues. - **Fix**: Use `curl -fsSL` so a missing asset fails and retries; fail loudly (`exit 1`) if the asset never appears. -### 7. DMG name didn't match the tag +### 9. DMG name didn't match the tag - **Symptom**: A `v0.1.1` tag would build `Tokenscope_0.1.0_universal.dmg`, which the cask step (computing the name from the tag) couldn't download → 404. @@ -99,7 +155,7 @@ fix. Newest first. Useful as a reference for similar issues. ## App behavior & packaging -### 8. Two menu-bar icons after reinstall +### 10. Two menu-bar icons after reinstall - **Symptom**: Reinstalling/relaunching left two Tokenscope icons in the menu bar. @@ -108,7 +164,7 @@ fix. Newest first. Useful as a reference for similar issues. - **Fix**: Added `tauri-plugin-single-instance` (registered first) so a second launch hands off to the running instance (showing the popover) and exits. -### 9. Unsigned app blocked by Gatekeeper on first open +### 11. Unsigned app blocked by Gatekeeper on first open - **Symptom**: "Apple cannot verify Tokenscope.app is free of malware." - **Cause**: The build is unsigned/unnotarized, and Homebrew adds a quarantine @@ -118,7 +174,7 @@ fix. Newest first. Useful as a reference for similar issues. manual right-click → Open or `xattr -cr`; a full fix needs Developer ID signing + notarization.) -### 10. App icon had opaque white corners +### 12. App icon had opaque white corners - **Symptom**: The rounded app icon showed white square corners in Launchpad. - **Cause**: The icon PNGs had a white (opaque) background in the corners @@ -131,7 +187,7 @@ fix. Newest first. Useful as a reference for similar issues. ## UI / charts -### 11. Bar-chart tooltip overlapped the legend above it +### 13. Bar-chart tooltip overlapped the legend above it - **Symptom**: Hovering a token bar showed its tooltip floating up over the Total-tokens "Input … cached" legend, even for short bars. @@ -143,7 +199,7 @@ fix. Newest first. Useful as a reference for similar issues. *visible bar top* (`r.bottom − barPx`, baseline minus bar height) instead of the column top, so short bars get a low tooltip clear of the legend. -### 12. Mockup tooltip drifted to the panel centre (backdrop-filter) +### 14. Mockup tooltip drifted to the panel centre (backdrop-filter) - **Symptom**: In `tokenscope-panel.html` only, the heatmap/bar tooltips appeared near the middle of the panel instead of next to the hovered cell/bar. @@ -157,7 +213,7 @@ fix. Newest first. Useful as a reference for similar issues. relative to each chart's own wrapper (coords offset from the wrapper rect). The mockup never scrolls, so it doesn't need `fixed`. -### 13. Total-tokens bar showed slivers when usage was zero +### 15. Total-tokens bar showed slivers when usage was zero - **Symptom**: With no usage in the period (Total = 0.00M), the input/output split bar still showed a small coloured sliver instead of being empty. @@ -171,7 +227,7 @@ fix. Newest first. Useful as a reference for similar issues. ## Theme -### 14. "System" theme mode didn't follow the macOS appearance +### 16. "System" theme mode didn't follow the macOS appearance - **Symptom**: On macOS, the "System" theme option didn't track the OS dark/light mode — neither when toggling system appearance with the popover open, nor after @@ -200,7 +256,7 @@ fix. Newest first. Useful as a reference for similar issues. `block` re-exports already imported in `lib.rs`. (`src-tauri/src/lib.rs`, `src/App.tsx`) -### 15. Selected period pill flashed white→transparent on a light→dark switch +### 17. Selected period pill flashed white→transparent on a light→dark switch - **Symptom**: After the fix above, switching the system theme from light to dark while the popover was hidden, then opening it, showed a brief "white → @@ -223,7 +279,7 @@ fix. Newest first. Useful as a reference for similar issues. Day/Week/Month still animates). Skipped on the very first render. (`src/main.tsx`, `src/App.tsx`) -### 16. Total-tokens split bar didn't fill — gray track showed through +### 18. Total-tokens split bar didn't fill — gray track showed through - **Symptom**: The split bar under Total tokens was only partly filled, leaving gray track visible on the right. It was most obvious with a lopsided split. From e6edfffd652a3c5049ab1ab80c1a74e5234cd5a1 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 00:43:00 -0700 Subject: [PATCH 36/67] fix(store): assign Codex quota snapshots by active model, not limit_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex keeps two independent rate-limit pools — the standard models and Spark (`codex_bengalfox`) — but recent builds report `limit_id: "codex"` for Spark turns too, so the raw id can no longer tell them apart. Spark's weekly usage was being folded into the standard pool, corrupting both percentages. Determine the pool from the turn's active model instead (`codex_quota_pool`), falling back to the legacy `limit_id` only when no model is known (pre-model/legacy records). QUOTA_CACHE_VERSION 1 -> 3 re-derives the cached snapshots. rebuild_codex_quotas is reworked for the migration: - clear quota_history; the old points were recorded under the wrong pool - walk files oldest-first so the rebuilt history is chronological - fully read (from byte 0) the files the event cache shows contain Spark usage, instead of only their 64 KiB tail: a session can switch away from Spark after its last Spark snapshot, leaving the turn_context that names the model outside the tail - seed tail reads from the persisted manifest so the model carries over - blank state.source so this quota-only pass cannot touch turn telemetry - feed turn_context lines through, not just rate_limits, to learn the model Also feed session_meta through: it is what arms the forked-thread replay skip added in b40963d. Without it `replaying` was never set on a full scan, so a forked file's replayed parent rate_limits — 216 lines in one sampled fork, every one restamped with the fork instant — would have been ingested as fresh quota readings. Tails are unaffected: they inherit the flag from the manifest and begin well past the replay window. Co-Authored-By: Claude Opus 5 (1M context) --- src-tauri/src/store.rs | 178 +++++++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 4e3f094..d0a4cbe 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -14,7 +14,7 @@ use crate::model::Quota; use chrono::DateTime; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; @@ -178,10 +178,17 @@ pub struct Store { // v9: drop the replayed parent history at the head of a forked Codex thread // (it was double-counting tokens and turn telemetry). const STORE_VERSION: u32 = 9; -const QUOTA_CACHE_VERSION: u32 = 1; +// v2: quota pools are determined by the active model. Recent Codex logs report +// Spark token_count snapshots with `limit_id: "codex"`, even though Spark and +// the other Codex models have independent allowances. +// v3: during the v2 migration, fully read source files that contain Spark +// usage. A session can switch away from Spark after its last Spark snapshot, +// leaving its model context outside the usual 64 KiB quota-file tail. +const QUOTA_CACHE_VERSION: u32 = 3; const PROJECT_CACHE_VERSION: u32 = 1; const TELEMETRY_CACHE_VERSION: u32 = 1; -// One-time quota migration: at most 32 × 64 KiB = 2 MiB of log content. +// One-time quota migration: tail the 32 newest files, and fully read only +// files which already contain Spark usage in the local event cache. const RECENT_QUOTA_FILES: usize = 32; const QUOTA_TAIL_BYTES: u64 = 64 * 1024; @@ -297,6 +304,38 @@ fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { } } +const CODEX_QUOTA_POOL: &str = "codex"; +const SPARK_QUOTA_POOL: &str = "codex_bengalfox"; + +fn is_codex_spark_model(model: &str) -> bool { + let model = model + .trim() + .rsplit('/') + .next() + .unwrap_or_default() + .to_ascii_lowercase(); + model == "gpt-5.3-codex-spark" || model.starts_with("gpt-5.3-codex-spark-") +} + +/// Assign a quota snapshot to the pool consumed by its active model. +/// +/// Codex now emits `limit_id: "codex"` for Spark turns as well, so that raw +/// id cannot distinguish the two independent pools. The old ids are retained +/// only as a fallback for pre-model/legacy records. +fn codex_quota_pool(model: &str, rl: &serde_json::Value) -> Option<&'static str> { + if is_codex_spark_model(model) { + return Some(SPARK_QUOTA_POOL); + } + if !model.trim().is_empty() { + return Some(CODEX_QUOTA_POOL); + } + match rl.get("limit_id").and_then(|value| value.as_str()) { + None | Some(CODEX_QUOTA_POOL) => Some(CODEX_QUOTA_POOL), + Some(SPARK_QUOTA_POOL) => Some(SPARK_QUOTA_POOL), + _ => None, + } +} + impl Store { /// Load persisted events + offset manifest (empty on first run). pub fn load() -> Self { @@ -403,6 +442,9 @@ impl Store { store.rebuild_codex_quotas(); if let Some(dir) = cache_dir() { store.save_quota_cache(&dir); + // The quota trend uses the same pool id as the current + // snapshot, so persist the rebuilt history with it. + store.save_telemetry_cache(&dir); } } if version_ok && store.refresh_projects() { @@ -488,6 +530,14 @@ impl Store { fn rebuild_codex_quotas(&mut self) { self.codex_quota = None; self.codex_spark_quota = None; + self.quota_history.clear(); + let spark_sources: HashSet = self + .events + .iter() + .filter(|event| is_codex_spark_model(&event.model)) + .map(|event| event.source.clone()) + .filter(|source| !source.is_empty()) + .collect(); let Some(root) = codex_dir() else { return; }; @@ -506,16 +556,29 @@ impl Store { Some((modified, entry.into_path())) }) .collect(); - files.sort_by_key(|item| std::cmp::Reverse(item.0)); + // Read the recent files oldest-first so quota history remains ordered. + // `update_codex_quota` still compares timestamps before replacing a + // current snapshot, so this is safe if filesystem mtimes are imperfect. + files.sort_by_key(|item| item.0); + let recent_start = files.len().saturating_sub(RECENT_QUOTA_FILES); - for (_, path) in files.into_iter().take(RECENT_QUOTA_FILES) { - let Ok(mut file) = fs::File::open(path) else { + for (index, (_, path)) in files.into_iter().enumerate() { + let key = path.to_string_lossy().to_string(); + let full_scan = spark_sources.contains(&key); + if index < recent_start && !full_scan { + continue; + } + let Ok(mut file) = fs::File::open(&path) else { continue; }; let Ok(size) = file.metadata().map(|meta| meta.len()) else { continue; }; - let start = size.saturating_sub(QUOTA_TAIL_BYTES); + let start = if full_scan { + 0 + } else { + size.saturating_sub(QUOTA_TAIL_BYTES) + }; if file.seek(SeekFrom::Start(start)).is_err() { continue; } @@ -532,12 +595,32 @@ impl Store { .map(|i| i + 1) .unwrap_or(bytes.len()) }; - let mut state = FileState::default(); + // Tails usually begin after their last turn_context, so seed them + // with the final model from normal ingest. Spark source files are + // read from their start, where each turn_context is available. + // Keep `source` blank so this quota-only migration cannot alter + // persisted turn telemetry. + let mut state = if full_scan { + FileState::default() + } else { + self.manifest.files.get(&key).cloned().unwrap_or_default() + }; + state.source.clear(); for line in bytes[skip..].split(|byte| *byte == b'\n') { let Ok(text) = std::str::from_utf8(line) else { continue; }; - if text.contains("\"rate_limits\"") { + // `session_meta` is what arms the forked-thread replay skip, so + // a full scan must feed it through too — otherwise `replaying` + // never gets set and the parent's restamped rate_limits (216 in + // one sampled fork, all stamped with the fork instant) would be + // ingested as fresh quota readings. Tails don't need it: they + // inherit the flag from the manifest and start well past the + // replay window anyway. + if text.contains("\"session_meta\"") + || text.contains("\"turn_context\"") + || text.contains("\"rate_limits\"") + { let _ = self.parse_codex_line(text, &mut state); } } @@ -915,7 +998,7 @@ impl Store { _ => return None, } if let Some(rl) = payload.get("rate_limits") { - self.update_codex_quota(rl, ts_ms); + self.update_codex_quota(rl, ts_ms, &state.model); } // Per-turn delta. `input_tokens` INCLUDES `cached_input_tokens` // (unlike Claude, where cache_read is separate) — subtract so @@ -1130,15 +1213,15 @@ impl Store { parse_claude_value(&value) } - /// Keep the newest rate-limit snapshot (files are walked in arbitrary order, - /// so compare timestamps rather than trusting encounter order). - fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64) { - let limit_id = match rl.get("limit_id").and_then(|value| value.as_str()) { - None | Some("codex") => "codex", - Some("codex_bengalfox") => "codex_bengalfox", - _ => return, + /// Keep the newest rate-limit snapshot. Spark and standard Codex models + /// consume separate pools, but recent JSONL snapshots no longer reflect + /// that in `rate_limits.limit_id`; classify known models before using the + /// legacy limit id as a fallback. + fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64, model: &str) { + let Some(limit_id) = codex_quota_pool(model, rl) else { + return; }; - let previous = if limit_id == "codex" { + let previous = if limit_id == CODEX_QUOTA_POOL { self.codex_quota.as_ref() } else { self.codex_spark_quota.as_ref() @@ -1170,7 +1253,7 @@ impl Store { as_of_ms: ts_ms, }; self.record_quota_history(limit_id, "a); - if limit_id == "codex" { + if limit_id == CODEX_QUOTA_POOL { self.codex_quota = Some(quota); } else { self.codex_spark_quota = Some(quota); @@ -1510,7 +1593,7 @@ mod tests { } #[test] - fn keeps_general_and_spark_quotas_separate() { + fn keeps_spark_and_standard_codex_quotas_separate_by_model() { let mut store = empty_store(); let snapshot = |limit_id: &str, used_percent: f64| { serde_json::json!({ @@ -1523,15 +1606,62 @@ mod tests { }) }; - store.update_codex_quota(&snapshot("codex", 24.0), 100); - store.update_codex_quota(&snapshot("codex_bengalfox", 7.0), 200); - store.update_codex_quota(&snapshot("codex", 24.05), 300); + // Recent Spark JSONL events use `limit_id: codex`; model identity must + // still put that weekly usage in Spark's independent pool. + store.update_codex_quota(&snapshot("codex", 7.0), 100, "gpt-5.3-codex-spark"); + store.update_codex_quota(&snapshot("codex", 24.05), 200, "gpt-5.6-sol"); + // Conversely, a stale legacy id cannot move a non-Spark model into + // Spark's pool. + store.update_codex_quota( + &snapshot("codex_bengalfox", 24.1), + 300, + "gpt-5.6-sol", + ); - assert_eq!(store.codex_quota.unwrap().primary_pct, 24.05); + assert_eq!(store.codex_quota.unwrap().primary_pct, 24.1); assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 7.0); assert_eq!(store.quota_history.len(), 2); } + #[test] + fn uses_legacy_limit_id_only_when_model_is_unavailable() { + let legacy_spark = serde_json::json!({ + "limit_id": "codex_bengalfox", + "primary": { + "used_percent": 7.0, + "window_minutes": 10080, + "resets_at": 123 + } + }); + + assert_eq!( + codex_quota_pool("", &legacy_spark), + Some(SPARK_QUOTA_POOL) + ); + assert_eq!( + codex_quota_pool("chatgpt/gpt-5.3-codex-spark", &legacy_spark), + Some(SPARK_QUOTA_POOL) + ); + assert_eq!( + codex_quota_pool("gpt-5.6-sol", &legacy_spark), + Some(CODEX_QUOTA_POOL) + ); + } + + #[test] + fn routes_codex_limit_id_to_spark_after_a_spark_turn_context() { + let mut store = empty_store(); + let mut state = FileState::default(); + let spark_context = r#"{"timestamp":"2026-07-15T09:50:00Z","type":"turn_context","payload":{"model":"gpt-5.3-codex-spark","turn_id":"spark-turn"}}"#; + let spark_usage = r#"{"timestamp":"2026-07-15T09:51:13Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}},"rate_limits":{"limit_id":"codex","primary":{"used_percent":21,"window_minutes":10080,"resets_at":1784684425}}}}"#; + + assert!(store.parse_codex_line(spark_context, &mut state).is_none()); + assert!(store.parse_codex_line(spark_usage, &mut state).is_some()); + + assert!(store.codex_quota.is_none()); + assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 21.0); + } + // A forked/subagent thread replays the parent's whole history — usage, turn // telemetry, tool calls, even the parent's session_meta — at the head of its // rollout file, restamped with the fork instant. The parent file still holds From e87b6fc967d607543b795965a1693dc859e6c047 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 01:52:33 -0700 Subject: [PATCH 37/67] release: v1.4.0 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f5ed37b..a9528a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.3.8", + "version": "1.4.0", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a110e35..923c446 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.3.8" +version = "1.4.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 82307c5..eafb6a5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.3.8" +version = "1.4.0" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7492f89..c1d9e49 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.3.8", + "version": "1.4.0", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 8fd9b9b848fee2ed9e9e4329fc5ee7b782d26d55 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 27 Jul 2026 02:41:14 -0700 Subject: [PATCH 38/67] docs: state the bug magnitudes as ratios, not measured usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug log quoted the absolute figures the two data-accuracy bugs were found with — monthly dollar amounts, token counts, replayed-event counts, session counts. Those describe one machine's usage, not the defects, and the repository is public. Replaced with the ratios and relative statements that actually carry the explanation: a several-fold cost undercount, a single-digit percentage token inflation, and the qualitative fresh-vs-forked contrast that makes the replay window unambiguous. The cache-read share is now quoted as upstream's public ~95%+ rather than the share measured locally. No behaviour change; documentation only. Co-Authored-By: Claude Opus 5 (1M context) --- docs/BUGFIXES.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/BUGFIXES.md b/docs/BUGFIXES.md index ba62988..d134f15 100644 --- a/docs/BUGFIXES.md +++ b/docs/BUGFIXES.md @@ -54,9 +54,9 @@ fix. Newest first. Useful as a reference for similar issues. ### 4. Cache tokens priced at $0, undercounting Claude cost ~4× -- **Symptom**: Claude's monthly cost read far too low ($328 where the real - API-equivalent figure was $1409), while Codex looked correct. The error came - and went depending on which model was in use. +- **Symptom**: Claude's monthly cost read several-fold too low (~4× on the logs + it was found with), while Codex looked correct. The error came and went + depending on which model was in use. - **Cause**: models.dev lists the same model under many providers (first-party vendor plus resellers / gateways / clouds). `ingest_modelsdev` is first-writer-wins over providers iterated in key order — and `serde_json` @@ -65,9 +65,10 @@ fix. Newest first. Useful as a reference for similar issues. alphabetically first provider won. For `claude-fable-5` that is `abacus`, whose entry is not a cheaper price but an **incomplete record**: identical `input`/`output` to Anthropic's, with `cache_read`/`cache_write` simply - absent, so both defaulted to `0`. Claude Code usage is ~98% cache-read - tokens, so this charged for only ~2% of the volume. `claude-sonnet-5` was - worse — the shadowing entry also carried a 50% higher input price. + absent, so both defaulted to `0`. Claude Code usage is ~95%+ cache-read + tokens, so this charged for only a small fraction of the volume. + `claude-sonnet-5` was worse — the shadowing entry also carried a 50% higher + input price. - **Fix**: Added `is_first_party()` and ordered entries best-first in `pricing.rs`: the model's first-party vendor, then entries that actually carry cache pricing, then bare ids over `vendor/model` duplicates. Ported @@ -75,9 +76,9 @@ fix. Newest first. Useful as a reference for similar issues. ### 5. Forked Codex threads double-counted their parent's history -- **Symptom**: Codex token totals ran high — measured 8.5% (day), 4.7% (week), - 5.7% (month) — and the model breakdown didn't sum to Total tokens. Some - events were attributed to the wrong session and project. +- **Symptom**: Codex token totals ran high by a single-digit percentage, varying + by period, and the model breakdown didn't sum to Total tokens. Some events + were attributed to the wrong session and project. - **Cause**: Codex forks a subagent/resumed thread by replaying the parent thread's **entire history** — `token_count`, `task_started`/`task_complete`, tool calls, even the parent's `session_meta` — into the head of the child's @@ -90,11 +91,11 @@ fix. Newest first. Useful as a reference for similar issues. breakdown, and cost. - **Fix**: `FileState` gained `replaying`/`meta_seen`; a file whose `session_meta` names a `forked_from_id`/`parent_thread_id` skips **every** - line until its own first `turn_context`. The window is unambiguous: across - 131 fresh sessions there were zero `token_count` lines before the first - `turn_context`, versus 7234 in forked ones — and all 7234 were verified to - still have an exact usage match in a parent file, so nothing is lost. - Bumped `STORE_VERSION` to 9 for a one-time rescan. + line until its own first `turn_context`. The window is unambiguous: fresh + sessions emit no `token_count` lines before their first `turn_context`, while + forked ones emit many — and every replayed event sampled was verified to still + have an exact usage match in a parent file, so nothing is lost. Bumped + `STORE_VERSION` to 9 for a one-time rescan. - **Known limitation**: this is a *window skip*, not content-level dedup, so it assumes the parent rollout file is still on disk. Existing installs are safe (the event cache retains what it already ingested), but Codex rotates From f31d8f6d43a52e53bb13de88f8eacc905f032a45 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Wed, 29 Jul 2026 00:11:04 -0700 Subject: [PATCH 39/67] fix(store): dedup Codex usage by turn id and position, not by file window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v9 assumed a forked thread's replayed parent history always precedes the fork's own first turn_context. That holds for one Codex layout. In the more common one the fork's turn_context sits at line ~7 and the parent's history is replayed *after* it, so the window closed immediately and the replay was ingested anyway — the check caught a small minority of replayed events and missed an order of magnitude more. Left in, that meant most ingested Codex volume was duplicate, and worse: a replayed rate_limits reading carries the parent's stale percentages restamped with the fork instant, so update_codex_quota's newest-wins rule handed the display a long-expired 100% reading. That is what pinned the weekly quota bar at 100% for days after the window had rolled over. Dedup by content instead of by position. Codex token_count lines carry no id, but a replay reproduces the parent's turns verbatim — same turn ids, in the same order — so (turn_id, position-within-turn) identifies a replayed event as one already ingested, the same way message.id does for Claude. The check runs before every side effect, so a replay can no longer double-count tokens, duplicate turn telemetry, or win the quota race. Events with no known turn get no id and are kept, which is the conservative direction. Sources are now walked in path order. Codex lays sessions out as YYYY/MM/DD/rollout--, so path order is chronological and the original event, not the restamped replay, becomes the first writer that dedup keeps. Validated by replaying the rule over the local corpus: it removes the duplicates and lands within ~4% of an estimate derived independently, by classifying raw log events by originating session rather than by parser logic. STORE_VERSION 10 forces the rescan. Co-Authored-By: Claude Opus 5 (1M context) --- docs/BUGFIXES.md | 46 ++++++---- src-tauri/src/store.rs | 193 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 212 insertions(+), 27 deletions(-) diff --git a/docs/BUGFIXES.md b/docs/BUGFIXES.md index d134f15..264c029 100644 --- a/docs/BUGFIXES.md +++ b/docs/BUGFIXES.md @@ -76,9 +76,10 @@ fix. Newest first. Useful as a reference for similar issues. ### 5. Forked Codex threads double-counted their parent's history -- **Symptom**: Codex token totals ran high by a single-digit percentage, varying - by period, and the model breakdown didn't sum to Total tokens. Some events - were attributed to the wrong session and project. +- **Symptom**: Codex token totals ran several-fold high — most of the ingested + Codex volume was duplicate — the model breakdown didn't sum to Total tokens, + some events were attributed to the wrong session and project, and the weekly + quota bar sat pinned at 100% long after the window had rolled over. - **Cause**: Codex forks a subagent/resumed thread by replaying the parent thread's **entire history** — `token_count`, `task_started`/`task_complete`, tool calls, even the parent's `session_meta` — into the head of the child's @@ -89,21 +90,30 @@ fix. Newest first. Useful as a reference for similar issues. only set by `turn_context`, the replayed events also had an empty model, so they landed in Total tokens but were excluded from requests, the model breakdown, and cost. -- **Fix**: `FileState` gained `replaying`/`meta_seen`; a file whose - `session_meta` names a `forked_from_id`/`parent_thread_id` skips **every** - line until its own first `turn_context`. The window is unambiguous: fresh - sessions emit no `token_count` lines before their first `turn_context`, while - forked ones emit many — and every replayed event sampled was verified to still - have an exact usage match in a parent file, so nothing is lost. Bumped - `STORE_VERSION` to 9 for a one-time rescan. -- **Known limitation**: this is a *window skip*, not content-level dedup, so it - assumes the parent rollout file is still on disk. Existing installs are safe - (the event cache retains what it already ingested), but Codex rotates - `~/.codex/sessions`, so a **fresh install after a parent was rotated away - would silently lose that history**. A content-level key (session id + - original timestamp + usage tuple) would close this, but the replay restamps - timestamps, so the original would have to be recoverable first. Left as-is: - correct for every case observed, and strictly simpler. +- **First attempt (v9, incomplete)**: `FileState` gained `replaying`/`meta_seen` + and a forked file skipped every line until its own first `turn_context`. That + assumed the replay always *precedes* the fork's first turn. It does in one + layout; in the more common one the fork's `turn_context` sits at line ~7 and + the parent's history is replayed **after** it, so the window closed + immediately and the replay was ingested anyway. The window check caught a + small minority of replayed events and missed an order of magnitude more. +- **Why the first verification missed it**: the parser was reconciled against a + "ground truth" that summed every `token_count` in the raw logs. Both sides + shared the same duplicate-counting flaw, so agreeing to 0.01% proved only that + they were consistent, not that either was right. A reconciliation has to be + derived independently of the code under test. +- **Fix (v10)**: dedup by content instead of by position. Codex `token_count` + lines carry no id, but a replay reproduces the parent's turns verbatim — same + turn ids, same order — so `(turn_id, position-within-turn)` identifies a + replayed event as one already ingested, exactly the way `message.id` does for + Claude. The check runs *before* any side effect, so a replay can no longer + double-count tokens, duplicate turn telemetry, or let the parent's stale + `rate_limits` win the newest-snapshot race. Files are now walked in path order + (chronological for Codex's `YYYY/MM/DD/rollout-` layout) so the original, + not the restamped replay, is the first writer. `STORE_VERSION` 10. +- **Known limitation**: dedup needs a turn id. Events parsed before any + `turn_context` is seen get no id and are still counted as-is; that is the + conservative direction (keep, don't drop), and such events are rare. --- diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index d0a4cbe..59ba6df 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -132,6 +132,10 @@ struct FileState { // first `turn_context`, which is where its own turns begin. #[serde(default)] replaying: bool, + // Position of the next usage event within `turn_id`. Together they form a + // Codex event's dedup id — see the `codex_event_id` doc comment. + #[serde(default)] + turn_seq: u64, // Whether this file's own `session_meta` (its first line) has been read. // Replayed parent metas follow it and must not overwrite session/cwd. #[serde(default)] @@ -177,7 +181,11 @@ pub struct Store { // v8: retain complete history for custom-range tracking and settlement. // v9: drop the replayed parent history at the head of a forked Codex thread // (it was double-counting tokens and turn telemetry). -const STORE_VERSION: u32 = 9; +// v10: dedup Codex usage events by (turn id, position). v9 only caught replays +// that precede a fork's first turn_context; the common layout puts the +// replay *after* it, leaving ~80% of Codex tokens double-counted and the +// weekly quota pinned at the parent's stale reading. +const STORE_VERSION: u32 = 10; // v2: quota pools are determined by the active model. Recent Codex logs report // Spark token_count snapshots with `limit_id: "codex"`, even though Spark and // the other Codex models have independent allowances. @@ -289,6 +297,21 @@ fn telemetry_key(source: &str, turn_id: &str) -> String { format!("{source}\0{turn_id}") } +/// Dedup id for a Codex usage event: its turn id plus its position within that +/// turn. Codex `token_count` lines carry no id of their own, but a forked or +/// resumed thread replays its parent's turns verbatim into the head of its own +/// rollout file — same turn ids, same order, restamped with the fork instant. +/// Keying on (turn, position) therefore identifies a replayed event as the one +/// already ingested from the parent, the same way `message.id` does for Claude. +/// Empty when the turn is unknown, which disables dedup for that event rather +/// than risking a collision between unrelated turns. +fn codex_event_id(state: &FileState) -> String { + if state.turn_id.is_empty() { + return String::new(); + } + format!("codex:{}#{}", state.turn_id, state.turn_seq) +} + fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { let root = cwd.map(project_root).unwrap_or_else(|| PathBuf::from(fallback)); let key = root.to_string_lossy(); @@ -784,12 +807,20 @@ impl Store { fn ingest_root(&mut self, agent: &'static str, root: &PathBuf) -> bool { let mut dirty = false; - for entry in WalkDir::new(root) + // Sort by path so a parent thread is ingested before the forks that + // replay it: Codex lays sessions out as YYYY/MM/DD/rollout--, + // so path order is chronological. Codex dedup is first-writer-wins, and + // the first writer should be the original — the replay carries the fork + // instant instead of the turn's real timestamp. + let mut entries: Vec = WalkDir::new(root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map(|x| x == "jsonl").unwrap_or(false)) - { - let path = entry.path(); + .map(|e| e.into_path()) + .collect(); + entries.sort(); + for path in entries { + let path = path.as_path(); let key = path.to_string_lossy().to_string(); let Ok(meta) = fs::metadata(path) else { continue }; let size = meta.len(); @@ -926,6 +957,7 @@ impl Store { if state.turn_id != turn_id { state.turn_id = turn_id.to_string(); state.skills_seen.clear(); + state.turn_seq = 0; } } None @@ -937,6 +969,7 @@ impl Store { if state.turn_id != turn_id { state.turn_id = turn_id.to_string(); state.skills_seen.clear(); + state.turn_seq = 0; } } match event_type { @@ -997,9 +1030,6 @@ impl Store { "token_count" => {} _ => return None, } - if let Some(rl) = payload.get("rate_limits") { - self.update_codex_quota(rl, ts_ms, &state.model); - } // Per-turn delta. `input_tokens` INCLUDES `cached_input_tokens` // (unlike Claude, where cache_read is separate) — subtract so // in_tok means "uncached new input" for both agents and @@ -1011,7 +1041,27 @@ impl Store { let out = g("output_tokens"); let reasoning = g("reasoning_output_tokens").min(out); if raw_in + out <= 0.0 { - return None; // rate-limit-only heartbeat, nothing to count + // Rate-limit-only heartbeat: no usage to count, but the + // reading itself is this thread's own and worth keeping. + if let Some(rl) = payload.get("rate_limits") { + self.update_codex_quota(rl, ts_ms, &state.model); + } + return None; + } + // Replay check must precede every side effect below. A forked + // thread reproduces its parent's turns verbatim — same turn ids, + // same order — so a usage event already seen under this + // (turn, position) is that replay, not new work. Counting it + // would double the tokens, duplicate the turn telemetry, and + // (worst) let the parent's stale rate_limits win the "newest + // snapshot" race, since the replay is restamped with the fork + // instant. That is what pinned the weekly quota at 100%. + let id = codex_event_id(state); + if !id.is_empty() && self.index.contains_key(&id) { + return None; + } + if let Some(rl) = payload.get("rate_limits") { + self.update_codex_quota(rl, ts_ms, &state.model); } let context_window = payload .get("info") @@ -1026,6 +1076,7 @@ impl Store { turn.context_tokens = turn.context_tokens.max(raw_in); turn.context_window = turn.context_window.max(context_window); } + state.turn_seq += 1; Some(RawEvent { ts_ms, session: state.session.clone(), @@ -1036,7 +1087,7 @@ impl Store { out_tok: out, mcp: Vec::new(), skills: Vec::new(), - id: String::new(), // no message id; file purge keeps re-reads idempotent + id, source: String::new(), agent: String::new(), }) @@ -1662,6 +1713,130 @@ mod tests { assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 21.0); } + // The replay is not always a head-of-file window. In the common Codex + // layout the fork's own turn_context sits at line ~7 and the parent's + // history is replayed *after* it, which the v9 window check disarmed on and + // therefore ingested. Dedup by (turn id, position) catches it regardless of + // layout: the replay reproduces the parent's turn ids in the same order, so + // the ids collide with what the parent file already contributed. + #[test] + fn dedupes_replayed_usage_that_follows_the_forks_first_turn_context() { + let mut store = empty_store(); + let usage = |turn: &str, ts: &str, input: u64| { + format!( + r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":{input},"cached_input_tokens":0,"output_tokens":10}}}}}}}}"# + ) + .replace("TURN", turn) + }; + let context = |turn: &str, ts: &str| { + format!( + r#"{{"timestamp":"{ts}","type":"turn_context","payload":{{"model":"gpt-5.6-sol","turn_id":"{turn}"}}}}"# + ) + }; + + // Parent thread: one turn, two usage events. + let mut parent = FileState { + source: "/tmp/parent.jsonl".to_string(), + session: "parent-1".to_string(), + ..FileState::default() + }; + store.parse_codex_line(&context("turn-a", "2026-07-27T00:00:00Z"), &mut parent); + for (index, line) in [ + usage("turn-a", "2026-07-27T00:00:01Z", 100), + usage("turn-a", "2026-07-27T00:00:02Z", 200), + ] + .iter() + .enumerate() + { + let event = store + .parse_codex_line(line, &mut parent) + .expect("parent usage counts"); + assert_eq!(event.id, format!("codex:turn-a#{index}")); + store.index.insert(event.id.clone(), store.events.len()); + store.events.push(event); + } + + // Forked child: its own turn_context first (so the v9 window is closed), + // then the parent's turn replayed verbatim, then its own new turn. + let mut child = FileState { + source: "/tmp/child.jsonl".to_string(), + session: "child-1".to_string(), + ..FileState::default() + }; + let meta = r#"{"timestamp":"2026-07-27T01:00:00Z","type":"session_meta","payload":{"id":"child-1","forked_from_id":"parent-1","parent_thread_id":"parent-1"}}"#; + store.parse_codex_line(meta, &mut child); + store.parse_codex_line(&context("turn-own", "2026-07-27T01:00:00Z"), &mut child); + store.parse_codex_line(&context("turn-a", "2026-07-27T01:00:00Z"), &mut child); + for line in [ + usage("turn-a", "2026-07-27T01:00:00Z", 100), + usage("turn-a", "2026-07-27T01:00:00Z", 200), + ] { + assert!( + store.parse_codex_line(&line, &mut child).is_none(), + "replayed usage must not be counted again" + ); + } + + // A genuinely new turn in the child still counts. + store.parse_codex_line(&context("turn-b", "2026-07-27T01:05:00Z"), &mut child); + let fresh = store + .parse_codex_line(&usage("turn-b", "2026-07-27T01:05:01Z", 300), &mut child) + .expect("the child's own work counts"); + assert_eq!(fresh.id, "codex:turn-b#0"); + assert_eq!(fresh.in_tok, 300.0); + } + + // A replayed usage event must not update the quota either: it carries the + // parent's stale rate_limits restamped with the fork instant, so letting it + // through made a long-past 100% reading beat the real current one. + #[test] + fn replayed_rate_limits_do_not_overwrite_the_current_quota() { + let mut store = empty_store(); + let line = |pct: f64, ts: &str| { + format!( + r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":5,"cached_input_tokens":0,"output_tokens":1}}}},"rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{pct},"window_minutes":10080,"resets_at":100}}}}}}}}"# + ) + }; + let context = |turn: &str, ts: &str| { + format!( + r#"{{"timestamp":"{ts}","type":"turn_context","payload":{{"model":"gpt-5.6-sol","turn_id":"{turn}"}}}}"# + ) + }; + + let mut parent = FileState { + source: "/tmp/p.jsonl".to_string(), + ..FileState::default() + }; + store.parse_codex_line(&context("cap", "2026-07-23T00:00:00Z"), &mut parent); + let capped = store + .parse_codex_line(&line(100.0, "2026-07-23T00:00:01Z"), &mut parent) + .expect("real reading"); + store.index.insert(capped.id.clone(), store.events.len()); + store.events.push(capped); + assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 100.0); + + // The window rolled over; a real later reading drops back to 9%. + store.parse_codex_line(&context("now", "2026-07-29T00:00:00Z"), &mut parent); + let fresh = store + .parse_codex_line(&line(9.0, "2026-07-29T00:00:01Z"), &mut parent) + .expect("real reading"); + store.index.insert(fresh.id.clone(), store.events.len()); + store.events.push(fresh); + assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 9.0); + + // A fork now replays the capped turn, restamped as "now". It must not + // drag the displayed quota back to 100%. + let mut child = FileState { + source: "/tmp/c.jsonl".to_string(), + ..FileState::default() + }; + store.parse_codex_line(&context("cap", "2026-07-29T00:05:00Z"), &mut child); + assert!(store + .parse_codex_line(&line(100.0, "2026-07-29T00:05:00Z"), &mut child) + .is_none()); + assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 9.0); + } + // A forked/subagent thread replays the parent's whole history — usage, turn // telemetry, tool calls, even the parent's session_meta — at the head of its // rollout file, restamped with the fork instant. The parent file still holds From 4bb5787858370a67f567cc55aebc56357f5c538d Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Wed, 29 Jul 2026 00:11:04 -0700 Subject: [PATCH 40/67] release: v1.4.1 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a9528a5..f3c940c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.0", + "version": "1.4.1", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 923c446..0d1990a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.0" +version = "1.4.1" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eafb6a5..830815d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.0" +version = "1.4.1" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c1d9e49..f2cb266 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.0", + "version": "1.4.1", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 2e71126d44932328a3135c106f17647d737beb66 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 30 Jul 2026 00:55:15 -0700 Subject: [PATCH 41/67] release: v1.4.2 --- README-zh.md | 2 +- README.md | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/parser.rs | 15 +++++++-------- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 4 +++- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README-zh.md b/README-zh.md index a445a46..ffaf25d 100644 --- a/README-zh.md +++ b/README-zh.md @@ -19,7 +19,7 @@ brew install --cask sunchj/tokenscope/tokenscope ## 它做什么 - 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) -- 点击打开面板:Day / Week / Month 切换,以及按起止日期(含首尾日)的自定义区间筛选 +- 点击打开面板:默认显示 Day,与菜单栏 Token 数保持一致;可切换 Week / Month,也可按起止日期(含首尾日)筛选自定义区间 - **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 diff --git a/README.md b/README.md index 3d255c5..9e10695 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ brew install --cask sunchj/tokenscope/tokenscope ## What it does - Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) -- Click to open the panel: Day / Week / Month toggle plus an inclusive custom date-range filter +- Click to open the panel: it starts on Day so its total matches the menu-bar count, with Week / Month toggles plus an inclusive custom date-range filter - **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content diff --git a/package.json b/package.json index f3c940c..e4e3233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.1", + "version": "1.4.2", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0d1990a..e5f4882 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.1" +version = "1.4.2" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 830815d..ca2121a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.1" +version = "1.4.2" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index f5dbeea..63061eb 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -206,8 +206,6 @@ pub fn build_dashboard() -> Dashboard { .map(|turn| compute_turn(turn, &pricing)) .collect(); - let today = now.date_naive(); - // Keep historical agents selectable even when their latest event is outside // the preset window; custom ranges can still contain their usage. let present_ids: HashSet<&str> = store @@ -295,12 +293,13 @@ pub fn build_dashboard() -> Dashboard { v }; - // today's displayed tokens (M) for the tray — across all agents - let today_tokens: f64 = events - .iter() - .filter(|e| e.ts.date_naive() == today) - .map(|e| (e.input + e.cache + e.output) / 1e6) - .sum(); + // Keep the tray and the panel on one source of truth. The aggregate scope's + // Day report is the same all-agent, local-calendar-day value shown when the + // panel opens. + let today_tokens = scopes + .first() + .map(|scope| scope.day.metrics.total_tokens) + .unwrap_or_default(); Dashboard { scopes, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f2cb266..aabda1d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.1", + "version": "1.4.2", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index 08b5cda..0f9cd7b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -805,7 +805,9 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen // / dismisses and never arms the hide-suppression guard. const canDrag = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window && !navigator.userAgent.includes("Macintosh"); const dragRef = useRef<{ x: number; y: number } | null>(null); - const [period, setPeriod] = useState<"Day" | "Week" | "Month">("Week"); + // The menu-bar label is today's aggregate. Open on the same period so the + // number in the panel matches the number the user just clicked. + const [period, setPeriod] = useState<"Day" | "Week" | "Month">("Day"); const today = localIso(new Date()); const [rangeOpen, setRangeOpen] = useState(false); const [draftRange, setDraftRange] = useState(() => { From 0e0882348aa1a73efc0272a5b3a23f1246ca7e44 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 30 Jul 2026 04:59:52 -0700 Subject: [PATCH 42/67] fix(store): account Codex cumulative usage correctly --- README-zh.md | 2 +- README.md | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/codex_adapter.rs | 272 ++++++++++++++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/store.rs | 508 +++++++++++++++++++++++++++------ src-tauri/tauri.conf.json | 2 +- 9 files changed, 704 insertions(+), 89 deletions(-) create mode 100644 src-tauri/src/codex_adapter.rs diff --git a/README-zh.md b/README-zh.md index ffaf25d..104054a 100644 --- a/README-zh.md +++ b/README-zh.md @@ -44,7 +44,7 @@ brew install --cask sunchj/tokenscope/tokenscope ### 关键处理 - Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 -- Codex:取 `token_count` 事件的每回合增量(`last_token_usage`),模型归属来自前置的 `turn_context`;Codex 的 `cached_input_tokens` 是其 `input_tokens` 的**子集**,已拆分成与 Claude 一致的独立 cache-read 口径——跨 Agent 的 token 数可直接对比 +- Codex:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中分别拆出 `cached_input_tokens` 与 `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 - token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 - 成本按四类 token 分别计价;模型带 `priced` 标记,**两源都查不到的模型只计 Token、UI 标注「暂无定价」** diff --git a/README.md b/README.md index 9e10695..1c28d40 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ brew install --cask sunchj/tokenscope/tokenscope ### Key processing - Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once -- Codex: per-turn deltas from `token_count` events (`last_token_usage`), model attribution from the preceding `turn_context`; Codex's `cached_input_tokens` is a *subset* of its `input_tokens`, so it's split out to match Claude's separate cache-read accounting — token math is comparable across agents +- Codex: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, while `cached_input_tokens` and `cache_write_input_tokens` are split out of Codex's inclusive `input_tokens` for comparable four-category accounting - Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins - Cost is priced per the four token types; each model carries a `priced` flag — **models not found in either source still count tokens but are labelled "no price" in the UI** diff --git a/package.json b/package.json index e4e3233..abf8b17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.2", + "version": "1.4.3", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e5f4882..00e3017 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.2" +version = "1.4.3" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ca2121a..38af395 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.2" +version = "1.4.3" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/codex_adapter.rs b/src-tauri/src/codex_adapter.rs new file mode 100644 index 0000000..6188517 --- /dev/null +++ b/src-tauri/src/codex_adapter.rs @@ -0,0 +1,272 @@ +//! Codex-specific token accounting. +//! +//! Codex `token_count` events expose two different views of usage: +//! `last_token_usage` is the exact latest upstream response, while +//! `total_token_usage` is the accumulated session snapshot. The same positive +//! `last_token_usage` can be emitted again when only rate limits change, so the +//! accumulated snapshot is the authority for whether a new response occurred. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default)] +pub(crate) struct TokenUsage { + pub(crate) input_tokens: i64, + pub(crate) cached_input_tokens: i64, + pub(crate) cache_write_input_tokens: i64, + pub(crate) output_tokens: i64, + pub(crate) reasoning_output_tokens: i64, + pub(crate) total_tokens: i64, +} + +impl TokenUsage { + fn from_value(value: &Value) -> Self { + let number = |key: &str| { + value + .get(key) + .and_then(Value::as_i64) + .unwrap_or_default() + .max(0) + }; + Self { + input_tokens: number("input_tokens"), + cached_input_tokens: number("cached_input_tokens"), + cache_write_input_tokens: number("cache_write_input_tokens"), + output_tokens: number("output_tokens"), + reasoning_output_tokens: number("reasoning_output_tokens"), + total_tokens: number("total_tokens"), + } + } + + fn has_response_usage(&self) -> bool { + self.input_tokens > 0 || self.output_tokens > 0 + } + + fn event_id(&self, turn_id: &str) -> String { + if turn_id.is_empty() { + return String::new(); + } + format!( + "codex:{turn_id}@{}:{}:{}:{}:{}:{}", + self.input_tokens, + self.cached_input_tokens, + self.cache_write_input_tokens, + self.output_tokens, + self.reasoning_output_tokens, + self.total_tokens, + ) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(default)] +pub(crate) struct State { + last_total: Option, +} + +impl State { + /// Advance the cumulative cursor without producing usage. Fork heads need + /// this while their replayed lines are otherwise ignored. + pub(crate) fn remember_total(&mut self, info: Option<&Value>) { + if let Some(total) = info + .and_then(|value| value.get("total_token_usage")) + .map(TokenUsage::from_value) + { + self.last_total = Some(total); + } + } + + pub(crate) fn observe(&mut self, info: Option<&Value>, turn_id: &str) -> TokenCountOutcome { + let Some(info) = info else { + return TokenCountOutcome::NoUsage; + }; + let last = info + .get("last_token_usage") + .map(TokenUsage::from_value) + .unwrap_or_default(); + let total = info.get("total_token_usage").map(TokenUsage::from_value); + let previous = total + .as_ref() + .and_then(|current| self.last_total.replace(current.clone())); + + // Local context estimates populate only total_tokens. They are useful + // for compaction, but are not billable upstream usage. + if !last.has_response_usage() { + return TokenCountOutcome::NoUsage; + } + + // Rate-limit-only TokenCount events retain the previous positive + // last_token_usage. An unchanged accumulated snapshot proves that no + // new upstream response completed. + if previous + .as_ref() + .zip(total.as_ref()) + .is_some_and(|(before, current)| before == current) + { + return TokenCountOutcome::RepeatedSnapshot; + } + + let event_id = total + .as_ref() + .map(|snapshot| snapshot.event_id(turn_id)) + // Old/incomplete logs without a total snapshot cannot be safely + // deduplicated across files. Keep their usage rather than risk + // silently dropping two equally-sized genuine responses. + .unwrap_or_default(); + + let raw_input = last.input_tokens.max(0); + let cache_read = last.cached_input_tokens.clamp(0, raw_input); + let cache_write = last + .cache_write_input_tokens + .clamp(0, raw_input - cache_read); + let input = raw_input - cache_read - cache_write; + let output = last.output_tokens.max(0); + let reasoning = last.reasoning_output_tokens.clamp(0, output); + + TokenCountOutcome::Usage(Usage { + event_id, + input_tokens: input, + cache_write_input_tokens: cache_write, + cache_read_input_tokens: cache_read, + output_tokens: output, + reasoning_output_tokens: reasoning, + raw_input_tokens: raw_input, + }) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Usage { + pub(crate) event_id: String, + pub(crate) input_tokens: i64, + pub(crate) cache_write_input_tokens: i64, + pub(crate) cache_read_input_tokens: i64, + pub(crate) output_tokens: i64, + pub(crate) reasoning_output_tokens: i64, + pub(crate) raw_input_tokens: i64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum TokenCountOutcome { + NoUsage, + RepeatedSnapshot, + Usage(Usage), +} + +/// Stable across Codex fork/replay restamping because the outer JSONL +/// timestamp is intentionally not part of the fingerprint. +pub(crate) fn token_count_fingerprint( + turn_id: &str, + info: Option<&Value>, + rate_limits: Option<&Value>, +) -> String { + let encoded = serde_json::to_vec(&(turn_id, info, rate_limits)).unwrap_or_default(); + let mut hash = 0xcbf29ce484222325u64; + for byte in encoded { + hash ^= byte as u64; + hash = hash.wrapping_mul(0x100000001b3); + } + format!("codex-token-count:{hash:016x}") +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn info(last_input: i64, last_cached: i64, last_write: i64, total_input: i64) -> Value { + json!({ + "last_token_usage": { + "input_tokens": last_input, + "cached_input_tokens": last_cached, + "cache_write_input_tokens": last_write, + "output_tokens": 10, + "reasoning_output_tokens": 4, + "total_tokens": last_input + 10 + }, + "total_token_usage": { + "input_tokens": total_input, + "cached_input_tokens": last_cached, + "cache_write_input_tokens": last_write, + "output_tokens": 10, + "reasoning_output_tokens": 4, + "total_tokens": total_input + 10 + } + }) + } + + #[test] + fn splits_cache_read_and_write_out_of_codex_input() { + let mut state = State::default(); + let TokenCountOutcome::Usage(usage) = + state.observe(Some(&info(100, 40, 50, 100)), "turn-a") + else { + panic!("expected usage"); + }; + + assert_eq!(usage.input_tokens, 10); + assert_eq!(usage.cache_read_input_tokens, 40); + assert_eq!(usage.cache_write_input_tokens, 50); + assert_eq!(usage.output_tokens, 10); + assert_eq!(usage.reasoning_output_tokens, 4); + assert_eq!(usage.raw_input_tokens, 100); + assert_eq!( + usage.input_tokens + + usage.cache_read_input_tokens + + usage.cache_write_input_tokens + + usage.output_tokens, + 110 + ); + } + + #[test] + fn rejects_positive_last_usage_when_total_snapshot_is_unchanged() { + let mut state = State::default(); + let first = info(100, 40, 0, 100); + assert!(matches!( + state.observe(Some(&first), "turn-a"), + TokenCountOutcome::Usage(_) + )); + assert_eq!( + state.observe(Some(&first), "turn-b"), + TokenCountOutcome::RepeatedSnapshot + ); + } + + #[test] + fn cumulative_snapshot_makes_ids_stable_but_not_position_based() { + let first = info(100, 40, 0, 100); + let second = info(100, 40, 0, 200); + + let mut original = State::default(); + let TokenCountOutcome::Usage(first_original) = original.observe(Some(&first), "turn-a") + else { + panic!("expected usage"); + }; + let TokenCountOutcome::Usage(second_original) = original.observe(Some(&second), "turn-a") + else { + panic!("expected usage"); + }; + + let mut replay = State::default(); + let TokenCountOutcome::Usage(first_replay) = replay.observe(Some(&first), "turn-a") else { + panic!("expected replay candidate"); + }; + + assert_eq!(first_original.event_id, first_replay.event_id); + assert_ne!(first_original.event_id, second_original.event_id); + } + + #[test] + fn remembers_replayed_total_as_the_incremental_baseline() { + let first = info(100, 40, 0, 100); + let mut state = State::default(); + state.remember_total(Some(&first)); + + assert_eq!( + state.observe(Some(&first), "child-turn"), + TokenCountOutcome::RepeatedSnapshot + ); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9df4de5..c3be567 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod codex_adapter; mod config; mod model; mod parser; diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 59ba6df..01e8c47 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -11,6 +11,7 @@ // Two sources are ingested, normalized to the same RawEvent shape: // claude — ~/.claude/projects/**/*.jsonl (assistant messages) // codex — ~/.codex/sessions/**/*.jsonl (token_count turn deltas) +use crate::codex_adapter::{self, TokenCountOutcome}; use crate::model::Quota; use chrono::DateTime; use serde::{Deserialize, Serialize}; @@ -79,8 +80,8 @@ pub struct RawEvent { pub session: String, pub model: String, // raw model id (price lookup), normalized later for grouping pub in_tok: f64, // uncached new input only (both agents) - pub cc: f64, // cache creation (claude only; codex has no such concept → 0) - pub cr: f64, // cache read (codex: cached_input_tokens, a subset of its raw input) + pub cc: f64, // cache creation/write + pub cr: f64, // cache read (codex: cached_input_tokens, a subset of its raw input) pub out_tok: f64, pub mcp: Vec, // all mcp__ names called (unfiltered) pub skills: Vec, // all detected Skill ids called (unfiltered) @@ -132,10 +133,11 @@ struct FileState { // first `turn_context`, which is where its own turns begin. #[serde(default)] replaying: bool, - // Position of the next usage event within `turn_id`. Together they form a - // Codex event's dedup id — see the `codex_event_id` doc comment. + // Codex cumulative usage cursor. A positive last_token_usage can be emitted + // repeatedly while this total stays unchanged, especially for quota-only + // updates, so it must survive incremental reads and app restarts. #[serde(default)] - turn_seq: u64, + codex_usage: codex_adapter::State, // Whether this file's own `session_meta` (its first line) has been read. // Replayed parent metas follow it and must not overwrite session/cwd. #[serde(default)] @@ -148,6 +150,10 @@ struct FileState { #[derive(Serialize, Deserialize, Default)] struct Manifest { files: HashMap, + // Stable token-count payload fingerprint -> original source file. This + // prevents a fork's restamped replay from mutating either usage or quota. + #[serde(default)] + codex_token_counts: HashMap, } pub struct Store { @@ -185,7 +191,10 @@ pub struct Store { // that precede a fork's first turn_context; the common layout puts the // replay *after* it, leaving ~80% of Codex tokens double-counted and the // weekly quota pinned at the parent's stale reading. -const STORE_VERSION: u32 = 10; +// v11: route Codex through its own cumulative-snapshot adapter. Stable +// (turn,total) ids replace position ids; unchanged totals are quota-only +// repeats; cache-write usage is split and priced independently. +const STORE_VERSION: u32 = 11; // v2: quota pools are determined by the active model. Recent Codex logs report // Spark token_count snapshots with `limit_id: "codex"`, even though Spark and // the other Codex models have independent allowances. @@ -297,21 +306,6 @@ fn telemetry_key(source: &str, turn_id: &str) -> String { format!("{source}\0{turn_id}") } -/// Dedup id for a Codex usage event: its turn id plus its position within that -/// turn. Codex `token_count` lines carry no id of their own, but a forked or -/// resumed thread replays its parent's turns verbatim into the head of its own -/// rollout file — same turn ids, same order, restamped with the fork instant. -/// Keying on (turn, position) therefore identifies a replayed event as the one -/// already ingested from the parent, the same way `message.id` does for Claude. -/// Empty when the turn is unknown, which disables dedup for that event rather -/// than risking a collision between unrelated turns. -fn codex_event_id(state: &FileState) -> String { - if state.turn_id.is_empty() { - return String::new(); - } - format!("codex:{}#{}", state.turn_id, state.turn_seq) -} - fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { let root = cwd.map(project_root).unwrap_or_else(|| PathBuf::from(fallback)); let key = root.to_string_lossy(); @@ -786,6 +780,9 @@ impl Store { self.rebuild_index(); self.turns.retain(|turn| turn.source != key); self.rebuild_telemetry_index(); + self.manifest + .codex_token_counts + .retain(|_, source| source != key); } /// Incrementally read only the new bytes of new/changed JSONL files across @@ -914,8 +911,14 @@ impl Store { let payload = v.get("payload")?; let line_type = v.get("type")?.as_str()?; // A forked thread replays its parent's history before its own first - // turn_context. Drop that whole window (see FileState::replaying). + // turn_context. Drop that whole window, but retain the final cumulative + // token snapshot as the baseline for the child's first own event. if state.replaying && line_type != "turn_context" { + if line_type == "event_msg" + && payload.get("type").and_then(|value| value.as_str()) == Some("token_count") + { + state.codex_usage.remember_total(payload.get("info")); + } return None; } match line_type { @@ -957,7 +960,6 @@ impl Store { if state.turn_id != turn_id { state.turn_id = turn_id.to_string(); state.skills_seen.clear(); - state.turn_seq = 0; } } None @@ -969,7 +971,6 @@ impl Store { if state.turn_id != turn_id { state.turn_id = turn_id.to_string(); state.skills_seen.clear(); - state.turn_seq = 0; } } match event_type { @@ -1030,34 +1031,47 @@ impl Store { "token_count" => {} _ => return None, } - // Per-turn delta. `input_tokens` INCLUDES `cached_input_tokens` - // (unlike Claude, where cache_read is separate) — subtract so - // in_tok means "uncached new input" for both agents and - // in + cr + out reproduces Codex's own total. - let usage = payload.get("info")?.get("last_token_usage")?; - let g = |k: &str| usage.get(k).and_then(|x| x.as_f64()).unwrap_or(0.0); - let raw_in = g("input_tokens"); - let cached = g("cached_input_tokens").min(raw_in); - let out = g("output_tokens"); - let reasoning = g("reasoning_output_tokens").min(out); - if raw_in + out <= 0.0 { - // Rate-limit-only heartbeat: no usage to count, but the - // reading itself is this thread's own and worth keeping. + let info = payload.get("info"); + let fingerprint = if state.source.is_empty() || state.turn_id.is_empty() { + None + } else { + Some(codex_adapter::token_count_fingerprint( + &state.turn_id, + info, + payload.get("rate_limits"), + )) + }; + let already_seen = fingerprint + .as_ref() + .is_some_and(|id| self.manifest.codex_token_counts.contains_key(id)); + + // Always advance the per-file cumulative cursor, including for + // replays. Side effects are allowed only for the first original + // occurrence of this timestamp-independent payload. + let outcome = state.codex_usage.observe(info, &state.turn_id); + if already_seen { + return None; + } + if let Some(fingerprint) = fingerprint { + self.manifest + .codex_token_counts + .insert(fingerprint, state.source.clone()); + } + + let TokenCountOutcome::Usage(usage) = outcome else { + // Estimated context snapshots and unchanged-total quota + // heartbeats are not new upstream usage. if let Some(rl) = payload.get("rate_limits") { self.update_codex_quota(rl, ts_ms, &state.model); } return None; - } - // Replay check must precede every side effect below. A forked - // thread reproduces its parent's turns verbatim — same turn ids, - // same order — so a usage event already seen under this - // (turn, position) is that replay, not new work. Counting it - // would double the tokens, duplicate the turn telemetry, and - // (worst) let the parent's stale rate_limits win the "newest - // snapshot" race, since the replay is restamped with the fork - // instant. That is what pinned the weekly quota at 100%. - let id = codex_event_id(state); - if !id.is_empty() && self.index.contains_key(&id) { + }; + + // A replay can carry a different rate-limit payload while still + // naming the same upstream response. The stable (turn,total) + // usage id is the second line of defense and must precede quota + // and telemetry side effects. + if !usage.event_id.is_empty() && self.index.contains_key(&usage.event_id) { return None; } if let Some(rl) = payload.get("rate_limits") { @@ -1069,25 +1083,25 @@ impl Store { .and_then(|value| value.as_f64()) .unwrap_or(0.0); if let Some(turn) = self.turn_mut(state, AGENT_CODEX, ts_ms) { - turn.input_tokens += raw_in - cached; - turn.cache_read_tokens += cached; - turn.output_tokens += out; - turn.reasoning_tokens += reasoning; - turn.context_tokens = turn.context_tokens.max(raw_in); + turn.input_tokens += usage.input_tokens as f64; + turn.cache_creation_tokens += usage.cache_write_input_tokens as f64; + turn.cache_read_tokens += usage.cache_read_input_tokens as f64; + turn.output_tokens += usage.output_tokens as f64; + turn.reasoning_tokens += usage.reasoning_output_tokens as f64; + turn.context_tokens = turn.context_tokens.max(usage.raw_input_tokens as f64); turn.context_window = turn.context_window.max(context_window); } - state.turn_seq += 1; Some(RawEvent { ts_ms, session: state.session.clone(), model: state.model.clone(), - in_tok: raw_in - cached, - cc: 0.0, - cr: cached, - out_tok: out, + in_tok: usage.input_tokens as f64, + cc: usage.cache_write_input_tokens as f64, + cr: usage.cache_read_input_tokens as f64, + out_tok: usage.output_tokens as f64, mcp: Vec::new(), skills: Vec::new(), - id, + id: usage.event_id, source: String::new(), agent: String::new(), }) @@ -1713,20 +1727,151 @@ mod tests { assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 21.0); } + #[test] + fn ignores_unchanged_codex_usage_but_keeps_its_new_quota() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/codex-heartbeat.jsonl".to_string(), + ..FileState::default() + }; + let context = |turn: &str| { + format!( + r#"{{"timestamp":"2026-07-30T00:00:00Z","type":"turn_context","payload":{{"model":"gpt-5.6-sol","turn_id":"{turn}"}}}}"# + ) + }; + let token_count = |ts: &str, last_input: u64, total_input: u64, pct: f64| { + format!( + r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":{last_input},"cached_input_tokens":0,"output_tokens":10,"total_tokens":{last_total}}},"total_token_usage":{{"input_tokens":{total_input},"cached_input_tokens":0,"output_tokens":{total_output},"total_tokens":{total}}}}},"rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{pct},"window_minutes":10080,"resets_at":200}}}}}}}}"#, + last_total = last_input + 10, + total_output = if total_input == 100 { 10 } else { 20 }, + total = total_input + if total_input == 100 { 10 } else { 20 }, + ) + }; + + store.parse_codex_line(&context("turn-a"), &mut state); + let first = store + .parse_codex_line( + &token_count("2026-07-30T00:00:01Z", 100, 100, 10.0), + &mut state, + ) + .expect("first response counts"); + store.index.insert(first.id.clone(), store.events.len()); + store.events.push(first); + + // A new turn starts by publishing fresher quota with the previous + // positive last_token_usage and an unchanged accumulated total. + store.parse_codex_line(&context("turn-b"), &mut state); + assert!(store + .parse_codex_line( + &token_count("2026-07-30T00:00:02Z", 100, 100, 11.0), + &mut state, + ) + .is_none()); + assert_eq!(store.events.len(), 1); + assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 11.0); + + let second = store + .parse_codex_line( + &token_count("2026-07-30T00:00:03Z", 100, 200, 12.0), + &mut state, + ) + .expect("next completed response counts"); + assert_eq!(second.in_tok, 100.0); + assert_ne!(second.id, store.events[0].id); + } + + #[test] + fn counts_new_cumulative_snapshots_when_codex_returns_to_an_old_turn_id() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/codex-returned-turn.jsonl".to_string(), + ..FileState::default() + }; + let context = |turn: &str| { + format!( + r#"{{"timestamp":"2026-07-30T00:00:00Z","type":"turn_context","payload":{{"model":"gpt-5.6-sol","turn_id":"{turn}"}}}}"# + ) + }; + let usage = |total_input: u64| { + let total_output = total_input / 10; + serde_json::json!({ + "timestamp": "2026-07-30T00:00:01Z", + "type": "event_msg", + "payload": { + "type": "token_count", + "info": { + "last_token_usage": { + "input_tokens": 100, + "cached_input_tokens": 0, + "output_tokens": 10, + "total_tokens": 110 + }, + "total_token_usage": { + "input_tokens": total_input, + "cached_input_tokens": 0, + "output_tokens": total_output, + "total_tokens": total_input + total_output + } + } + } + }) + .to_string() + }; + + for (turn, total_input) in [("turn-a", 100), ("turn-b", 200), ("turn-a", 300)] { + store.parse_codex_line(&context(turn), &mut state); + let event = store + .parse_codex_line(&usage(total_input), &mut state) + .expect("each new cumulative snapshot counts"); + assert!(!store.index.contains_key(&event.id)); + store.index.insert(event.id.clone(), store.events.len()); + store.events.push(event); + } + + assert_eq!(store.events.len(), 3); + assert_eq!(store.events.iter().map(|event| event.in_tok).sum::(), 300.0); + } + + #[test] + fn stores_codex_cache_write_as_its_own_pricing_category() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/codex-cache-write.jsonl".to_string(), + ..FileState::default() + }; + let context = r#"{"timestamp":"2026-07-30T00:00:00Z","type":"turn_context","payload":{"model":"gpt-5.6-sol","turn_id":"turn-cache"}}"#; + let usage = r#"{"timestamp":"2026-07-30T00:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":100,"cached_input_tokens":40,"cache_write_input_tokens":50,"output_tokens":10,"reasoning_output_tokens":4,"total_tokens":110},"total_token_usage":{"input_tokens":100,"cached_input_tokens":40,"cache_write_input_tokens":50,"output_tokens":10,"reasoning_output_tokens":4,"total_tokens":110}}}}"#; + + store.parse_codex_line(context, &mut state); + let event = store + .parse_codex_line(usage, &mut state) + .expect("cache-write response counts"); + + assert_eq!(event.in_tok, 10.0); + assert_eq!(event.cc, 50.0); + assert_eq!(event.cr, 40.0); + assert_eq!(event.out_tok, 10.0); + let turn = store.turns.first().expect("turn telemetry"); + assert_eq!(turn.input_tokens, 10.0); + assert_eq!(turn.cache_creation_tokens, 50.0); + assert_eq!(turn.cache_read_tokens, 40.0); + assert_eq!(turn.output_tokens, 10.0); + } + // The replay is not always a head-of-file window. In the common Codex // layout the fork's own turn_context sits at line ~7 and the parent's // history is replayed *after* it, which the v9 window check disarmed on and - // therefore ingested. Dedup by (turn id, position) catches it regardless of - // layout: the replay reproduces the parent's turn ids in the same order, so - // the ids collide with what the parent file already contributed. + // therefore ingested. Stable (turn id, cumulative snapshot) ids catch it + // regardless of layout without conflating later work in a resumed turn. #[test] fn dedupes_replayed_usage_that_follows_the_forks_first_turn_context() { let mut store = empty_store(); - let usage = |turn: &str, ts: &str, input: u64| { + let usage = |ts: &str, input: u64, total_input: u64, total_output: u64| { format!( - r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":{input},"cached_input_tokens":0,"output_tokens":10}}}}}}}}"# + r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":{input},"cached_input_tokens":0,"output_tokens":10,"total_tokens":{last_total}}},"total_token_usage":{{"input_tokens":{total_input},"cached_input_tokens":0,"output_tokens":{total_output},"total_tokens":{total}}}}}}}}}"#, + last_total = input + 10, + total = total_input + total_output, ) - .replace("TURN", turn) }; let context = |turn: &str, ts: &str| { format!( @@ -1741,17 +1886,20 @@ mod tests { ..FileState::default() }; store.parse_codex_line(&context("turn-a", "2026-07-27T00:00:00Z"), &mut parent); - for (index, line) in [ - usage("turn-a", "2026-07-27T00:00:01Z", 100), - usage("turn-a", "2026-07-27T00:00:02Z", 200), - ] - .iter() - .enumerate() - { + for (line, expected_id) in [ + ( + usage("2026-07-27T00:00:01Z", 100, 100, 10), + "codex:turn-a@100:0:0:10:0:110", + ), + ( + usage("2026-07-27T00:00:02Z", 200, 300, 20), + "codex:turn-a@300:0:0:20:0:320", + ), + ] { let event = store - .parse_codex_line(line, &mut parent) + .parse_codex_line(&line, &mut parent) .expect("parent usage counts"); - assert_eq!(event.id, format!("codex:turn-a#{index}")); + assert_eq!(event.id, expected_id); store.index.insert(event.id.clone(), store.events.len()); store.events.push(event); } @@ -1768,8 +1916,8 @@ mod tests { store.parse_codex_line(&context("turn-own", "2026-07-27T01:00:00Z"), &mut child); store.parse_codex_line(&context("turn-a", "2026-07-27T01:00:00Z"), &mut child); for line in [ - usage("turn-a", "2026-07-27T01:00:00Z", 100), - usage("turn-a", "2026-07-27T01:00:00Z", 200), + usage("2026-07-27T01:00:00Z", 100, 100, 10), + usage("2026-07-27T01:00:00Z", 200, 300, 20), ] { assert!( store.parse_codex_line(&line, &mut child).is_none(), @@ -1780,9 +1928,12 @@ mod tests { // A genuinely new turn in the child still counts. store.parse_codex_line(&context("turn-b", "2026-07-27T01:05:00Z"), &mut child); let fresh = store - .parse_codex_line(&usage("turn-b", "2026-07-27T01:05:01Z", 300), &mut child) + .parse_codex_line( + &usage("2026-07-27T01:05:01Z", 300, 600, 30), + &mut child, + ) .expect("the child's own work counts"); - assert_eq!(fresh.id, "codex:turn-b#0"); + assert_eq!(fresh.id, "codex:turn-b@600:0:0:30:0:630"); assert_eq!(fresh.in_tok, 300.0); } @@ -1792,9 +1943,10 @@ mod tests { #[test] fn replayed_rate_limits_do_not_overwrite_the_current_quota() { let mut store = empty_store(); - let line = |pct: f64, ts: &str| { + let line = |pct: f64, ts: &str, total_input: u64, total_output: u64| { format!( - r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":5,"cached_input_tokens":0,"output_tokens":1}}}},"rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{pct},"window_minutes":10080,"resets_at":100}}}}}}}}"# + r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":5,"cached_input_tokens":0,"output_tokens":1,"total_tokens":6}},"total_token_usage":{{"input_tokens":{total_input},"cached_input_tokens":0,"output_tokens":{total_output},"total_tokens":{total}}}}},"rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{pct},"window_minutes":10080,"resets_at":100}}}}}}}}"#, + total = total_input + total_output, ) }; let context = |turn: &str, ts: &str| { @@ -1809,7 +1961,10 @@ mod tests { }; store.parse_codex_line(&context("cap", "2026-07-23T00:00:00Z"), &mut parent); let capped = store - .parse_codex_line(&line(100.0, "2026-07-23T00:00:01Z"), &mut parent) + .parse_codex_line( + &line(100.0, "2026-07-23T00:00:01Z", 5, 1), + &mut parent, + ) .expect("real reading"); store.index.insert(capped.id.clone(), store.events.len()); store.events.push(capped); @@ -1818,7 +1973,10 @@ mod tests { // The window rolled over; a real later reading drops back to 9%. store.parse_codex_line(&context("now", "2026-07-29T00:00:00Z"), &mut parent); let fresh = store - .parse_codex_line(&line(9.0, "2026-07-29T00:00:01Z"), &mut parent) + .parse_codex_line( + &line(9.0, "2026-07-29T00:00:01Z", 10, 2), + &mut parent, + ) .expect("real reading"); store.index.insert(fresh.id.clone(), store.events.len()); store.events.push(fresh); @@ -1832,7 +1990,10 @@ mod tests { }; store.parse_codex_line(&context("cap", "2026-07-29T00:05:00Z"), &mut child); assert!(store - .parse_codex_line(&line(100.0, "2026-07-29T00:05:00Z"), &mut child) + .parse_codex_line( + &line(100.0, "2026-07-29T00:05:00Z", 5, 1), + &mut child, + ) .is_none()); assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 9.0); } @@ -2035,4 +2196,185 @@ mod tests { vec!["chrome_devtools"] ); } + + /// Opt-in audit for one real, immutable rollout. Production Store output is + /// checked turn by turn against a small independent cumulative-snapshot + /// reducer. Only token metadata is printed. + #[test] + #[ignore = "set TOKENSCOPE_AUDIT_SESSION to a real Codex rollout path"] + fn audits_one_real_codex_session() { + #[derive(Clone, Default)] + struct TurnAudit { + responses: u64, + repeated: u64, + input: i64, + cache_write: i64, + cache_read: i64, + output: i64, + } + + impl TurnAudit { + fn physical(&self) -> i64 { + self.input + self.cache_write + self.cache_read + self.output + } + } + + let source = std::env::var("TOKENSCOPE_AUDIT_SESSION") + .expect("TOKENSCOPE_AUDIT_SESSION must name one rollout JSONL file"); + let text = fs::read_to_string(&source).expect("read audit rollout"); + let mut store = empty_store(); + let mut state = FileState { + source: source.clone(), + session: codex_session_from_filename(Path::new(&source)), + ..FileState::default() + }; + + // Exercise the exact production parser and outer event-indexing path. + for line in text.lines() { + let Some(mut event) = store.parse_codex_line(line, &mut state) else { + continue; + }; + event.source = source.clone(); + event.agent = AGENT_CODEX.to_string(); + if !event.id.is_empty() { + if store.index.contains_key(&event.id) { + continue; + } + store + .index + .insert(event.id.clone(), store.events.len()); + } + store.events.push(event); + } + + let number = |value: &serde_json::Value, key: &str| { + value + .get(key) + .and_then(serde_json::Value::as_i64) + .unwrap_or_default() + .max(0) + }; + let total_vec = |value: &serde_json::Value| { + [ + number(value, "input_tokens"), + number(value, "cached_input_tokens"), + number(value, "cache_write_input_tokens"), + number(value, "output_tokens"), + number(value, "reasoning_output_tokens"), + number(value, "total_tokens"), + ] + }; + + // Independent oracle: the accumulated total decides whether the + // positive last usage belongs to a new upstream response. + let mut oracle: HashMap = HashMap::new(); + let mut current_turn = String::new(); + let mut previous_total: Option<[i64; 6]> = None; + for line in text.lines() { + let Ok(value) = serde_json::from_str::(line) else { + continue; + }; + let Some(payload) = value.get("payload") else { + continue; + }; + let line_type = value.get("type").and_then(|item| item.as_str()); + if line_type == Some("turn_context") { + if let Some(id) = payload.get("turn_id").and_then(|item| item.as_str()) { + current_turn = id.to_string(); + } + continue; + } + if line_type != Some("event_msg") { + continue; + } + if let Some(id) = payload.get("turn_id").and_then(|item| item.as_str()) { + current_turn = id.to_string(); + } + if payload.get("type").and_then(|item| item.as_str()) != Some("token_count") { + continue; + } + let Some(info) = payload.get("info") else { + continue; + }; + let total = info.get("total_token_usage").map(&total_vec); + let before = previous_total; + if let Some(total) = total { + previous_total = Some(total); + } + let Some(last) = info.get("last_token_usage") else { + continue; + }; + let raw_input = number(last, "input_tokens"); + let output = number(last, "output_tokens"); + if raw_input + output <= 0 { + continue; + } + + let turn = oracle.entry(current_turn.clone()).or_default(); + if before.zip(total).is_some_and(|(left, right)| left == right) { + turn.repeated += 1; + continue; + } + let cache_read = number(last, "cached_input_tokens").clamp(0, raw_input); + let cache_write = + number(last, "cache_write_input_tokens").clamp(0, raw_input - cache_read); + turn.responses += 1; + turn.input += raw_input - cache_read - cache_write; + turn.cache_write += cache_write; + turn.cache_read += cache_read; + turn.output += output; + } + + let actual_events = store + .events + .iter() + .filter(|event| event.in_tok + event.cc + event.cr + event.out_tok > 0.0) + .count() as u64; + let expected_events: u64 = oracle.values().map(|turn| turn.responses).sum(); + assert_eq!(actual_events, expected_events); + + let mut ordered: Vec<_> = oracle.into_iter().collect(); + ordered.sort_by_key(|(turn_id, _)| { + store + .turns + .iter() + .find(|actual| actual.turn_id == *turn_id) + .map(|actual| actual.ts_ms) + .unwrap_or_default() + }); + let total_physical: i64 = ordered.iter().map(|(_, turn)| turn.physical()).sum(); + for (turn_id, expected) in &ordered { + let actual = store + .turns + .iter() + .find(|turn| turn.turn_id == *turn_id) + .expect("production telemetry for audited turn"); + assert_eq!(actual.input_tokens as i64, expected.input); + assert_eq!( + actual.cache_creation_tokens as i64, + expected.cache_write + ); + assert_eq!(actual.cache_read_tokens as i64, expected.cache_read); + assert_eq!(actual.output_tokens as i64, expected.output); + eprintln!( + "turn={} responses={} repeated={} input={} cache_write={} cache_read={} output={} physical={}", + &turn_id[..turn_id.len().min(8)], + expected.responses, + expected.repeated, + expected.input, + expected.cache_write, + expected.cache_read, + expected.output, + expected.physical(), + ); + } + + eprintln!( + "session audit passed: file={}, turns={}, responses={}, physical={}", + source, + ordered.len(), + expected_events, + total_physical, + ); + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index aabda1d..770cda7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.2", + "version": "1.4.3", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From d8513ad96d581b351daeee3999875d58ca90c103 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 3 Aug 2026 20:21:47 -0700 Subject: [PATCH 43/67] fix: align model cache totals (v1.4.4) --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/model.rs | 4 +++- src-tauri/src/parser.rs | 18 ++++++++++++++++++ src-tauri/tauri.conf.json | 2 +- src/App.tsx | 39 +++++++++++++++++++++++++++------------ src/data.ts | 6 +++++- 8 files changed, 57 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index abf8b17..542c610 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.3", + "version": "1.4.4", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 00e3017..a8e5dce 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.3" +version = "1.4.4" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 38af395..ccfa265 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.3" +version = "1.4.4" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index e42d187..4f6ab1c 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -14,7 +14,9 @@ pub struct SeriesPoint { pub struct ModelStat { pub name: String, pub vendor: String, - pub tokens: f64, // M tokens (input+output, weighted) + pub tokens: f64, // M tokens (input + cache + output) + #[serde(rename = "cacheTokens")] + pub cache_tokens: f64, // M tokens (cache creation + read) pub cost: f64, // USD estimate pub color: String, pub priced: bool, // false = no pricing data in LiteLLM (cost is unknown, not $0) diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 63061eb..57642e0 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -719,6 +719,7 @@ struct Agg { mcp_calls: u64, skill_calls: u64, model_tok: HashMap, + model_cache_tok: HashMap, model_cost: HashMap, model_priced: HashMap, model_agent: HashMap, @@ -752,6 +753,7 @@ impl Agg { self.requests += 1; // model totals keep all token types so shares sum to Total tokens *self.model_tok.entry(e.model.clone()).or_default() += e.input + e.cache + e.output; + *self.model_cache_tok.entry(e.model.clone()).or_default() += e.cache; *self.model_cost.entry(e.model.clone()).or_default() += e.cost; // a model is "priced" if any of its messages had a known price *self.model_priced.entry(e.model.clone()).or_default() |= e.priced; @@ -793,6 +795,8 @@ impl Agg { ModelStat { vendor: vendor_of(&name).to_string(), tokens: tok / 1e6, + cache_tokens: self.model_cache_tok.get(&name).copied().unwrap_or_default() + / 1e6, cost: (cost * 1_000_000.0).round() / 1_000_000.0, color: if i < palette.len() { palette[i] } else { OVERFLOW_GRAY }.to_string(), priced, @@ -1330,6 +1334,20 @@ mod tests { assert_eq!(report.series[1].input, 2.0); } + #[test] + fn model_stats_keep_cache_tokens_in_the_selected_model_total() { + let mut cached = event(2026, 7, 13, 12, 100.0); + cached.cache = 800.0; + cached.output = 100.0; + let day = NaiveDate::from_ymd_opt(2026, 7, 13).unwrap(); + let report = report_range(&[cached], &[], day, day, PALETTE); + + assert_eq!(report.models.len(), 1); + assert_eq!(report.models[0].tokens, 0.001); + assert_eq!(report.models[0].cache_tokens, 0.0008); + assert_eq!(report.metrics.total_tokens, report.models[0].tokens); + } + #[test] fn custom_single_day_uses_hours_and_long_ranges_cap_chart_buckets() { let day = NaiveDate::from_ymd_opt(2026, 7, 13).unwrap(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 770cda7..ed8f5a6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.3", + "version": "1.4.4", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index 0f9cd7b..8e48b7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -892,12 +892,16 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen // The All scope can contain the same model once per agent. The selector is // model-based, so combine those rows before calculating its Total and cost. const modelTotals = Array.from(models.reduce((totals, model) => { - const prev = totals.get(model.name) ?? { name: model.name, tokens: 0, cost: 0 }; + const prev = totals.get(model.name) ?? { name: model.name, tokens: 0, cacheTokens: 0, cost: 0 }; prev.tokens += model.tokens; + // Live payloads carry the exact per-model cache split. The proportional + // fallback keeps old/static snapshots internally consistent. + prev.cacheTokens += model.cacheTokens + ?? (M.totalTokens > 0 ? model.tokens * M.cacheTokens / M.totalTokens : 0); prev.cost += model.cost; totals.set(model.name, prev); return totals; - }, new Map()).values()) + }, new Map()).values()) .filter((model) => model.tokens > 0) .sort((a, b) => b.tokens - a.tokens); const selectedModel = modelTotals.find((model) => model.name === totalModel); @@ -906,15 +910,26 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen // Per-agent slices — non-empty only in the All scope with >=2 sources; they // switch the hero bar + chart from Cached/New to a by-agent breakdown. const slices = P.agents; + // Keep the hero's agent split on the same model scope as its Total. The + // historical chart below remains the complete period view. + const heroSlices = selectedModel && slices.length > 0 + ? slices.map((slice) => ({ + ...slice, + tokens: models + .filter((model) => model.name === selectedModel.name && model.agent === slice.id) + .reduce((sum, model) => sum + model.tokens, 0), + })).filter((slice) => slice.tokens > 0) + : slices; // animated Total tokens: counts up from 0 on each open / period / scope // / model switch; held at 0 while hidden so it never flashes. const animTotal = useCountUp(totalTokens, `${viewKey}:${scope.id}:${totalModel}:${openGen}`, active); const heroTotal = tokenAmount(animTotal, totalTokens, text.tokens); // Explicit percentages avoid WebKit's incorrect flexGrow + flexBasis:0 sizing. - const splitTot = M.inputTokens + M.cacheTokens + M.outputTokens; - const cachePct = splitTot > 0 ? (M.cacheTokens / splitTot) * 100 : 0; - const restPct = splitTot > 0 ? ((M.inputTokens + M.outputTokens) / splitTot) * 100 : 0; - const agentTotal = slices.reduce((sum, s) => sum + s.tokens, 0); + const displayCacheTokens = selectedModel?.cacheTokens ?? M.cacheTokens; + const displayRestTokens = Math.max(0, totalTokens - displayCacheTokens); + const cachePct = totalTokens > 0 ? (displayCacheTokens / totalTokens) * 100 : 0; + const restPct = totalTokens > 0 ? (displayRestTokens / totalTokens) * 100 : 0; + const heroAgentTotal = heroSlices.reduce((sum, s) => sum + s.tokens, 0); // Hide noise: 0% token-share rows, and $0 entries in the cost donut. // Show models whose share is at least 0.1% when rounded to 1 decimal; below // that it'd render a meaningless "0.0%" (a negligible token share). Such a @@ -1139,18 +1154,18 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen
{/* All scope: one segment per agent. Single scope: cached vs new. */}
- {M.totalTokens > 0 && (slices.length > 0 ? ( - slices.map((s) => ( -
0 ? (s.tokens / agentTotal) * 100 : 0}%`, background: s.color }} /> + {totalTokens > 0 && (heroSlices.length > 0 ? ( + heroSlices.map((s) => ( +
0 ? (s.tokens / heroAgentTotal) * 100 : 0}%`, background: s.color }} /> )) ) : <>
)}
- {slices.length > 0 - ? - : } + {heroSlices.length > 0 + ? + : } {/* bar chart — stacked by agent in the All scope */} 0 ? [...slices].reverse().map((s) => ({ color: s.color, values: s.values })) : undefined} /> diff --git a/src/data.ts b/src/data.ts index 0663887..92ef1bf 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,7 +1,11 @@ import { invoke } from "@tauri-apps/api/core"; export interface SeriesPoint { label: string; full: string; input: number; cache: number; output: number } -export interface ModelStat { name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean; agent: string } +export interface ModelStat { + name: string; vendor: string; tokens: number; cost: number; color: string; priced: boolean; agent: string; + // Optional only for compatibility with older/static dashboard payloads. + cacheTokens?: number; +} export interface NamedCount { name: string; count: number } export interface ProjectStat { id: string; name: string; tokens: number; cost: number; requests: number; sessions: number } export interface ReliabilityStats { From 8be7f4d6e520664f0663203e978d4d7bd576db85 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 3 Aug 2026 23:14:03 -0700 Subject: [PATCH 44/67] fix: disambiguate tray quota pools (v1.4.5) --- README-zh.md | 2 +- README.md | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 71 ++++++++++++++++++++++++++++++++------- src-tauri/tauri.conf.json | 2 +- 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/README-zh.md b/README-zh.md index 104054a..c29db41 100644 --- a/README-zh.md +++ b/README-zh.md @@ -135,7 +135,7 @@ App 会在启动时及此后每小时检测新版本。当前版本和更新操 - **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) - **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) - 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) -- **Weekly Remaining** 可配置菜单栏 Token 数后的周剩余额度:**Off**、仅 **Codex**(如 `12.40M-76%`),或 **Codex + Spark**(`codex_bengalfox`,显示为 `12.40M-C76%-S93%`);默认关闭,选择会被记住 +- **Weekly Remaining** 可配置菜单栏 Token 数后的周剩余额度:**Off**、仅 **Codex**(如 `12.40M-C76%`),或 **Codex + Spark**(`codex_bengalfox`,显示为 `12.40M-C76%-S93%`);已过重置时间的额度快照不会继续显示,默认关闭,选择会被记住 - **Dashboard Shortcut** 可用全局快捷键切换面板的显示/隐藏(默认 macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);通过 **Change Dashboard Shortcut…** 可直接录入自定义组合键。默认关闭,选择会被记住 - 已自动设置**登录自启**,无需手动配置 diff --git a/README.md b/README.md index 1c28d40..8fcf585 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ right-click menu to check immediately. - **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) - **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) - Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) -- **Weekly Remaining** controls the quota shown beside the menu-bar token count: **Off**, **Codex** (for example `12.40M-76%`), or **Codex + Spark** (`codex_bengalfox`, shown as `12.40M-C76%-S93%`). It is off by default and remembers your choice +- **Weekly Remaining** controls the quota shown beside the menu-bar token count: **Off**, **Codex** (for example `12.40M-C76%`), or **Codex + Spark** (`codex_bengalfox`, shown as `12.40M-C76%-S93%`). Expired quota snapshots are hidden; the setting is off by default and remembers your choice - **Dashboard Shortcut** toggles the panel with a global hotkey (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux by default); use **Change Dashboard Shortcut…** to record your own combination. It is off by default and remembers your choice - **Launch-at-login is set up automatically** — no manual configuration needed diff --git a/package.json b/package.json index 542c610..e94f956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.4", + "version": "1.4.5", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a8e5dce..59ffed1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.4" +version = "1.4.5" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ccfa265..fe9c070 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.4" +version = "1.4.5" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c3be567..280829e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -228,14 +228,19 @@ fn apply_tray_language(menu: &TrayMenuState, language: AppLanguage, shortcut: &s let _ = menu.quit.set_text(copy.quit); } -fn weekly_remaining_from_quota(quota: &model::Quota) -> Option { - let used = if quota.primary_minutes == 7 * 24 * 60 { - quota.primary_pct +fn weekly_remaining_from_quota(quota: &model::Quota, now_s: i64) -> Option { + let (used, resets_at) = if quota.primary_minutes == 7 * 24 * 60 { + (quota.primary_pct, quota.primary_resets_at) } else if quota.secondary_minutes == 7 * 24 * 60 { - quota.secondary_pct + (quota.secondary_pct, quota.secondary_resets_at) } else { return None; }; + // A dormant pool may not emit another snapshot after its window resets. + // Do not keep advertising that expired reading in the native tray. + if resets_at > 0 && resets_at <= now_s { + return None; + } Some((100.0 - used).clamp(0.0, 100.0).round() as u8) } @@ -251,20 +256,29 @@ fn weekly_label_suffix( display: WeeklyQuotaDisplay, codex: Option<&model::Quota>, spark: Option<&model::Quota>, + now_s: i64, ) -> String { let mut suffix = String::new(); match display { WeeklyQuotaDisplay::Off => {} WeeklyQuotaDisplay::Codex => { - if let Some(remaining) = codex.and_then(weekly_remaining_from_quota) { - suffix.push_str(&format!("-{remaining}%")); + if let Some(remaining) = + codex.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) + { + // Keep the pool identity explicit even when only one quota is + // selected; an unlabeled percentage is easy to mistake for Spark. + suffix.push_str(&format!("-C{remaining}%")); } } WeeklyQuotaDisplay::CodexAndSpark => { - if let Some(remaining) = codex.and_then(weekly_remaining_from_quota) { + if let Some(remaining) = + codex.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) + { suffix.push_str(&format!("-C{remaining}%")); } - if let Some(remaining) = spark.and_then(weekly_remaining_from_quota) { + if let Some(remaining) = + spark.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) + { suffix.push_str(&format!("-S{remaining}%")); } } @@ -275,7 +289,7 @@ fn weekly_label_suffix( fn tray_label(dash: &Dashboard, display: WeeklyQuotaDisplay, language: AppLanguage) -> String { let mut label = fmt_tokens_m(dash.today_tokens, language); let (codex, spark) = dashboard_quotas(dash); - label.push_str(&weekly_label_suffix(display, codex, spark)); + label.push_str(&weekly_label_suffix(display, codex, spark, now_ms() / 1000)); label } @@ -1577,21 +1591,52 @@ mod tests { ..codex.clone() }; - assert_eq!(weekly_remaining_from_quota(&codex), Some(81)); + assert_eq!(weekly_remaining_from_quota(&codex, 1), Some(81)); assert_eq!( - weekly_label_suffix(WeeklyQuotaDisplay::Codex, Some(&codex), Some(&spark)), - "-81%" + weekly_label_suffix(WeeklyQuotaDisplay::Codex, Some(&codex), Some(&spark), 1), + "-C81%" ); assert_eq!( weekly_label_suffix( WeeklyQuotaDisplay::CodexAndSpark, Some(&codex), - Some(&spark) + Some(&spark), + 1, ), "-C81%-S93%" ); } + #[test] + fn weekly_label_hides_expired_pool_snapshots() { + let current = model::Quota { + plan: "pro".into(), + primary_pct: 20.0, + primary_minutes: 7 * 24 * 60, + primary_resets_at: 2_000, + secondary_pct: 0.0, + secondary_minutes: 0, + secondary_resets_at: 0, + as_of_ms: 0, + }; + let expired = model::Quota { + primary_pct: 7.0, + primary_resets_at: 999, + ..current.clone() + }; + + assert_eq!(weekly_remaining_from_quota(&expired, 1_000), None); + assert_eq!( + weekly_label_suffix( + WeeklyQuotaDisplay::CodexAndSpark, + Some(¤t), + Some(&expired), + 1_000, + ), + "-C80%" + ); + } + #[test] fn old_tray_preferences_are_migrated() { let preferences = parse_tray_preferences( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ed8f5a6..79230e0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.4", + "version": "1.4.5", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 9786d3b48fdc00db6bbd7ffbdf8adbc42c932f16 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 3 Aug 2026 23:47:07 -0700 Subject: [PATCH 45/67] fix: keep Codex and Spark quota pools separate (v1.4.6) --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/store.rs | 56 +++++++++++++++++++++------------------ src-tauri/tauri.conf.json | 2 +- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e94f956..9d5f6a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.5", + "version": "1.4.6", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 59ffed1..eea1cde 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.5" +version = "1.4.6" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fe9c070..3ebdf92 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.5" +version = "1.4.6" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 01e8c47..e9c86d4 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -201,7 +201,11 @@ const STORE_VERSION: u32 = 11; // v3: during the v2 migration, fully read source files that contain Spark // usage. A session can switch away from Spark after its last Spark snapshot, // leaving its model context outside the usual 64 KiB quota-file tail. -const QUOTA_CACHE_VERSION: u32 = 3; +// v4: an explicit `codex_bengalfox` limit id always identifies Spark. Real +// concurrent sessions can report that id while the active model context still +// says `gpt-5.6-sol`; trusting the model first temporarily put Spark's quota in +// the standard Codex cache. +const QUOTA_CACHE_VERSION: u32 = 4; const PROJECT_CACHE_VERSION: u32 = 1; const TELEMETRY_CACHE_VERSION: u32 = 1; // One-time quota migration: tail the 32 newest files, and fully read only @@ -334,23 +338,23 @@ fn is_codex_spark_model(model: &str) -> bool { model == "gpt-5.3-codex-spark" || model.starts_with("gpt-5.3-codex-spark-") } -/// Assign a quota snapshot to the pool consumed by its active model. +/// Assign a quota snapshot to its independent rate-limit pool. /// -/// Codex now emits `limit_id: "codex"` for Spark turns as well, so that raw -/// id cannot distinguish the two independent pools. The old ids are retained -/// only as a fallback for pre-model/legacy records. +/// An explicit `codex_bengalfox` id is authoritative. The `codex` id is +/// ambiguous because Codex also emits it for Spark turns, so model identity +/// disambiguates that case. fn codex_quota_pool(model: &str, rl: &serde_json::Value) -> Option<&'static str> { - if is_codex_spark_model(model) { + let limit_id = rl.get("limit_id").and_then(|value| value.as_str()); + if limit_id == Some(SPARK_QUOTA_POOL) { return Some(SPARK_QUOTA_POOL); } - if !model.trim().is_empty() { - return Some(CODEX_QUOTA_POOL); + if !matches!(limit_id, None | Some(CODEX_QUOTA_POOL)) { + return None; } - match rl.get("limit_id").and_then(|value| value.as_str()) { - None | Some(CODEX_QUOTA_POOL) => Some(CODEX_QUOTA_POOL), - Some(SPARK_QUOTA_POOL) => Some(SPARK_QUOTA_POOL), - _ => None, + if is_codex_spark_model(model) { + return Some(SPARK_QUOTA_POOL); } + Some(CODEX_QUOTA_POOL) } impl Store { @@ -1279,9 +1283,9 @@ impl Store { } /// Keep the newest rate-limit snapshot. Spark and standard Codex models - /// consume separate pools, but recent JSONL snapshots no longer reflect - /// that in `rate_limits.limit_id`; classify known models before using the - /// legacy limit id as a fallback. + /// consume separate pools. Honor an explicit Spark pool id first, then use + /// the active model to disambiguate snapshots carrying the shared `codex` + /// id. fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64, model: &str) { let Some(limit_id) = codex_quota_pool(model, rl) else { return; @@ -1675,22 +1679,22 @@ mod tests { // still put that weekly usage in Spark's independent pool. store.update_codex_quota(&snapshot("codex", 7.0), 100, "gpt-5.3-codex-spark"); store.update_codex_quota(&snapshot("codex", 24.05), 200, "gpt-5.6-sol"); - // Conversely, a stale legacy id cannot move a non-Spark model into - // Spark's pool. + // Explicit pool ids remain authoritative even when concurrent session + // context reports the same non-Spark model for both snapshots. store.update_codex_quota( &snapshot("codex_bengalfox", 24.1), 300, "gpt-5.6-sol", ); - assert_eq!(store.codex_quota.unwrap().primary_pct, 24.1); - assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 7.0); - assert_eq!(store.quota_history.len(), 2); + assert_eq!(store.codex_quota.unwrap().primary_pct, 24.05); + assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 24.1); + assert_eq!(store.quota_history.len(), 3); } #[test] - fn uses_legacy_limit_id_only_when_model_is_unavailable() { - let legacy_spark = serde_json::json!({ + fn explicit_spark_limit_id_overrides_model_context() { + let explicit_spark = serde_json::json!({ "limit_id": "codex_bengalfox", "primary": { "used_percent": 7.0, @@ -1700,16 +1704,16 @@ mod tests { }); assert_eq!( - codex_quota_pool("", &legacy_spark), + codex_quota_pool("", &explicit_spark), Some(SPARK_QUOTA_POOL) ); assert_eq!( - codex_quota_pool("chatgpt/gpt-5.3-codex-spark", &legacy_spark), + codex_quota_pool("chatgpt/gpt-5.3-codex-spark", &explicit_spark), Some(SPARK_QUOTA_POOL) ); assert_eq!( - codex_quota_pool("gpt-5.6-sol", &legacy_spark), - Some(CODEX_QUOTA_POOL) + codex_quota_pool("gpt-5.6-sol", &explicit_spark), + Some(SPARK_QUOTA_POOL) ); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 79230e0..16006cd 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.5", + "version": "1.4.6", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 7038b1904ab093f9610da1092e1f0210f4777a7c Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 3 Aug 2026 23:55:59 -0700 Subject: [PATCH 46/67] fix: hide providers without period usage (v1.4.7) --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 32 ++++++++++++++++++++++++-------- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 9d5f6a0..3397cda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.6", + "version": "1.4.7", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index eea1cde..1b0b69f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.6" +version = "1.4.7" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3ebdf92..13e92a7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.6" +version = "1.4.7" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 16006cd..c5c6260 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.6", + "version": "1.4.7", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index 8e48b7f..950c5fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -780,13 +780,11 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen { dash: Dashboard; dark: boolean; themePref: "dark" | "light" | "system"; onToggleTheme: () => void; onToggleLanguage: () => void; openGen: number; active: boolean; updater: UpdaterController }) { const { locale, text } = useI18n(); - // Agent scope filter. scopes[0] is always the aggregate; a stale selection - // (e.g. a source disappeared after a rescan) falls back to it. + // The backend retains historical provider scopes so custom ranges can still + // query them. The selector below narrows that stable list to providers with + // usage in the period currently on screen. const scopes = dash.scopes; const [scopeId, setScopeId] = useState(scopes[0].id); - const scope = scopes.find((s) => s.id === scopeId) ?? scopes[0]; - // Filtering to one agent re-tints the whole panel with its accent. - const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); // In-app update lifecycle (checking/current → available → downloading → ready). const [updSt, updInstall, updDismiss] = updater; const [shortcutEditor, setShortcutEditor] = useState(null); @@ -821,6 +819,24 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen const rangeRequest = useRef(0); const rangeBaseGeneration = useRef(""); + const reportForScope = (candidate: Scope): PeriodReport | undefined => { + if (activeRange) { + return rangeDash?.scopes.find((item) => item.id === candidate.id)?.report; + } + return period === "Day" ? candidate.day : period === "Month" ? candidate.month : candidate.week; + }; + const visibleScopes = scopes.filter((candidate, index) => + index === 0 || (reportForScope(candidate)?.metrics.totalTokens ?? 0) > 0 + ); + // A provider selected in one period may have no usage in the next. Fall back + // to the aggregate immediately, then keep the state in sync for later renders. + const scope = visibleScopes.find((candidate) => candidate.id === scopeId) ?? visibleScopes[0]; + useEffect(() => { + if (scopeId !== scope.id) setScopeId(scope.id); + }, [scope.id, scopeId]); + // Filtering to one agent re-tints the whole panel with its accent. + const t = themeForScope(TH[dark ? "dark" : "light"], scope, dark); + const clearDateRange = () => { rangeRequest.current += 1; setActiveRange(null); @@ -1116,9 +1132,9 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen }}>✕
)} - {/* agent filter — only when several sources have data */} - {scopes.length > 1 && ( - + {/* provider filter — only providers with usage in this period */} + {visibleScopes.length > 1 && ( + )} {/* hero */}
From 3506cd598131656df45c7e434fca93e9135e913e Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Tue, 4 Aug 2026 02:34:02 -0700 Subject: [PATCH 47/67] feat: show independent Spark weekly quota --- src/App.tsx | 56 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 950c5fd..e1a1cc3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -534,21 +534,22 @@ function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) // Codex rate-limit card straight from the session logs. Keep collecting every // window for compatibility, but do not render the retired 5h limit or empty // windows omitted by newer Codex clients. Bars turn amber near the cap. -function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; theme: Theme }) { +function QuotaCard({ q, sparkQ, trend, theme }: { q?: Quota | null; sparkQ?: Quota | null; trend: QuotaTrendPoint[]; theme: Theme }) { const t = theme; const { locale, text } = useI18n(); const now = Date.now(); - const stale = now - q.asOfMs > 60 * 60 * 1000; + const stale = q ? now - q.asOfMs > 60 * 60 * 1000 : false; const winLabel = (min: number) => min === 10080 ? text.weekly : min % 60 === 0 && min > 0 ? `${min / 60}h ${text.window}` : `${min}m ${text.window}`; const resetsIn = (unixS: number) => { + if (unixS <= 0) return ""; const ms = unixS * 1000 - now; if (ms <= 0) return text.resetting; const h = Math.floor(ms / 3.6e6), m = Math.round((ms % 3.6e6) / 6e4); if (h >= 48) return `${text.resetsIn} ${Math.round(h / 24)}d`; return h > 0 ? `${text.resetsIn} ${h}h ${m}m` : `${text.resetsIn} ${m}m`; }; - const asOf = new Date(q.asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }); + const asOf = q ? new Date(q.asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }) : ""; const quotaValues = trend.map((point) => point.usedPct); const first = trend[0], last = trend[trend.length - 1]; const elapsedHours = first && last ? (last.tsMs - first.tsMs) / 3.6e6 : 0; @@ -557,10 +558,12 @@ function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; th const horizon = hoursToCap > 0 ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h ${text.toCap}` : `${(hoursToCap / 24).toFixed(1)}d ${text.toCap}` : text.stable; - const windows = [ - { id: "primary", minutes: q.primaryMinutes, pct: q.primaryPct, resetsAt: q.primaryResetsAt }, - { id: "secondary", minutes: q.secondaryMinutes, pct: q.secondaryPct, resetsAt: q.secondaryResetsAt }, - ].filter((window) => window.minutes > 0 && window.minutes !== 5 * 60); + const quotaWindows = (quota?: Quota | null) => quota ? [ + { id: "primary", minutes: quota.primaryMinutes, pct: quota.primaryPct, resetsAt: quota.primaryResetsAt }, + { id: "secondary", minutes: quota.secondaryMinutes, pct: quota.secondaryPct, resetsAt: quota.secondaryResetsAt }, + ] : []; + const windows = quotaWindows(q).filter((window) => window.minutes > 0 && window.minutes !== 5 * 60); + const sparkWeekly = quotaWindows(sparkQ).find((window) => window.minutes === 7 * 24 * 60); const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { const p = Math.max(0, Math.min(100, pctUsed)); const col = p >= 80 ? "#e0795f" : t.accent; @@ -576,19 +579,30 @@ function QuotaCard({ q, trend, theme }: { q: Quota; trend: QuotaTrendPoint[]; th ); }; return ( -
- {windows.map((window) => ( - - ))} -
- - - {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%{text.perDay} · {horizon} : text.collectingWeeklyTrend} - -
-
- {q.plan && <>{text.plan}: {q.plan}}{stale && · {text.asOf} {asOf}} +
+
+ {windows.map((window) => ( + + ))}
+ {sparkWeekly && ( +
60 * 60 * 1000 ? 0.65 : 1 }}> + +
+ )} + {q && ( +
+
+ + + {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%{text.perDay} · {horizon} : text.collectingWeeklyTrend} + +
+
+ {q.plan && <>{text.plan}: {q.plan}}{stale && · {text.asOf} {asOf}} +
+
+ )}
); } @@ -1203,11 +1217,11 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen
)} {/* Codex quota — rate-limit windows read straight from the session logs */} - {scope.quota && ( + {(scope.quota || scope.sparkQuota) && ( <>
- + )} {projects.length > 0 && ( From a56a7ab58e3efb40f1bee61b2a4373c1ec358ce7 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Tue, 4 Aug 2026 02:41:03 -0700 Subject: [PATCH 48/67] chore: bump version to 1.4.8 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3397cda..a24bd3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.7", + "version": "1.4.8", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1b0b69f..70e3d88 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.7" +version = "1.4.8" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 13e92a7..52241a2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.7" +version = "1.4.8" description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c5c6260..c1a1767 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.7", + "version": "1.4.8", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 25efe9443e11309749467508cddc32e823670773 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 01:15:30 -0700 Subject: [PATCH 49/67] feat: add first-class Pi session support --- README-zh.md | 20 +- README.md | 20 +- docs/DESIGN-multi-agent.md | 2 + docs/DESIGN-pi-adapter.md | 51 ++++ src-tauri/Cargo.toml | 2 +- src-tauri/src/config.rs | 218 +++++++++++++- src-tauri/src/model.rs | 10 +- src-tauri/src/parser.rs | 63 ++-- src-tauri/src/store.rs | 586 ++++++++++++++++++++++++++++++++++++- src/App.tsx | 4 +- src/data.ts | 3 +- src/i18n.tsx | 4 +- 12 files changed, 914 insertions(+), 69 deletions(-) create mode 100644 docs/DESIGN-pi-adapter.md diff --git a/README-zh.md b/README-zh.md index c29db41..b587b28 100644 --- a/README-zh.md +++ b/README-zh.md @@ -2,7 +2,7 @@ [English](README.md) · **中文** -**macOS 菜单栏 / Windows 系统托盘工具**,统一展示本机 **AI 编码 Agent**(Claude Code **与 Codex CLI**)的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**——所有 Agent 一个仪表盘。 +**macOS 菜单栏 / Windows 系统托盘工具**,统一展示本机 **AI 编码 Agent**(Pi、Claude Code 与 Codex CLI)的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**——所有 Agent 一个仪表盘。 技术栈:**Tauri 2 + React + TypeScript**(前端)/ **Rust**(数据层)。 @@ -20,11 +20,11 @@ brew install --cask sunchj/tokenscope/tokenscope - 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) - 点击打开面板:默认显示 Day,与菜单栏 Token 数保持一致;可切换 Week / Month,也可按起止日期(含首尾日)筛选自定义区间 -- **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿);只装一个 Agent 则界面与经典版完全一致 +- **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex / Pi)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿 / Pi 紫);只装一个 Agent 则界面与经典版完全一致 - 指标:总 Token(input/output)、估算花费、Requests / Sessions - 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 - 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 -- Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数与 Codex reasoning Token 占比 +- Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数,以及 Agent 可提供时的 reasoning Token 占比 - 周额度趋势:对 Codex 快照下采样,展示当前重置周期的每日消耗速度与预计触顶时间 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** - **Codex 配额卡**:直接从 Codex 自己的日志读取当前周额度的已用百分比、plan 与重置倒计时 @@ -37,19 +37,21 @@ brew install --cask sunchj/tokenscope/tokenscope |------|------| | Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | | Codex 会话日志(Token / 模型 / 配额) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | +| Pi 会话日志(Token / 模型 / 工具 / 遥测) | `~/.pi/agent/sessions/**/*.jsonl`(支持 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR` 与绝对路径 `settings.sessionDir`) | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | -| 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/` | +| 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/`;Pi:全局、共享、项目级 Pi Skill 目录及 settings 显式路径 | | 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | ### 关键处理 - Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 - Codex:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中分别拆出 `cached_input_tokens` 与 `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 +- Pi:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` 或 `/clone` 复制的历史,session header 提供会话与项目归属;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 会进入对应遥测 - token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 -- 成本按四类 token 分别计价;模型带 `priced` 标记,**两源都查不到的模型只计 Token、UI 标注「暂无定价」** +- 成本按四类 token 分别计价;Pi 有持久化请求成本时优先采用。模型带 `priced` 标记,**所有价格源都查不到的模型只计 Token、UI 标注「暂无定价」** - 日志只有裸模型名、无厂商信息 → 第三方模型默认取官方厂商价(估算) -- 工具分类:MCP 调用从直接的 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用中归属 server,再按**所属 Agent**配置过滤;Claude 的 Skill 工具/斜杠命令及 Codex 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配;其余忽略 +- 工具分类:MCP 调用从直接的 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用中归属 server,再按**所属 Agent**配置过滤;Claude 的 Skill 工具/斜杠命令,以及 Codex 与 Pi 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配;其余忽略 > 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 @@ -64,6 +66,8 @@ brew install --cask sunchj/tokenscope/tokenscope | **Cache 命中**(读) | `cache_read_input_tokens` | 从缓存重放的上下文 | 约 0.1×(便宜很多) | | **Output** | `output_tokens` | 模型生成的 token | 约 5× | +Pi 以 `usage.input`、`usage.cacheWrite`、`usage.cacheRead`、`usage.output` 持久化相同四类口径;可选的 `usage.reasoning` 已是 output 的子集,不会重复相加。 + **Tokens**(按周期对消息求和): ``` @@ -187,10 +191,10 @@ src/ React 前端 charts.tsx 图表原语(柱状/甜甜圈/sparkline/热力图/分段控件) App.tsx 主面板 src-tauri/src/ - store.rs 多源 JSONL 增量摄取(Claude + Codex 适配器) + store.rs 多源 JSONL 增量摄取(Claude + Codex + Pi 适配器) parser.rs 按 Scope 聚合(All / 单 Agent;预设周期 + 自定义区间 + 热力图) pricing.rs models.dev / LiteLLM 价格加载与计价 - config.rs 用户 MCP / Skill 白名单(Claude + Codex) + config.rs 用户 MCP / Skill 白名单(Claude + Codex + Pi) model.rs 返回给前端的数据结构 lib.rs Tauri 命令 + 菜单栏托盘 ``` diff --git a/README.md b/README.md index 8fcf585..ad004f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **English** · [中文](README-zh.md) -A **menu-bar / system-tray app for macOS and Windows** that shows your **local AI coding agents'** (Claude Code **and Codex CLI**) **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown** — one unified dashboard for every agent on your machine. +A **menu-bar / system-tray app for macOS and Windows** that shows your **local AI coding agents'** (Pi, Claude Code, and Codex CLI) **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown** — one unified dashboard for every agent on your machine. Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). @@ -20,11 +20,11 @@ brew install --cask sunchj/tokenscope/tokenscope - Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) - Click to open the panel: it starts on Day so its total matches the menu-bar count, with Week / Month toggles plus an inclusive custom date-range filter -- **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal). With a single agent the classic UI is unchanged +- **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex / Pi) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal / Pi violet). With a single agent the classic UI is unchanged - Metrics: total tokens (input/output), estimated cost, requests / sessions - Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content - Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward -- Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and Codex reasoning-token share +- Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and reasoning-token share when reported by the agent - Weekly quota trend: downsampled Codex snapshots with daily burn rate and time-to-cap projection for the current reset cycle - Three breakdowns: **by model** / **by MCP call** / **by Skill call** - **Codex quota card**: the active weekly rate-limit window (used %, plan, reset countdown) read straight from Codex's own logs @@ -37,19 +37,21 @@ brew install --cask sunchj/tokenscope/tokenscope |---------|------| | Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | | Codex session logs (tokens / model / rate limits) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | +| Pi session logs (tokens / model / tools / telemetry) | `~/.pi/agent/sessions/**/*.jsonl` (honors `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, and absolute `settings.sessionDir`) | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | -| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/` | +| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | | Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | ### Key processing - Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once - Codex: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, while `cached_input_tokens` and `cache_write_input_tokens` are split out of Codex's inclusive `input_tokens` for comparable four-category accounting +- Pi: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`, while the session header supplies the session/project identity. Persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching dashboard telemetry - Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins -- Cost is priced per the four token types; each model carries a `priced` flag — **models not found in either source still count tokens but are labelled "no price" in the UI** +- Cost is priced per the four token types; Pi's persisted request cost is preferred when available. Each model carries a `priced` flag — **models not found in any price source still count tokens but are labelled "no price" in the UI** - Logs contain only the bare model name (no vendor) → third-party models default to the official vendor price (an estimate) -- Tool classification: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill tool/slash-command calls and Codex `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored +- Tool classification: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill tool/slash-command calls plus Codex and Pi `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored > Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". @@ -64,6 +66,8 @@ Every assistant message's `usage` reports four **mutually exclusive** token coun | **Cache read** (hit) | `cache_read_input_tokens` | Context replayed from the cache | ~0.1× (much cheaper) | | **Output** | `output_tokens` | Tokens the model generated | ~5× | +Pi persists the same categories as `usage.input`, `usage.cacheWrite`, `usage.cacheRead`, and `usage.output`; its optional `usage.reasoning` is already a subset of output and is never added again. + **Tokens** (per period, summed over messages): ``` @@ -189,10 +193,10 @@ src/ React frontend charts.tsx chart primitives (bars / donut / sparkline / heatmap / segmented control) App.tsx main panel src-tauri/src/ - store.rs incremental multi-source JSONL ingest (Claude + Codex adapters) + store.rs incremental multi-source JSONL ingest (Claude + Codex + Pi adapters) parser.rs aggregation into scopes (All / per-agent; presets + custom ranges + heatmap) pricing.rs models.dev / LiteLLM price loading and costing - config.rs user MCP / Skill whitelists (Claude + Codex) + config.rs user MCP / Skill whitelists (Claude + Codex + Pi) model.rs data structures returned to the frontend lib.rs Tauri commands + menu-bar tray ``` diff --git a/docs/DESIGN-multi-agent.md b/docs/DESIGN-multi-agent.md index 3e4ce1b..99cb20e 100644 --- a/docs/DESIGN-multi-agent.md +++ b/docs/DESIGN-multi-agent.md @@ -3,6 +3,8 @@ > 版本:v0.1 草案 · 2026-07-02 > 目标:从「Claude 用量仪表盘」演进为「本地所有 AI Agent 的统一用量仪表盘」。 > 第一步:支持 Codex CLI。 +> +> 此文档保留 Codex Phase 1 的历史设计;后续 Pi 一等数据源设计见 [DESIGN-pi-adapter.md](DESIGN-pi-adapter.md)。 --- diff --git a/docs/DESIGN-pi-adapter.md b/docs/DESIGN-pi-adapter.md new file mode 100644 index 0000000..e7f32f6 --- /dev/null +++ b/docs/DESIGN-pi-adapter.md @@ -0,0 +1,51 @@ +# Pi Session Adapter + +## Decision + +Pi is a first-class usage source (`agent = "pi"`). It is not merged into Codex when Pi happens to use the `openai-codex` provider: the dashboard's agent dimension represents the harness, while provider/model identity controls request metadata and pricing. + +## Source discovery + +The adapter always scans the default root under `PI_CODING_AGENT_DIR` (normally `~/.pi/agent/sessions`) and additionally scans an absolute session override from: + +1. `PI_CODING_AGENT_SESSION_DIR` +2. `settings.json` → `sessionDir` + +Pi resolves relative session directories against each CLI process's cwd. A background desktop app has no single equivalent cwd, so only absolute overrides can be discovered globally. + +## Normalization + +| Pi session data | Normalized data | +|---|---| +| Session header `id` / `cwd` | Session and project identity | +| Assistant `provider` / `model` | Active provider/model metadata | +| `usage.input` | Uncached input | +| `usage.cacheWrite` | Cache creation | +| `usage.cacheRead` | Cache read | +| `usage.output` | Output (already includes `reasoning`) | +| `usage.reasoning` | Reasoning telemetry only; never added to token total again | +| `usage.cost.total` | Preferred persisted request cost | +| Assistant `stopReason` | Completed/aborted turn outcome | +| Tool result `isError` | Tool-error telemetry | +| `compaction` entry | Compaction count; optional summary usage is also billed | + +Model context windows are resolved from Pi's `models-store.json` and `models.json`. Turn duration and first-response latency are derived from persisted user/assistant timestamps. + +## Tree and copy semantics + +All physical assistant entries in one tree file represent real upstream requests, including abandoned branches, and therefore count as usage. + +`/fork` and `/clone` can copy an active path into a new file. Pi preserves each entry's stable id, so the store persists a global `entry id → original source` manifest and accepts only the first occurrence. Files are ordered by their timestamp-prefixed filename so the original normally wins. This deduplication runs before token, tool, and reliability side effects. + +## Tools and Skills + +- Direct `mcp____` calls are treated as user-extension MCP calls; Pi has no built-in MCP registry. +- A `read` tool call targeting `...//SKILL.md` records one invocation per skill per user turn. +- `/skill:` user commands are recognized when persisted directly. +- Skill whitelists include Pi's global/shared/project locations and explicit non-glob settings paths. + +## Non-goals + +- Pi sessions do not persist Codex rate-limit snapshots, so the Pi scope does not synthesize a quota card. +- Relative custom `sessionDir` values cannot be globally discovered outside their originating project cwd. +- Generic extension tools are not classified as MCP unless their persisted name uses the `mcp__` convention. diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 52241a2..f52e5e2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tokenscope" version = "1.4.8" -description = "Menu-bar dashboard for local AI coding agents (Claude Code, Codex) token usage" +description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 4f8ca4e..dbc7993 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -9,6 +9,7 @@ pub struct UserConfig { pub codex_mcp_servers: HashSet, // codex, from ~/.codex/config.toml pub claude_skills: HashSet, pub codex_skills: HashSet, + pub pi_skills: HashSet, } fn home() -> Option { @@ -78,6 +79,12 @@ fn codex_home() -> Option { .or_else(|| home().map(|h| h.join(".codex"))) } +fn pi_agent_dir() -> Option { + std::env::var_os("PI_CODING_AGENT_DIR") + .map(PathBuf::from) + .or_else(|| home().map(|h| h.join(".pi").join("agent"))) +} + /// User Codex skills live in $CODEX_HOME/skills and ~/.agents/skills. Also /// include project `.agents/skills` directories from session working dirs. /// Hidden directories such as $CODEX_HOME/skills/.system are built-ins. @@ -101,6 +108,177 @@ fn load_codex_skills(project_dirs: &[PathBuf]) -> HashSet { set } +fn pi_frontmatter_name(path: &Path) -> Option { + let text = fs::read_to_string(path).ok()?; + let mut lines = text.lines(); + if lines.next()?.trim() != "---" { + return None; + } + for line in lines { + let line = line.trim(); + if line == "---" { + break; + } + if let Some(name) = line.strip_prefix("name:") { + let name = name.trim().trim_matches(['\'', '"']); + if !name.is_empty() { + return Some(name.to_string()); + } + } + } + None +} + +fn scan_pi_skill_path(path: &Path, set: &mut HashSet) { + if path.is_file() { + if path.extension().and_then(|ext| ext.to_str()) == Some("md") { + if let Some(name) = pi_frontmatter_name(path).or_else(|| { + path.file_stem() + .and_then(|name| name.to_str()) + .map(str::to_owned) + }) { + set.insert(name); + } + } + return; + } + for entry in walkdir::WalkDir::new(path) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() && entry.file_name() == "SKILL.md" { + if let Some(parent_name) = entry + .path() + .parent() + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + { + set.insert(parent_name.to_string()); + } + if let Some(name) = pi_frontmatter_name(entry.path()) { + set.insert(name); + } + } + } +} + +fn expand_pi_path(path: &str, base: &Path) -> Option { + if path == "~" { + return home(); + } + if let Some(rest) = path.strip_prefix("~/").or_else(|| path.strip_prefix("~\\")) { + return Some(home()?.join(rest)); + } + let path = PathBuf::from(path); + Some(if path.is_absolute() { path } else { base.join(path) }) +} + +fn read_json(path: &Path) -> Option { + serde_json::from_str(&fs::read_to_string(path).ok()?).ok() +} + +fn add_pi_settings_skills(settings: &Path, base: &Path, set: &mut HashSet) { + let Some(json) = read_json(settings) else { + return; + }; + let Some(skills) = json.get("skills").and_then(|value| value.as_array()) else { + return; + }; + for skill in skills.iter().filter_map(|value| value.as_str()) { + if skill.starts_with(['!', '-', '+']) || skill.contains('*') { + continue; + } + if let Some(path) = expand_pi_path(skill, base) { + scan_pi_skill_path(&path, set); + } + } +} + +fn npm_package_name(source: &str) -> &str { + let source = source.strip_prefix("npm:").unwrap_or(source); + if let Some(unscoped) = source.strip_prefix('@') { + unscoped + .find('@') + .map(|index| &source[..index + 1]) + .unwrap_or(source) + } else { + source.split('@').next().unwrap_or(source) + } +} + +fn pi_package_path(source: &str, base: &Path) -> Option { + let source = source.split('#').next()?.trim(); + if let Some(repo) = source.strip_prefix("git:") { + return Some(base.join("git").join(repo)); + } + if let Some(repo) = source.strip_prefix("github:") { + return Some(base.join("git").join("github.com").join(repo)); + } + if source.contains("://") { + return None; + } + let name = npm_package_name(source); + (!name.is_empty()).then(|| base.join("npm").join("node_modules").join(name)) +} + +fn add_pi_package_skills(settings: &Path, base: &Path, set: &mut HashSet) { + let Some(json) = read_json(settings) else { + return; + }; + let Some(packages) = json.get("packages").and_then(|value| value.as_array()) else { + return; + }; + for package in packages { + let source = if let Some(source) = package.as_str() { + source + } else { + let skills_enabled = package + .get("skills") + .and_then(|value| value.as_array()) + .is_none_or(|skills| !skills.is_empty()); + if !skills_enabled { + continue; + } + let Some(source) = package.get("source").and_then(|value| value.as_str()) else { + continue; + }; + source + }; + if let Some(path) = pi_package_path(source, base) { + scan_pi_skill_path(&path, set); + } + } +} + +/// Pi discovers global, shared, project, package, and explicit settings skills. +fn load_pi_skills(project_dirs: &[PathBuf]) -> HashSet { + let mut set = HashSet::new(); + if let Some(agent_dir) = pi_agent_dir() { + scan_pi_skill_path(&agent_dir.join("skills"), &mut set); + let settings = agent_dir.join("settings.json"); + add_pi_settings_skills(&settings, &agent_dir, &mut set); + add_pi_package_skills(&settings, &agent_dir, &mut set); + } + if let Some(home) = home() { + scan_pi_skill_path(&home.join(".agents").join("skills"), &mut set); + } + for project in project_dirs { + for dir in project.ancestors() { + scan_pi_skill_path(&dir.join(".pi").join("skills"), &mut set); + scan_pi_skill_path(&dir.join(".agents").join("skills"), &mut set); + let settings_dir = dir.join(".pi"); + let settings = settings_dir.join("settings.json"); + add_pi_settings_skills(&settings, &settings_dir, &mut set); + add_pi_package_skills(&settings, &settings_dir, &mut set); + if dir.join(".git").exists() { + break; + } + } + } + set +} + /// Codex normalizes MCP server names to snake_case in tool names (a config /// entry `chrome-devtools` calls tools named `mcp__chrome_devtools__…`), so /// both the whitelist and lookups go through this. @@ -146,34 +324,37 @@ fn load_codex_mcps(project_dirs: &[PathBuf]) -> HashSet { } impl UserConfig { - pub fn load(codex_project_dirs: &[PathBuf]) -> Self { + pub fn load(project_dirs: &[PathBuf]) -> Self { // Parse ~/.claude.json a single time and derive the MCP whitelist from it. let json = read_user_config(); UserConfig { mcp_servers: mcps_from(json.as_ref()), - codex_mcp_servers: load_codex_mcps(codex_project_dirs), + codex_mcp_servers: load_codex_mcps(project_dirs), claude_skills: load_user_skills(), - codex_skills: load_codex_skills(codex_project_dirs), + codex_skills: load_codex_skills(project_dirs), + pi_skills: load_pi_skills(project_dirs), } } /// A tool name like "mcp____" → is server user-installed? /// Checked against the owning agent's own config. pub fn is_user_mcp(&self, agent: &str, server: &str) -> bool { - if agent == crate::store::AGENT_CODEX { - self.codex_mcp_servers.contains(&norm_mcp(server)) - } else { - self.mcp_servers.contains(server) + match agent { + crate::store::AGENT_CODEX => self.codex_mcp_servers.contains(&norm_mcp(server)), + // Pi has no built-in MCP registry. A persisted mcp__ tool is supplied + // by a user extension, so the invocation itself is authoritative. + crate::store::AGENT_PI => !server.is_empty(), + _ => self.mcp_servers.contains(server), } } /// A skill id (may be "plugin:skill") → strip plugin prefix, check dir. pub fn is_user_skill(&self, agent: &str, skill: &str) -> bool { let key = skill.rsplit(':').next().unwrap_or(skill); - if agent == crate::store::AGENT_CODEX { - self.codex_skills.contains(key) - } else { - self.claude_skills.contains(key) + match agent { + crate::store::AGENT_CODEX => self.codex_skills.contains(key), + crate::store::AGENT_PI => self.pi_skills.contains(key), + _ => self.claude_skills.contains(key), } } } @@ -182,6 +363,21 @@ impl UserConfig { mod tests { use super::*; + #[test] + fn resolves_pi_package_install_paths() { + let base = Path::new("/agent"); + assert_eq!(npm_package_name("npm:toolkit@1.2.3"), "toolkit"); + assert_eq!(npm_package_name("npm:@scope/toolkit@1.2.3"), "@scope/toolkit"); + assert_eq!( + pi_package_path("npm:@scope/toolkit@1.2.3", base), + Some(base.join("npm/node_modules/@scope/toolkit")) + ); + assert_eq!( + pi_package_path("git:github.com/org/toolkit#main", base), + Some(base.join("git/github.com/org/toolkit")) + ); + } + #[test] fn parses_and_normalizes_codex_mcp_server_names() { let mut servers = HashSet::new(); diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 4f6ab1c..e7e3d55 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -20,7 +20,7 @@ pub struct ModelStat { pub cost: f64, // USD estimate pub color: String, pub priced: bool, // false = no pricing data in LiteLLM (cost is unknown, not $0) - // Owning agent id ("claude" / "codex"). In the All scope the same model name + // Owning agent id ("claude" / "codex" / "pi"). In the All scope the same model name // can appear once per agent (e.g. gpt-5 via a router AND via Codex). pub agent: String, } @@ -127,8 +127,8 @@ pub struct Metrics { /// hero split bar, `values` (aligned with `series`) drives the stacked chart. #[derive(Debug, Clone, Serialize)] pub struct AgentSlice { - pub id: String, // "claude" | "codex" - pub label: String, // "Claude" | "Codex" + pub id: String, // "claude" | "codex" | "pi" + pub label: String, // "Claude" | "Codex" | "Pi" pub color: String, // base hex, same across charts pub tokens: f64, // M tokens in the period pub values: Vec, // M tokens per series bucket @@ -187,8 +187,8 @@ pub struct Quota { /// one scope per agent, and the UI shows filter chips. #[derive(Debug, Clone, Serialize)] pub struct Scope { - pub id: String, // "all" | "claude" | "codex" - pub label: String, // "All" | "Claude" | "Codex" + pub id: String, // "all" | "claude" | "codex" | "pi" + pub label: String, // "All" | "Claude" | "Codex" | "Pi" pub color: String, // agent accent hex; "" = default theme accent pub day: PeriodReport, pub week: PeriodReport, diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 57642e0..63f9187 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -1,4 +1,4 @@ -// Aggregate the store's normalized events (Claude + Codex) into per-scope +// Aggregate the store's normalized events (Claude + Codex + Pi) into per-scope // Day / Week / Month reports + a daily heatmap. With one data source the // dashboard has a single scope and looks exactly like the classic single-agent // UI; with several, the first scope aggregates everything ("All") and is @@ -6,7 +6,7 @@ use crate::config::UserConfig; use crate::model::*; use crate::pricing::Pricing; -use crate::store::{RawEvent, Store, AGENT_CLAUDE, AGENT_CODEX}; +use crate::store::{RawEvent, Store, AGENT_CLAUDE, AGENT_CODEX, AGENT_PI}; use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, Timelike}; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; @@ -76,6 +76,12 @@ const AGENTS: &[AgentDef] = &[ color: "#10a37f", // OpenAI teal palette: ["#0f8a6c", "#10a37f", "#4dbf9f", "#93dcc5", "#4a5f57"], }, + AgentDef { + id: AGENT_PI, + label: "Pi", + color: "#a855f7", // distinct violet for the model-agnostic harness + palette: ["#8b3fd1", "#a855f7", "#c084fc", "#ddb7ff", "#594267"], + }, ]; fn agent_def(id: &str) -> &'static AgentDef { @@ -185,7 +191,7 @@ pub fn build_dashboard() -> Dashboard { let telemetry_since_ms = store.telemetry_since_ms; // 2. Aggregate: apply current config + prices, slice by current time. - let cfg = UserConfig::load(&store.codex_project_dirs()); + let cfg = UserConfig::load(&store.project_dirs()); // Memoized price table (cheap clone); loaded/refreshed off-thread elsewhere // so neither parsing nor the network runs while we hold BUILD_LOCK. let pricing = Pricing::shared(); @@ -322,7 +328,7 @@ pub fn build_range_dashboard(start: NaiveDate, end: NaiveDate) -> Result (cfg.codex_mcp_servers.len(), cfg.codex_skills.len()), AGENT_CLAUDE => (cfg.mcp_servers.len(), cfg.claude_skills.len()), + AGENT_PI => (report.metrics.servers as usize, cfg.pi_skills.len()), _ => ( - cfg.mcp_servers.len() + cfg.codex_mcp_servers.len(), - cfg.claude_skills.len() + cfg.codex_skills.len(), + (cfg.mcp_servers.len() + cfg.codex_mcp_servers.len()) + .max(report.metrics.servers as usize), + cfg.claude_skills.len() + cfg.codex_skills.len() + cfg.pi_skills.len(), ), }; report.metrics.servers = servers as u64; @@ -529,9 +537,11 @@ fn compute_event( .with_timezone(&Local); let model = normalize_model(&r.model); // price lookup uses the raw (possibly dated) id, then the normalized one - let cost_opt = pricing - .cost(&r.model, r.in_tok, r.out_tok, r.cc, r.cr) - .or_else(|| pricing.cost(&model, r.in_tok, r.out_tok, r.cc, r.cr)); + let cost_opt = r.reported_cost.or_else(|| { + pricing + .cost(&r.model, r.in_tok, r.out_tok, r.cc, r.cr) + .or_else(|| pricing.cost(&model, r.in_tok, r.out_tok, r.cc, r.cr)) + }); let mcp = r .mcp .iter() @@ -567,24 +577,26 @@ fn compute_turn(turn: &crate::store::TurnTelemetry, pricing: &Pricing) -> TurnEv let ts = DateTime::from_timestamp_millis(turn.ts_ms) .unwrap_or_default() .with_timezone(&Local); - let cost = pricing - .cost( - &turn.model, - turn.input_tokens, - turn.output_tokens, - turn.cache_creation_tokens, - turn.cache_read_tokens, - ) - .or_else(|| { - pricing.cost( - &normalize_model(&turn.model), + let cost = turn.reported_cost.unwrap_or_else(|| { + pricing + .cost( + &turn.model, turn.input_tokens, turn.output_tokens, turn.cache_creation_tokens, turn.cache_read_tokens, ) - }) - .unwrap_or(0.0); + .or_else(|| { + pricing.cost( + &normalize_model(&turn.model), + turn.input_tokens, + turn.output_tokens, + turn.cache_creation_tokens, + turn.cache_read_tokens, + ) + }) + .unwrap_or(0.0) + }); TurnEvent { ts, agent: agent_def(&turn.agent).id, @@ -815,7 +827,12 @@ impl Agg { count: *c, }) .collect(); - v.sort_by_key(|item| std::cmp::Reverse(item.count)); + v.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.name.cmp(&right.name)) + }); v } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index e9c86d4..6595beb 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -8,9 +8,10 @@ // events — cheap, and recomputed per request because preset windows are // relative to "now" and custom date ranges are selected at runtime. // -// Two sources are ingested, normalized to the same RawEvent shape: +// Three sources are ingested, normalized to the same RawEvent shape: // claude — ~/.claude/projects/**/*.jsonl (assistant messages) // codex — ~/.codex/sessions/**/*.jsonl (token_count turn deltas) +// pi — ~/.pi/agent/sessions/**/*.jsonl (assistant messages) use crate::codex_adapter::{self, TokenCountOutcome}; use crate::model::Quota; use chrono::DateTime; @@ -23,6 +24,7 @@ use walkdir::WalkDir; pub const AGENT_CLAUDE: &str = "claude"; pub const AGENT_CODEX: &str = "codex"; +pub const AGENT_PI: &str = "pi"; #[derive(Serialize, Deserialize, Clone)] pub struct ProjectRef { @@ -53,6 +55,9 @@ pub struct TurnTelemetry { pub compactions: u64, pub context_tokens: f64, pub context_window: f64, + // Pi persists the model catalog's request cost alongside usage. Prefer it + // over a later public-price estimate when available (including $0 local models). + pub reported_cost: Option, // Claude can repeat one assistant message across several JSONL lines. // Persist ids so incremental restarts never count its usage twice. usage_ids: Vec, @@ -79,13 +84,17 @@ pub struct RawEvent { pub ts_ms: i64, pub session: String, pub model: String, // raw model id (price lookup), normalized later for grouping - pub in_tok: f64, // uncached new input only (both agents) + pub in_tok: f64, // uncached new input only (all agents) pub cc: f64, // cache creation/write pub cr: f64, // cache read (codex: cached_input_tokens, a subset of its raw input) pub out_tok: f64, pub mcp: Vec, // all mcp__ names called (unfiltered) pub skills: Vec, // all detected Skill ids called (unfiltered) pub id: String, // message id (dedup); "" = no cross-line dedup needed + // Pi records the effective request cost in the session. Other adapters leave + // this unset and pricing is derived from the current public model table. + #[serde(default)] + pub reported_cost: Option, // Source log file (manifest key). Lets a truncated/rewritten file purge its // own stale events before being re-read, so re-ingestion stays idempotent. #[serde(default)] @@ -111,12 +120,16 @@ struct FileState { #[serde(default)] model: String, #[serde(default)] + provider: String, + #[serde(default)] session: String, #[serde(default)] cwd: String, #[serde(default)] turn_id: String, #[serde(default)] + turn_started_ms: i64, + #[serde(default)] skills_seen: Vec, // Newer Codex logs emit tool_search_output groups, then call the selected // tool by its short name (for example `js`). Persist the short-name mapping @@ -154,6 +167,10 @@ struct Manifest { // prevents a fork's restamped replay from mutating either usage or quota. #[serde(default)] codex_token_counts: HashMap, + // Pi tree entry id -> original source file. Fork/clone files copy the + // active path verbatim, including ids, and must not replay usage/telemetry. + #[serde(default)] + pi_entries: HashMap, } pub struct Store { @@ -194,7 +211,11 @@ pub struct Store { // v11: route Codex through its own cumulative-snapshot adapter. Stable // (turn,total) ids replace position ids; unchanged totals are quota-only // repeats; cache-write usage is split and priced independently. -const STORE_VERSION: u32 = 11; +// v12: ingest Pi's tree-structured sessions, persisted four-way usage/cost, +// tool outcomes, reasoning, compactions, projects, and Skill reads. +// v13: persist every Pi tree entry id so copied fork/clone paths cannot replay +// either usage or reliability side effects across source files. +const STORE_VERSION: u32 = 13; // v2: quota pools are determined by the active model. Recent Codex logs report // Spark token_count snapshots with `limit_id: "codex"`, even though Spark and // the other Codex models have independent allowances. @@ -226,6 +247,46 @@ fn claude_dir() -> Option { Some(dirs::home_dir()?.join(".claude").join("projects")) } +fn pi_agent_dir() -> Option { + std::env::var_os("PI_CODING_AGENT_DIR") + .map(PathBuf::from) + .or_else(|| Some(dirs::home_dir()?.join(".pi").join("agent"))) +} + +fn expand_home_path(path: &str) -> Option { + if path == "~" { + return dirs::home_dir(); + } + if let Some(rest) = path.strip_prefix("~/").or_else(|| path.strip_prefix("~\\")) { + return Some(dirs::home_dir()?.join(rest)); + } + Some(PathBuf::from(path)) +} + +/// Pi's default session root plus an explicit absolute override. Relative +/// `sessionDir` values are cwd-relative in Pi and cannot identify one global +/// directory for a background desktop app, so the default root remains active. +fn pi_dirs() -> Vec { + let Some(agent_dir) = pi_agent_dir() else { + return Vec::new(); + }; + let mut dirs = vec![agent_dir.join("sessions")]; + let env_dir = std::env::var("PI_CODING_AGENT_SESSION_DIR") + .ok() + .and_then(|path| expand_home_path(&path)); + let settings_dir = fs::read_to_string(agent_dir.join("settings.json")) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|json| json.get("sessionDir")?.as_str().map(str::to_owned)) + .and_then(|path| expand_home_path(&path)); + if let Some(dir) = env_dir.or(settings_dir).filter(|path| path.is_absolute()) { + if !dirs.contains(&dir) { + dirs.push(dir); + } + } + dirs +} + /// ~/.codex/sessions, honoring the CODEX_HOME override the Codex CLI supports. fn codex_dir() -> Option { let home = std::env::var_os("CODEX_HOME") @@ -244,6 +305,9 @@ pub fn source_roots() -> Vec<(&'static str, PathBuf)> { if let Some(d) = codex_dir() { v.push((AGENT_CODEX, d)); } + for d in pi_dirs() { + v.push((AGENT_PI, d)); + } v } @@ -648,9 +712,9 @@ impl Store { } } - /// Working directories seen in Codex sessions. Config uses these to find - /// project-scoped `.agents/skills` without crawling the user's home dir. - pub fn codex_project_dirs(&self) -> Vec { + /// Working directories seen in session headers. Config uses these to find + /// project-scoped agent resources without crawling the user's home dir. + pub fn project_dirs(&self) -> Vec { let mut dirs: Vec = self .manifest .files @@ -787,6 +851,9 @@ impl Store { self.manifest .codex_token_counts .retain(|_, source| source != key); + self.manifest + .pi_entries + .retain(|_, source| source != key); } /// Incrementally read only the new bytes of new/changed JSONL files across @@ -819,7 +886,18 @@ impl Store { .filter(|e| e.path().extension().map(|x| x == "jsonl").unwrap_or(false)) .map(|e| e.into_path()) .collect(); - entries.sort(); + if agent == AGENT_PI { + // Pi copies stable entry ids into a fork/clone. Ordering by the + // timestamp-prefixed filename makes the original session win global + // id dedup even when its encoded project directory sorts later. + entries.sort_by(|left, right| { + left.file_name() + .cmp(&right.file_name()) + .then_with(|| left.cmp(right)) + }); + } else { + entries.sort(); + } for path in entries { let path = path.as_path(); let key = path.to_string_lossy().to_string(); @@ -867,7 +945,7 @@ impl Store { }; // Codex session ids also live in the filename (rollout--); // seed from it so events parsed before/without session_meta still - // group into a session. + // group into a session. Pi gets its authoritative id from the header. if agent == AGENT_CODEX && state.session.is_empty() { state.session = codex_session_from_filename(path); } @@ -878,6 +956,7 @@ impl Store { let Ok(s) = std::str::from_utf8(line) else { continue }; let parsed = match agent { AGENT_CODEX => self.parse_codex_line(s, &mut state), + AGENT_PI => self.parse_pi_line(s, &mut state), _ => self.parse_claude_line(s, &mut state), }; if let Some(mut ev) = parsed { @@ -1106,6 +1185,7 @@ impl Store { mcp: Vec::new(), skills: Vec::new(), id: usage.event_id, + reported_cost: None, source: String::new(), agent: String::new(), }) @@ -1162,6 +1242,7 @@ impl Store { mcp: vec![server], skills: Vec::new(), id: String::new(), + reported_cost: None, source: String::new(), agent: String::new(), }) @@ -1170,6 +1251,198 @@ impl Store { } } + /// Parse one Pi session entry. Pi persists one exact Usage object per + /// assistant response in a tree-structured JSONL file, so physical entries + /// represent real upstream work even when the active branch later changes. + /// Stable entry ids dedupe history copied by `/fork` or `/clone`. + fn parse_pi_line(&mut self, line: &str, state: &mut FileState) -> Option { + let value: serde_json::Value = serde_json::from_str(line).ok()?; + let entry_type = value.get("type")?.as_str()?; + let ts_ms = parse_ts(&value).unwrap_or(0); + if entry_type != "session" && !state.source.is_empty() { + if let Some(entry_id) = value.get("id").and_then(|item| item.as_str()) { + if self.manifest.pi_entries.contains_key(entry_id) { + return None; + } + self.manifest + .pi_entries + .insert(entry_id.to_string(), state.source.clone()); + } + } + + match entry_type { + "session" => { + if let Some(id) = value.get("id").and_then(|item| item.as_str()) { + state.session = id.to_string(); + } + if let Some(cwd) = value.get("cwd").and_then(|item| item.as_str()) { + state.cwd = cwd.to_string(); + } + None + } + "model_change" => { + if let Some(provider) = value.get("provider").and_then(|item| item.as_str()) { + state.provider = provider.to_string(); + } + if let Some(model) = value.get("modelId").and_then(|item| item.as_str()) { + state.model = model.to_string(); + } + None + } + "message" => { + let message = value.get("message")?; + match message.get("role").and_then(|item| item.as_str())? { + "user" => { + if let Some(id) = value.get("id").and_then(|item| item.as_str()) { + state.turn_id = id.to_string(); + state.turn_started_ms = ts_ms; + state.skills_seen.clear(); + } + let skill = pi_skill_command(message)?; + state.skills_seen.push(skill.clone()); + Some(pi_event( + &value, + state, + "command", + PiEventData { + skills: vec![skill], + ..PiEventData::default() + }, + )) + } + "assistant" => { + if let Some(provider) = message.get("provider").and_then(|item| item.as_str()) { + state.provider = provider.to_string(); + } + if let Some(model) = message.get("model").and_then(|item| item.as_str()) { + state.model = model.to_string(); + } + if state.turn_id.is_empty() { + state.turn_id = value + .get("id") + .and_then(|item| item.as_str()) + .unwrap_or("unknown") + .to_string(); + state.turn_started_ms = ts_ms; + } + + let usage = message.get("usage").unwrap_or(&serde_json::Value::Null); + let number = |key: &str| { + usage + .get(key) + .and_then(|item| item.as_f64()) + .unwrap_or(0.0) + }; + let input = number("input"); + let cache_read = number("cacheRead"); + let cache_write = number("cacheWrite"); + let output = number("output"); + let reasoning = number("reasoning"); + let reported_cost = pi_usage_cost(usage); + let (mcp, skills) = pi_tool_calls(message, state); + let context_window = pi_context_window(&state.provider, &state.model); + let started_ms = state.turn_started_ms; + let stop_reason = message + .get("stopReason") + .and_then(|item| item.as_str()) + .unwrap_or(""); + + if let Some(turn) = self.turn_mut(state, AGENT_PI, ts_ms) { + turn.input_tokens += input; + turn.cache_creation_tokens += cache_write; + turn.cache_read_tokens += cache_read; + turn.output_tokens += output; + turn.reasoning_tokens += reasoning; + turn.context_tokens = turn.context_tokens.max(input + cache_write + cache_read); + turn.context_window = turn.context_window.max(context_window); + if let Some(cost) = reported_cost { + turn.reported_cost = Some(turn.reported_cost.unwrap_or_default() + cost); + } + if turn.ttft_ms == 0 && started_ms > 0 && ts_ms >= started_ms { + turn.ttft_ms = (ts_ms - started_ms) as u64; + } + match stop_reason { + "stop" | "length" => { + turn.outcome = "completed".to_string(); + if started_ms > 0 && ts_ms >= started_ms { + turn.duration_ms = (ts_ms - started_ms) as u64; + } + } + "aborted" | "error" => { + turn.outcome = "aborted".to_string(); + turn.abort_reason = stop_reason.to_string(); + if started_ms > 0 && ts_ms >= started_ms { + turn.duration_ms = (ts_ms - started_ms) as u64; + } + } + _ => {} + } + } + + Some(pi_event( + &value, + state, + "assistant", + PiEventData { + model: state.model.clone(), + input, + cache_write, + cache_read, + output, + reasoning, + reported_cost, + mcp, + skills, + }, + )) + } + "toolResult" => { + let is_error = message.get("isError").and_then(|item| item.as_bool()) + == Some(true); + let usage = message.get("usage"); + let data = usage.map(|usage| pi_event_data(usage, state.model.clone())); + if is_error || data.is_some() { + if let Some(turn) = self.turn_mut(state, AGENT_PI, ts_ms) { + turn.tool_errors += u64::from(is_error); + if let Some(data) = data.as_ref() { + turn.input_tokens += data.input; + turn.cache_creation_tokens += data.cache_write; + turn.cache_read_tokens += data.cache_read; + turn.output_tokens += data.output; + turn.reasoning_tokens += data.reasoning; + turn.context_tokens = turn + .context_tokens + .max(data.input + data.cache_write + data.cache_read); + if let Some(cost) = data.reported_cost { + turn.reported_cost = + Some(turn.reported_cost.unwrap_or_default() + cost); + } + } + } + } + Some(pi_event(&value, state, "tool-usage", data?)) + } + _ => None, + } + } + "compaction" | "branch_summary" => { + if entry_type == "compaction" { + if let Some(turn) = self.turn_mut(state, AGENT_PI, ts_ms) { + turn.compactions += 1; + } + } + let usage = value.get("usage")?; + Some(pi_event( + &value, + state, + entry_type, + pi_event_data(usage, state.model.clone()), + )) + } + _ => None, + } + } + fn parse_claude_line(&mut self, line: &str, state: &mut FileState) -> Option { let value: serde_json::Value = serde_json::from_str(line).ok()?; let ts_ms = parse_ts(&value).unwrap_or(0); @@ -1368,6 +1641,195 @@ impl Store { } } +fn pi_usage_cost(usage: &serde_json::Value) -> Option { + usage + .get("cost") + .and_then(|cost| cost.get("total")) + .and_then(|total| total.as_f64()) +} + +#[derive(Default)] +struct PiEventData { + model: String, + input: f64, + cache_write: f64, + cache_read: f64, + output: f64, + reasoning: f64, + reported_cost: Option, + mcp: Vec, + skills: Vec, +} + +fn pi_event_data(usage: &serde_json::Value, model: String) -> PiEventData { + let number = |key: &str| { + usage + .get(key) + .and_then(|item| item.as_f64()) + .unwrap_or(0.0) + }; + PiEventData { + model, + input: number("input"), + cache_write: number("cacheWrite"), + cache_read: number("cacheRead"), + output: number("output"), + reasoning: number("reasoning"), + reported_cost: pi_usage_cost(usage), + ..PiEventData::default() + } +} + +fn pi_event( + entry: &serde_json::Value, + state: &FileState, + kind: &str, + data: PiEventData, +) -> RawEvent { + let entry_id = entry.get("id").and_then(|item| item.as_str()).unwrap_or(""); + RawEvent { + ts_ms: parse_ts(entry).unwrap_or(0), + session: state.session.clone(), + model: data.model, + in_tok: data.input, + cc: data.cache_write, + cr: data.cache_read, + out_tok: data.output, + mcp: data.mcp, + skills: data.skills, + id: if entry_id.is_empty() { + String::new() + } else { + format!("pi:{entry_id}:{kind}") + }, + reported_cost: data.reported_cost, + source: String::new(), + agent: String::new(), + } +} + +fn pi_skill_command(message: &serde_json::Value) -> Option { + let content = message.get("content")?; + let text = if let Some(text) = content.as_str() { + text.to_string() + } else { + content + .as_array()? + .iter() + .filter(|block| block.get("type").and_then(|item| item.as_str()) == Some("text")) + .filter_map(|block| block.get("text").and_then(|item| item.as_str())) + .collect::>() + .join("\n") + }; + let command = text.split_whitespace().next()?; + let skill = command.strip_prefix("/skill:")?.trim(); + (!skill.is_empty()).then(|| skill.to_string()) +} + +fn pi_skill_from_path(path: &str) -> Option { + let normalized = path.replace('\\', "/"); + let path = Path::new(&normalized); + if !path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md")) + { + return None; + } + path.parent()? + .file_name()? + .to_str() + .filter(|name| !name.is_empty()) + .map(str::to_owned) +} + +fn pi_tool_calls(message: &serde_json::Value, state: &mut FileState) -> (Vec, Vec) { + let mut mcp = Vec::new(); + let mut skills = Vec::new(); + let Some(content) = message.get("content").and_then(|item| item.as_array()) else { + return (mcp, skills); + }; + for block in content { + if block.get("type").and_then(|item| item.as_str()) != Some("toolCall") { + continue; + } + let name = block.get("name").and_then(|item| item.as_str()).unwrap_or(""); + if let Some(server) = name + .strip_prefix("mcp__") + .and_then(|rest| rest.split("__").next()) + .filter(|server| !server.is_empty()) + { + mcp.push(server.to_string()); + } + if name != "read" { + continue; + } + let Some(path) = block + .get("arguments") + .and_then(|arguments| arguments.get("path")) + .and_then(|path| path.as_str()) + else { + continue; + }; + let Some(skill) = pi_skill_from_path(path) else { + continue; + }; + if !state.skills_seen.contains(&skill) { + state.skills_seen.push(skill.clone()); + skills.push(skill); + } + } + (mcp, skills) +} + +fn pi_context_window(provider: &str, model: &str) -> f64 { + static WINDOWS: std::sync::OnceLock> = std::sync::OnceLock::new(); + let windows = WINDOWS.get_or_init(|| { + let mut windows = HashMap::new(); + let Some(agent_dir) = pi_agent_dir() else { + return windows; + }; + for (path, nested_providers) in [ + (agent_dir.join("models-store.json"), false), + (agent_dir.join("models.json"), true), + ] { + let Ok(text) = fs::read_to_string(path) else { + continue; + }; + let Ok(json) = serde_json::from_str::(&text) else { + continue; + }; + let root = if nested_providers { + json.get("providers").and_then(|item| item.as_object()) + } else { + json.as_object() + }; + let Some(root) = root else { continue }; + for (provider_id, config) in root { + let Some(models) = config.get("models").and_then(|item| item.as_array()) else { + continue; + }; + for model in models { + let Some(id) = model.get("id").and_then(|item| item.as_str()) else { + continue; + }; + let Some(window) = model.get("contextWindow").and_then(|item| item.as_f64()) else { + continue; + }; + windows.insert(format!("{provider_id}\0{id}"), window); + windows.entry(id.to_string()).or_insert(window); + } + } + } + windows + }); + windows + .get(&format!("{provider}\0{model}")) + .or_else(|| windows.get(model)) + .copied() + .unwrap_or(0.0) +} + /// RFC3339 top-level `timestamp` → epoch ms. fn parse_ts(v: &serde_json::Value) -> Option { let ts = v.get("timestamp")?.as_str()?; @@ -1500,6 +1962,7 @@ fn codex_exec_event( mcp, skills, id: String::new(), + reported_cost: None, source: String::new(), agent: String::new(), }) @@ -1571,6 +2034,7 @@ fn parse_user_command(v: &serde_json::Value) -> Option { mcp: Vec::new(), skills: vec![skill], id, + reported_cost: None, source: String::new(), agent: String::new(), }) @@ -1637,6 +2101,7 @@ fn parse_assistant(v: &serde_json::Value) -> Option { mcp, skills, id, + reported_cost: None, source: String::new(), agent: String::new(), }) @@ -2201,6 +2666,111 @@ mod tests { ); } + #[test] + fn parses_pi_usage_tools_cost_and_turn_telemetry() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/pi-session.jsonl".to_string(), + ..FileState::default() + }; + let header = r#"{"type":"session","version":3,"id":"session-pi","timestamp":"2026-08-01T10:00:00Z","cwd":"/work/project"}"#; + let user = r#"{"type":"message","id":"user0001","parentId":null,"timestamp":"2026-08-01T10:00:01Z","message":{"role":"user","content":"Build it","timestamp":1785578401000}}"#; + let assistant = r#"{"type":"message","id":"assist01","parentId":"user0001","timestamp":"2026-08-01T10:00:04Z","message":{"role":"assistant","provider":"test-provider","model":"test-model","content":[{"type":"toolCall","id":"call-1","name":"read","arguments":{"path":"/Users/me/.agents/skills/archify/SKILL.md"}},{"type":"toolCall","id":"call-2","name":"mcp__browser__open","arguments":{}}],"usage":{"input":100,"cacheRead":800,"cacheWrite":50,"output":25,"reasoning":10,"totalTokens":975,"cost":{"input":0.1,"cacheRead":0.08,"cacheWrite":0.05,"output":0.05,"total":0.28}},"stopReason":"stop","timestamp":1785578404000}}"#; + + assert!(store.parse_pi_line(header, &mut state).is_none()); + assert!(store.parse_pi_line(user, &mut state).is_none()); + let event = store + .parse_pi_line(assistant, &mut state) + .expect("assistant usage event"); + + assert_eq!(state.session, "session-pi"); + assert_eq!(state.cwd, "/work/project"); + assert_eq!(event.id, "pi:assist01:assistant"); + assert_eq!(event.model, "test-model"); + assert_eq!((event.in_tok, event.cc, event.cr, event.out_tok), (100.0, 50.0, 800.0, 25.0)); + assert_eq!(event.reported_cost, Some(0.28)); + assert_eq!(event.skills, vec!["archify"]); + assert_eq!(event.mcp, vec!["browser"]); + + let turn = store.turns.first().expect("Pi turn telemetry"); + assert_eq!(turn.agent, AGENT_PI); + assert_eq!(turn.session, "session-pi"); + assert_eq!(turn.outcome, "completed"); + assert_eq!(turn.reasoning_tokens, 10.0); + assert_eq!(turn.context_tokens, 950.0); + assert_eq!(turn.reported_cost, Some(0.28)); + assert_eq!(turn.duration_ms, 3_000); + assert_eq!(turn.ttft_ms, 3_000); + + // A fork/clone copies the active path into another file with the same + // entry ids. Neither usage nor telemetry may be replayed there. + let mut copied = FileState { + source: "/tmp/pi-copy.jsonl".to_string(), + ..FileState::default() + }; + store.parse_pi_line(header, &mut copied); + assert!(store.parse_pi_line(user, &mut copied).is_none()); + assert!(store.parse_pi_line(assistant, &mut copied).is_none()); + assert_eq!(store.turns.len(), 1); + } + + #[test] + fn dedupes_pi_skill_reads_within_a_turn_and_tracks_failures() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/pi-failure.jsonl".to_string(), + session: "session-pi".to_string(), + ..FileState::default() + }; + let user = r#"{"type":"message","id":"user0002","parentId":null,"timestamp":"2026-08-01T11:00:00Z","message":{"role":"user","content":"Run skill"}}"#; + let response = |id: &str, stop: &str| { + format!( + r#"{{"type":"message","id":"{id}","parentId":"user0002","timestamp":"2026-08-01T11:00:01Z","message":{{"role":"assistant","provider":"test","model":"test","content":[{{"type":"toolCall","id":"call","name":"read","arguments":{{"path":"/tmp/demo/SKILL.md"}}}}],"usage":{{"input":1,"cacheRead":2,"cacheWrite":3,"output":4,"reasoning":2,"totalTokens":10,"cost":{{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}}},"stopReason":"{stop}"}}}}"# + ) + }; + let tool_error = r#"{"type":"message","id":"tool0001","parentId":"assist02","timestamp":"2026-08-01T11:00:02Z","message":{"role":"toolResult","toolCallId":"call","toolName":"read","content":[],"isError":true}}"#; + + store.parse_pi_line(user, &mut state); + let first = store.parse_pi_line(&response("assist02", "toolUse"), &mut state).unwrap(); + let second = store.parse_pi_line(&response("assist03", "aborted"), &mut state).unwrap(); + assert_eq!(first.skills, vec!["demo"]); + assert!(second.skills.is_empty()); + assert!(store.parse_pi_line(tool_error, &mut state).is_none()); + + let turn = store.turns.first().unwrap(); + assert_eq!(turn.outcome, "aborted"); + assert_eq!(turn.abort_reason, "aborted"); + assert_eq!(turn.tool_errors, 1); + assert_eq!(turn.input_tokens, 2.0); + assert_eq!(turn.cache_creation_tokens, 6.0); + assert_eq!(turn.cache_read_tokens, 4.0); + assert_eq!(turn.output_tokens, 8.0); + assert_eq!(turn.reported_cost, Some(0.0)); + } + + #[test] + fn parses_pi_skill_slash_command_and_summary_usage() { + let mut store = empty_store(); + let mut state = FileState { + source: "/tmp/pi-summary.jsonl".to_string(), + session: "session-pi".to_string(), + model: "summary-model".to_string(), + ..FileState::default() + }; + let command = r#"{"type":"message","id":"user0003","parentId":null,"timestamp":"2026-08-01T12:00:00Z","message":{"role":"user","content":"/skill:archify workflow"}}"#; + let compaction = r#"{"type":"compaction","id":"compact1","parentId":"user0003","timestamp":"2026-08-01T12:00:01Z","summary":"...","tokensBefore":1000,"usage":{"input":20,"cacheRead":30,"cacheWrite":0,"output":10,"totalTokens":60,"cost":{"total":0.02}}}"#; + + let skill = store.parse_pi_line(command, &mut state).unwrap(); + assert_eq!(skill.skills, vec!["archify"]); + assert!(skill.model.is_empty()); + let summary = store.parse_pi_line(compaction, &mut state).unwrap(); + assert_eq!(summary.id, "pi:compact1:compaction"); + assert_eq!(summary.model, "summary-model"); + assert_eq!((summary.in_tok, summary.cr, summary.out_tok), (20.0, 30.0, 10.0)); + assert_eq!(summary.reported_cost, Some(0.02)); + assert_eq!(store.turns.first().unwrap().compactions, 1); + } + /// Opt-in audit for one real, immutable rollout. Production Store output is /// checked turn by turn against a small independent cumulative-snapshot /// reducer. Only token metadata is printed. diff --git a/src/App.tsx b/src/App.tsx index e1a1cc3..5aff43d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -370,7 +370,7 @@ function ShortcutEditor({ current, theme, dark, onClose, onSaved }: ); } -// Agent filter chips (All / Claude / Codex …). Rendered only when several +// Agent filter chips (All / Claude / Codex / Pi …). Rendered only when several // sources have data; a single-source install never sees them. function AgentChips({ scopes, value, theme, onSelect }: { scopes: Scope[]; value: string; theme: Theme; onSelect: (id: string) => void }) { @@ -525,7 +525,7 @@ function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) {item(text.median, stats.trackedTurns ? `${stats.medianPct.toFixed(1)}%` : "—", text.context)} {item(text.peak, stats.trackedTurns ? `${stats.peakPct.toFixed(1)}%` : "—", `${fmtInt(stats.nearLimitTurns)} ≥80%`, stats.peakPct >= 80)} {item(text.compacted, fmtInt(stats.compactions), text.times)} - {item(text.reasoning, stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : text.codexOnly)} + {item(text.reasoning, stats.reasoningTokens > 0 ? `${stats.reasoningPct.toFixed(1)}%` : "—", stats.reasoningTokens > 0 ? fmtTokens(stats.reasoningTokens) : text.whenReported)}
); diff --git a/src/data.ts b/src/data.ts index 92ef1bf..98a1e7c 100644 --- a/src/data.ts +++ b/src/data.ts @@ -43,7 +43,7 @@ export interface Quota { secondaryPct: number; secondaryMinutes: number; secondaryResetsAt: number; asOfMs: number; } -// One selectable dashboard view. A single data source yields exactly one scope +// One selectable dashboard view (Claude, Codex, Pi, …). A single data source yields exactly one scope // (id "all", empty color → classic UI, no chips); several sources yield // [All, agent, agent, …] and the header shows filter chips. export interface Scope { @@ -65,6 +65,7 @@ export interface RangeDashboard extends DateRange { scopes: RangeScope[] } export const AGENT_ACCENTS: Record = { claude: { dark: ["#d97757", "#eda380"], light: ["#c05b39", "#e8a184"] }, codex: { dark: ["#10a37f", "#5ec9a8"], light: ["#0c8465", "#6fcdb0"] }, + pi: { dark: ["#a855f7", "#c084fc"], light: ["#8436c7", "#a855f7"] }, }; /// Theme with the scope's agent accent applied (identity for the All scope). diff --git a/src/i18n.tsx b/src/i18n.tsx index 415fdd7..734bfb0 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -50,7 +50,7 @@ const en = { errors: "errors", denied: "denied", median: "Median", - codexOnly: "Codex only", + whenReported: "When reported", contextHealth: "Context health", measuredTurns: "measured turns", context: "context", @@ -179,7 +179,7 @@ const zh: Copy = { errors: "错误", denied: "拒绝", median: "中位数", - codexOnly: "仅 Codex", + whenReported: "模型支持时", contextHealth: "上下文健康度", measuredTurns: "轮已测量对话", context: "上下文", From 47303f22a8219e43a20d4e8b921b6dfc70cba43c Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 02:45:48 -0700 Subject: [PATCH 50/67] feat: source provider limits through HappyUsage CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provider quota (Claude 5-hour/weekly, Codex weekly/Spark) now comes from the happyusage `hu` CLI instead of a self-hosted credential + OAuth + HTTP stack. Limits are global (never attached to a usage scope), shown as a two-column card on the dashboard with official Anthropic/OpenAI glyphs and a Provider Limits submenu in the tray. Menu bar gains a Menu Bar Display picker (Off / Compact / Detailed): Compact shows the tightest window per provider (52.8M · Cl79% · Cx29%), Detailed lists every window (52.8M · Cl 5h0/W79 · Cx W29/S31). Old weekly-remaining preferences migrate to Compact. Claude falls back to ~/.claude.json cachedUsageUtilization when `hu` cannot refresh; Codex trend merges session-log quota history. Parsing tolerates missing providers and renamed quota pools (0.2.11 Spark_weekly). Zero-baseline percentage deltas are defined as +100%. --- README-zh.md | 13 +- README.md | 13 +- docs/DESIGN-pi-adapter.md | 5 +- docs/DESIGN-usage-api.md | 69 +++++ src-tauri/src/lib.rs | 500 ++++++++++++++++++++++++----------- src-tauri/src/model.rs | 63 ++++- src-tauri/src/parser.rs | 149 +++++++---- src-tauri/src/quota_api.rs | 519 +++++++++++++++++++++++++++++++++++++ src-tauri/src/store.rs | 9 +- src/App.tsx | 155 ++++++----- src/data.ts | 13 +- src/i18n.tsx | 16 +- 12 files changed, 1236 insertions(+), 288 deletions(-) create mode 100644 docs/DESIGN-usage-api.md create mode 100644 src-tauri/src/quota_api.rs diff --git a/README-zh.md b/README-zh.md index b587b28..9c2193e 100644 --- a/README-zh.md +++ b/README-zh.md @@ -27,25 +27,29 @@ brew install --cask sunchj/tokenscope/tokenscope - Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数,以及 Agent 可提供时的 reasoning Token 占比 - 周额度趋势:对 Codex 快照下采样,展示当前重置周期的每日消耗速度与预计触顶时间 - 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** -- **Codex 配额卡**:直接从 Codex 自己的日志读取当前周额度的已用百分比、plan 与重置倒计时 +- **额度详情卡**:实时订阅额度,**Claude**(5 小时 + 每周)与 **Codex**(每周 + Spark),经由 HappyUsage `hu` CLI 获取,采用 Codex status 风格(`29% left (resets 05:01 on 9 Aug)`);session 日志快照作为离线回退 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 - **只统计用户自己安装的 MCP / Skill**,过滤所有内置工具与自带 MCP -## 数据来源(零侵入,只读) +## 数据来源(本地优先;session 日志只读) | 用途 | 路径 | |------|------| | Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | -| Codex 会话日志(Token / 模型 / 配额) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | +| Codex 会话日志(Token + 离线配额回退) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | +| Provider 额度(Claude + Codex) | HappyUsage `hu usage --json`(凭据/OAuth/API 均由 `hu` 负责;Claude 回退:`~/.claude.json` 的 `cachedUsageUtilization`) | | Pi 会话日志(Token / 模型 / 工具 / 遥测) | `~/.pi/agent/sessions/**/*.jsonl`(支持 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR` 与绝对路径 `settings.sessionDir`) | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | | 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/`;Pi:全局、共享、项目级 Pi Skill 目录及 settings 显式路径 | | 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | +配额请求统一走 HappyUsage `hu` CLI,由它负责凭据发现、OAuth 刷新与 provider 调用;Tokenscope 只缓存额度元数据,从不缓存凭据。 + ### 关键处理 - Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 - Codex:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中分别拆出 `cached_input_tokens` 与 `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 +- Provider 额度:每 5 分钟在 dashboard build lock 外通过 `hu usage claude --json` + `hu usage codex --json` 刷新;最近一次成功快照落盘 `provider_limits.json`,`hu` 或网络失败时仍可展示。Claude `session`→5 小时 / `weekly`→7 天;Codex 主池→Weekly / Spark→Spark - Pi:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` 或 `/clone` 复制的历史,session header 提供会话与项目归属;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 会进入对应遥测 - token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 @@ -139,7 +143,7 @@ App 会在启动时及此后每小时检测新版本。当前版本和更新操 - **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) - **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) - 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) -- **Weekly Remaining** 可配置菜单栏 Token 数后的周剩余额度:**Off**、仅 **Codex**(如 `12.40M-C76%`),或 **Codex + Spark**(`codex_bengalfox`,显示为 `12.40M-C76%-S93%`);已过重置时间的额度快照不会继续显示,默认关闭,选择会被记住 +- **菜单栏显示** 控制菜单栏 Token 数后展示多少额度信息:**关闭**(`52.8M`)、**紧凑**(`52.8M · Cl79% · Cx29%`,各 provider 最紧张窗口)、**详细**(`52.8M · Cl 5h0/W79 · Cx W29/S31`)。百分比与 dashboard(`29% left (resets 05:01 on 9 Aug)`)采用相同的四舍五入**剩余值**。已过重置时间的窗口不会显示,默认关闭,选择会被记住 - **Dashboard Shortcut** 可用全局快捷键切换面板的显示/隐藏(默认 macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);通过 **Change Dashboard Shortcut…** 可直接录入自定义组合键。默认关闭,选择会被记住 - 已自动设置**登录自启**,无需手动配置 @@ -194,6 +198,7 @@ src-tauri/src/ store.rs 多源 JSONL 增量摄取(Claude + Codex + Pi 适配器) parser.rs 按 Scope 聚合(All / 单 Agent;预设周期 + 自定义区间 + 热力图) pricing.rs models.dev / LiteLLM 价格加载与计价 + quota_api.rs 已认证 provider 配额 + 安全凭据刷新/缓存 config.rs 用户 MCP / Skill 白名单(Claude + Codex + Pi) model.rs 返回给前端的数据结构 lib.rs Tauri 命令 + 菜单栏托盘 diff --git a/README.md b/README.md index ad004f4..9cd9196 100644 --- a/README.md +++ b/README.md @@ -27,25 +27,29 @@ brew install --cask sunchj/tokenscope/tokenscope - Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and reasoning-token share when reported by the agent - Weekly quota trend: downsampled Codex snapshots with daily burn rate and time-to-cap projection for the current reset cycle - Three breakdowns: **by model** / **by MCP call** / **by Skill call** -- **Codex quota card**: the active weekly rate-limit window (used %, plan, reset countdown) read straight from Codex's own logs +- **Provider limits card**: live subscription quota for **Claude** (5-hour + weekly) and **Codex** (weekly + Spark), sourced from the HappyUsage `hu` CLI and shown Codex-status style (`29% left (resets 05:01 on 9 Aug)`); session-log snapshots remain the offline fallback - Cost donut (hover for a single model), year-long activity heatmap - **Counts only the MCP servers / Skills you installed yourself** — all built-in tools and bundled MCP servers are filtered out -## Data sources (zero-intrusion, read-only) +## Data sources (local-first; session logs are read-only) | Purpose | Path | |---------|------| | Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | -| Codex session logs (tokens / model / rate limits) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | +| Codex session logs (tokens + offline quota fallback) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | +| Provider quota (Claude + Codex) | HappyUsage `hu usage --json` (credential + OAuth + API owned by `hu`; Claude fallback: `~/.claude.json` `cachedUsageUtilization`) | | Pi session logs (tokens / model / tools / telemetry) | `~/.pi/agent/sessions/**/*.jsonl` (honors `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, and absolute `settings.sessionDir`) | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | | User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | | Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | +Quota requests go through the HappyUsage `hu` CLI, which owns credential discovery, OAuth refresh, and provider calls. Tokenscope caches only quota metadata, never credentials. + ### Key processing - Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once - Codex: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, while `cached_input_tokens` and `cache_write_input_tokens` are split out of Codex's inclusive `input_tokens` for comparable four-category accounting +- Provider quota: refreshed every 5 minutes outside the dashboard build lock via `hu usage claude --json` + `hu usage codex --json`; last successful snapshot is cached (`provider_limits.json`) and shown when `hu` or the network fails. Claude `session`→5-hour / `weekly`→7d; Codex primary→Weekly / Spark→Spark - Pi: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`, while the session header supplies the session/project identity. Persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching dashboard telemetry - Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins @@ -141,7 +145,7 @@ right-click menu to check immediately. - **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) - **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) - Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) -- **Weekly Remaining** controls the quota shown beside the menu-bar token count: **Off**, **Codex** (for example `12.40M-C76%`), or **Codex + Spark** (`codex_bengalfox`, shown as `12.40M-C76%-S93%`). Expired quota snapshots are hidden; the setting is off by default and remembers your choice +- **Menu Bar Display** controls how much quota the menu-bar title shows next to the token count: **Off** (`52.8M`), **Compact** (`52.8M · Cl79% · Cx29%` — the tightest window per provider), or **Detailed** (`52.8M · Cl 5h0/W79 · Cx W29/S31`). Percentages use the same rounded **left** values as the dashboard (`29% left (resets 05:01 on 9 Aug)`). Expired windows are hidden; the setting is off by default and remembers your choice - **Dashboard Shortcut** toggles the panel with a global hotkey (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux by default); use **Change Dashboard Shortcut…** to record your own combination. It is off by default and remembers your choice - **Launch-at-login is set up automatically** — no manual configuration needed @@ -196,6 +200,7 @@ src-tauri/src/ store.rs incremental multi-source JSONL ingest (Claude + Codex + Pi adapters) parser.rs aggregation into scopes (All / per-agent; presets + custom ranges + heatmap) pricing.rs models.dev / LiteLLM price loading and costing + quota_api.rs authenticated provider quota + credential-safe refresh/cache config.rs user MCP / Skill whitelists (Claude + Codex + Pi) model.rs data structures returned to the frontend lib.rs Tauri commands + menu-bar tray diff --git a/docs/DESIGN-pi-adapter.md b/docs/DESIGN-pi-adapter.md index e7f32f6..df089dd 100644 --- a/docs/DESIGN-pi-adapter.md +++ b/docs/DESIGN-pi-adapter.md @@ -44,8 +44,11 @@ All physical assistant entries in one tree file represent real upstream requests - `/skill:` user commands are recognized when persisted directly. - Skill whitelists include Pi's global/shared/project locations and explicit non-glob settings paths. +## Provider quota + +Pi sessions do not persist Codex rate limits, so quota is intentionally separate from session ingestion. Tokenscope queries provider quota through the HappyUsage `hu` CLI (see [DESIGN-usage-api.md](DESIGN-usage-api.md)); the resulting Claude / Codex limits are global and never attached to the Pi scope. + ## Non-goals -- Pi sessions do not persist Codex rate-limit snapshots, so the Pi scope does not synthesize a quota card. - Relative custom `sessionDir` values cannot be globally discovered outside their originating project cwd. - Generic extension tools are not classified as MCP unless their persisted name uses the `mcp__` convention. diff --git a/docs/DESIGN-usage-api.md b/docs/DESIGN-usage-api.md new file mode 100644 index 0000000..4cf0122 --- /dev/null +++ b/docs/DESIGN-usage-api.md @@ -0,0 +1,69 @@ +# Provider Quota via HappyUsage + +## Decision + +Live provider subscription quota is collected through the **HappyUsage `hu` +CLI** instead of a self-hosted credential + HTTP stack. `hu` already owns: + +- Credential discovery (Claude Code keychain / `.credentials.json`, + Codex CLI `auth.json`, …) and OAuth refresh, +- Provider API calls (Claude via the Anthropic OAuth usage API, Codex via the + ChatGPT `wham/usage` endpoint), +- Per-provider error handling. + +Tokenscope runs `hu usage --json` and parses the envelope. This +keeps one credential implementation instead of duplicating it in Rust. + +## Architecture + +``` +refresh thread (every 5 min) + └─ quota_api::reload() + ├─ hu usage claude --json → claude ProviderLimit + │ └─ on failure: ~/.claude.json cachedUsageUtilization (local fallback) + └─ hu usage codex --json → codex ProviderLimit + └─ persist ProviderCache → provider_limits.json (no credentials) +parser::build_dashboard() + └─ quota_api::shared() → provider_limits (dashboard + tray) + └─ Codex windows merge session-log quota_history for trend depth +``` + +`provider_limits.json` caches the last successful snapshot per provider so a +later offline refresh (or a missing `hu`) still has data. The cache holds +quota metadata only — never tokens. + +## Normalization + +| Provider | Source | Windows | +|---|---|---| +| Claude | `hu` envelope `quotas[]` | `session` → 5-hour, `weekly` → 7d | +| Claude (fallback) | `~/.claude.json` `cachedUsageUtilization` | `five_hour`, `seven_day` | +| Codex | `hu` envelope `quotas[]` | primary → Weekly, `Spark` → Spark | + +- HappyUsage hardcodes Claude's period labels, so they are trusted. +- Codex's `period` label is **not** trusted: `limit_window_seconds` can be + absent from the window payload (observed on live API responses), which makes + `hu` fall back to a misleading `5h`. The 5h Codex window is retired anyway, + so both Codex windows are classified as weekly here. +- `used_pct` is retained in storage; presentation converts to rounded + percentage left (`29% left (resets 05:01 on 9 Aug)`). + +## Menu bar and dashboard + +- **Provider Limits** submenu: one read-only row per provider + (`Claude — 5h 0% · W 79%`), refreshed with every dashboard build. +- **Menu Bar Display**: Off / Compact / Detailed. + - Compact: `52.8M · Cl79% · Cx29%` — tightest window per provider. + - Detailed: `52.8M · Cl 5h0/W79 · Cx W29/S31`. +- Dashboard: a global two-column card (Claude | Codex), each window showing + remaining capacity, the reset date, and an optional burn-rate projection. + Provider limits never attach to a token-usage scope (Pi / Codex CLI remain + only usage sources). + +## Fallback chain + +1. `hu usage --json` +2. Last successful `provider_limits.json` snapshot +3. Claude-only: `~/.claude.json` `cachedUsageUtilization` (updated by Claude + Code when it runs) +4. Codex trend: session-log `rate_limits` history merged into the live window diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 280829e..037c27d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod config; mod model; mod parser; mod pricing; +mod quota_api; mod store; use model::{Dashboard, RangeDashboard}; @@ -38,13 +39,32 @@ fn now_ms() -> i64 { const DASHBOARD_SHORTCUT: &str = "CommandOrControl+Alt+T"; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +/// What the menu-bar title shows next to today's token count. Compact shows +/// the tightest window per provider; Detailed lists every window. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "snake_case")] -enum WeeklyQuotaDisplay { +enum MenuBarQuotaDisplay { #[default] Off, - Codex, - CodexAndSpark, + Compact, + Detailed, +} + +impl<'de> serde::Deserialize<'de> for MenuBarQuotaDisplay { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Ok(match value.as_str() { + "off" => Self::Off, + // Legacy weekly-remaining values map to the compact summary. + "codex" | "codex_and_spark" => Self::Compact, + "compact" => Self::Compact, + "detailed" => Self::Detailed, + _ => Self::Off, + }) + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] @@ -75,7 +95,9 @@ impl AppLanguage { #[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(default)] struct TrayPreferences { - weekly_quota_display: WeeklyQuotaDisplay, + // Persisted key stays `weekly_quota_display` for forward compatibility; + // the semantics are now Menu Bar Display (off / compact / detailed). + weekly_quota_display: MenuBarQuotaDisplay, dashboard_shortcut: bool, dashboard_shortcut_key: String, language: AppLanguage, @@ -84,7 +106,7 @@ struct TrayPreferences { impl Default for TrayPreferences { fn default() -> Self { Self { - weekly_quota_display: WeeklyQuotaDisplay::Off, + weekly_quota_display: MenuBarQuotaDisplay::Off, dashboard_shortcut: false, dashboard_shortcut_key: DASHBOARD_SHORTCUT.to_string(), language: AppLanguage::En, @@ -97,8 +119,13 @@ struct TrayMenuState { open: MenuItem, refresh: MenuItem, check_updates: MenuItem, - weekly: Submenu, - weekly_off: CheckMenuItem, + provider_limits: Submenu, + provider_claude: MenuItem, + provider_codex: MenuItem, + menu_bar_display: Submenu, + display_off: CheckMenuItem, + display_compact: CheckMenuItem, + display_detailed: CheckMenuItem, dashboard_shortcut: CheckMenuItem, change_dashboard_shortcut: MenuItem, autostart: CheckMenuItem, @@ -112,8 +139,11 @@ struct TrayCopy { open: &'static str, refresh: &'static str, check_updates: &'static str, - weekly: &'static str, + provider_limits: &'static str, + menu_bar_display: &'static str, off: &'static str, + compact: &'static str, + detailed: &'static str, dashboard_shortcut: &'static str, change_dashboard_shortcut: &'static str, autostart: &'static str, @@ -130,8 +160,11 @@ fn tray_copy(language: AppLanguage) -> &'static TrayCopy { open: "Open Tokenscope", refresh: "Refresh", check_updates: "Check for Updates…", - weekly: "Weekly Remaining", + provider_limits: "Provider Limits", + menu_bar_display: "Menu Bar Display", off: "Off", + compact: "Compact", + detailed: "Detailed", dashboard_shortcut: "Dashboard Shortcut", change_dashboard_shortcut: "Change Dashboard Shortcut…", autostart: "Launch at Login", @@ -146,8 +179,11 @@ fn tray_copy(language: AppLanguage) -> &'static TrayCopy { open: "打开 Tokenscope", refresh: "刷新", check_updates: "检查更新…", - weekly: "周剩余额度", + provider_limits: "额度详情", + menu_bar_display: "菜单栏显示", off: "关闭", + compact: "紧凑", + detailed: "详细", dashboard_shortcut: "Dashboard 快捷键", change_dashboard_shortcut: "修改 Dashboard 快捷键…", autostart: "登录时启动", @@ -211,8 +247,11 @@ fn apply_tray_language(menu: &TrayMenuState, language: AppLanguage, shortcut: &s let _ = menu.open.set_text(copy.open); let _ = menu.refresh.set_text(copy.refresh); let _ = menu.check_updates.set_text(copy.check_updates); - let _ = menu.weekly.set_text(copy.weekly); - let _ = menu.weekly_off.set_text(copy.off); + let _ = menu.provider_limits.set_text(copy.provider_limits); + let _ = menu.menu_bar_display.set_text(copy.menu_bar_display); + let _ = menu.display_off.set_text(copy.off); + let _ = menu.display_compact.set_text(copy.compact); + let _ = menu.display_detailed.set_text(copy.detailed); let _ = menu .dashboard_shortcut .set_text(dashboard_shortcut_menu_label(shortcut, language)); @@ -228,69 +267,135 @@ fn apply_tray_language(menu: &TrayMenuState, language: AppLanguage, shortcut: &s let _ = menu.quit.set_text(copy.quit); } -fn weekly_remaining_from_quota(quota: &model::Quota, now_s: i64) -> Option { - let (used, resets_at) = if quota.primary_minutes == 7 * 24 * 60 { - (quota.primary_pct, quota.primary_resets_at) - } else if quota.secondary_minutes == 7 * 24 * 60 { - (quota.secondary_pct, quota.secondary_resets_at) - } else { - return None; - }; - // A dormant pool may not emit another snapshot after its window resets. - // Do not keep advertising that expired reading in the native tray. - if resets_at > 0 && resets_at <= now_s { +/// Percentage left for a window, hidden once its reset timestamp has passed +/// (a dormant pool may stop emitting snapshots after the window rolls over). +fn window_left(window: &model::LimitWindow, now_s: i64) -> Option { + if window.resets_at > 0 && window.resets_at <= now_s { return None; } - Some((100.0 - used).clamp(0.0, 100.0).round() as u8) + Some((100.0 - window.used_pct).clamp(0.0, 100.0).round() as u8) } -fn dashboard_quotas(dash: &Dashboard) -> (Option<&model::Quota>, Option<&model::Quota>) { - dash.scopes +/// Provider prefix + window token used in menu-bar summaries. +/// Claude → Cl, Codex → Cx; windows: 5h / W / S. +fn provider_prefix(provider: &str) -> &'static str { + match provider { + "claude" => "Cl", + "codex" => "Cx", + _ => "", + } +} + +fn window_token(window: &model::LimitWindow) -> &'static str { + match window.id.as_str() { + "5h" => "5h", + "spark" => "S", + _ => "W", + } +} + +/// One provider's menu-bar segment: compact shows only the tightest window, +/// detailed lists every active window (`Cl 5h64/W82`). +fn provider_summary(limit: &model::ProviderLimit, detailed: bool, now_s: i64) -> String { + let prefix = provider_prefix(&limit.provider); + let active: Vec<&model::LimitWindow> = limit + .windows + .iter() + .filter(|window| window_left(window, now_s).is_some()) + .collect(); + if active.is_empty() { + return String::new(); + } + if !detailed { + let tightest = limit + .windows + .iter() + .filter(|window| window_left(window, now_s).is_some()) + .max_by(|a, b| { + a.used_pct + .partial_cmp(&b.used_pct) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .expect("non-empty"); + let left = window_left(tightest, now_s).expect("filtered"); + return format!("{prefix}{left}%"); + } + let mut sorted = active; + sorted.sort_by_key(|window| match window.id.as_str() { + "5h" => 0, + "weekly" => 1, + _ => 2, + }); + let parts: Vec = sorted .iter() - .find(|scope| scope.quota.is_some() || scope.spark_quota.is_some()) - .map(|scope| (scope.quota.as_ref(), scope.spark_quota.as_ref())) - .unwrap_or((None, None)) + .filter_map(|window| { + window_left(window, now_s).map(|left| { + format!("{}{}%", window_token(window), left) + }) + }) + .collect(); + if parts.is_empty() { + String::new() + } else { + format!("{prefix} {}", parts.join("/")) + } } -fn weekly_label_suffix( - display: WeeklyQuotaDisplay, - codex: Option<&model::Quota>, - spark: Option<&model::Quota>, - now_s: i64, -) -> String { - let mut suffix = String::new(); +fn tray_label(dash: &Dashboard, display: MenuBarQuotaDisplay, language: AppLanguage) -> String { + let mut label = fmt_tokens_m(dash.today_tokens, language); + let now_s = now_ms() / 1000; match display { - WeeklyQuotaDisplay::Off => {} - WeeklyQuotaDisplay::Codex => { - if let Some(remaining) = - codex.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) - { - // Keep the pool identity explicit even when only one quota is - // selected; an unlabeled percentage is easy to mistake for Spark. - suffix.push_str(&format!("-C{remaining}%")); - } - } - WeeklyQuotaDisplay::CodexAndSpark => { - if let Some(remaining) = - codex.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) - { - suffix.push_str(&format!("-C{remaining}%")); - } - if let Some(remaining) = - spark.and_then(|quota| weekly_remaining_from_quota(quota, now_s)) - { - suffix.push_str(&format!("-S{remaining}%")); + MenuBarQuotaDisplay::Off => {} + MenuBarQuotaDisplay::Compact | MenuBarQuotaDisplay::Detailed => { + let detailed = display == MenuBarQuotaDisplay::Detailed; + let mut segments: Vec = dash + .provider_limits + .iter() + .map(|limit| provider_summary(limit, detailed, now_s)) + .filter(|segment| !segment.is_empty()) + .collect(); + if !segments.is_empty() { + segments.insert(0, label); + label = segments.join(" · "); } } } - suffix + label } -fn tray_label(dash: &Dashboard, display: WeeklyQuotaDisplay, language: AppLanguage) -> String { - let mut label = fmt_tokens_m(dash.today_tokens, language); - let (codex, spark) = dashboard_quotas(dash); - label.push_str(&weekly_label_suffix(display, codex, spark, now_ms() / 1000)); - label +/// Menu-bar text for each provider row under Provider Limits, e.g. +/// "Claude — 5h 64% · W 82%" (Claude) or "Codex — W 29% · S 31%" (Codex). +fn provider_menu_row(limit: Option<&model::ProviderLimit>, now_s: i64) -> String { + let Some(limit) = limit else { + return String::new(); + }; + let mut sorted: Vec<&model::LimitWindow> = limit + .windows + .iter() + .filter(|window| window_left(window, now_s).is_some()) + .collect(); + sorted.sort_by_key(|window| match window.id.as_str() { + "5h" => 0, + "weekly" => 1, + _ => 2, + }); + let parts: Vec = sorted + .iter() + .filter_map(|window| { + window_left(window, now_s).map(|left| { + let label = match window.id.as_str() { + "5h" => "5h".to_string(), + "weekly" => "W".to_string(), + _ => "S".to_string(), + }; + format!("{label} {left}%") + }) + }) + .collect(); + if parts.is_empty() { + return format!("{} — —", limit.label); + } + format!("{} — {}", limit.label, parts.join(" · ")) } fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { @@ -319,14 +424,49 @@ fn update_tray_label(app: &tauri::AppHandle, dash: &Dashboard) { }); } -/// Rebuild the dashboard (incremental), update the tray's token count, and push -/// the fresh data to the UI so an open popover updates live. +/// Rebuild the dashboard (incremental), update the tray's token count and the +/// Provider Limits menu rows, then push the fresh data to the UI so an open +/// popover updates live. fn refresh(app: &tauri::AppHandle) { let dash = parser::build_dashboard(); update_tray_label(app, &dash); + update_provider_rows(app, &dash); let _ = app.emit("dashboard-updated", &dash); } +/// Keep the two Provider Limits menu rows in sync with the latest snapshot. +fn update_provider_rows(app: &tauri::AppHandle, dash: &Dashboard) { + let Some(state) = app.try_state::() else { + return; + }; + let now_s = now_ms() / 1000; + let (claude, codex) = provider_rows(&dash.provider_limits); + let claude_text = provider_menu_row(claude, now_s); + let codex_text = provider_menu_row(codex, now_s); + let provider_claude = state.provider_claude.clone(); + let provider_codex = state.provider_codex.clone(); + let handle = app.clone(); + let _ = handle.run_on_main_thread(move || { + let _ = provider_claude.set_text(claude_text); + let _ = provider_codex.set_text(codex_text); + }); +} + +fn provider_rows( + limits: &[model::ProviderLimit], +) -> (Option<&model::ProviderLimit>, Option<&model::ProviderLimit>) { + let mut claude = None; + let mut codex = None; + for limit in limits { + match limit.provider.as_str() { + "claude" => claude = Some(limit), + "codex" => codex = Some(limit), + _ => {} + } + } + (claude, codex) +} + /// Cooldown for manual force-refreshes (the tray "Refresh" item). Price tables /// change at most a few times a day, so back-to-back clicks inside this window /// coalesce into one fetch. @@ -412,7 +552,7 @@ fn parse_tray_preferences(text: &str) -> TrayPreferences { .and_then(|value| value.get("show_weekly_remaining")?.as_bool()) .unwrap_or(false); if legacy_weekly_on { - preferences.weekly_quota_display = WeeklyQuotaDisplay::Codex; + preferences.weekly_quota_display = MenuBarQuotaDisplay::Compact; } preferences } @@ -1014,6 +1154,9 @@ fn save_project_export(csv: String, label: String) -> Result { /// For CLI/example validation against real logs. pub fn dashboard_json() -> String { + // CLI/example dumps are explicit diagnostics, so fetch synchronously instead + // of relying on the desktop app's background quota refresh loop. + quota_api::reload(); serde_json::to_string_pretty(&parser::build_dashboard()).unwrap_or_default() } @@ -1223,6 +1366,8 @@ pub fn run() { let dash = parser::build_dashboard(); let label = tray_label(&dash, weekly_quota_display, language); let copy = tray_copy(language); + let now_s = now_ms() / 1000; + let (claude_limit, codex_limit) = provider_rows(&dash.provider_limits); let open_i = MenuItem::with_id(app, "open", copy.open, true, None::<&str>)?; let refresh_i = MenuItem::with_id(app, "refresh", copy.refresh, true, None::<&str>)?; @@ -1233,35 +1378,58 @@ pub fn run() { true, None::<&str>, )?; - let weekly_off_i = CheckMenuItem::with_id( + // Provider Limits: one read-only row per provider, refreshed with + // every dashboard build. Clicking a row opens the dashboard. + let provider_claude_i = MenuItem::with_id( + app, + "provider-claude", + provider_menu_row(claude_limit, now_s), + true, + None::<&str>, + )?; + let provider_codex_i = MenuItem::with_id( + app, + "provider-codex", + provider_menu_row(codex_limit, now_s), + true, + None::<&str>, + )?; + let provider_limits_menu = Submenu::with_items( + app, + copy.provider_limits, + true, + &[&provider_claude_i, &provider_codex_i], + )?; + // Menu Bar Display: how much quota detail the title shows. + let display_off_i = CheckMenuItem::with_id( app, - "weekly-off", + "display-off", copy.off, true, - weekly_quota_display == WeeklyQuotaDisplay::Off, + weekly_quota_display == MenuBarQuotaDisplay::Off, None::<&str>, )?; - let weekly_codex_i = CheckMenuItem::with_id( + let display_compact_i = CheckMenuItem::with_id( app, - "weekly-codex", - "Codex", + "display-compact", + copy.compact, true, - weekly_quota_display == WeeklyQuotaDisplay::Codex, + weekly_quota_display == MenuBarQuotaDisplay::Compact, None::<&str>, )?; - let weekly_codex_spark_i = CheckMenuItem::with_id( + let display_detailed_i = CheckMenuItem::with_id( app, - "weekly-codex-spark", - "Codex + Spark", + "display-detailed", + copy.detailed, true, - weekly_quota_display == WeeklyQuotaDisplay::CodexAndSpark, + weekly_quota_display == MenuBarQuotaDisplay::Detailed, None::<&str>, )?; - let weekly_menu = Submenu::with_items( + let display_menu = Submenu::with_items( app, - copy.weekly, + copy.menu_bar_display, true, - &[&weekly_off_i, &weekly_codex_i, &weekly_codex_spark_i], + &[&display_off_i, &display_compact_i, &display_detailed_i], )?; let dashboard_shortcut_i = CheckMenuItem::with_id( app, @@ -1318,7 +1486,8 @@ pub fn run() { &refresh_i, &check_updates_i, &PredefinedMenuItem::separator(app)?, - &weekly_menu, + &provider_limits_menu, + &display_menu, &dashboard_shortcut_i, &change_dashboard_shortcut_i, &autostart_i, @@ -1331,8 +1500,13 @@ pub fn run() { open: open_i.clone(), refresh: refresh_i.clone(), check_updates: check_updates_i.clone(), - weekly: weekly_menu.clone(), - weekly_off: weekly_off_i.clone(), + provider_limits: provider_limits_menu.clone(), + provider_claude: provider_claude_i.clone(), + provider_codex: provider_codex_i.clone(), + menu_bar_display: display_menu.clone(), + display_off: display_off_i.clone(), + display_compact: display_compact_i.clone(), + display_detailed: display_detailed_i.clone(), dashboard_shortcut: dashboard_shortcut_i.clone(), change_dashboard_shortcut: change_dashboard_shortcut_i.clone(), autostart: autostart_i.clone(), @@ -1412,11 +1586,15 @@ pub fn run() { show_popover(app); let _ = app.emit("check-for-updates", ()); } - "weekly-off" | "weekly-codex" | "weekly-codex-spark" => { + "provider-claude" | "provider-codex" => { + // Rows are read-only summaries; clicking opens the panel. + show_popover(app); + } + "display-off" | "display-compact" | "display-detailed" => { let display = match event.id.as_ref() { - "weekly-codex" => WeeklyQuotaDisplay::Codex, - "weekly-codex-spark" => WeeklyQuotaDisplay::CodexAndSpark, - _ => WeeklyQuotaDisplay::Off, + "display-compact" => MenuBarQuotaDisplay::Compact, + "display-detailed" => MenuBarQuotaDisplay::Detailed, + _ => MenuBarQuotaDisplay::Off, }; if let Some(state) = app.try_state::() { let Some(preferences) = state.0.lock().ok().map(|mut prefs| { @@ -1425,10 +1603,11 @@ pub fn run() { }) else { return; }; - let _ = weekly_off_i.set_checked(display == WeeklyQuotaDisplay::Off); - let _ = weekly_codex_i.set_checked(display == WeeklyQuotaDisplay::Codex); - let _ = weekly_codex_spark_i - .set_checked(display == WeeklyQuotaDisplay::CodexAndSpark); + let _ = display_off_i.set_checked(display == MenuBarQuotaDisplay::Off); + let _ = display_compact_i + .set_checked(display == MenuBarQuotaDisplay::Compact); + let _ = display_detailed_i + .set_checked(display == MenuBarQuotaDisplay::Detailed); save_tray_preferences(&preferences); refresh(app); } @@ -1511,6 +1690,17 @@ pub fn run() { std::thread::sleep(Duration::from_secs(24 * 60 * 60)); }); + // Provider quotas are network-backed and independent from session + // ingest. Refresh them off the dashboard build lock; a failed request + // keeps the last successful API cache/log snapshot fallback intact. + let handle = app.handle().clone(); + std::thread::spawn(move || loop { + if quota_api::reload() { + refresh(&handle); + } + std::thread::sleep(Duration::from_secs(5 * 60)); + }); + // Background refresh: keep the tray's token count current and push // live updates to an open popover. Cheap thanks to incremental ingest. let handle = app.handle().clone(); @@ -1574,67 +1764,72 @@ mod tests { use super::*; #[test] - fn weekly_remaining_uses_seven_day_quota() { - let codex = model::Quota { - plan: "pro".into(), - primary_pct: 20.0, - primary_minutes: 300, - primary_resets_at: 0, - secondary_pct: 19.4, - secondary_minutes: 7 * 24 * 60, - secondary_resets_at: 0, - as_of_ms: 0, - }; - let spark = model::Quota { - primary_pct: 7.0, - primary_minutes: 7 * 24 * 60, - ..codex.clone() + fn provider_summaries_use_left_percentages() { + let limit = model::ProviderLimit { + provider: "codex".to_string(), + label: "Codex".to_string(), + plan: "Pro 5x".to_string(), + windows: vec![ + model::LimitWindow { + id: "weekly".to_string(), + label: "Weekly".to_string(), + duration_minutes: 7 * 24 * 60, + used_pct: 71.0, + resets_at: 0, + as_of_ms: 0, + trend: Vec::new(), + }, + model::LimitWindow { + id: "spark".to_string(), + label: "Spark".to_string(), + duration_minutes: 7 * 24 * 60, + used_pct: 69.0, + resets_at: 0, + as_of_ms: 0, + trend: Vec::new(), + }, + ], }; - - assert_eq!(weekly_remaining_from_quota(&codex, 1), Some(81)); - assert_eq!( - weekly_label_suffix(WeeklyQuotaDisplay::Codex, Some(&codex), Some(&spark), 1), - "-C81%" - ); + assert_eq!(provider_summary(&limit, false, 1), "Cx29%"); + assert_eq!(provider_summary(&limit, true, 1), "Cx W29%/S31%"); assert_eq!( - weekly_label_suffix( - WeeklyQuotaDisplay::CodexAndSpark, - Some(&codex), - Some(&spark), - 1, - ), - "-C81%-S93%" + provider_menu_row(Some(&limit), 1), + "Codex — W 29% · S 31%" ); } #[test] - fn weekly_label_hides_expired_pool_snapshots() { - let current = model::Quota { - plan: "pro".into(), - primary_pct: 20.0, - primary_minutes: 7 * 24 * 60, - primary_resets_at: 2_000, - secondary_pct: 0.0, - secondary_minutes: 0, - secondary_resets_at: 0, - as_of_ms: 0, - }; - let expired = model::Quota { - primary_pct: 7.0, - primary_resets_at: 999, - ..current.clone() + fn provider_summary_hides_expired_windows() { + let limit = model::ProviderLimit { + provider: "claude".to_string(), + label: "Claude".to_string(), + plan: String::new(), + windows: vec![ + model::LimitWindow { + id: "weekly".to_string(), + label: "Weekly".to_string(), + duration_minutes: 7 * 24 * 60, + used_pct: 20.0, + resets_at: 999, + as_of_ms: 0, + trend: Vec::new(), + }, + model::LimitWindow { + id: "5h".to_string(), + label: "5-hour".to_string(), + duration_minutes: 300, + used_pct: 36.0, + resets_at: 2_000, + as_of_ms: 0, + trend: Vec::new(), + }, + ], }; - - assert_eq!(weekly_remaining_from_quota(&expired, 1_000), None); - assert_eq!( - weekly_label_suffix( - WeeklyQuotaDisplay::CodexAndSpark, - Some(¤t), - Some(&expired), - 1_000, - ), - "-C80%" - ); + // The expired weekly window drops out; only the 5h window remains. + assert_eq!(provider_summary(&limit, true, 1_000), "Cl 5h64%"); + assert_eq!(provider_menu_row(Some(&limit), 1_000), "Claude — 5h 64%"); + // Once every window is expired the provider disappears entirely. + assert_eq!(provider_summary(&limit, false, 2_001), ""); } #[test] @@ -1643,7 +1838,7 @@ mod tests { r#"{"show_weekly_remaining":true,"dashboard_shortcut":true}"#, ); - assert_eq!(preferences.weekly_quota_display, WeeklyQuotaDisplay::Codex); + assert_eq!(preferences.weekly_quota_display, MenuBarQuotaDisplay::Compact); assert_eq!(preferences.dashboard_shortcut_key, DASHBOARD_SHORTCUT); assert_eq!(preferences.language, AppLanguage::En); assert!(DASHBOARD_SHORTCUT.parse::().is_ok()); @@ -1657,4 +1852,17 @@ mod tests { } ); } + + #[test] + fn legacy_weekly_display_values_map_to_compact() { + let off: MenuBarQuotaDisplay = serde_json::from_str(r#""off""#).unwrap(); + let compact: MenuBarQuotaDisplay = serde_json::from_str(r#""codex""#).unwrap(); + let compact_spark: MenuBarQuotaDisplay = + serde_json::from_str(r#""codex_and_spark""#).unwrap(); + let detailed: MenuBarQuotaDisplay = serde_json::from_str(r#""detailed""#).unwrap(); + assert_eq!(off, MenuBarQuotaDisplay::Off); + assert_eq!(compact, MenuBarQuotaDisplay::Compact); + assert_eq!(compact_spark, MenuBarQuotaDisplay::Compact); + assert_eq!(detailed, MenuBarQuotaDisplay::Detailed); + } } diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index e7e3d55..82e2441 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -90,7 +90,7 @@ pub struct ContextStats { pub reasoning_pct: f64, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, serde::Deserialize)] pub struct QuotaTrendPoint { #[serde(rename = "tsMs")] pub ts_ms: i64, @@ -160,8 +160,11 @@ pub struct HeatDay { pub level: u8, // 0..4 } -/// Codex rate-limit snapshot, straight from the newest token_count event's -/// rate_limits. `as_of_ms` lets the UI flag a stale reading. +/// Codex provider rate-limit snapshot. The authenticated usage API is primary; +/// session-log rate_limits are the offline fallback. `as_of_ms` flags staleness. +pub const WEEKLY_WINDOW_MINUTES: u64 = 7 * 24 * 60; +pub const RESET_CYCLE_TOLERANCE_SECONDS: i64 = 5 * 60; + #[derive(Debug, Clone, Serialize, serde::Deserialize)] pub struct Quota { pub plan: String, @@ -181,6 +184,53 @@ pub struct Quota { pub as_of_ms: i64, } +impl Quota { + pub fn weekly_window(&self) -> Option<(f64, i64)> { + if self.primary_minutes == WEEKLY_WINDOW_MINUTES { + Some((self.primary_pct, self.primary_resets_at)) + } else if self.secondary_minutes == WEEKLY_WINDOW_MINUTES { + Some((self.secondary_pct, self.secondary_resets_at)) + } else { + None + } + } +} + +pub fn same_reset_cycle(left: i64, right: i64) -> bool { + left == right + || (left > 0 + && right > 0 + && left.abs_diff(right) <= RESET_CYCLE_TOLERANCE_SECONDS as u64) +} + +/// One window of a provider quota (Claude 5-hour / weekly, Codex weekly / Spark). +#[derive(Debug, Clone, Serialize, serde::Deserialize)] +pub struct LimitWindow { + pub id: String, // "5h" | "weekly" | "spark" + pub label: String, + pub duration_minutes: u64, + #[serde(rename = "usedPct")] + pub used_pct: f64, + #[serde(rename = "resetsAt")] + pub resets_at: i64, // unix seconds + #[serde(rename = "asOfMs")] + pub as_of_ms: i64, + pub trend: Vec, +} + +/// Provider subscription limits, independent of the token-usage source scopes. +/// Providers are the paid subscription names (Claude / Codex); sources remain +/// the CLI harnesses (Claude Code / Codex CLI / Pi) that produced the logs. +#[derive(Debug, Clone, Serialize, serde::Deserialize)] +pub struct ProviderLimit { + pub provider: String, // "claude" | "codex" + pub label: String, // "Claude" | "Codex" + pub plan: String, + pub windows: Vec, +} + +impl ProviderLimit {} + /// One selectable view of the dashboard. With a single data source there is /// exactly one scope (id "all", empty color → default theme, UI unchanged); /// with several sources the first scope is the aggregate ("all") followed by @@ -194,7 +244,7 @@ pub struct Scope { pub week: PeriodReport, pub month: PeriodReport, pub heatmap: Vec, - pub quota: Option, // Codex only + pub quota: Option, // Codex provider quota; may be owned by Pi scope #[serde(rename = "sparkQuota")] pub spark_quota: Option, // codex_bengalfox only #[serde(rename = "quotaTrend")] @@ -212,6 +262,11 @@ pub struct Dashboard { pub generated_at: String, #[serde(rename = "telemetrySinceMs")] pub telemetry_since_ms: i64, + // Provider subscription limits (Claude / Codex), global and independent of + // the selected scope. Populated by build_dashboard; empty when no provider + // has a usable snapshot. + #[serde(rename = "providerLimits")] + pub provider_limits: Vec, } /// One scope's aggregation for an inclusive, user-selected date range. diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 63f9187..31530ee 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -6,6 +6,7 @@ use crate::config::UserConfig; use crate::model::*; use crate::pricing::Pricing; +use crate::quota_api; use crate::store::{RawEvent, Store, AGENT_CLAUDE, AGENT_CODEX, AGENT_PI}; use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, Timelike}; use std::collections::{HashMap, HashSet}; @@ -128,36 +129,39 @@ fn vendor_of(model: &str) -> &'static str { } } -fn quota_trend( +fn window_trend( history: &[crate::store::QuotaHistoryPoint], limit_id: &str, - current: Option<&Quota>, + used_pct: f64, + resets_at: i64, + as_of_ms: i64, + live_points: &[QuotaTrendPoint], ) -> Vec { - let Some(current) = current else { - return Vec::new(); - }; - let (used_pct, resets_at) = if current.primary_minutes == 7 * 24 * 60 { - (current.primary_pct, current.primary_resets_at) - } else if current.secondary_minutes == 7 * 24 * 60 { - (current.secondary_pct, current.secondary_resets_at) - } else { - return Vec::new(); - }; + // API reset timestamps can drift by a few seconds between calls. Merge + // same-cycle log history with the live points instead of starting a + // trend from scratch, otherwise the dashboard lingers in "Collecting". let mut points: Vec = history .iter() - .filter(|point| point.limit_id == limit_id && point.resets_at == resets_at) + .filter(|point| point.limit_id == limit_id && same_reset_cycle(point.resets_at, resets_at)) .map(|point| QuotaTrendPoint { ts_ms: point.ts_ms, used_pct: point.used_pct, }) + .chain(live_points.iter().cloned()) .collect(); + points.push(QuotaTrendPoint { + ts_ms: as_of_ms, + used_pct, + }); points.sort_by_key(|point| point.ts_ms); - if points.last().map(|point| point.ts_ms) != Some(current.as_of_ms) { - points.push(QuotaTrendPoint { - ts_ms: current.as_of_ms, - used_pct, - }); - } + points.dedup_by(|right, left| { + if right.ts_ms == left.ts_ms { + left.used_pct = right.used_pct; + true + } else { + false + } + }); const MAX_POINTS: usize = 48; if points.len() <= MAX_POINTS { return points; @@ -170,6 +174,28 @@ fn quota_trend( .collect() } +/// Attach session-log Codex quota history to the matching provider windows so +/// the trend has depth even before the next `hu` refresh. +fn merge_log_trends(limits: &mut [ProviderLimit], store: &crate::store::Store) { + for limit in limits.iter_mut().filter(|limit| limit.provider == "codex") { + for window in &mut limit.windows { + let limit_id = if window.id == "spark" { + "codex_bengalfox" + } else { + "codex" + }; + window.trend = window_trend( + &store.quota_history, + limit_id, + window.used_pct, + window.resets_at, + window.as_of_ms, + &window.trend, + ); + } + } +} + pub fn build_dashboard() -> Dashboard { let _guard = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); @@ -180,14 +206,11 @@ pub fn build_dashboard() -> Dashboard { if store.ingest() { store.save(); } - let codex_quota = store.codex_quota.clone(); - let codex_spark_quota = store.codex_spark_quota.clone(); - let codex_quota_trend = quota_trend(&store.quota_history, "codex", codex_quota.as_ref()); - let codex_spark_quota_trend = quota_trend( - &store.quota_history, - "codex_bengalfox", - codex_spark_quota.as_ref(), - ); + // Provider subscription limits (Claude / Codex) come from the `hu` CLI via + // quota_api; session-log Codex rate limits add trend depth. Provider limits + // are global — they never attach to a token-usage scope. + let mut provider_limits = quota_api::shared(); + merge_log_trends(&mut provider_limits, &store); let telemetry_since_ms = store.telemetry_since_ms; // 2. Aggregate: apply current config + prices, slice by current time. @@ -224,16 +247,9 @@ pub fn build_dashboard() -> Dashboard { let scopes = if present.len() <= 1 { // Single (or no) source → one scope, classic green UI, no chips. let agent = present.first().map(|a| a.id).unwrap_or(AGENT_CLAUDE); - let mut scope = build_scope( + vec![build_scope( "all", "All", "", &events, &turns, PALETTE, agent, &cfg, now, - ); - if agent == AGENT_CODEX { - scope.quota = codex_quota; - scope.spark_quota = codex_spark_quota; - scope.quota_trend = codex_quota_trend; - scope.spark_quota_trend = codex_spark_quota_trend; - } - vec![scope] + )] } else { // Aggregate scope first (default palette; models/slices merged below), // then one accent-colored scope per agent. @@ -245,7 +261,7 @@ pub fn build_dashboard() -> Dashboard { .filter(|turn| turn.agent == a.id) .cloned() .collect(); - let mut s = build_scope( + let s = build_scope( a.id, a.label, a.color, @@ -256,12 +272,6 @@ pub fn build_dashboard() -> Dashboard { &cfg, now, ); - if a.id == AGENT_CODEX { - s.quota = codex_quota.clone(); - s.spark_quota = codex_spark_quota.clone(); - s.quota_trend = codex_quota_trend.clone(); - s.spark_quota_trend = codex_spark_quota_trend.clone(); - } per_agent.push(s); } let mut all = build_scope("all", "All", "", &events, &turns, PALETTE, "", &cfg, now); @@ -312,6 +322,7 @@ pub fn build_dashboard() -> Dashboard { today_tokens, generated_at: now.to_rfc3339(), telemetry_since_ms, + provider_limits, } } @@ -882,10 +893,12 @@ impl Agg { } /// Percentage change of `cur` vs `prev`, e.g. +20.0 for a 20% increase, -/// rounded to 2 decimals. Returns 0 when there's no baseline to compare. +/// rounded to 2 decimals. When the baseline is zero the change is undefined +/// mathematically; a move from nothing to something is defined as +100%, +/// while staying at zero stays 0%. fn pct_delta(cur: f64, prev: f64) -> f64 { if prev <= 0.0 { - return 0.0; + return if cur > 0.0 { 100.0 } else { 0.0 }; } ((cur - prev) / prev * 10000.0).round() / 100.0 } @@ -1323,6 +1336,52 @@ mod tests { } } + #[test] + fn zero_baseline_delta_is_defined_as_100_percent() { + // Nothing → something: defined as +100%. + assert_eq!(pct_delta(5.0, 0.0), 100.0); + assert_eq!(pct_delta(1.0, -2.0), 100.0); + // Nothing → nothing: stays 0%. + assert_eq!(pct_delta(0.0, 0.0), 0.0); + // Normal relative change is unchanged. + assert_eq!(pct_delta(120.0, 100.0), 20.0); + assert_eq!(pct_delta(0.0, 100.0), -100.0); + } + + #[test] + fn quota_trend_merges_api_points_with_same_cycle_log_history() { + let history = vec![ + crate::store::QuotaHistoryPoint { + ts_ms: 1_000, + limit_id: "codex".to_string(), + used_pct: 10.0, + resets_at: 20_000, + }, + crate::store::QuotaHistoryPoint { + ts_ms: 2_000, + limit_id: "codex".to_string(), + used_pct: 20.0, + resets_at: 20_010, + }, + crate::store::QuotaHistoryPoint { + ts_ms: 3_000, + limit_id: "codex".to_string(), + used_pct: 99.0, + resets_at: 30_000, + }, + ]; + let live = vec![QuotaTrendPoint { + ts_ms: 3_500, + used_pct: 25.0, + }]; + + let trend = window_trend(&history, "codex", 30.0, 20_020, 4_000, &live); + assert_eq!(trend.len(), 4); + assert_eq!(trend[0].used_pct, 10.0); + assert_eq!(trend[2].used_pct, 25.0); + assert_eq!(trend[3].used_pct, 30.0); + } + #[test] fn custom_range_is_inclusive_and_compares_equal_previous_range() { let events = vec![ diff --git a/src-tauri/src/quota_api.rs b/src-tauri/src/quota_api.rs new file mode 100644 index 0000000..658cf8f --- /dev/null +++ b/src-tauri/src/quota_api.rs @@ -0,0 +1,519 @@ +//! Provider subscription quota, sourced from the HappyUsage `hu` CLI. +//! +//! `hu` already owns credential discovery, OAuth refresh, and provider API +//! calls (Claude via the Anthropic OAuth usage API, Codex via the ChatGPT +//! usage API). Tokenscope only invokes `hu usage --json`, parses +//! the envelope, and caches the result so a later offline refresh still has +//! the last successful reading. Codex session-log rate limits remain a +//! fallback in the parser. + +use crate::model::{same_reset_cycle, LimitWindow, ProviderLimit, QuotaTrendPoint, WEEKLY_WINDOW_MINUTES}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::sync::{OnceLock, RwLock}; + +const CACHE_FILE: &str = "provider_limits.json"; +const CACHE_VERSION: u32 = 2; + +#[derive(Clone, Default, Serialize, Deserialize)] +#[serde(default)] +struct ProviderCache { + claude: Option, + codex: Option, +} + +static CACHE: OnceLock> = OnceLock::new(); + +fn cache_dir() -> Option { + let dir = dirs::cache_dir()?.join("tokenscope"); + let _ = fs::create_dir_all(&dir); + Some(dir) +} + +fn cache_path() -> Option { + Some(cache_dir()?.join(CACHE_FILE)) +} + +fn load_cache() -> ProviderCache { + let Some(path) = cache_path() else { + return ProviderCache::default(); + }; + fs::read_to_string(path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .filter(|cache| cache.version == CACHE_VERSION) + .map(|cache| cache.cache) + .unwrap_or_default() +} + +#[derive(Serialize, Deserialize)] +struct CacheEnvelope { + version: u32, + cache: ProviderCache, +} + +fn cache_lock() -> &'static RwLock { + CACHE.get_or_init(|| RwLock::new(load_cache())) +} + +/// Last successfully fetched provider limits (disk + memory). Empty when no +/// provider has ever produced a usable snapshot. +pub fn shared() -> Vec { + let cache = cache_lock() + .read() + .map(|cache| cache.clone()) + .unwrap_or_default(); + let mut limits = Vec::new(); + if let Some(claude) = cache.claude { + limits.push(claude); + } + if let Some(codex) = cache.codex { + limits.push(codex); + } + limits +} + +fn save_cache(cache: &ProviderCache) { + let Some(path) = cache_path() else { return }; + let envelope = CacheEnvelope { + version: CACHE_VERSION, + cache: cache.clone(), + }; + let Ok(text) = serde_json::to_vec(&envelope) else { + return; + }; + let tmp = path.with_extension(format!("tmp-{}", std::process::id())); + if fs::write(&tmp, text).is_ok() && fs::rename(&tmp, path).is_err() { + let _ = fs::remove_file(tmp); + } +} + +fn hu_executables() -> Vec { + let mut candidates = Vec::new(); + if let Some(path) = std::env::var_os("PATH").and_then(|paths| { + std::env::split_paths(&paths) + .map(|dir| dir.join(if cfg!(windows) { "hu.exe" } else { "hu" })) + .find(|path| path.is_file()) + }) { + candidates.push(path); + } + for path in ["/opt/homebrew/bin/hu", "/usr/local/bin/hu"] { + let path = PathBuf::from(path); + if path.is_file() && !candidates.contains(&path) { + candidates.push(path); + } + } + candidates +} + +/// Run `hu usage --json` and return the provider envelope +/// (with `ok: true`). The CLI owns credential handling and HTTP timeouts; +/// Tokenscope's refresh runs on a background thread, so a slow run only +/// delays that provider's next refresh. +fn run_hu(provider: &str) -> Option { + for executable in hu_executables() { + let output = Command::new(executable) + .args(["usage", provider, "--json"]) + .output() + .ok()?; + if !output.status.success() { + continue; + } + let json: Value = serde_json::from_slice(&output.stdout).ok()?; + let envelope = json.get("provider")?; + if envelope.get("ok").and_then(Value::as_bool) != Some(true) { + continue; + } + return Some(envelope.clone()); + } + None +} + +fn rfc3339_ms(value: Option<&Value>) -> i64 { + value + .and_then(Value::as_str) + .and_then(|text| chrono::DateTime::parse_from_rfc3339(text).ok()) + .map(|date| date.timestamp_millis()) + .unwrap_or(0) +} + +/// Claude windows from the `hu` envelope. `session` → 5-hour, `weekly` → 7d. +/// Matching is case-insensitive and ignores model-scoped sub-pools +/// (sonnet_weekly / opus_weekly / …) so a renamed quota never breaks the card. +fn claude_from_hu(envelope: &Value) -> Option { + let checked_at = rfc3339_ms(envelope.get("checked_at")); + let plan = envelope + .get("plan") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let mut windows = Vec::new(); + for quota in envelope.get("quotas")?.as_array()? { + let name = quota.get("name").and_then(Value::as_str)?; + let lower = name.to_ascii_lowercase(); + let (id, label, duration) = if lower.contains("session") { + ("5h", "5-hour", 300) + } else if lower == "weekly" { + ("weekly", "Weekly", WEEKLY_WINDOW_MINUTES) + } else { + // Model-scoped weekly sub-pools (sonnet/opus/…) are not shown. + continue; + }; + let used_pct = quota.get("used_pct").and_then(Value::as_f64)?; + let resets_at = rfc3339_ms(quota.get("resets_at")) / 1000; + windows.push(LimitWindow { + id: id.to_string(), + label: label.to_string(), + duration_minutes: duration, + used_pct, + resets_at, + as_of_ms: checked_at, + trend: Vec::new(), + }); + } + if windows.is_empty() { + return None; + } + Some(ProviderLimit { + provider: "claude".to_string(), + label: "Claude".to_string(), + plan, + windows, + }) +} + +/// Claude windows from Claude Code's local `~/.claude.json` usage cache. +/// Used only when `hu` cannot reach the API (e.g. expired OAuth token). +fn claude_from_cache_file() -> Option { + let home = dirs::home_dir()?; + let json: Value = serde_json::from_str( + &fs::read_to_string(home.join(".claude.json")).ok()?, + ) + .ok()?; + let cache = json.get("cachedUsageUtilization")?; + let fetched_at = cache.get("fetchedAtMs").and_then(Value::as_i64)?; + let utilization = cache.get("utilization")?; + let mut windows = Vec::new(); + for (key, id, label, duration) in [ + ("five_hour", "5h", "5-hour", 300), + ( + "seven_day", + "weekly", + "Weekly", + WEEKLY_WINDOW_MINUTES as i64, + ), + ] { + let Some(window) = utilization.get(key) else { continue }; + let Some(used_pct) = window.get("utilization").and_then(Value::as_f64) else { + continue; + }; + let resets_at = rfc3339_ms(window.get("resets_at")) / 1000; + if resets_at <= 0 { + continue; + } + windows.push(LimitWindow { + id: id.to_string(), + label: label.to_string(), + duration_minutes: duration as u64, + used_pct, + resets_at, + as_of_ms: fetched_at, + trend: Vec::new(), + }); + } + if windows.is_empty() { + return None; + } + Some(ProviderLimit { + provider: "claude".to_string(), + label: "Claude".to_string(), + plan: String::new(), + windows, + }) +} + +/// Codex windows from the `hu` envelope. The primary pool and Spark are both +/// rolling weekly windows today (the 5h window is retired); HappyUsage's quota +/// names have changed across versions (`session`/`weekly`, `Spark`/`Spark_weekly`), +/// so matching is case-insensitive and name-agnostic. +fn codex_from_hu(envelope: &Value) -> Option { + let checked_at = rfc3339_ms(envelope.get("checked_at")); + let plan = envelope + .get("plan") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let mut windows = Vec::new(); + for quota in envelope.get("quotas")?.as_array()? { + let name = quota.get("name").and_then(Value::as_str)?; + let lower = name.to_ascii_lowercase(); + let (id, label) = if lower.contains("spark") { + ("spark", "Spark") + } else if lower == "session" || lower == "weekly" { + ("weekly", "Weekly") + } else { + continue; + }; + let used_pct = quota.get("used_pct").and_then(Value::as_f64)?; + let resets_at = rfc3339_ms(quota.get("resets_at")) / 1000; + windows.push(LimitWindow { + id: id.to_string(), + label: label.to_string(), + duration_minutes: WEEKLY_WINDOW_MINUTES, + used_pct, + resets_at, + as_of_ms: checked_at, + trend: Vec::new(), + }); + } + if windows.is_empty() { + return None; + } + Some(ProviderLimit { + provider: "codex".to_string(), + label: "Codex".to_string(), + plan, + windows, + }) +} + +/// Merge the Codex weekly trend points from the process cache into the window +/// trend. Claude has no history source, so its windows stay trend-free. +fn merge_trend( + previous: &ProviderCache, + next: &mut ProviderCache, +) { + let Some(codex) = next.codex.as_mut() else { return }; + let previous_resets: Vec<(String, i64)> = previous + .codex + .as_ref() + .map(|limit| { + limit + .windows + .iter() + .map(|window| (window.id.clone(), window.resets_at)) + .collect() + }) + .unwrap_or_default(); + for window in &mut codex.windows { + if window.resets_at <= 0 { + continue; + } + let same_cycle = previous_resets + .iter() + .find(|(id, _)| id == &window.id) + .map(|(_, reset)| { + same_reset_cycle(*reset, window.resets_at) + }) + .unwrap_or(false); + if !same_cycle { + window.trend.clear(); + } + window.trend.push(QuotaTrendPoint { + ts_ms: window.as_of_ms, + used_pct: window.used_pct, + }); + window.trend.sort_by_key(|point| point.ts_ms); + window.trend.dedup_by(|right, left| { + if right.ts_ms == left.ts_ms { + left.used_pct = right.used_pct; + true + } else { + false + } + }); + const MAX_POINTS: usize = 48; + if window.trend.len() > MAX_POINTS { + let source = window.trend.len() - MAX_POINTS; + window.trend = window.trend[source..].to_vec(); + } + } +} + +/// Refresh provider limits through `hu`. Each provider is independent: a +/// failure keeps the previous successful snapshot for that provider. +pub fn reload() -> bool { + let previous = cache_lock() + .read() + .map(|cache| cache.clone()) + .unwrap_or_default(); + let mut next = previous.clone(); + + if let Some(claude) = run_hu("claude").and_then(|envelope| claude_from_hu(&envelope)) { + next.claude = Some(claude); + } else if next.claude.is_none() { + // `hu` cannot refresh Claude (expired OAuth) — fall back to the local + // Claude Code usage cache so the card still shows the last reading. + next.claude = claude_from_cache_file(); + } + if let Some(codex) = run_hu("codex").and_then(|envelope| codex_from_hu(&envelope)) { + next.codex = Some(codex); + } + + let changed = next.claude.is_some() || next.codex.is_some(); + if !changed { + return false; + } + merge_trend(&previous, &mut next); + save_cache(&next); + if let Ok(mut cache) = cache_lock().write() { + *cache = next; + true + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_claude_envelope_windows() { + let envelope = serde_json::json!({ + "provider": "claude", + "ok": true, + "checked_at": "2026-08-06T09:16:00Z", + "plan": "Pro", + "quotas": [ + {"name": "session", "period": "5h", "used_pct": 12.5, "left_pct": 87.5, "resets_at": "2026-08-06T14:16:00Z"}, + {"name": "weekly", "period": "7d", "used_pct": 43, "left_pct": 57, "resets_at": "2026-08-09T21:00:00Z"} + ] + }); + let limit = claude_from_hu(&envelope).unwrap(); + assert_eq!(limit.provider, "claude"); + assert_eq!(limit.windows.len(), 2); + assert_eq!(limit.windows[0].id, "5h"); + assert_eq!(limit.windows[0].duration_minutes, 300); + assert_eq!(limit.windows[0].used_pct, 12.5); + assert_eq!(limit.windows[1].id, "weekly"); + assert_eq!(limit.windows[1].duration_minutes, WEEKLY_WINDOW_MINUTES); + assert_eq!(limit.windows[1].resets_at, 1_786_309_200); // 2026-08-09T21:00:00Z + } + + #[test] + fn parses_codex_envelope_as_weekly_windows() { + let envelope = serde_json::json!({ + "provider": "codex", + "ok": true, + "checked_at": "2026-08-06T09:16:00Z", + "plan": "Pro 5x", + "quotas": [ + {"name": "session", "period": "5h", "used_pct": 71, "left_pct": 29, "resets_at": "2026-08-09T12:01:05Z"}, + {"name": "Spark", "period": "5h", "used_pct": 69, "left_pct": 31, "resets_at": "2026-08-08T08:01:04Z"} + ] + }); + let limit = codex_from_hu(&envelope).unwrap(); + assert_eq!(limit.provider, "codex"); + assert_eq!(limit.windows.len(), 2); + assert_eq!(limit.windows[0].id, "weekly"); + assert_eq!(limit.windows[0].duration_minutes, WEEKLY_WINDOW_MINUTES); + assert_eq!(limit.windows[0].used_pct, 71.0); + assert_eq!(limit.windows[1].id, "spark"); + assert_eq!(limit.windows[1].used_pct, 69.0); + } + + #[test] + fn parses_claude_local_cache_shape() { + let json = serde_json::json!({ + "cachedUsageUtilization": { + "fetchedAtMs": 1_785_485_070_985i64, + "utilization": { + "five_hour": {"utilization": 0, "resets_at": "2026-07-31T12:50:00Z"}, + "seven_day": {"utilization": 21, "resets_at": "2026-07-31T21:00:00Z"} + } + } + }); + let cache = json.get("cachedUsageUtilization").unwrap(); + let fetched_at = cache.get("fetchedAtMs").and_then(Value::as_i64).unwrap(); + let utilization = cache.get("utilization").unwrap(); + let mut windows = Vec::new(); + for (key, id, label, duration) in [ + ("five_hour", "5h", "5-hour", 300), + ("seven_day", "weekly", "Weekly", WEEKLY_WINDOW_MINUTES as i64), + ] { + let Some(window) = utilization.get(key) else { continue }; + let Some(used_pct) = window.get("utilization").and_then(Value::as_f64) else { + continue; + }; + let resets_at = rfc3339_ms(window.get("resets_at")) / 1000; + windows.push(LimitWindow { + id: id.to_string(), + label: label.to_string(), + duration_minutes: duration as u64, + used_pct, + resets_at, + as_of_ms: fetched_at, + trend: Vec::new(), + }); + } + assert_eq!(windows.len(), 2); + assert_eq!(windows[0].id, "5h"); + assert_eq!(windows[0].used_pct, 0.0); + assert_eq!(windows[1].id, "weekly"); + assert_eq!(windows[1].used_pct, 21.0); + assert_eq!(windows[1].duration_minutes, WEEKLY_WINDOW_MINUTES); + } + + #[test] + fn parses_codex_0211_names_and_ignores_missing_provider() { + // happyusage 0.2.11 renames primary to `weekly` and Spark to + // `Spark_weekly`; both must still map to the same windows. + let envelope = serde_json::json!({ + "provider": "codex", + "ok": true, + "checked_at": "2026-08-06T09:42:16Z", + "plan": "Pro 5x", + "quotas": [ + {"name": "weekly", "period": "7d", "used_pct": 71, "left_pct": 29, "resets_at": "2026-08-09T12:01:06Z"}, + {"name": "Spark_weekly", "period": "7d", "used_pct": 69, "left_pct": 31, "resets_at": "2026-08-08T08:01:04Z"} + ] + }); + let limit = codex_from_hu(&envelope).unwrap(); + assert_eq!(limit.windows.len(), 2); + assert_eq!(limit.windows[0].id, "weekly"); + assert_eq!(limit.windows[1].id, "spark"); + assert_eq!(limit.windows[1].used_pct, 69.0); + + // A provider that only has unknown quota names yields no windows. + let empty = serde_json::json!({"provider": "codex", "ok": true, "quotas": []}); + assert!(codex_from_hu(&empty).is_none()); + let unknown = serde_json::json!({ + "provider": "codex", + "ok": true, + "quotas": [{"name": "gpt_6_daily", "used_pct": 5}] + }); + assert!(codex_from_hu(&unknown).is_none()); + } + + #[test] + fn claude_ignores_model_scoped_sub_pools() { + let envelope = serde_json::json!({ + "provider": "claude", + "ok": true, + "checked_at": "2026-08-06T09:16:00Z", + "quotas": [ + {"name": "session", "period": "5h", "used_pct": 12.5, "resets_at": "2026-08-06T14:16:00Z"}, + {"name": "weekly", "period": "7d", "used_pct": 43, "resets_at": "2026-08-09T21:00:00Z"}, + {"name": "sonnet_weekly", "period": "7d", "used_pct": 99, "resets_at": "2026-08-09T21:00:00Z"} + ] + }); + let limit = claude_from_hu(&envelope).unwrap(); + assert_eq!(limit.windows.len(), 2); + assert!(limit.windows.iter().all(|w| w.id != "sonnet_weekly")); + } + + #[test] + fn ignores_unknown_quota_names() { + let envelope = serde_json::json!({ + "provider": "claude", + "ok": true, + "checked_at": "2026-08-06T09:16:00Z", + "quotas": [{"name": "some_new_pool", "used_pct": 5}] + }); + assert!(claude_from_hu(&envelope).is_none()); + } +} diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 6595beb..9edb502 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -1603,14 +1603,7 @@ impl Store { } fn record_quota_history(&mut self, limit_id: &str, quota: &Quota) { - let weekly = if quota.primary_minutes == 7 * 24 * 60 { - Some((quota.primary_pct, quota.primary_resets_at)) - } else if quota.secondary_minutes == 7 * 24 * 60 { - Some((quota.secondary_pct, quota.secondary_resets_at)) - } else { - None - }; - let Some((used_pct, resets_at)) = weekly else { + let Some((used_pct, resets_at)) = quota.weekly_window() else { return; }; let last = self diff --git a/src/App.tsx b/src/App.tsx index 5aff43d..aa24fac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, + ProviderLimit, LimitWindow, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; import { @@ -531,78 +532,94 @@ function ContextSection({ stats, theme }: { stats: ContextStats; theme: Theme }) ); } -// Codex rate-limit card straight from the session logs. Keep collecting every -// window for compatibility, but do not render the retired 5h limit or empty -// windows omitted by newer Codex clients. Bars turn amber near the cap. -function QuotaCard({ q, sparkQ, trend, theme }: { q?: Quota | null; sparkQ?: Quota | null; trend: QuotaTrendPoint[]; theme: Theme }) { +// Provider subscription limits (Claude / Codex), independent of the usage +// source scopes. Two-column layout: one column per provider, one bar per +// window with remaining capacity and the reset date. No windows → no card. +function ProviderMark({ provider, color }: { provider: string; color: string }) { + if (provider === "claude") { + // Anthropic official glyph (Simple Icons path). + return ( + + ); + } + // ChatGPT official knot glyph (Simple Icons path). + return ( + + ); +} + +function ProviderLimitsCard({ limits, theme }: { limits: ProviderLimit[]; theme: Theme }) { const t = theme; const { locale, text } = useI18n(); const now = Date.now(); - const stale = q ? now - q.asOfMs > 60 * 60 * 1000 : false; - const winLabel = (min: number) => - min === 10080 ? text.weekly : min % 60 === 0 && min > 0 ? `${min / 60}h ${text.window}` : `${min}m ${text.window}`; - const resetsIn = (unixS: number) => { - if (unixS <= 0) return ""; - const ms = unixS * 1000 - now; - if (ms <= 0) return text.resetting; - const h = Math.floor(ms / 3.6e6), m = Math.round((ms % 3.6e6) / 6e4); - if (h >= 48) return `${text.resetsIn} ${Math.round(h / 24)}d`; - return h > 0 ? `${text.resetsIn} ${h}h ${m}m` : `${text.resetsIn} ${m}m`; + if (limits.length === 0) return null; + + const status = (w: LimitWindow) => { + const left = Math.round(Math.max(0, Math.min(100, 100 - w.usedPct))); + if (w.resetsAt <= 0) return `${left}% ${text.left}`; + const reset = new Date(w.resetsAt * 1000); + if (reset.getTime() <= now) return `${left}% ${text.left} (${text.resetting})`; + const time = reset.toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit", hourCycle: "h23" }); + const date = reset.toLocaleDateString(locale === "zh" ? "zh-CN" : "en-GB", { day: "numeric", month: "short" }); + return locale === "zh" + ? `${left}% ${text.left}(${date} ${time} ${text.resets})` + : `${left}% ${text.left} (${text.resets} ${time} ${text.on} ${date})`; }; - const asOf = q ? new Date(q.asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }) : ""; - const quotaValues = trend.map((point) => point.usedPct); - const first = trend[0], last = trend[trend.length - 1]; - const elapsedHours = first && last ? (last.tsMs - first.tsMs) / 3.6e6 : 0; - const burnPerDay = elapsedHours > 0 ? ((last.usedPct - first.usedPct) / elapsedHours) * 24 : 0; - const hoursToCap = burnPerDay > 0 && last ? ((100 - last.usedPct) / burnPerDay) * 24 : 0; - const horizon = hoursToCap > 0 - ? hoursToCap < 24 ? `${Math.round(hoursToCap)}h ${text.toCap}` : `${(hoursToCap / 24).toFixed(1)}d ${text.toCap}` - : text.stable; - const quotaWindows = (quota?: Quota | null) => quota ? [ - { id: "primary", minutes: quota.primaryMinutes, pct: quota.primaryPct, resetsAt: quota.primaryResetsAt }, - { id: "secondary", minutes: quota.secondaryMinutes, pct: quota.secondaryPct, resetsAt: quota.secondaryResetsAt }, - ] : []; - const windows = quotaWindows(q).filter((window) => window.minutes > 0 && window.minutes !== 5 * 60); - const sparkWeekly = quotaWindows(sparkQ).find((window) => window.minutes === 7 * 24 * 60); - const Bar = ({ label, pctUsed, sub }: { label: string; pctUsed: number; sub: string }) => { - const p = Math.max(0, Math.min(100, pctUsed)); - const col = p >= 80 ? "#e0795f" : t.accent; - return ( -
- {label} -
-
-
- = 80 ? col : t.text, flex: "0 0 34px", textAlign: "right" }}>{Math.round(p)}% - {sub} -
- ); + const trendNote = (w: LimitWindow) => { + if (w.trend.length < 2) return null; + const first = w.trend[0], last = w.trend[w.trend.length - 1]; + const elapsedHours = (last.tsMs - first.tsMs) / 3.6e6; + if (elapsedHours <= 0) return null; + const burnPerDay = ((last.usedPct - first.usedPct) / elapsedHours) * 24; + const hoursToCap = burnPerDay > 0 ? ((100 - last.usedPct) / burnPerDay) * 24 : 0; + if (hoursToCap <= 0) return null; + const horizon = hoursToCap < 24 ? `${Math.round(hoursToCap)}h` : `${(hoursToCap / 24).toFixed(1)}d`; + return `${burnPerDay >= 0 ? "+" : ""}${burnPerDay.toFixed(1)}%${text.perDay} · ${horizon} ${text.toCap}`; }; + return ( -
-
- {windows.map((window) => ( - - ))} -
- {sparkWeekly && ( -
60 * 60 * 1000 ? 0.65 : 1 }}> - -
- )} - {q && ( -
-
- - - {trend.length >= 2 && elapsedHours > 0 ? <>{burnPerDay >= 0 ? "+" : ""}{burnPerDay.toFixed(1)}%{text.perDay} · {horizon} : text.collectingWeeklyTrend} - -
-
- {q.plan && <>{text.plan}: {q.plan}}{stale && · {text.asOf} {asOf}} +
= 2 ? "1fr 1fr" : "1fr", gap: 16 }}> + {limits.map((limit) => { + const stale = now - limit.windows[0].asOfMs > 60 * 60 * 1000; + const asOf = new Date(limit.windows[0].asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }); + return ( +
+
+ + {limit.label} + {limit.plan && {limit.plan}} +
+ {limit.windows.map((w) => { + const left = Math.max(0, Math.min(100, 100 - w.usedPct)); + const col = left <= 20 ? "#e0795f" : t.accent; + const note = trendNote(w); + return ( +
+
+ {w.label} +
+
+
+
+
+ {status(w)} + {note && {note}} +
+
+ ); + })} + {stale && ( +
+ {text.asOf} {asOf} +
+ )}
-
- )} + ); + })}
); } @@ -1216,12 +1233,12 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen {unpricedModels.map((m) => m.name).join(", ")}
)} - {/* Codex quota — rate-limit windows read straight from the session logs */} - {(scope.quota || scope.sparkQuota) && ( + {/* Provider subscription limits (Claude / Codex) — global, not scope-owned */} + {dash.providerLimits.length > 0 && ( <> -
- +
+ )} {projects.length > 0 && ( diff --git a/src/data.ts b/src/data.ts index 98a1e7c..5ad512b 100644 --- a/src/data.ts +++ b/src/data.ts @@ -36,13 +36,23 @@ export interface PeriodReport { agents: AgentSlice[]; } export interface HeatDay { date: string; tokens: number; level: number } -// Codex rate-limit snapshot (from the newest token_count event). +// Codex provider quota (authenticated usage API; token_count log fallback). export interface Quota { plan: string; primaryPct: number; primaryMinutes: number; primaryResetsAt: number; secondaryPct: number; secondaryMinutes: number; secondaryResetsAt: number; asOfMs: number; } +// Provider subscription limits (Claude / Codex), independent of usage scopes. +export interface LimitWindow { + id: string; label: string; durationMinutes: number; + usedPct: number; resetsAt: number; asOfMs: number; + trend: QuotaTrendPoint[]; +} +export interface ProviderLimit { + provider: string; label: string; plan: string; + windows: LimitWindow[]; +} // One selectable dashboard view (Claude, Codex, Pi, …). A single data source yields exactly one scope // (id "all", empty color → classic UI, no chips); several sources yield // [All, agent, agent, …] and the header shows filter chips. @@ -54,6 +64,7 @@ export interface Scope { } export interface Dashboard { scopes: Scope[]; todayTokens: number; generatedAt: string; telemetrySinceMs?: number; + providerLimits: ProviderLimit[]; } export interface DateRange { startDate: string; endDate: string } export interface RangeScope { id: string; report: PeriodReport } diff --git a/src/i18n.tsx b/src/i18n.tsx index 734bfb0..f94fef1 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -60,11 +60,14 @@ const en = { reasoning: "Reasoning", weekly: "Weekly", window: "window", + providerLimits: "Provider Limits", + fiveHour: "5-hour", resetting: "resetting…", - resetsIn: "resets in", + left: "left", + resets: "resets", + on: "on", stable: "stable", toCap: "to cap", - collectingWeeklyTrend: "Collecting weekly trend", perDay: "/day", plan: "Plan", asOf: "as of", @@ -107,7 +110,6 @@ const en = { withoutPricing: "without pricing data (cost not counted)", requestStats: "Requests", costTrend: "Cost trend", - codexQuota: "Codex quota", mcpCalls: "MCP calls", servers: "servers", noMcpCalls: "No MCP calls in this period", @@ -189,11 +191,14 @@ const zh: Copy = { reasoning: "推理", weekly: "每周", window: "窗口", + providerLimits: "额度详情", + fiveHour: "5 小时", resetting: "重置中…", - resetsIn: "距重置", + left: "剩余", + resets: "重置", + on: "于", stable: "稳定", toCap: "后触顶", - collectingWeeklyTrend: "正在收集周趋势", perDay: "/天", plan: "计划", asOf: "数据截至", @@ -236,7 +241,6 @@ const zh: Copy = { withoutPricing: "缺少价格数据(未计入费用)", requestStats: "请求数", costTrend: "费用趋势", - codexQuota: "Codex 额度", mcpCalls: "MCP 调用", servers: "个服务", noMcpCalls: "此周期暂无 MCP 调用", From 98180077d20bde4bc8ea09906a15ab9b28292a81 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 02:45:48 -0700 Subject: [PATCH 51/67] chore: bump version to 1.5.1 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a24bd3d..5f2a226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.4.8", + "version": "1.5.1", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 70e3d88..5a5971a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.4.8" +version = "1.5.1" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f52e5e2..c27a6ee 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.4.8" +version = "1.5.1" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c1a1767..47d24bc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.4.8", + "version": "1.5.1", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 2ca2234d7f61eeef6def34148d7fbba7edcb7137 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 03:08:06 -0700 Subject: [PATCH 52/67] feat: bundle happyusage CLI with the app Ship `hu` inside the app bundle (Resources/bin/hu) so provider quota works with no system install. build-hu.sh downloads the matching release asset before `tauri build`; lib.rs injects resource_dir at startup and quota_api prefers the bundled binary, then system installs. When `hu` is missing entirely (development builds), ensure_hu auto-installs it once per 24h (Homebrew tap -> install script -> go install), and the native wham/usage call with Pi / Codex CLI OAuth remains the Codex fallback, with Claude falling back to Claude Code's local usage cache. --- .gitignore | 2 + README-zh.md | 6 +- README.md | 6 +- docs/DESIGN-usage-api.md | 26 +- src-tauri/bin/build-hu.sh | 53 ++++ src-tauri/src/lib.rs | 4 + src-tauri/src/quota_api.rs | 560 ++++++++++++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 3 +- 8 files changed, 636 insertions(+), 24 deletions(-) create mode 100755 src-tauri/bin/build-hu.sh diff --git a/.gitignore b/.gitignore index 158acf8..36b40d2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ public/dev-dashboard.json .history/ *.log claude-design-files/ + +src-tauri/bin/hu diff --git a/README-zh.md b/README-zh.md index 9c2193e..8a8e19d 100644 --- a/README-zh.md +++ b/README-zh.md @@ -37,19 +37,19 @@ brew install --cask sunchj/tokenscope/tokenscope |------|------| | Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | | Codex 会话日志(Token + 离线配额回退) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | -| Provider 额度(Claude + Codex) | HappyUsage `hu usage --json`(凭据/OAuth/API 均由 `hu` 负责;Claude 回退:`~/.claude.json` 的 `cachedUsageUtilization`) | +| Provider 额度(Claude + Codex) | HappyUsage `hu` CLI,随应用 bundle 分发(`Resources/bin/hu`);Claude 回退:`~/.claude.json` 的 `cachedUsageUtilization`,Codex 回退:基于 Pi / Codex CLI OAuth 的原生 usage API | | Pi 会话日志(Token / 模型 / 工具 / 遥测) | `~/.pi/agent/sessions/**/*.jsonl`(支持 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR` 与绝对路径 `settings.sessionDir`) | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | | 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/`;Pi:全局、共享、项目级 Pi Skill 目录及 settings 显式路径 | | 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | -配额请求统一走 HappyUsage `hu` CLI,由它负责凭据发现、OAuth 刷新与 provider 调用;Tokenscope 只缓存额度元数据,从不缓存凭据。 +配额请求统一走 HappyUsage `hu` CLI,由它负责凭据发现、OAuth 刷新与 provider 调用。二进制随应用包内置,无需系统安装;缺失时(如开发构建)回退到原生 Codex usage 调用。Tokenscope 只缓存额度元数据,从不缓存凭据。 ### 关键处理 - Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 - Codex:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中分别拆出 `cached_input_tokens` 与 `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 -- Provider 额度:每 5 分钟在 dashboard build lock 外通过 `hu usage claude --json` + `hu usage codex --json` 刷新;最近一次成功快照落盘 `provider_limits.json`,`hu` 或网络失败时仍可展示。Claude `session`→5 小时 / `weekly`→7 天;Codex 主池→Weekly / Spark→Spark +- Provider 额度:每 5 分钟在 dashboard build lock 外通过内置 `hu usage claude --json` + `hu usage codex --json` 刷新;最近一次成功快照落盘 `provider_limits.json`,`hu` 或网络失败时仍可展示。Claude `session`→5 小时 / `weekly`→7 天;Codex 主池→Weekly / Spark→Spark - Pi:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` 或 `/clone` 复制的历史,session header 提供会话与项目归属;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 会进入对应遥测 - token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 - 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 diff --git a/README.md b/README.md index 9cd9196..c3baf25 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,19 @@ brew install --cask sunchj/tokenscope/tokenscope |---------|------| | Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | | Codex session logs (tokens + offline quota fallback) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | -| Provider quota (Claude + Codex) | HappyUsage `hu usage --json` (credential + OAuth + API owned by `hu`; Claude fallback: `~/.claude.json` `cachedUsageUtilization`) | +| Provider quota (Claude + Codex) | HappyUsage `hu` CLI bundled inside the app (`Resources/bin/hu`); Claude fallback: `~/.claude.json` `cachedUsageUtilization`, Codex fallback: native usage API with Pi / Codex CLI OAuth | | Pi session logs (tokens / model / tools / telemetry) | `~/.pi/agent/sessions/**/*.jsonl` (honors `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, and absolute `settings.sessionDir`) | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | | User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | | Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | -Quota requests go through the HappyUsage `hu` CLI, which owns credential discovery, OAuth refresh, and provider calls. Tokenscope caches only quota metadata, never credentials. +Quota requests go through the HappyUsage `hu` CLI, which owns credential discovery, OAuth refresh, and provider calls. The binary ships inside the app bundle, so no system install is needed; if it is missing (e.g. development builds), Tokenscope falls back to a native Codex usage call. Tokenscope caches only quota metadata, never credentials. ### Key processing - Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once - Codex: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, while `cached_input_tokens` and `cache_write_input_tokens` are split out of Codex's inclusive `input_tokens` for comparable four-category accounting -- Provider quota: refreshed every 5 minutes outside the dashboard build lock via `hu usage claude --json` + `hu usage codex --json`; last successful snapshot is cached (`provider_limits.json`) and shown when `hu` or the network fails. Claude `session`→5-hour / `weekly`→7d; Codex primary→Weekly / Spark→Spark +- Provider quota: refreshed every 5 minutes outside the dashboard build lock via the bundled `hu usage claude --json` + `hu usage codex --json`; last successful snapshot is cached (`provider_limits.json`) and shown when `hu` or the network fails. Claude `session`→5-hour / `weekly`→7d; Codex primary→Weekly / Spark→Spark - Pi: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`, while the session header supplies the session/project identity. Persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching dashboard telemetry - Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" - Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins diff --git a/docs/DESIGN-usage-api.md b/docs/DESIGN-usage-api.md index 4cf0122..eda9992 100644 --- a/docs/DESIGN-usage-api.md +++ b/docs/DESIGN-usage-api.md @@ -19,9 +19,10 @@ keeps one credential implementation instead of duplicating it in Rust. ``` refresh thread (every 5 min) └─ quota_api::reload() - ├─ hu usage claude --json → claude ProviderLimit + ├─ bundled hu (Resources/bin/hu) → `hu usage claude --json` → claude ProviderLimit │ └─ on failure: ~/.claude.json cachedUsageUtilization (local fallback) - └─ hu usage codex --json → codex ProviderLimit + ├─ bundled hu → `hu usage codex --json` → codex ProviderLimit + │ └─ on failure: native wham/usage call (Pi / Codex CLI OAuth) └─ persist ProviderCache → provider_limits.json (no credentials) parser::build_dashboard() └─ quota_api::shared() → provider_limits (dashboard + tray) @@ -32,6 +33,16 @@ parser::build_dashboard() later offline refresh (or a missing `hu`) still has data. The cache holds quota metadata only — never tokens. +## Bundling + +`src-tauri/bin/build-hu.sh` downloads the matching `hu` release asset for the +current OS/arch before `tauri build`; `bundle.resources` ships it as +`Resources/bin/hu`. At runtime `lib.rs` injects `resource_dir()` into +`quota_api`, which prefers the bundled binary, then system installs. If no +`hu` exists at all, `ensure_hu()` attempts a one-per-24h auto-install +(Homebrew tap → official install script → `go install`) before the native +Codex fallback. + ## Normalization | Provider | Source | Windows | @@ -62,8 +73,11 @@ quota metadata only — never tokens. ## Fallback chain -1. `hu usage --json` -2. Last successful `provider_limits.json` snapshot -3. Claude-only: `~/.claude.json` `cachedUsageUtilization` (updated by Claude +1. Bundled `hu` → `hu usage --json` +2. System `hu` (user-installed, may be newer) +3. One-per-24h auto-install of `hu` (Homebrew → install script → `go install`) +4. Native wham/usage call for Codex (Pi `openai-codex` OAuth → Codex CLI OAuth) +5. Last successful `provider_limits.json` snapshot +6. Claude-only: `~/.claude.json` `cachedUsageUtilization` (updated by Claude Code when it runs) -4. Codex trend: session-log `rate_limits` history merged into the live window +7. Codex trend: session-log `rate_limits` history merged into the live window diff --git a/src-tauri/bin/build-hu.sh b/src-tauri/bin/build-hu.sh new file mode 100755 index 0000000..f27c01e --- /dev/null +++ b/src-tauri/bin/build-hu.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# Bundle the happyusage `hu` binary into the app package. +# Downloads the matching release asset for the current OS/arch so the app is +# self-contained; override the version with HAPPYUSAGE_VERSION=vX.Y.Z. +set -e +cd "$(dirname "$0")" + +REPO="SunChJ/happyusage" +VERSION="${HAPPYUSAGE_VERSION:-}" + +if [ -z "$VERSION" ]; then + VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" +fi +[ -n "$VERSION" ] || { echo "failed to resolve latest version" >&2; exit 1; } + +case "$(uname -s)" in + Darwin) OS_GO="darwin" ;; + Linux) OS_GO="linux" ;; + MINGW*|MSYS*|CYGWIN*) OS_GO="windows" ;; + *) echo "unsupported OS: $(uname -s)" >&2; exit 1 ;; +esac +case "$(uname -m)" in + x86_64|amd64) ARCH="amd64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +if [ "$OS_GO" = "windows" ]; then + EXT="zip" +else + EXT="tar.gz" +fi +ASSET="hu-${OS_GO}-${ARCH}.${EXT}" +URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +echo "==> fetching ${URL}" +if [ "$OS_GO" = "windows" ]; then + curl -fsSL -o "$TMP/hu.zip" "$URL" + unzip -o -q "$TMP/hu.zip" -d "$TMP" +else + curl -fsSL -o "$TMP/hu.tgz" "$URL" + tar -xzf "$TMP/hu.tgz" -C "$TMP" +fi + +BIN_PATH="$(find "$TMP" -type f \( -name 'hu' -o -name 'hu.exe' \) | head -1)" +[ -n "$BIN_PATH" ] || { echo "hu binary not found in ${ASSET}" >&2; exit 1; } + +cp "$BIN_PATH" hu +chmod +x hu +echo "==> bundled hu ${VERSION}" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 037c27d..1060bc3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1228,6 +1228,10 @@ pub fn run() { refresh_pricing ]) .setup(move |app| { + // Point quota_api at the bundled `hu` binary (app Resources/bin/hu) + // so provider quota works without any system install. + quota_api::set_bundle_dir(app.path().resource_dir().ok()); + // Menu-bar–only app: no Dock icon, runs in the background. #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); diff --git a/src-tauri/src/quota_api.rs b/src-tauri/src/quota_api.rs index 658cf8f..adc1802 100644 --- a/src-tauri/src/quota_api.rs +++ b/src-tauri/src/quota_api.rs @@ -1,20 +1,25 @@ //! Provider subscription quota, sourced from the HappyUsage `hu` CLI. //! -//! `hu` already owns credential discovery, OAuth refresh, and provider API -//! calls (Claude via the Anthropic OAuth usage API, Codex via the ChatGPT -//! usage API). Tokenscope only invokes `hu usage --json`, parses -//! the envelope, and caches the result so a later offline refresh still has -//! the last successful reading. Codex session-log rate limits remain a -//! fallback in the parser. +//! `hu` owns credential discovery, OAuth refresh, and provider API calls. +//! Tokenscope invokes `hu usage --json`, parses the envelope, and +//! caches the result. When `hu` is missing or fails, Codex falls back to the +//! native wham/usage call (Pi / Codex CLI OAuth) so users without HappyUsage +//! still get quota; Claude falls back to Claude Code's local usage cache. +//! Codex session-log rate limits remain a parser-level fallback. use crate::model::{same_reset_cycle, LimitWindow, ProviderLimit, QuotaTrendPoint, WEEKLY_WINDOW_MINUTES}; +use base64::Engine; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; use std::path::PathBuf; use std::process::Command; use std::sync::{OnceLock, RwLock}; +use std::time::Duration; +const CODEX_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage"; +const CODEX_TOKEN_URL: &str = "https://auth.openai.com/oauth/token"; +const CODEX_OAUTH_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; const CACHE_FILE: &str = "provider_limits.json"; const CACHE_VERSION: u32 = 2; @@ -93,15 +98,32 @@ fn save_cache(cache: &ProviderCache) { fn hu_executables() -> Vec { let mut candidates = Vec::new(); + // 1. Bundled copy (app Resources/bin/hu) — the primary source so the app + // is self-contained and the hu version ships with Tokenscope. + if let Some(path) = bundled_hu() { + candidates.push(path); + } + // 2. System installs (PATH + common locations; install.sh drops `hu` into + // ~/.local/bin, which may not be on the app's PATH at launch). if let Some(path) = std::env::var_os("PATH").and_then(|paths| { std::env::split_paths(&paths) .map(|dir| dir.join(if cfg!(windows) { "hu.exe" } else { "hu" })) .find(|path| path.is_file()) }) { - candidates.push(path); + if !candidates.contains(&path) { + candidates.push(path); + } } - for path in ["/opt/homebrew/bin/hu", "/usr/local/bin/hu"] { - let path = PathBuf::from(path); + let mut extras = vec![ + PathBuf::from("/opt/homebrew/bin/hu"), + PathBuf::from("/usr/local/bin/hu"), + ]; + if let Some(home) = dirs::home_dir() { + extras.push(home.join(".local").join("bin").join("hu")); + extras.push(home.join("bin").join("hu")); + extras.push(home.join("go").join("bin").join("hu")); + } + for path in extras { if path.is_file() && !candidates.contains(&path) { candidates.push(path); } @@ -109,6 +131,115 @@ fn hu_executables() -> Vec { candidates } +/// Directory injected by lib.rs at startup: `app.path().resource_dir()`. +static BUNDLE_DIR: OnceLock> = OnceLock::new(); + +pub fn set_bundle_dir(dir: Option) { + let _ = BUNDLE_DIR.set(dir); +} + +fn bundled_hu() -> Option { + let dir = BUNDLE_DIR.get()?; + let base = dir.as_ref()?; + let path = base + .join("bin") + .join(if cfg!(windows) { "hu.exe" } else { "hu" }); + path.is_file().then_some(path) +} + +const HAPPYUSAGE_INSTALL_URL: &str = + "https://raw.githubusercontent.com/SunChJ/happyusage/main/scripts/install.sh"; +const INSTALL_RETRY_MS: i64 = 24 * 60 * 60 * 1000; +static LAST_INSTALL_ATTEMPT_MS: std::sync::atomic::AtomicI64 = + std::sync::atomic::AtomicI64::new(0); + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as i64) + .unwrap_or(0) +} + +/// Make sure `hu` exists, installing it once per 24h when missing. Returns +/// true when a usable executable is present afterwards. Runs on the +/// background refresh thread; a slow brew install only delays this refresh. +fn ensure_hu() -> bool { + if !hu_executables().is_empty() { + return true; + } + let now = now_ms(); + let last = LAST_INSTALL_ATTEMPT_MS.load(std::sync::atomic::Ordering::Relaxed); + if now - last < INSTALL_RETRY_MS { + return false; + } + let installed = try_install_hu(); + LAST_INSTALL_ATTEMPT_MS.store(now, std::sync::atomic::Ordering::Relaxed); + installed +} + +fn try_install_hu() -> bool { + // 1. Homebrew: tap + install (covers macOS and Linux with brew). + if let Some(brew) = find_on_path("brew") { + let tap_ok = Command::new(&brew) + .args(["tap", "SunChJ/happyusage"]) + .status() + .map(|status| status.success()) + .unwrap_or(false); + if tap_ok + && Command::new(&brew) + .args(["install", "hu"]) + .status() + .map(|status| status.success()) + .unwrap_or(false) + && !hu_executables().is_empty() + { + return true; + } + } + // 2. Official install script (needs curl + sh): fetch, run, then re-scan. + if let Some(curl) = find_on_path("curl") { + if let Ok(script) = Command::new(&curl).args(["-fsSL", HAPPYUSAGE_INSTALL_URL]).output() { + if script.status.success() { + let tmp = std::env::temp_dir().join(format!( + "happyusage-install-{}.sh", + std::process::id() + )); + if fs::write(&tmp, &script.stdout).is_ok() { + let ok = Command::new("sh") + .arg(&tmp) + .status() + .map(|status| status.success()) + .unwrap_or(false); + let _ = fs::remove_file(tmp); + if ok && !hu_executables().is_empty() { + return true; + } + } + } + } + } + // 3. Go toolchain as a last resort. + if let Some(go) = find_on_path("go") { + if Command::new(&go) + .args(["install", "github.com/SunChJ/happyusage/cmd/hu@latest"]) + .status() + .map(|status| status.success()) + .unwrap_or(false) + && !hu_executables().is_empty() + { + return true; + } + } + false +} + +fn find_on_path(name: &str) -> Option { + let paths = std::env::var_os("PATH")?; + std::env::split_paths(&paths) + .map(|dir| dir.join(if cfg!(windows) { format!("{name}.exe") } else { name.to_string() })) + .find(|path| path.is_file()) +} + /// Run `hu usage --json` and return the provider envelope /// (with `ok: true`). The CLI owns credential handling and HTTP timeouts; /// Tokenscope's refresh runs on a background thread, so a slow run only @@ -333,8 +464,10 @@ fn merge_trend( } } -/// Refresh provider limits through `hu`. Each provider is independent: a -/// failure keeps the previous successful snapshot for that provider. +/// Refresh provider limits, installing `hu` first when missing. Each provider +/// is independent: a failure keeps the previous successful snapshot. When `hu` +/// is absent or its Codex call fails, the native wham/usage call is the next +/// fallback, and Claude falls back to Claude Code's local usage cache. pub fn reload() -> bool { let previous = cache_lock() .read() @@ -342,6 +475,9 @@ pub fn reload() -> bool { .unwrap_or_default(); let mut next = previous.clone(); + // First reload attempt: make sure `hu` exists (auto-install when missing). + ensure_hu(); + if let Some(claude) = run_hu("claude").and_then(|envelope| claude_from_hu(&envelope)) { next.claude = Some(claude); } else if next.claude.is_none() { @@ -351,6 +487,10 @@ pub fn reload() -> bool { } if let Some(codex) = run_hu("codex").and_then(|envelope| codex_from_hu(&envelope)) { next.codex = Some(codex); + } else if next.codex.is_none() { + // No `hu` (or its Codex call failed): try the native usage API using + // the Pi / Codex CLI OAuth credentials. + next.codex = codex_from_native(); } let changed = next.claude.is_some() || next.codex.is_some(); @@ -367,6 +507,404 @@ pub fn reload() -> bool { } } +// ── Native Codex fallback (no HappyUsage) ──────────────────────────── + +#[derive(Clone)] +struct Credential { + access: String, + account_id: String, + refresh: String, +} + +fn pi_agent_dir() -> Option { + std::env::var_os("PI_CODING_AGENT_DIR") + .map(PathBuf::from) + .or_else(|| Some(dirs::home_dir()?.join(".pi").join("agent"))) +} + +fn jwt_account_id(token: &str) -> String { + let Some(payload) = token.split('.').nth(1) else { + return String::new(); + }; + let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload) else { + return String::new(); + }; + serde_json::from_slice::(&bytes) + .ok() + .and_then(|json| { + json.get("https://api.openai.com/auth")? + .get("chatgpt_account_id")? + .as_str() + .map(str::to_owned) + }) + .unwrap_or_default() +} + +/// Pi's own `openai-codex` OAuth credential (~/.pi/agent/auth.json). +fn read_pi_credential() -> Option { + let path = pi_agent_dir()?.join("auth.json"); + let json: Value = serde_json::from_str(&fs::read_to_string(path).ok()?).ok()?; + let auth = json.get("openai-codex")?; + if auth.get("type").and_then(Value::as_str) != Some("oauth") { + return None; + } + let access = auth.get("access")?.as_str()?.to_string(); + if access.is_empty() { + return None; + } + let account_id = auth + .get("accountId") + .and_then(Value::as_str) + .map(str::to_owned) + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| jwt_account_id(&access)); + Some(Credential { + access, + account_id, + refresh: auth + .get("refresh") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(), + }) +} + +fn decode_hex(text: &str) -> Option> { + let text = text + .strip_prefix("0x") + .or_else(|| text.strip_prefix("0X")) + .unwrap_or(text); + if text.is_empty() || !text.is_ascii() || !text.len().is_multiple_of(2) { + return None; + } + (0..text.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&text[index..index + 2], 16).ok()) + .collect() +} + +fn parse_json_or_hex(text: &str) -> Option { + let text = text.trim(); + serde_json::from_str(text).ok().or_else(|| { + let bytes = decode_hex(text)?; + serde_json::from_slice(&bytes).ok() + }) +} + +fn codex_auth_paths() -> Vec { + if let Some(home) = std::env::var_os("CODEX_HOME") { + return vec![PathBuf::from(home).join("auth.json")]; + } + let Some(home) = dirs::home_dir() else { + return Vec::new(); + }; + vec![ + home.join(".config").join("codex").join("auth.json"), + home.join(".codex").join("auth.json"), + ] +} + +fn read_codex_credential() -> Option { + codex_auth_paths() + .into_iter() + .filter_map(|path| { + let json = parse_json_or_hex(&fs::read_to_string(&path).ok()?)?; + let tokens = json.get("tokens")?; + let access = tokens.get("access_token")?.as_str()?.to_string(); + if access.is_empty() { + return None; + } + let account_id = tokens + .get("account_id") + .and_then(Value::as_str) + .map(str::to_owned) + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| jwt_account_id(&access)); + Some(Credential { + access, + account_id, + refresh: tokens + .get("refresh_token") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(), + }) + }) + .next() +} + +fn pi_executables() -> Vec { + let mut candidates = Vec::new(); + if let Some(path) = std::env::var_os("PATH").and_then(|paths| { + std::env::split_paths(&paths) + .map(|dir| dir.join(if cfg!(windows) { "pi.exe" } else { "pi" })) + .find(|path| path.is_file()) + }) { + candidates.push(path); + } + for path in ["/opt/homebrew/bin/pi", "/usr/local/bin/pi"] { + let path = PathBuf::from(path); + if path.is_file() && !candidates.contains(&path) { + candidates.push(path); + } + } + candidates +} + +/// Ask Pi itself for a fresh token so its credential lock and persistence +/// stay authoritative; Tokenscope never rewrites Pi's auth.json. +fn refreshed_pi_credential(_previous: &Credential) -> Option { + for executable in pi_executables() { + let output = Command::new(executable) + .args([ + "auth", + "print-bearer-token", + "--provider", + "openai-codex", + "--model", + "gpt-5.5", + "--min-expiry", + "30m", + ]) + .output() + .ok()?; + if !output.status.success() { + continue; + } + let Ok(stdout) = String::from_utf8(output.stdout) else { + continue; + }; + let access = stdout.trim().to_string(); + if access.is_empty() { + continue; + } + let stored = read_pi_credential(); + let account_id = stored + .as_ref() + .map(|credential| credential.account_id.clone()) + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| jwt_account_id(&access)); + return Some(Credential { + access, + account_id, + refresh: stored + .map(|credential| credential.refresh) + .unwrap_or_else(|| _previous.refresh.clone()), + }); + } + None +} + +fn persist_codex_credential( + path: &PathBuf, + previous_refresh: &str, + access: &str, + refresh: &str, +) -> bool { + let Some(mut json) = fs::read_to_string(path) + .ok() + .and_then(|text| parse_json_or_hex(&text)) + else { + return false; + }; + let Some(tokens) = json.get_mut("tokens").and_then(Value::as_object_mut) else { + return false; + }; + if tokens + .get("refresh_token") + .and_then(Value::as_str) + .unwrap_or("") + != previous_refresh + { + return false; + } + tokens.insert("access_token".to_string(), Value::String(access.to_string())); + tokens.insert("refresh_token".to_string(), Value::String(refresh.to_string())); + let account_id = jwt_account_id(access); + if !account_id.is_empty() { + tokens.insert("account_id".to_string(), Value::String(account_id)); + } + let Ok(bytes) = serde_json::to_vec_pretty(&json) else { + return false; + }; + let tmp = path.with_extension(format!("tmp-{}", std::process::id())); + if fs::write(&tmp, bytes).is_err() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o600)); + } + if fs::rename(&tmp, path).is_err() { + let _ = fs::remove_file(tmp); + return false; + } + true +} + +fn refreshed_codex_credential(previous: &Credential) -> Option { + if previous.refresh.is_empty() { + return None; + } + let response = ureq::post(CODEX_TOKEN_URL) + .timeout(Duration::from_secs(15)) + .set("Content-Type", "application/x-www-form-urlencoded") + .send_form(&[ + ("grant_type", "refresh_token"), + ("client_id", CODEX_OAUTH_CLIENT_ID), + ("refresh_token", previous.refresh.as_str()), + ]) + .ok()?; + let json: Value = response.into_json().ok()?; + let access = json.get("access_token")?.as_str()?.to_string(); + let refresh = json + .get("refresh_token") + .and_then(Value::as_str) + .unwrap_or(&previous.refresh) + .to_string(); + let path = codex_auth_paths().into_iter().find(|path| path.is_file())?; + if !persist_codex_credential(&path, &previous.refresh, &access, &refresh) { + return None; + } + Some(Credential { + account_id: jwt_account_id(&access), + access, + refresh, + }) +} + +fn refresh_credential(previous: &Credential) -> Option { + // Try the Pi CLI refresh first (most likely to have a working session), + // then the Codex CLI OAuth refresh. + refreshed_pi_credential(previous).or_else(|| refreshed_codex_credential(previous)) +} + +fn request_usage(credential: &Credential) -> Result<(Value, Option, Option), u16> { + let mut request = ureq::get(CODEX_USAGE_URL) + .timeout(Duration::from_secs(15)) + .set("Authorization", &format!("Bearer {}", credential.access)) + .set("Accept", "application/json") + .set("User-Agent", "Tokenscope"); + if !credential.account_id.is_empty() { + request = request.set("ChatGPT-Account-Id", &credential.account_id); + } + let response = match request.call() { + Ok(response) => response, + Err(ureq::Error::Status(status, _)) => return Err(status), + Err(_) => return Err(0), + }; + let primary = response + .header("X-Codex-Primary-Used-Percent") + .and_then(|value| value.parse().ok()); + let secondary = response + .header("X-Codex-Secondary-Used-Percent") + .and_then(|value| value.parse().ok()); + response + .into_json::() + .map(|body| (body, primary, secondary)) + .map_err(|_| 0) +} + +fn native_number(value: Option<&Value>) -> Option { + match value? { + Value::Number(number) => number.as_f64(), + Value::String(number) => number.parse().ok(), + _ => None, + } +} + +/// Native wham/usage decoding → the same ProviderLimit shape as `hu`'s Codex +/// envelope: primary pool becomes Weekly, `codex_bengalfox` becomes Spark. +fn codex_from_native() -> Option { + let credential = read_pi_credential().or_else(read_codex_credential)?; + let (body, primary_header, _) = request_usage(&credential) + .or_else(|status| { + if status != 401 { + return Err(status); + } + let refreshed = refresh_credential(&credential).ok_or(status)?; + request_usage(&refreshed) + }) + .ok()?; + let plan = body.get("plan_type").and_then(Value::as_str).unwrap_or(""); + let now_ms = now_ms(); + let mut windows = Vec::new(); + if let Some(rate_limit) = body.get("rate_limit") { + if let Some(primary) = rate_limit.get("primary_window") { + let used = primary_header + .or_else(|| native_number(primary.get("used_percent"))) + .unwrap_or(0.0); + let resets_at = primary + .get("reset_at") + .and_then(|value| value.as_i64()) + .or_else(|| { + native_number(primary.get("reset_after_seconds")) + .map(|seconds| now_ms / 1000 + seconds as i64) + }) + .unwrap_or(0); + windows.push(LimitWindow { + id: "weekly".to_string(), + label: "Weekly".to_string(), + duration_minutes: WEEKLY_WINDOW_MINUTES, + used_pct: used, + resets_at, + as_of_ms: now_ms, + trend: Vec::new(), + }); + } + } + if let Some(limits) = body + .get("additional_rate_limits") + .and_then(Value::as_array) + { + for entry in limits { + let is_spark = entry + .get("metered_feature") + .and_then(Value::as_str) + == Some("codex_bengalfox") + || entry + .get("limit_name") + .and_then(Value::as_str) + .is_some_and(|name| name.to_ascii_lowercase().contains("spark")); + if !is_spark { + continue; + } + if let Some(rate_limit) = entry.get("rate_limit") { + if let Some(primary) = rate_limit.get("primary_window") { + let used = native_number(primary.get("used_percent")).unwrap_or(0.0); + let resets_at = primary + .get("reset_at") + .and_then(|value| value.as_i64()) + .or_else(|| { + native_number(primary.get("reset_after_seconds")) + .map(|seconds| now_ms / 1000 + seconds as i64) + }) + .unwrap_or(0); + windows.push(LimitWindow { + id: "spark".to_string(), + label: "Spark".to_string(), + duration_minutes: WEEKLY_WINDOW_MINUTES, + used_pct: used, + resets_at, + as_of_ms: now_ms, + trend: Vec::new(), + }); + } + } + } + } + if windows.is_empty() { + return None; + } + Some(ProviderLimit { + provider: "codex".to_string(), + label: "Codex".to_string(), + plan: plan.to_string(), + windows, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 47d24bc..85f87f5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", - "beforeBuildCommand": "pnpm build", + "beforeBuildCommand": "pnpm build && bash src-tauri/bin/build-hu.sh", "frontendDist": "../dist" }, "app": { @@ -45,6 +45,7 @@ "active": true, "targets": ["app", "dmg", "nsis"], "createUpdaterArtifacts": true, + "resources": ["bin/hu"], "icon": [ "icons/32x32.png", "icons/128x128.png", From c9699f95d3bb71b7ce24757df26da457adbd2a0c Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 18:44:38 -0700 Subject: [PATCH 53/67] docs: refresh README for 1.5.1 feature set Restructure around the current feature set: bundled HappyUsage quota with its lookup/fallback chain and Menu Bar Display modes, three-agent support, and updated structure/release sections. --- README-zh.md | 191 ++++++++++++++++++----------------------------- README.md | 205 ++++++++++++++++++++------------------------------- 2 files changed, 152 insertions(+), 244 deletions(-) diff --git a/README-zh.md b/README-zh.md index 8a8e19d..2969a39 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,151 +1,114 @@ -# Tokenscope Remix +# Tokenscope [English](README.md) · **中文** -**macOS 菜单栏 / Windows 系统托盘工具**,统一展示本机 **AI 编码 Agent**(Pi、Claude Code 与 Codex CLI)的 **每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计**——所有 Agent 一个仪表盘。 +**macOS 菜单栏 / Windows 系统托盘工具**,为本机 **AI 编码 Agent**(Pi、Claude Code 与 Codex CLI)提供一个统一仪表盘:**每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计、可靠性遥测,以及实时订阅额度**。 技术栈:**Tauri 2 + React + TypeScript**(前端)/ **Rust**(数据层)。 ![Tokenscope 面板(深色 / 浅色)](docs/screenshot.png) -## macOS 快速安装 +## 安装 -Homebrew 是推荐的安装和升级方式: +### macOS:Homebrew(推荐) ```bash brew install --cask sunchj/tokenscope/tokenscope ``` -## 它做什么 - -- 菜单栏图标旁显示当日 Token 数(所有 Agent 合计,如 `⬡ 14.00M`) -- 点击打开面板:默认显示 Day,与菜单栏 Token 数保持一致;可切换 Week / Month,也可按起止日期(含首尾日)筛选自定义区间 -- **多 Agent**:检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex / Pi)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成该 Agent 的品牌色(Claude 珊瑚橙 / Codex 青绿 / Pi 紫);只装一个 Agent 则界面与经典版完全一致 -- 指标:总 Token(input/output)、估算花费、Requests / Sessions -- 项目结算:按本地 Git 仓库 / 工作目录归集用量,可将当前时间段导出为 CSV,不暴露原始路径或对话正文 -- 可靠性遥测:统计完成/中止 Turn、工具错误/拒绝,以及中止任务消耗的 Token 与估算成本;从本版本开始增量采集 -- Context 健康度:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数,以及 Agent 可提供时的 reasoning Token 占比 -- 周额度趋势:对 Codex 快照下采样,展示当前重置周期的每日消耗速度与预计触顶时间 -- 三个切片:**按模型** / **按 MCP 调用** / **按 Skill 调用** -- **额度详情卡**:实时订阅额度,**Claude**(5 小时 + 每周)与 **Codex**(每周 + Spark),经由 HappyUsage `hu` CLI 获取,采用 Codex status 风格(`29% left (resets 05:01 on 9 Aug)`);session 日志快照作为离线回退 +cask 的 `postflight` 已内置 `xattr -cr`,首次直接打开即可,不会弹「Apple 无法验证」。打开一次后注册为登录项,之后每次开机自动在菜单栏运行。 + +升级:`brew update && brew upgrade --cask tokenscope` + +### macOS:下载 .dmg(备用) + +1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_universal.dmg` +2. 拖入「应用程序」 +3. 因为是**未签名 / 未公证**构建,首次打开会被 Gatekeeper 拦截——右键 →「打开」→ 再次确认,或执行 `xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app` + +> 未签名是当前的已知限制;要「双击直开」需 Apple Developer ID 签名 + 公证。 + +### Windows + +1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` +2. 按当前用户安装(无需管理员权限)。SmartScreen 首次会拦截——点 **"更多信息" → "仍要运行"** +3. 系统要求:**Windows 10 1803 及以上 / Windows 11**,需要 WebView2 运行时(Win 11 预装) + +### 更新 + +App 启动时及此后每小时检测新版本,提示条位于左上角 **Tokenscope** 下方;也可用托盘菜单的 **Check for Updates…** 立即检查。Homebrew 用户继续使用 `brew upgrade --cask tokenscope`。 + +## 功能 + +- **Token 仪表盘**:今天 / 本周 / 本月 + 自定义日期区间(含首尾日)。检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex / Pi)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成其品牌色(Claude 珊瑚橙 / Codex 青绿 / Pi 紫) +- **三类切片**:**按模型**、**按 MCP 调用**、**按 Skill 调用**(只统计你自己安装的 server / skill,内置工具全部过滤) +- **项目结算**:按本地 Git 仓库 / 工作目录归集用量,导出 CSV,不暴露原始路径或对话正文 +- **可靠性遥测**:完成/中止 Turn、工具错误/拒绝,以及中止任务浪费的 Token 与估算成本 +- **Context 健康度**:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数、reasoning Token 占比 +- **额度详情**:**Claude**(5 小时 + 每周)与 **Codex**(每周 + Spark)的实时订阅额度,Codex status 风格展示——`29% left (resets 05:01 on 9 Aug)`,趋势点足够时给出每日消耗速率与预计触顶时间 +- **菜单栏显示**:在菜单栏 Token 数后展示各 provider 最紧张窗口(`52.8M · Cl79% · Cx29%`)、完整窗口列表,或完全关闭 - 成本甜甜圈(hover 看单模型)、年度活跃热力图 -- **只统计用户自己安装的 MCP / Skill**,过滤所有内置工具与自带 MCP + +## Provider 额度 + +订阅额度通过 **HappyUsage `hu` CLI** 获取,由它负责凭据发现、OAuth 刷新与 provider API 调用(Claude 走 Anthropic OAuth usage API,Codex 走 ChatGPT usage API)。 + +- `hu` 二进制**随应用包内置**(`Resources/bin/hu`),无需系统安装;每次发布由 `src-tauri/bin/build-hu.sh` 下载最新版并打进包内 +- 运行时查找顺序:内置 `hu` → 系统安装的 `hu` → 24h 节流自动安装(brew tap → 官方脚本 → `go install`)→ 原生 Codex usage 调用(Pi / Codex CLI OAuth)→ 缓存快照 +- 额度每 5 分钟在后台线程刷新,最近一次成功快照落盘 `provider_limits.json`,离线 / API 失败时仍可展示 +- Claude 回退到 `~/.claude.json` 的 `cachedUsageUtilization`(Claude Code 运行时更新);Codex 趋势合并 session 日志的 rate-limit 历史 +- Tokenscope **从不缓存凭据**,只缓存额度元数据 + +菜单栏标签:**关闭**(`52.8M`)/ **紧凑**(`52.8M · Cl79% · Cx29%`,各 provider 最紧张窗口)/ **详细**(`52.8M · Cl 5h0/W79 · Cx W29/S31`)。右键托盘菜单另有 **额度详情** 子菜单,列出每个窗口的剩余百分比。 ## 数据来源(本地优先;session 日志只读) | 用途 | 路径 | |------|------| | Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | -| Codex 会话日志(Token + 离线配额回退) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | -| Provider 额度(Claude + Codex) | HappyUsage `hu` CLI,随应用 bundle 分发(`Resources/bin/hu`);Claude 回退:`~/.claude.json` 的 `cachedUsageUtilization`,Codex 回退:基于 Pi / Codex CLI OAuth 的原生 usage API | +| Codex 会话日志(Token + 离线额度回退) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | | Pi 会话日志(Token / 模型 / 工具 / 遥测) | `~/.pi/agent/sessions/**/*.jsonl`(支持 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR` 与绝对路径 `settings.sessionDir`) | +| Provider 额度(Claude + Codex) | 内置 HappyUsage `hu`(`Resources/bin/hu`);回退:`~/.claude.json` 的 `cachedUsageUtilization`、基于 Pi / Codex CLI OAuth 的原生 usage API | | Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | | 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/`;Pi:全局、共享、项目级 Pi Skill 目录及 settings 显式路径 | -| 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新,离线回退 | +| 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新 | -配额请求统一走 HappyUsage `hu` CLI,由它负责凭据发现、OAuth 刷新与 provider 调用。二进制随应用包内置,无需系统安装;缺失时(如开发构建)回退到原生 Codex usage 调用。Tokenscope 只缓存额度元数据,从不缓存凭据。 +## 关键处理 -### 关键处理 -- Claude:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 -- Codex:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中分别拆出 `cached_input_tokens` 与 `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 -- Provider 额度:每 5 分钟在 dashboard build lock 外通过内置 `hu usage claude --json` + `hu usage codex --json` 刷新;最近一次成功快照落盘 `provider_limits.json`,`hu` 或网络失败时仍可展示。Claude `session`→5 小时 / `weekly`→7 天;Codex 主池→Weekly / Spark→Spark -- Pi:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` 或 `/clone` 复制的历史,session header 提供会话与项目归属;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 会进入对应遥测 -- token 拆分:`input`(未缓存) / `cache`(creation+read) / `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 -- 价格匹配:精确名 → 归一化名(去厂商前缀 + `.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价 -- 成本按四类 token 分别计价;Pi 有持久化请求成本时优先采用。模型带 `priced` 标记,**所有价格源都查不到的模型只计 Token、UI 标注「暂无定价」** -- 日志只有裸模型名、无厂商信息 → 第三方模型默认取官方厂商价(估算) -- 工具分类:MCP 调用从直接的 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用中归属 server,再按**所属 Agent**配置过滤;Claude 的 Skill 工具/斜杠命令,以及 Codex 与 Pi 对 `SKILL.md` 的读取,会按所属 Agent 的用户/项目 skills 目录匹配;其余忽略 +- **Claude**:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 +- **Codex**:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中拆出 `cached_input_tokens` / `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 +- **Pi**:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` / `/clone` 复制的历史;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 进入对应遥测 +- **Token 拆分**:`input`(未缓存)/ `cache`(creation + read)/ `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 +- **计价**:精确名 → 归一化名(去厂商前缀,`.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价。Pi 有持久化请求成本时优先采用。查不到价格的模型仍计入 Token,UI 标注「暂无定价」 +- **工具分类**:MCP 调用从 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用归属 server,再按**所属 Agent** 配置过滤;Claude 的 Skill 调用与 Codex / Pi 的 `SKILL.md` 读取按 Agent 的用户/项目 skills 目录匹配;其余忽略 > 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 ### 四类 Token 与计价公式 -每条 assistant 消息的 `usage` 给出四个**互斥**的 token 计数(同一 token 不会被重复统计): +每条 assistant 消息的 `usage` 给出四个**互斥**的 token 计数(同一 token 不会被重复统计): -| 阶段 | `usage` 字段 | 含义 | 单价(相对 input) | +| 阶段 | `usage` 字段 | 含义 | 单价(相对 input) | |------|-------------|------|------------------| -| **Input**(未缓存) | `input_tokens` | 本轮新发送的提示词 token | 1× | +| **Input**(未缓存) | `input_tokens` | 本轮新发送的提示词 token | 1× | | **Cache 写入** | `cache_creation_input_tokens` | 写入提示缓存的上下文 | 约 1.25× | -| **Cache 命中**(读) | `cache_read_input_tokens` | 从缓存重放的上下文 | 约 0.1×(便宜很多) | +| **Cache 命中**(读) | `cache_read_input_tokens` | 从缓存重放的上下文 | 约 0.1× | | **Output** | `output_tokens` | 模型生成的 token | 约 5× | Pi 以 `usage.input`、`usage.cacheWrite`、`usage.cacheRead`、`usage.output` 持久化相同四类口径;可选的 `usage.reasoning` 已是 output 的子集,不会重复相加。 -**Tokens**(按周期对消息求和): - ``` total = input + cache_creation + cache_read + output # UI 展示: In = input + cache_creation + cache_read, Out = output, cached % = cache_read / total -``` -**Cost**(每个阶段各按价格表里自己的单价计算): - -``` cost = input × price.input + cache_creation × price.cache_creation - + cache_read × price.cache_read # 缓存命中按折扣后的 read 单价计费 + + cache_read × price.cache_read # 缓存命中按折扣后的 read 单价计费 + output × price.output ``` -所以缓存命中**不会**按普通 input 计费,而是用专门(更便宜)的 `cache_read` 单价——这就是重度缓存场景下 token 量很大、花费却不高的原因。UI 只是把 cache 折进「In」做展示,计费始终按上面四个独立单价。 - -## 安装 - -### macOS:Homebrew(推荐) - -Homebrew 是 macOS 的主要安装方式:一条命令完成安装,自动处理 -Gatekeeper 隔离属性,后续升级也更简单可控。 - -```bash -brew install --cask sunchj/tokenscope/tokenscope -``` - -安装后会自动清除隔离属性(cask 的 `postflight` 已内置 `xattr -cr`),**首次直接打开即可,不会弹「Apple 无法验证」**。 - -打开一次后即注册为登录项,之后**每次开机自动在菜单栏运行**。 - -升级: - -```bash -brew update && brew upgrade --cask tokenscope -``` - -### macOS:下载 .dmg(备用) - -1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_universal.dmg`(同时支持 Apple Silicon 与 Intel) -2. 拖入「应用程序」 -3. 因为是**未签名 / 未公证**构建,首次打开会被 Gatekeeper 拦截,二选一: - - 右键 App →「打开」→ 再次确认「打开」,或 - - 终端执行一次: - ```bash - xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app - ``` - -> 未签名是当前的已知限制。要彻底「双击直开」需 Apple Developer ID 签名 + 公证,见 `PRD.md` §6.4。 - -### Windows - -1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` -2. 双击安装。因为是**未签名**构建,首次运行会被 SmartScreen 拦截 —— 点 **"更多信息" → "仍要运行"** 即可 -3. 安装器按当前用户安装(无需管理员权限),并**自动注册开机自启** -4. 系统要求:**Windows 10 1803 及以上 / Windows 11**,需要 WebView2 运行时(Win 11 预装;Win 10 用户若没装,安装器会引导补装) - -### 更新 - -App 会在启动时及此后每小时检测新版本。当前版本和更新操作常驻于左上角 -**Tokenscope** 下方。Homebrew 用户建议继续使用 `brew upgrade --cask tokenscope`; -直接下载安装的用户可在 App 内完成更新和重启。右键托盘菜单中的 -**Check for Updates…** 可立即手动检查。 - -### 首次启动后 - -- **macOS**:菜单栏出现图标 + 当日 Token 数(如 `⬡ 12.40M`) -- **Windows**:系统托盘出现图标。Windows 任务栏托盘 API 不支持在图标旁显示文字,**鼠标悬停托盘图标**即可看到当日 Token 数(提示气泡形如 `Tokenscope · today 12.40M`) -- 左键点击图标开/关面板,右键出菜单(Open / Refresh / Check for Updates / 显示偏好 / Quit) -- **菜单栏显示** 控制菜单栏 Token 数后展示多少额度信息:**关闭**(`52.8M`)、**紧凑**(`52.8M · Cl79% · Cx29%`,各 provider 最紧张窗口)、**详细**(`52.8M · Cl 5h0/W79 · Cx W29/S31`)。百分比与 dashboard(`29% left (resets 05:01 on 9 Aug)`)采用相同的四舍五入**剩余值**。已过重置时间的窗口不会显示,默认关闭,选择会被记住 -- **Dashboard Shortcut** 可用全局快捷键切换面板的显示/隐藏(默认 macOS 为 `⌥⌘T`,Windows/Linux 为 `Ctrl+Alt+T`);通过 **Change Dashboard Shortcut…** 可直接录入自定义组合键。默认关闭,选择会被记住 -- 已自动设置**登录自启**,无需手动配置 +缓存命中**不会**按普通 input 计费,而是用专门(更便宜)的 `cache_read` 单价——这就是重度缓存场景下 token 量很大、花费却不高的原因。UI 只是把 cache 折进「In」做展示,计费始终按四个独立单价。 ## 开发 @@ -162,30 +125,22 @@ pnpm dev # http://localhost:1420 cd src-tauri && cargo run --example dump > ../public/dev-dashboard.json ``` -## 构建 +## 构建与发布 ```bash -pnpm tauri build # macOS 产出 .app / .dmg,Windows 产出 .exe (NSIS),均位于 src-tauri/target/release/bundle/ +pnpm tauri build # macOS:.app / .dmg;Windows:NSIS .exe → src-tauri/target/release/bundle/ ``` -分发见 `PRD.md` §6.3(macOS 推荐 Homebrew Cask;`.dmg` / `.exe` 直接下载建议代码签名 + 公证)。 - -## 发布 - -使用仓库内置脚本同步所有版本文件: +构建时会自动下载并内置 `hu` 二进制(`src-tauri/bin/build-hu.sh`)。同步版本号后推送: ```bash -pnpm version:set 1.3.4 +pnpm version:set 1.5.1 pnpm version:check -git commit -am "release: v1.3.4" +git commit -am "release: v1.5.1" git push origin main ``` -版本号变更推送到 `main` 后,GitHub Actions 会校验版本、创建 `vX.Y.Z` -标签,并自动构建和发布 macOS / Windows 安装包、签名更新包和 `latest.json`。 -发布失败时,也可以在 Release workflow 中选择已有的同版本标签手工重跑。 -发布流程需要配置 `TAURI_SIGNING_PRIVATE_KEY` 和 -`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets。 +版本号变更推送到 `main` 后,GitHub Actions 会校验版本、创建 `vX.Y.Z` 标签,构建并发布 macOS / Windows 安装包、签名更新包与 `latest.json`(需配置 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets)。 ## 结构 @@ -198,19 +153,19 @@ src-tauri/src/ store.rs 多源 JSONL 增量摄取(Claude + Codex + Pi 适配器) parser.rs 按 Scope 聚合(All / 单 Agent;预设周期 + 自定义区间 + 热力图) pricing.rs models.dev / LiteLLM 价格加载与计价 - quota_api.rs 已认证 provider 配额 + 安全凭据刷新/缓存 + quota_api.rs provider 额度:内置 HappyUsage `hu` + 原生 / 自动安装回退 config.rs 用户 MCP / Skill 白名单(Claude + Codex + Pi) model.rs 返回给前端的数据结构 lib.rs Tauri 命令 + 菜单栏托盘 +src-tauri/bin/ build-hu.sh——发布时下载内置的 hu 二进制 ``` ## Bug 记录 -开发过程中遇到的典型 bug(现象、根因、解决办法)汇总在 -[docs/BUGFIXES.md](docs/BUGFIXES.md)。 +开发过程中遇到的典型 bug(现象、根因、解决办法)汇总在 [docs/BUGFIXES.md](docs/BUGFIXES.md)。 ## 致谢 本项目源自 [@HduSy](https://github.com/HduSy) 的 [tokenscope](https://github.com/HduSy/tokenscope)——一个非常出色的 macOS 菜单栏 Claude CLI 用量仪表盘。本仓库继承的 Rust 数据摄取/聚合架构与面板设计均出自原作者之手,并沿用原项目的 MIT 协议,在此致以诚挚感谢!🙏 -Tokenscope Remix 在其基础上扩展为多 Agent 仪表盘(Codex 已支持,更多 Agent 规划中——见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)),并独立维护:问题与需求请在本仓库提交,不要打扰上游。 +Tokenscope Remix 在其基础上扩展为多 Agent 仪表盘(见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)),并独立维护:问题与需求请在本仓库提交,不要打扰上游。Provider 额度部分使用 [HappyUsage](https://github.com/SunChJ/happyusage)(`hu`)负责凭据处理与 provider API 调用。 diff --git a/README.md b/README.md index c3baf25..c82b068 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,65 @@ -# Tokenscope Remix +# Tokenscope **English** · [中文](README-zh.md) -A **menu-bar / system-tray app for macOS and Windows** that shows your **local AI coding agents'** (Pi, Claude Code, and Codex CLI) **daily token usage, estimated cost, and per-model / MCP / Skill call breakdown** — one unified dashboard for every agent on your machine. +A **menu-bar / system-tray app for macOS and Windows** that gives you one dashboard for everything your local AI coding agents consumed — **token usage, estimated cost, model / MCP / Skill breakdowns, reliability telemetry, and live provider subscription quota** for Pi, Claude Code, and Codex CLI. Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). ![Tokenscope panel (dark / light)](docs/screenshot.png) -## Quick install on macOS +## Install -Homebrew is the recommended installation and upgrade path: +### macOS: Homebrew (recommended) ```bash brew install --cask sunchj/tokenscope/tokenscope ``` -## What it does - -- Shows today's token count (all agents combined) next to the menu-bar icon (e.g. `⬡ 14.00M`) -- Click to open the panel: it starts on Day so its total matches the menu-bar count, with Week / Month toggles plus an inclusive custom date-range filter -- **Multi-agent**: with more than one agent detected, filter chips (All / Claude / Codex / Pi) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the whole panel with its accent (Claude coral / Codex teal / Pi violet). With a single agent the classic UI is unchanged -- Metrics: total tokens (input/output), estimated cost, requests / sessions -- Project settlement: groups usage by local Git repository / working directory and exports the selected period as CSV without exposing raw paths or conversation content -- Reliability telemetry: completed/aborted turns, tool errors/denials, and estimated tokens/cost spent on aborted work; collected incrementally from this version forward -- Context health: median/peak context-window pressure, ≥80% warnings, compaction count, and reasoning-token share when reported by the agent -- Weekly quota trend: downsampled Codex snapshots with daily burn rate and time-to-cap projection for the current reset cycle -- Three breakdowns: **by model** / **by MCP call** / **by Skill call** -- **Provider limits card**: live subscription quota for **Claude** (5-hour + weekly) and **Codex** (weekly + Spark), sourced from the HappyUsage `hu` CLI and shown Codex-status style (`29% left (resets 05:01 on 9 Aug)`); session-log snapshots remain the offline fallback -- Cost donut (hover for a single model), year-long activity heatmap -- **Counts only the MCP servers / Skills you installed yourself** — all built-in tools and bundled MCP servers are filtered out +The cask's `postflight` strips the quarantine attribute automatically, so it opens on first launch without the "Apple cannot verify" prompt. After opening it once it registers as a login item and launches in the menu bar on every boot. + +Upgrade: `brew update && brew upgrade --cask tokenscope` + +### macOS: Download the .dmg (fallback) + +1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) +2. Drag it into Applications +3. Because the build is **unsigned / unnotarized**, Gatekeeper blocks first launch — right-click → **Open** → confirm, or run `xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app` + +> Unsigned is a current known limitation; a "double-click to open" experience requires Apple Developer ID signing + notarization. + +### Windows + +1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) +2. Install (per-user, no admin). SmartScreen warns on first run — **More info → Run anyway** +3. Requirements: **Windows 10 1803+ / Windows 11** with the WebView2 runtime (preinstalled on Windows 11) + +### Updating + +The app checks for new releases on launch and hourly; the banner sits under **Tokenscope** in the top-left corner, or use **Check for Updates…** in the tray menu. Homebrew users keep using `brew upgrade --cask tokenscope`. + +## Features + +- **Token dashboard**: today / week / month plus an inclusive custom date-range filter. With more than one agent detected, filter chips (All / Claude / Codex / Pi) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the panel with its accent (Claude coral / Codex teal / Pi violet) +- **Breakdowns**: tokens and cost **by model**, **by MCP call**, **by Skill call** (only servers/skills you installed yourself — built-ins are filtered out) +- **Project settlement**: usage grouped by local Git repo / working directory, exported as CSV without raw paths or conversation content +- **Reliability telemetry**: completed/aborted turns, tool errors/denials, tokens & cost wasted on aborted work +- **Context health**: median/peak context-window pressure, ≥80% warnings, compaction count, reasoning-token share +- **Provider limits**: live subscription quota for **Claude** (5-hour + weekly) and **Codex** (weekly + Spark), shown Codex-status style — `29% left (resets 05:01 on 9 Aug)` — with a burn-rate projection when enough trend points exist +- **Menu Bar Display**: show the tightest window per provider right in the menu bar (`52.8M · Cl79% · Cx29%`), a detailed list, or nothing at all +- Cost donut, year-long activity heatmap + +## Provider quota + +Subscription limits are collected through the **HappyUsage `hu` CLI**, which owns credential discovery, OAuth refresh, and the provider API calls (Claude via the Anthropic OAuth usage API, Codex via the ChatGPT usage API). + +- The `hu` binary **ships inside the app bundle** (`Resources/bin/hu`) — no system install needed; it is rebuilt on every release via `src-tauri/bin/build-hu.sh` +- Runtime lookup: bundled `hu` → system installs → a one-per-24h auto-install (Homebrew tap → install script → `go install`) → native Codex usage call (Pi / Codex CLI OAuth) → cached snapshot +- Quota refreshes every 5 minutes on a background thread; the last successful snapshot is cached (`provider_limits.json`) and survives offline / API failures +- Claude falls back to `~/.claude.json` `cachedUsageUtilization` (updated by Claude Code when it runs); Codex trend merges session-log rate-limit history +- Tokenscope **never caches credentials** — only quota metadata + +Menu-bar label: **Off** (`52.8M`) / **Compact** (`52.8M · Cl79% · Cx29%`, tightest window per provider) / **Detailed** (`52.8M · Cl 5h0/W79 · Cx W29/S31`). The right-click tray menu also has a **Provider Limits** submenu with every window's remaining percentage. ## Data sources (local-first; session logs are read-only) @@ -37,25 +67,21 @@ brew install --cask sunchj/tokenscope/tokenscope |---------|------| | Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | | Codex session logs (tokens + offline quota fallback) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | -| Provider quota (Claude + Codex) | HappyUsage `hu` CLI bundled inside the app (`Resources/bin/hu`); Claude fallback: `~/.claude.json` `cachedUsageUtilization`, Codex fallback: native usage API with Pi / Codex CLI OAuth | | Pi session logs (tokens / model / tools / telemetry) | `~/.pi/agent/sessions/**/*.jsonl` (honors `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, and absolute `settings.sessionDir`) | +| Provider quota (Claude + Codex) | Bundled HappyUsage `hu` (`Resources/bin/hu`); fallbacks: `~/.claude.json` `cachedUsageUtilization`, native usage API with Pi / Codex CLI OAuth | | Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | | Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | -| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, and project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | -| Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching the CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h, with offline fallback | - -Quota requests go through the HappyUsage `hu` CLI, which owns credential discovery, OAuth refresh, and provider calls. The binary ships inside the app bundle, so no system install is needed; if it is missing (e.g. development builds), Tokenscope falls back to a native Codex usage call. Tokenscope caches only quota metadata, never credentials. - -### Key processing -- Claude: deduplicated by `message.id` (streaming/retries repeat the same usage); when one message spans multiple lines, its tool calls are merged and the token usage is counted once -- Codex: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, while `cached_input_tokens` and `cache_write_input_tokens` are split out of Codex's inclusive `input_tokens` for comparable four-category accounting -- Provider quota: refreshed every 5 minutes outside the dashboard build lock via the bundled `hu usage claude --json` + `hu usage codex --json`; last successful snapshot is cached (`provider_limits.json`) and shown when `hu` or the network fails. Claude `session`→5-hour / `weekly`→7d; Codex primary→Weekly / Spark→Spark -- Pi: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`, while the session header supplies the session/project identity. Persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching dashboard telemetry -- Token split: `input` (uncached) / `cache` (creation+read) / `output`; the UI folds cache into "In" by default and shows a separate "cached %" -- Price matching: exact id → normalized id (strip vendor prefix + `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins -- Cost is priced per the four token types; Pi's persisted request cost is preferred when available. Each model carries a `priced` flag — **models not found in any price source still count tokens but are labelled "no price" in the UI** -- Logs contain only the bare model name (no vendor) → third-party models default to the official vendor price (an estimate) -- Tool classification: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill tool/slash-command calls plus Codex and Pi `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored +| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | +| Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h | + +## Key processing + +- **Claude**: deduplicated by `message.id` (streaming/retries repeat usage); one message spanning multiple lines merges its tool calls and counts tokens once +- **Codex**: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, and `cached_input_tokens` / `cache_write_input_tokens` are split out of the inclusive `input_tokens` for a four-category accounting comparable with Claude +- **Pi**: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`; persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching telemetry +- **Token split**: `input` (uncached) / `cache` (creation + read) / `output`; the UI folds cache into "In" by default with a separate "cached %" +- **Pricing**: exact id → normalized id (strip vendor prefix, `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins. Pi's persisted request cost is preferred when available. Models without any price still count tokens but are labelled "no price" +- **Tool classification**: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill calls plus Codex/Pi `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored > Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". @@ -67,87 +93,22 @@ Every assistant message's `usage` reports four **mutually exclusive** token coun |-------|---------------|------------|---------------------------| | **Input** (uncached) | `input_tokens` | New prompt tokens sent this turn | 1× | | **Cache write** | `cache_creation_input_tokens` | Context written into the prompt cache | ~1.25× | -| **Cache read** (hit) | `cache_read_input_tokens` | Context replayed from the cache | ~0.1× (much cheaper) | +| **Cache read** (hit) | `cache_read_input_tokens` | Context replayed from the cache | ~0.1× | | **Output** | `output_tokens` | Tokens the model generated | ~5× | -Pi persists the same categories as `usage.input`, `usage.cacheWrite`, `usage.cacheRead`, and `usage.output`; its optional `usage.reasoning` is already a subset of output and is never added again. - -**Tokens** (per period, summed over messages): - -``` -total = input + cache_creation + cache_read + output -# the UI shows: In = input + cache_creation + cache_read, Out = output, cached % = cache_read / total -``` - -**Cost** (each stage priced at its own per-token rate from the price table): - -``` -cost = input × price.input - + cache_creation × price.cache_creation - + cache_read × price.cache_read # cache hits billed at the discounted read rate - + output × price.output -``` - -So a cache hit is **not** billed as normal input — it uses the dedicated (cheaper) `cache_read` rate, which is why heavily-cached usage shows a huge token count but a modest cost. The UI folds cache into "In" for display only; billing always uses the four separate rates above. - -## Install - -### macOS: Homebrew (recommended) - -Homebrew is the primary installation method for macOS. It provides one-command -installation, handles Gatekeeper quarantine automatically, and keeps upgrades -predictable. +Pi persists the same categories as `usage.input`, `usage.cacheWrite`, `usage.cacheRead`, `usage.output`; its optional `usage.reasoning` is already a subset of output and never added again. -```bash -brew install --cask sunchj/tokenscope/tokenscope ``` +total = input + cache_creation + cache_read + output +# UI shows: In = input + cache_creation + cache_read, Out = output, cached % = cache_read / total -The cask's `postflight` strips the quarantine attribute (`xattr -cr`) automatically, so **it opens on first launch without the "Apple cannot verify" prompt**. - -After you open it once it registers as a login item, then **launches in the menu bar automatically on every boot**. - -Upgrade: - -```bash -brew update && brew upgrade --cask tokenscope +cost = input × price.input + + cache_creation × price.cache_creation + + cache_read × price.cache_read # cache hits billed at the discounted read rate + + output × price.output ``` -### macOS: Download the .dmg (fallback) - -1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) (works on both Apple Silicon and Intel) -2. Drag it into Applications -3. Because the build is **unsigned / unnotarized**, Gatekeeper blocks the first launch — pick one: - - Right-click the app → **Open** → confirm **Open** again, or - - Run once in the terminal: - ```bash - xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app - ``` - -> Unsigned is a current known limitation. A true "double-click to open" experience requires Apple Developer ID signing + notarization — see `PRD.md` §6.4. - -### Windows - -1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) -2. Double-click to install. Because the build is **unsigned**, Windows SmartScreen will warn on first run — click **More info → Run anyway** -3. The app installs per-user (no admin required) and registers itself for **launch at login** automatically -4. Requirements: **Windows 10 1803+ / Windows 11** with the WebView2 runtime (preinstalled on Windows 11; Windows 10 users without it will be prompted by the installer) - -### Updating - -The app checks for new releases on launch and every hour. Its version and update -actions appear directly below **Tokenscope** in the top-left corner. Homebrew -users should keep using `brew upgrade --cask tokenscope`; direct-download users -can update and restart from the app. Use **Check for Updates…** in the tray's -right-click menu to check immediately. - -### After first launch - -- **macOS**: an icon plus today's token count appears in the menu bar (e.g. `⬡ 12.40M`) -- **Windows**: the tray icon appears in the notification area. The Windows tray API doesn't show a label beside the icon — **hover the tray icon** to see today's token count in the tooltip (e.g. `Tokenscope · today 12.40M`) -- Left-click the icon to toggle the panel; right-click for the menu (Open / Refresh / Check for Updates / display preferences / Quit) -- **Menu Bar Display** controls how much quota the menu-bar title shows next to the token count: **Off** (`52.8M`), **Compact** (`52.8M · Cl79% · Cx29%` — the tightest window per provider), or **Detailed** (`52.8M · Cl 5h0/W79 · Cx W29/S31`). Percentages use the same rounded **left** values as the dashboard (`29% left (resets 05:01 on 9 Aug)`). Expired windows are hidden; the setting is off by default and remembers your choice -- **Dashboard Shortcut** toggles the panel with a global hotkey (`⌥⌘T` on macOS, `Ctrl+Alt+T` on Windows/Linux by default); use **Change Dashboard Shortcut…** to record your own combination. It is off by default and remembers your choice -- **Launch-at-login is set up automatically** — no manual configuration needed +A cache hit is **not** billed as normal input — it uses the dedicated cheaper `cache_read` rate, which is why heavily-cached usage shows a huge token count but a modest cost. ## Develop @@ -164,30 +125,22 @@ pnpm dev # http://localhost:1420 cd src-tauri && cargo run --example dump > ../public/dev-dashboard.json ``` -## Build +## Build & release ```bash -pnpm tauri build # outputs .app / .dmg on macOS, .exe (NSIS) on Windows to src-tauri/target/release/bundle/ +pnpm tauri build # macOS: .app / .dmg; Windows: NSIS .exe → src-tauri/target/release/bundle/ ``` -For distribution see `PRD.md` §6.3 (Homebrew Cask recommended on macOS; direct `.dmg` / `.exe` downloads benefit from code signing + notarization). - -## Release - -Keep all app version files in sync with the bundled helper: +The build downloads and bundles the `hu` binary (`src-tauri/bin/build-hu.sh`). Keep version files in sync, then push: ```bash -pnpm version:set 1.3.4 +pnpm version:set 1.5.1 pnpm version:check -git commit -am "release: v1.3.4" +git commit -am "release: v1.5.1" git push origin main ``` -A version change pushed to `main` is validated by GitHub Actions, tagged as -`vX.Y.Z`, then built and published for macOS and Windows together with signed -updater artifacts and `latest.json`. A matching existing tag can also be run -manually from the Release workflow for recovery. The release workflow requires -`TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets. +A version change pushed to `main` is validated by GitHub Actions, tagged `vX.Y.Z`, built and published for macOS and Windows with signed updater artifacts and `latest.json` (requires `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets). ## Structure @@ -200,19 +153,19 @@ src-tauri/src/ store.rs incremental multi-source JSONL ingest (Claude + Codex + Pi adapters) parser.rs aggregation into scopes (All / per-agent; presets + custom ranges + heatmap) pricing.rs models.dev / LiteLLM price loading and costing - quota_api.rs authenticated provider quota + credential-safe refresh/cache + quota_api.rs provider quota via bundled HappyUsage `hu` + native/auto-install fallbacks config.rs user MCP / Skill whitelists (Claude + Codex + Pi) model.rs data structures returned to the frontend lib.rs Tauri commands + menu-bar tray +src-tauri/bin/ build-hu.sh — downloads the hu binary bundled into releases ``` ## Bug log -Notable bugs found during development — symptom, root cause, and fix — are -collected in [docs/BUGFIXES.md](docs/BUGFIXES.md). +Notable bugs found during development — symptom, root cause, and fix — are collected in [docs/BUGFIXES.md](docs/BUGFIXES.md). ## Acknowledgements This project started as a remix of [tokenscope](https://github.com/HduSy/tokenscope) by [@HduSy](https://github.com/HduSy) — a beautifully built macOS menu-bar dashboard for Claude CLI usage. The clean Rust ingest/aggregation architecture and the panel design this project inherits are his work, and this repo remains under the original MIT license. Huge thanks! 🙏 -Tokenscope Remix extends it into a multi-agent dashboard (Codex today, more planned — see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)) and is maintained independently: please file issues and feature requests here, not upstream. +Tokenscope Remix extends it into a multi-agent dashboard (see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)) and is maintained independently: please file issues and feature requests here, not upstream. Provider quota uses [HappyUsage](https://github.com/SunChJ/happyusage) (`hu`) for credential handling and provider API calls. From 8fddbc18720c68f2de6fcc259a1c418f517432f3 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 20:01:04 -0700 Subject: [PATCH 54/67] docs: refresh README screenshot for the 1.5.1 UI Render the current panel (provider limits card, new model list, Pi scope) in dark and light via headless Chrome and compose the dual-panel shot. --- docs/screenshot.png | Bin 762483 -> 197893 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshot.png b/docs/screenshot.png index 25f9da52b48a639a1e54f947bb74b1c4ca1e9837..7f01e893f1232e72f66afd6d59ce9cedeb2c724e 100644 GIT binary patch literal 197893 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�yXWJW0Ug0+EV|S)OEBq}w$bX@0 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

|8-fy(sTpy{rVTQu`eB~G*q!zfIU~9 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%0A-PTZb;8|^ZD7OgmG z!#(#U*UM>*ZF;q!Lw7$Z^=qOWOu=9T7tel% zV>Z3BkBE1Fxxo~}@#tJTc(%UVZtvRiy?V4yx_i`w^8#w0SCL=vbWBQkbeft{Y-GZ> z@r%b<68bxv!b9^)--SJRt{ql6jw}KlthEk8)K|e7;+@+U~#Y8_P&fFH>W(UR5~&U7o!|i{4&VXArDj z>I5Die2Va%-|Oy1URKR`A5ZM{^!CF1HY!GZ$Fv4{pOA3;@(_()u9EF+WIj@X zVRUmyNvwJ`%bd~5>h+BGOEH9vQ{QVLnXk^P2JBU!0{b>RRsUw(B39UMTW8FohU5Ny z;^b}DW4a2}dTxs@D#DNM;+l6IV`5@tlcI_TTW_|%F`AiJUZzVX^Y`Rn$}~p^!=x4S zs}_u$1=E_V*RvA3d#AW#M@w+7=9%U4*h>4J_keHRglcX9;n3!Nl zL=$3--4^;viKWTNfY9E*k5c-li}*g0dU+1#%3bFs&<-KuLa(ZuO;7Ae6@Kms46s06 zM~3(QJp_?p)7Ohgms60FkfgMJg3AyMbP$hZ4(<(_@3nlt%hj1kf4e>`pS|7goLb}97Xd>R1k2c6Qld4QD@W=fxGb?VEJ*$5A_!)n4-U#~8YU;NWDoRQW zL{1(zG!I4m(IZsp{d%zhUw{9ul`BF%J94v$s1S#hi_$z?G`ShST`!zUwE#x_f#B$oJP`S9SU6 zUW0~|zbm($4lmbxa`Q~WwRR59-t{myV7Ih#@!g|bO{j5kY`1*-c6LuO;VB!LgmNm4 zCkwq2&Ycrix+vShd5#uO&!~w|lu`OSk)19)CPai>3LJl!8LapbS~+e$zH7*|u+W!y zcBl(J+fl3*-ec%OLkPQA#V&=h*yXN?hlVnhU?3?p1@q_PP8&Erw6GX4yCbfrXRX{i za6jn&8&_A&Qgw1N(trE2rS*Ld6Y_VtpYv{RpO#FpbkvMAu8=Jzdh@I624PA-wg{Iw z=%4gzwe?b2Dpic@GXlveZl`<->0r!Q?3ku8n_TphTw+8;Rh7@eN!K$Cj@DZ=qZZXw ztP?1TsfuqyU4|ujM@KDqvod6}M{scyca9!7@7x;STadt={)2Kxv0IcV?IY2&n1{}U zziZzR_*e#^y?Zyiw8hHSR{4&GIM*yczbFQ5{`GMEw(FB?;}P%PAwJ_m&vOntf@l=o zeS25zgwnEDpU{s9DrD}!3935hhoP>n>S%82wq@1#cZ%VtR#D={Z{%hY&535+-HME6 zQBXAK{jUcaPz2tQQ(=e+VEC(_J)9RB?s*~Rwg0Dz^Yah;y(!IlkJU6qQzaXl4IWs1 z7c%!Dvctlhn74lj{!*>&-g+t zVTlb#A!K9c=jczIfJX*gk~_)x5ex>C?@~BSv;s>^WvCi$rUfanrna36)soZF(h_Fe z0CS{Hem5{RRC`<#BDIhH6XB#N(F zJ~#oYvH0lK$tfpz;R!_|elz8n)Xgx5Vr$%fpJ&eT8uw8VCURpD^X^NG7K3SA2WR~2 zz5%}qgX>GGzr6eQN|*d%YMF#&^*wsUomb4W@jl7DE0i~Hq9RT@sp`diH^ZS*gwT@e zR#_=)Ds%1pGH)OU^Z@Tb#sf>mTf6Z&`E}K`>4I*L@<;D=^$u;l4GRtBm>73*bQZde zueSF>ONIEsW`(zE>uMpsfj_77C{HH97?;{c1CI{ zU<}Rrf>59P`H|71xS#)(i}`-Lv0YNKx4k{K(MrQ*C?qDImfqaZq~PapR5nOdI4^!l zi)MIVJe&&&uvq$W@pEoNa+9ym*?TBpk&Kj-hcI+ETx$$jRz_bh|5&)Ra| zO=D%>Rr!SftM(#IlI&%_*wrg*MMX!?>+sLv_OOBf&57MC!1*O7CpW7zO7wLc&(nL* z&B3}$_Eq5HKuq@dx96xFk3!W)m=eNg1mH3-+km=cwm%c1M(dSTeF~e^E~s0IPmE=O z_%30`CmZ5n-kp8dH)n59#KEY|cQbor%4Tc*iD+EMs;S$H1|1H9Iem$z)=8EuLwDKv zYG~;<)|{J6%lc?Me~h@6PFU37%)ku$_P9%P;oswFjUG)Z(p6Ez`PC89V=*`>J{y)zi#dDBK+qY&w9b~2$K-QP4 z9;}!zxlp2*AS+`~6fkLKELKk{Xc|aFzRQ@%^s4KIps9&i{f}FT{xh0W_Xx2x;}1_f zBqcj%6&Y@lzsodBi;l$Is0{z$hKdzJbiL59jd~#jMKmF7srl1-VM^fI^Q%8wVro_e?|L|;=!vt&xSHmy z>t@#X&fG4~PXc=a8ykgk64&e-goN^%^Xpe%Q>eeO;6ntd@CT2fKE6ZXUoXgq-tuwy zU_&xoN#5T$=r6x&Y~o4ER)X`!WG-uU{gvT|FvkGrAg?)_BKr+VqP@ zqUkBg$%Clv?`7N!$H2n};+wRztXJDrS071}vr>|)3Ta#gJsJY?U!UqJWbsQJtIhf8 zz+l;+d45d4F{!kbmn{#XSvt}AIP&Y!+oxeqkwv2&=E&X{LK?CD4}Y(jTFJ;A_g|eA z9}aPZ4~3SwD=|J7NmbpSe0+ImlVIH;LbN;4#Y-Itu$%RabZ8h_^54?p>FMgyImUCF z6tSO~*n=@f^Y2j&6rZ(&(wLY7=BwLOFq$Ejrdv97dhIDHCaoa74`V+ReOH%km*GfW_;Wo@;i?{k=iN^V)xO+NBO|8_ z#l=8gQmp+boW^Pdhp5dNR(0eezF&vOQ~mw3kskj!6HFhkn8J@AX{%|+HXZr(EVwED zu*7Yhtfw1L$S@JohVaI*OiV2hAbddtV$D{p8AA)L7w&BRD)b>^1N9bp<`m%<;o%9m z7%lP)P2QtDl0lIyu-@iTI^@jOrD6hz_3&qy~qOfi= z-9z*hZUjLukdu;7D;$A=L=vi`tOS$7=q(mj0stNg@WK~8BcnDOncC%^t>F9LE7NDY zKexATQ`|nkpt*WaNMlnsW92N2ybF)W(wyOqeX@!+K92d?0;V}XZ*7Hzh4%LJh@NkB zDqqxGU>@BiVD-x?FkF4IJ5^G`7W*6%$t5y>OuLMe&Q$v5LpAn-ZekNj`Q+=W?;fHw z#2GD7q8M)3D`B{&b4{0tX=&2?t`!v(*a03|6&t0h8Vkeo^WeRIhCzvaNEx2@B92?v zbiuJlHQz!^Qfq}&`a{Ub=g%9a`C{vOzYYb~=d1~|h5+oJU6GOU^p2|swH&cd1rqut zik|2OcP9ldMw!Nbgcl_x)is~i*5b?WwV6SEKwfSWa+ugVKq-1khZSfLaa%-vVK`WC zcyzSMX2FYh6P}-5x`J(HY|L?S^a`!4)^RHVIZpqq-T<>QKE42Yx#$nWL*x4iA7n#` z(PNltHDCCA+n^;({C&~&l8cb}+biqTBu`5?oFsu;;MyQzQ^iYpC+$Ewq4gC6w*-c9&j<)3>`hhHf3vNJLUIqahO zoHsv=-Ca6x1NM0suJni3e9;f7;|(Yxd#OA&WDGkw5=a)-DL^8CpNy7CuTDxUiS@QS zG2mLjOs%!#5_|T@#=a}y_? zB1G=}tUI(zQQM=k;n^Oe`T7i4*Im17H|sxYQs;n%FL3ms8811{q0+$?$B>IqZsNIm zv6G?Ur}cwpq`AsW*2MU9*6B-o`>vw%XMLa@eveDoS)ZynnOZdtGsX8?$_3<%Y~m3a zDuuY{r~*&CB@^z8iZ0=2#4uy{y+8*rwPtSRf|22iBN=C}>-+nicX3{m{>r9lWg-m; zWRrBiJg2rM#l@hLw1ZHHJ9nrHdDr^V0EoHK`KM;&m$_!|-KV8amX^54Hy(RywK13# zz;!K|3>zIaOZoH4e(-|^+J-sno&l_DpRSEh;iID}=Eu+m-%4u8?tdSA??A4p^Cedp zK~6AWo^5iDdDp;W&KUjQ2ejVKFm*_Z+&AO!0zA(Y)w z8!`@AyMq%p1O*BT*o=$|Zl=fcN=J5Cypg>-j*T9o84G&^FEQzGG5mbFuuMa{4Z~$x zPC77=#TcbaXeIcyk>O(OzDF?SqDiIYx(vOzY#A~)d#T+Wdsk*wxxd#9rF~$@#ON^6 z&L5>@5dd`(7=Ht}udcss=U9QyJK6zz`pXy2-}oEcho}+PHB6`s!+2 z+|TNp1MJi{l2G{Ib^rUdd_C>jd;OmJIVYz>yzm4Ju$n-@6BHB_7#O9E{olXAm8w!~ z)1t~BG2`6=lKEdczt)@_O~JDwn%l~-ec{FWy_h{08}qf}mZ?HX{5M^blU9@}d-U?{ F{{#0EhVM`0Kgst0MPZ&pzl|7HHtLeFEm+;iYiHq ziUO5f94)Nv%mDzY$kcQwHPx?}VY5A)Jwia_sFW|+KyhF|$`{c@1N#s%Au8rDa1cC_ zU{bEahzKK`Ib{?GTA-;gofbCuxtoxfFaZ=Chtehlrm@AQ_f<_>lfr%5{Wp(%zZo9N zGbDf-g(jUsxHZ5mqBzM(P|?Cpj}Pq^Dlh;bB?JOsU{sw#L_h#7mw)P8ALRw$K5){H z+P*&D{<4wr6{bJ}P@x})eWYLnJ3u?eK8Pe&0VI%GZ5UOk@y)a;X(SdP6!J06j3@K) z%tWHae3IW63-kG82Mq`uJdjBODbOl%w!mb>sS(A=pVaw`6{R#I`jwv4l$q!NGev$5 zpLt^MhjyfYMe<8qR^&~VoXEi3IuW+9 zWH0)=4&6*5bh2u(9EbNFxLjO>C3~{9t^FCG|M4}KkMQbBenU1wI3uox6}0~wPAI|x zvjP)fbhbbeMv#D2Oc6d6y#D|;PcDfaXDG(sc+L<)ya1Ff0Fen$+K+lM4bl!kYTg4N zgOUj2Ye9PgA5A?u8}bcMR5}t%)qsUT9M&gf_)2sRQOTBs0C(H;Nt}cUvSH@X+mi}eN#gC zfX)Mv8(u4-|HZEy3pZp_*w7ffw4bJ)!w!BnNSg@{F@$sn!il9Dd?plb8^H-e4}mBM zZ~OMz?U7>xtS=}WB_II>vM^kWgzhsOjPPzEnOIP@SX6>bF>ZxuRwBnf#e8tLM6Zz( zHcwb&!{>IR7iMK_LJJrRatr=aR9C=4Ku1JJ?Acxszz9)~=}G=?{(&SuTVjy-UVr-i4*p+)hOb&-D&;#Bot9#_1gYIn!!s5aeDitzkv~z{^J8QFRmD=PUaxP^{X_BYqEH#)I zS;Fs{?_PeGd3XKbxs$k)zbm@C{c-k#2rmI%3sM@=9I^&dG^{ghAZ!xB7J&#GC00v< zT0%E=Ek*z*8+#PHj@^NckV%46%0gqd+g7^4OxHqtS?6AdxjLx&rUtiWbBZMgL(q1i zx>b9t=Avey=0QiWS-BOae!R`P)yvY>VX~>FvBzAp<+erFe8ud+y>-}b;5@hK>g&Qq z@Wskn+nT?t(}x7EMQ(T=TpkK82aoPa730vx<)&IL<6OR64eng`pS$PVbZ!ENFJ;js zvRRYD>4Dr7uHdfsGbgS~9tWM5?ibFco#gGW=gWJwM>B49?*6C7+XD})N8f|&M-kzh$_&yjH%Cys5bJ+O4|EKG!|t-M74Io2nZoIwLyn z8~?>1nLV_(=oAKu52gXG2VV>>0j>clfWU&31kMgMGypP?9ZWRMo<*CrjtwfpMVu{4 z7@ifv0Q1TYkA;b=fpO2bLE-D=sSD0;qWpOvB0e!MkvtKpNUq3Cxn?eE?zYIbP;l6D zG-c%O^GuXJiVwd6s{-~4dVG9*+!mG~->l_kn%C>?#Szv_UsJ!C@O@Af96MGiBVUu0 zGm6LDcW^rqxxs-DWe4cCEZ&?k>5V+D&$qkw2R*w!*OQOIpqj!J+u}*KD%SHtjkY(3 z`vd3?DK{xAB8f#^ev(%r7 z*4x@JOfydVp4P&!qTB23bp?AL^^NvZY9y_$CQdbt)>8gax`z?FwcFb1)=kZ=?5$vR zr1pYdfqsJKmxtgjq?S2_SJrwRp34Yc2)^<1yDwkNZ>o2noy`vBUtQd< ze^av8)9;09qqLp9?V0mg|7iTG(xm8W_`TQhS^U`tF@&gHYfnqnRq4z~nqH5#ihz&p zm(r2()sVbfnp@Wy&W~Zqew43?>5^!1o7|Y5)jzn-Jioydp{*;A#fvz#!|=f z%R9^Kmw_s@aCEWdzbu`uyq5h?(9BilS9MAI(7JuT+Ogp}M8BK1a+Ey#(86Hj=Sh4& zSQBlG#mfL;(CPB^;U<*BeZ7OWR#U;yt=Ib$_e^KcE&L4O;{Jltzvx7D|iyXi_KD1uXeS})#vhgYB2hiAd5w=X>SJTLpE zBcq9m;`!_XM2~|%=kM7M&!Q7ciXP{p94)r8`k+4gv!yX}P#ns%qp1Ldp>k5d`@3Gt z6(Ya_HemqPo5j6(Ojb971K@$bJ}2Y)4k{g}gm-@71B{q&%sC^>vs8?W@&h970QI@K zzH-((Fy90mnnlZLWX#va!TE0^0Kb~0<$tC3{yGWlKHYlh185FGgFq?qb-b%sJ98~* z3we0}&HFht02~At0P%hX@_q<_;QehGy8idA$EC2L?;kKuQrfNn04DkG14LSd>>2RJSd zNdPtwG9vIbCXoO8-nZm0L{}GURZx?vcisJm5MrpPB9jYDGu<7q_`h2W9F zJ_DGQ6mh8NV<{|@#A96+sho2MapJ6C)yDJq6|4q;Y6(e6DZntZW>Z@Vtvq#TyIAuHLQ4m-#ApD(>Xst~_XK?*F^=|Gtf^`9qyUn)K@#M+QdWh2v&Kc7z=X~1G|ia_{P z={CBQq{@W~_G*mH9iu6uk@%NMSee$Jh4xbed+e6kS%UTYUsAQK3yTGLiiKHy#~7ZD z$5-Q;U@1sSrRhTKg4xZ8RGxq=oe-4r0{EOMsdX&JBGUUs#&O0t3Id>)+1Pl!Z{EXE zsq1Zyb!+PC%LUcSUS4M!^t>LY8`atiey;s&H18f+UB^inWi$t##A`<-cVWZihPN9} zod>`wxF=AN!_x8GM&id}EQJ)=z78u$z$wcA<3tdX0tK9c!BC!({2;X7PI`aUbdX!R z`Ycgr%a9+wHz@oenJ`Qq;Nw<=pU6sSLax0eONpy8I>y3$T>?oB|EzLv3VG@|!oiZQ zm*AU1#qknC8qIfF5+L))Q6lsuIdLxb3oP)bJ(yTJUNy4@`ezf0CFZAN>R}{Rl0)Dz zyi@t$=rpmp3pwN;WKqF_?F3UJ;rK&OD(c8==nYXTjnR|p7zhV zmV8o5{*A)lU|fa6v1-RUl%xLp2FPIA*i4GBRZ_#bz~$R<$vfmyUZc26Vg}Jz*4x&sHc2V?$qPN6 zKdx@j1_fciLV835$G39X96W;xf8LX z7%KcfSA3_mqc}$t>zJlcK3zDW-J9*(oo5HdtLMnb9hA$;(*pkpiCD>H1m~8B3#~Vun{g6 z)KMb1|Dx5X&LjX9>%KBRI_WQ7#y$%T%7Op-kqyJ0ot&pQ0}}%HHV5kAVK^=X098li zn$iuDA?6fcu>9R83RLBZWSXz6#Hk;b6+-K-V$Ch#{I4 zNU~Cfa+qVkT`D@KoLi7~7OM6$vk}Ra8x$)&1gbnm=mnTBU@lHblm(V$ZKC};WvUB+ zeF3?$i1}4yoqG&5s$~Ei4>Ty{J5T!M1g<8B+nFjht+)wbmMMy_KSUMvfsZL#5t93Q z14Y|mIz&N)edB((uB6vXjr-TjJthIa zEEpVQes)XDaR2+(GkO65?gm1>8^}%u0wv86+LDRsWH2HBECR#;YxQf%m|PZW78N2a z?eGGyBbbZidssvcg3uQ^am|2A?(gM)w8(;=;lQQ2LEJU$WK+f924Xc=Y;Y06x?3zeJ2OuqX=AdAdkURyuJ5gA56M$VgaI zDisJ!eQxlmdi@rPd7=j{g+auY?&%+-L7u3H$OY=oMUyj5AVLv}R2y!YDTHPI^>%?4 zywZ+HLq;jrUut&y9N-ow^mfW%V&cuheS{49K@859fPmZ>1XS%3NbG@lib#^o3>$`= zrQb_xa}8lb5nm+~2$S~y`u9GNQ9QBPvG#j$`k66@Suq1%x0Wn^`S z)7zPY06W2M3PO5m0bN&TSpicBamFIp1-v@f6&=rwPCNKo?mpNFc&si;*BWMqQwdt= zWQEM&QL7>7q0*%2LAZY((+D{Iroo#P?iGEx!%CDo*l!>!&**|Y^^UBlTX3-nXp9kO z$iOZZDoc-8#=w!KjvPX({&xuq&Q`}WCOH|9^>>}mLJipOi+V7NM-g-hzjgh9Jt0#vOm z={fxmHb`wL?8I20Ai(oK{xn;xixLEdm~fWUa1})_vIh~t+e=9L3ozc_0OKPI{nR2V z^pjK#n_^yN0U64}I)xk-O0A$V00_1Xjcf_NGijt3cj&TBEG#^UWCYDGF8YZrls)rr zvPA}arg+DCz2nE)KdJ4v*!VdT!1288-5|)514#JQ9;>6pmPrtZ_1Gv;ZXSuL0)=?l zOI+YY8h~&XtdTeb5eP?j;HC#Lle!%VVq$b<+0W9GHyTKOHQQq}le{*EzVvEorl*7J zg*5toVd%1Acn|k+OxE1tPF%a$keL{T;IT~@jV(Pqo8+o3Vh8AObO5E+#hc@ zWgN7I8L1AtC#meR)BzDL9eU^D4TVkl4iK>s63bzZfYWCb zI5>IW|7#?OTL%0(W)C^&i$Wj>RXNGrPZNTkMxnW$K8H@i_sa|~UjC-pSt$Z2w<*bq zr-C6itHvfKvi{t!iuDH%;^qZkp4pgBa=570-?;bGS~zN@aBbRSh!=^!J5|_1cL#y& zF#66NYIhufc{uA6Zz>b8M8{6R>_ljEGd9KBffNMnQhcE-gy>zP#E3Le1HU|m3<3(X za#*0=99(AUI>-|kicYc_Adt$l634P3n~DC(GOwlu1-xyagEid+nsfa>Dat04<0^A+ z6pb2Z;EOqXT&J3WhymL>!i&0|CXQ9!jSkT()d4EAe<#8Pb)v~RK6Xv^A4bHD-!nm! z2rC5fU*W(5w(JG_mfafhbo|>K0f!N)zLGP}hwW>U=-XBVA2UKe7A0TdE?`}_6!XIh z{hL!*!4!B`$M^)8>?J|lPAuqSoq+Tqc#S-Egye2dL8rKSMq}G~B()q8f;mq6e7p#J zfPJ8+x|Vnyg1iTOL&6tmU+s}kn6@-9Qb6|Pe&O5H<;)Pk@}a@LQ!Cp|?d)0mO&$Tr zd&24O9SN|e%->?Bct@dUD_D*w%jp{{3j=*@clh2TVm*tlb5u|wPy0|7DYS>BVLVCk z#Ub`9UqaZRJ0YNmSn7`je+x8ZFwFOSKOh2M@s90(q_{5=a+XYj;2tZnWf&~JJ}Pu~ zLQOO&k>2*@Wfdv*2@P^J5wFx1c6Y{vcZv*ixO~GqoxG<~;L6t>_~2M>SSqw>e6CJX zHz_SRbjXzi0;AU{-vEA44N++NHe~b2BMOR#xw;NXeqZ@jn`4u+jwIR_>;i1zISaYC5bUkM(F{2PGR6`-RH z7qd+{d}C9Cfz8McW2k7LMAc{9VS);Y;*bwh&Y`4}{P$ww1SQ&#DT6k(R)^RPYfhl^ z&4?_C4bp#=Fc1=U7J$Kko@W~MZ_)Ju^7GYRA(RGan9H_cUh3wZ2?JN4<0&{x`*p+q z0JDtDX|wtY5KW!-KrZ82At3cp9J3LzY|L7P>m6IGXE;iS_R$<^p9$)Q=(=0m;E^QZ zQc#xTGBK6N#<7fqG^`1pBtbnGp`av($?qDXFdX-r4vE;W%Hgy4 z$W;F)CZ8T5+U;Y@!PDBis0;&~*T4tktb4Fi2c#24+mD&VK)a#Xc=nOSNfG|hN54a* z*yx=jR1(A}{wCCCNU#@9%Xh&y4QCdIGw3`n=p=WMI`CelW}9x8G$ivFpIC+liGQ3k z8=DI95y%uxB>l74lNs=;tM1y@yH(Bp`PV-8!Ld_{FWagaTV798MZqUk(9&>|l`q3D zA9WZp$Kek4k2^wYMa#h=pNzWUUq8{_KDyM`h8tauL1wdngs$E(3z$+9FfoJjxI+pI4GrFA7B1Ks*bGOyR0t$PB}8lp9|n zh%m!^zd&T0hb|1xzljR#_l1eKWAPXzTGhQ!%GsN z_Ha^}43-2L>`RPSdMO)6&zJe!R5a{q5}A{vH?k1Z&%A{zX+qoB(j)9p{K-^6wi9mn zuF(UWRyrHnGpDF5G6>S&YB~VqpkB9mj_lIYUJ_F@Y;BW;51(F1LW|czM<}tF4n*#3 z3<}=k0EgHF`Qj$acF?;l#cy!TvikHk7rl?ah%q?Xe6K5VsJdJ zMo5hUN)L*})aFoy$a11Af@DI6gvBsG>s7r57|t^$;1~?@u_#g8!F~(IejRLxtP`F8eq0*g-BDo_O26v8t~ecGSG%$LbwF+o$SQ zJ_-)V!;WXL>y}2XvceHx=ddXtvgT%qWaJE)yIk2a^0daVhbr+)SvIe2M^rzR$S!)P2t^*B{ecN za7v7zcPN+Lh=4lXkqpwN!XcUQv?)uPCg_8S6N7_ZNoPBukE)}^(e^~A2f$1JQ;xj9 zq>|^mi1~DluliTx1`e|J`(tAX#IXnE0e;cjv$wf}b5)=;b``@V5*QMgM`;NImrz`i z>Xwv;r9qYLOLAbfb-G|PRa_86?*E{FKRmGeyfMY=+D?1;JdMDjb7?g#Wl>1OY&;S) zX(Exb|66F?X*q>eS}5Z~qXktihL1yac(N(XQCbAib_6mYfJK1(o9whIa^cP+gHGN& zGYL4cnI^2^G~Dz&x&dF%A?*-F-bnxaDPBQH$)a}Xw<-FgV+-v0KXFoz#gtITZ}4I~ zBNJkORfGQ=)2p+#d`#0DRMDyHx73eM8I&-A#Wk2aDt}PYUfg)3+zpRHUL$M47@Nby zia4j9!#D0QG^?r<^jFTlr}7D*lMw`4INX5#|Ad|hYrz{L=VulF9=y)mf_bztT;_i1 z*o1H7;t}WDG!@c;K~x<+9l#K~C5bqfc~936N{@d+77j86X9s`uj3YY-ofBs|BwIs;AAs>^FQqVUIpMm16QP@A@~V11^9n8_L5(yh*Oo z{84Iwa#j`iUWLXUP?GI%Tg?Wctw`}y_8N0Ya{DKH;s~&xV4&1V&4?nRL|FdK-b#Yl z1sGVTqenv8e>K0EdBKdO=@0Nmg-OVFXz1zZocry;kB`N(jB}1rVqu-Y5mOg7vxlVl zMT(qKMM_!2yU4K10~-HW6le)~BdCkpSJpDSE)iaMA&#b^zL!-f#YP(^rnJ$Kz62rd z%1DXDt+o?Q}T3BxK;Oy~~usU6S#w(Or%? zJR)u|DG7Hhif#@)K`oAt`;x)8q=-F-hiqrECrKau*~}DN<$O-&V6dlq2Uyk2a9V&4 zUON+4G+<%B>;qh9Nahh2`if}c02_7KD$ zYxzE-Kc3KYdk9k`O|DWw`f#o3Pu;UAsGN2H6RR8s{bq+8Ir>UXAHYXV8czVyCy1@YUu*FN?w1HBz+q&y$}| z!bj}sUz)}U&YwI=J15V{LF05M>6@LBn9yL&a;1`wdVtQJlZ>KDTy|N(Zxe38jvN9TU zhHtx*b<-K_KJVT2Y!1hY%@#N;BO6;=9A$$%&(xsGJF^NS&^Jp*5N}}Uk39WrR2H~J zi;8~1Q#g1>!eo8jQ*TP?-kY}F4Bjc7n8`I`yr$uS!q!i#cO?yI5*&=do9slgdYah0 z`EgP+hnP^&YyDc?6ES=5Me|oaiu&L8f@llqW_R!_kG3;|#OUMnzD&&M1%Ftxw(XT! zz!}*#lAV{a+y{fxjxs1kPYE@%=;sYufn1%Kn4q1|ZMK?gkv)0~C-lCjF8gq~frHJr zrvx1ukPwmRx$>MG`$WurpqYn8mH-B6|GA;~wS>SZ#!RJyiq!ZXUpQo&E9$*y*w&-e z1C6dPUN7Pdjm{H-O?O&#q8-=Q!D~$YUt%aJKE9TC_n~<=1&C1DiX|_l9%~y#GFQ!o zD(rk@pn(M+?8DtLHUMWR76~_SVN553ogC|BVxUpcpNc%iE-$bl?(-ulrSqH%m4x%t z4B8mI-wnmT{_};j8ix2u^EcYhQ{lHMwQolJu_@Qh8@{|#9c{mKxDuBSrI|IK<;(`N zVq7-2>&!jDdd~#-@(@G|b*f%Ow+sZ@wV+Qja|lJ@5$qE#msG006#KKWhvF$Uldp)tq;r9UQHmcNPRz(WPcL4KohxAC5VVhsi8r;fg|)e5Qmy?gGu*<7@5ku zU0)Bh=ey%#c@Y-uLTkTIeY7cd&h7)$^}+E-bRJ)Z78=3Tu&hsEAYbZ*V;DK*B)$Or z+G0K`*LQ=6f0F(qNBRwB0^H#ztl1Jt6>yvP!IO;kkmY)v@5i`ylZ@&R=WpfB^EXwb zsWDh)C1VB$#0er$7e^PoZ#U6l)&rW5K8Hzp|4S1V+` zN_Tgx4FSW9zg?i$+g*)0?$9l6NVNz8?#jZ15B z?gf?20w0SWG-KM7L>HgV1~1{DsU>qA%`FBGt$#IjXEMJmx0RVd@ud06k`NL~H%Wts zlt^n#<&LGKfR2n6Y!jv18`WpvSEG|~E6;~>r|%F)h8%VooQEH_R2gHHOg1tJjEBfp z=|jVR&1~?Q3SlR*`>;K%QDaHZsSRAb2*CIk1d^KYSY=R{a8u~*Y>OdYDY?VrJ zrRuy%&YAtJ6SIp+-1067D?UA_i#}rwZK`7Fk-b6`&yMpkG;#3TxXZfkzr?Q4v$}FP zV1Hlsib#Nh7L;UF(N82&+~{4IoPm(b7Pn{l;r6}@NR;Pn{#%#F%H`MC1B`|d5)Ey- zZ~9d{m5ubs3qHZp<~f^5qmyQ{EW4LD217)0?m29qh8kiBL*@Iill~cazDJJG+wYF? zi?g)UU$+gL^z%g)iZia}L=p1a762#-f|C8b_u$ih;R{+X9#dZPK^x=(cFi+E`!G@M zW$+lp3`&Av9`k#sGoAlmUYq>uHFtz5Yge{56J6&i0t@;tH3Y3=zqu{UWMSO`)&5?i z$5tRso96$dy@~9aq2ILE_;mww{&G9Q^vMp6kdq>yfp#ceP?6W(lqYZAdTW*s9y;LReycl6{eW#W<|2pQc%kJ@ z*A~rN^b>31mr@&?h6L-BG!82*gUx#5p+?Bv$jtBcDCM4^nX8`pDLt5l^rzTL9`V@q zuSK#S{Yi#e9>nCL3nE@gouV8G)3wjQbLLj@x&8OtWG8cC@g%z42ub!q250AD)HL@; z(b|69brQAT@eA@!);l`;xij9Wt*EV>*D};>(xVh3A5kVrb0|2D?_sP7@X(;l#S^iQ zy^rrEp>yOa=3@WHUHrd06sH5>|4o{?Sd8+-=7eB5%21>7*ku4ohy&xBoC<+C^C}Ws z&DIP04=-lllS*DX!|9X&4=RUf4~^7a?FAedvFMorzrJS2(6FKKYmTk-3s;_t1p1F* zVQ&gBxFTUNJa+G&xJKq*vHC+w)V%wM`rSv*OA3-?mafsk;`aM=3f3debD-bs2+@|` zX>dJ&F281aBd^?aBLgr}3vc+*+_F}f=c1Y2u;c{Z)Ty4_x`g#6FqBT5QPX8U^Kxabe(hRImAi*q@VAx?GK>PwN+cIa8kDYH_|fSht7rNN)XvfbE1~8 z=mEv99*qyN$q5UlGLw|~-rkEDW$stCARKkFfP*&tn@h6YDxbmT1FCk*hDqB>3-W`|Zj9 zLf?=R-+6?iYoyS{J@ zVdL9L>zu^+8g-Uf_NOMXF))=d4p1euFWWX@z0A6Q6EX7a&FbN_TRi@0Tidx?7|jLe z{b4}gtU1*TFd%YiYd2CGG&**5xpAoGKYJ{e-jyvkBiyyS(ZxwW>&a=o&+ z_)w$NIA|NDsFI)R1dz4FoN%fT-KPN4O-|^aD4cv_UiUc-%6#!7n%-lvjb>K)L!S2V z|En8QyN@i7uuR~{K8@eUL14RUUJa6xwfnGCj}hmmMdT7LD=x<8=Y+*#p)g^*8@MmEAIA zX0UDe9U_b~mP%o?){omh?oh*yHm+l+8ltmh2=nXjFjKPgc5|P#NS3+EiZ}l_fiW^u zMw-;r@vaUPhCkVujWAnfYiMv-uQh|Fv*JZz?2*R}=-uN$NJ2$-h}*{;Q_~wlb_JJE zif?CSWmQ&I60En{uXikXSq?tj9BwP{+OvW$@cj0$Bql9C%A1?)Yyj;6;SjCp_1jy! zjGy@uboIrnB?flo$~b+z;Ld9PH@~)GiCJNHPtdqA-=6}TF<)uMi!iP+O2(Va2&bmE za?Zg_F9{Gn4MsU}^#tf3&{U_`$;wRNaZ)>KF?7KPfwSFZs>h7%I*xqO!H$@o0YyJ+ z%J*0kbo5cwS4!(+EcMc}uxFOAiOcEx-WfaazcMTS{mW*ilt4%I`<|CZ+jKEGg<(8{ zMjT`U`pobtQUhf4vX=Nl81V1{XM5Gfvj{)pdto11}J8d`29mb*ZG+(!RB96a7m1o~)if$eut0$KQSp z;4OG(`}uijQAcn4l{dXv|uN-3~}jo1+&KC8S-yq51h-+}i*FND^G!_L9Zxm{Pg_=*M6WgT}9-y|{ zST3#L^=-CP@jy5jnQXDI`+#1dC$Yv147i&$%%kmW8ByP(cjJPPexF2L&G5{BJ8k(Y z3uK~HB;D#(*eWRA-xfvuIppY1YRyti{PPh6cayGLNoOlmTPQ7+lqjZ;`zzGQ*!vtq zL!o|Aw#57cI%+~qlhW6AVQa86fLA!@6gXbpx+P<*HL+cx=#N6JMP{6KpU(joMEWZLJ_kl;S^u zyFC`*1t|_eJ!Ka~xWX?JkTAp9<0m7#MMv!&0IBhC{xrhrXxM3&d9**h9*y;#kVYn8 ze&I|8DH22$OngYJ?gZ|AbLj6tZ7Cy?(Q{)Shj_y<*fuiw-|d96Uq{Xdb3kT#Gvn4S@PL`8%B;!&Gi zm&fh{15QCR(zp-F0e7t0ZC7Q7%El--Sy@H&|n$H(wk;AV}Ei#)84FT8M` z`}3DFh$bhx$bu|(n!4)payA6&Lb#%Fhg^oQ%9=s=mRumwJa*_d9X=r&52Il}mtf2R9T#05mA3Srp-|P2 zK$>gqkKH%Zw-zK=a`L6OcQ?Z;5s2N-BYZPxa&5p!F4 zimeZh(IityWD54FKWSd)lz0JO^wxP}ieKm~f zw;t;Z!x^uZ57%O&<%6rXUMLNt48ouf4uXinZ$~BVxOm#DdCHVDktz~aUz$le-G0U= ziXbj8gOw1gm9wS*eD#6PS|e{)u&w*q$P{P@wC3pGOr?^j2MyyGAtnE}A((&!NuuaB z4Mk=Aw?ptgnP36SES<wgd==8i8%O>M+$2uxv#3;k7Bj@Bwd`}a;98MN>6thpZ zIB+JnD!T~clSx#L^UdAHBVBg#lr$tHJ%19Kk{)FJ%wQV5ZWNwtepFavynHqXyW^%W zk%9Lo1p%gy)l(Y10mBKEPL)dP2Tp}Srd=NHC?2RJ3Ti@9IXIoePhbj!sC3*NwfFb& zWcFgbpg;b{f*8MNJBEH6Y+8RR|9fvp5!S`Ix$QP{kk$=hun=?{9-O9~5tE-*4IJ>I z2h*${UCvp5{j{p2oEZ1D7-p~B9%qSDS0a_-M%T3YXATXlgJ-MxJpaAIVbHuZ=t1TC@4`TGn*e{OT1eM~1hXcx+bQ9| z4b(s|u5b4gP^S%DiP=ucX{4Y^;eR(30;8$t`6(wo9D;YuJN!ZlUvVJ*_+A$DZD(t8 zJf`2+GauRv2Zz<<={nZb8>A2m36vEZPHP9J+m%%izY>!s+pJ3GIvfX(EcTvALQRZD z?r;{${F4w~=O-C#zcz|<7-nNFvtmE*Yw7K`;(R_v;(06>pv*-#P3VAoQd$zRxL1?> zvf5}q>F4qd!YyMs{mEH1r-18o9aUZVpt0WygFzQS@fVOVcqf;MEaQ7})S^|z?oA4~ zi(JrhZN1|lay6AS!48%5^5Wl2CIo*@@ZM?}-2Be1oc5Re+=c-kjl(!zSf;v5t`WW@ z$AHQD2=vO+sX?INZso95l=_!@}#|5!T@^#F!q2U3nTbY!x%9ywgXnA>s?*Cc{4 zVfApBHIp4|j`knr`y1%$+D@$Be4;&lx;@IqcU|ha9eB>=b&D#X3MBPYX|P$cp+1te z+Gw^WQ45|3yO=rFo?9}8iE11{$R=28weI7<+tlgwdmH(T{G24bM-7Ux%NFEUgAY=}GN|gFzoLZ)X=5nj2p{`p zwW^~1*KY3H2cCmDj!VaQC|MpU&xkNEn3X1{2}pCcLWjtnG)>k|kIc+wBT$9f*Q$so zX9$hmh4VWwD4uTa2@dR-lj>2GuWY)jnZH_;g zE`d{w7#q&@Rfl|6#bIj@>Y^^a*xRE@D>tDNQ4<8@EhSyuHpEv-Gx5)J3t_-nHcFbC zsnHLRs6xUwUsinzOLh;<@E;?!0>jAu?TZCqOAWklu_y*c%KbYphJ}2LU;3F|DF~5I z!oP(oD#fB;nn~dhfQIP3g5)0+$;SxCT{R1FPeVpMrqsz);QuQS49aK^i^O6k3udQ5 zxhP06a@1(fFE?OY{?iYgY7~`6hDKF;-tl&nqr&JgV_fr%g;}$%6Bz`L`E4}aRvU=v zaH5ZoKSp|f=va)#5quY}(?6?&+$xpNuGQzM)9JDJ*6DU?bXHaEue{c3zf$5_=61I7 zem&}Fqam2HzmA38qi$%089}$re!bhPi5SMqc=pRt)v9?wF+tq7^|dCi$+S}WBwGky zfBQ<*Y2UIGi;hrt%e(U9`45U0dGre_cDn5i?w0cqN%Kh)Gt*fQj+&dj((9^SD-Km( zJ8Y^_6a8wzWVN=f68PwdWp{J-DT{26TKR%Hv%w-#t5P1 zMuoW&0)}ceQ%IS7`c6PRQ{lSZ178y#E#RYGmr|D1G=JS`_Wj)2dYwTkdLe*uVh0hw z?`OmNSdCQhc?rS|*$Kv;uW+2jEDW*@ew4lFByXoKH$2$#KuxV!hg>}9sfc#hLpqEi z@OOKS{eGG@i*W9>p7l9zwN`uGbJDL4tM6Qh znxCoADW6U-k0+Ipt=6is$X(Yu_5n`}KcXcWncw)zX%?2&3jc3dIsXAuYll7!!s^C!ah^8@E&@mS(z&-Y~jayEl{l zaFE4rt5oE)C(j8C6?|DCYIC&o@I&Z(>m=mwoXz96dJE3$aVD+7z|mh)o|0G9AEUc#Wfwz5gTTw@9nY7e!cB$x1IOJ zP0=)+HseUD-8lW{#XJka{o-T=4hD@JQ2&vsOvc{3B!JuwQ0{Bjx0kRT(#{VTg=-^O z;qdv`yzb*k>&P-YgYEs5|_CD!G_B`T7`usczd= zL(nG_ZDM5;7#b>0-&u5(#wfbTB)mtc1#+h0g0y^>0U4vPje=}&uFGB z|8lh>_y_B~2z(eWA!z=@D8s`sY5a}1Y&xBGGw5Vg{VVjZ_`r8fvRnU=3!eG*RKKfq zIwMZNk!_%={$b0jE}U59`_0o#n*;Z+Db`QB;nw}0k@e|9Ni^!$D47!v>ORD@kW&(8 z>e3&Y7ZVm;QCn!yQFcCm$>MPQTF@M5W1WH!j0!FB>|NaK>+;@k*}WuWHhPxSJl4g+ zZS0{m(=(TdtqVN z4iQ2hSDh?B>g*unTCiYbfwBKL3GcKB(9kPCQw-SsTl4RS2KveyQjhPus!)h~e{3h4 z1k!eylc)iLzDqbs#D5f#yNvd5h8ePTB_Fyb4+Lfp)%CsoJgW^Ic>n#7L@Z(KTPCYz zVPpwb95$EJ?zL_Bw9>x120wl*fP zST8Tb9_n$juuNR9DXQzu_Rq60Xq8nSOUd6=aK%jvP6ldFY$qvg@vAVkg77HoM0P~P z!<%X4)ZUvxHpf>fPFMxse~Lw?(LWQ+w@L3!C<^|EC#(Q`Ye*S5wN<|%#Hpj27-^rpRn>gde5)E_Kf0`<#IT_y;%Nw;^UPIUC}p! zeIHBdxsKPv`TRH^M=v3-(P`0ZI5qEE(2e+HolL=r*!QV><5^?70;$EPR6ZZk%=`p` zLbT{@6wA)&j1iDha}*HF;)8R0x6ti005{NVpqG|Td4luPqB=Vjlqs7kc0&pmTtH-A zFN9oHD&z6WUA8IE32BE4h#{Y~R*CtTs1Fd^!l86|23 z<8gzNntn-&>BhVCH&J@8+WX%UL~C^0dc~s;pGWBTFtoPpG)v&b2l9JwJZA4rg~Q=; zEZ~x|IqWAi@?Ew}J{qu+fq8JD%|tq0y#fc%9NQ-~?NU_i-Y(33j1gIQW}ZSC!1- zv1b{=29UVZ633FXVAt+N>LY|%9HQ-|lhA(|Lr?stl^K9d1^)XtBhVT!e}5}w zWM~HHgCyYGA`X3TN&~@oEq=$xj+oPkz-zqxX4Y-tXucOR8uEGW^2!R0c4-;=yoWK;iN``B z8Ljl6O%^O&hr$rDIql~lxg0^d%zd9GQjr92tmLO1_^>os%aP>uTD5z;ThhkUYJO_5 zfwgq_9z=x``N2nzI;da%z~QnoLfcT(*X~%sVA!7Lv?y5u`Puk{L}?a*At`df$L{pi z2Z_*#Xj7$l2;XU#?_4uR!>fw|I+#+d7-spgU&E6FoUYH?T6t-rhCREoa@nrxxZIGN zNd$rK<=XIN-_ix~A==CErO$AqO6Ojn{(;})r6Blb|NPzGF9!pC{!CAzOLRRka1Tho z{vV>=JDd%$ZU3*TQM*;EicXYTtyv>fTWwWQRXg^m6{$18KJuYImKBE18zSzqf-J342{*fHbS1*zi z^l|%}fbX)mh~*8J!^K9<9^H_^qbp^*z94GvoAuZpljK^^aB7BhorpB#!^9n6K4+c@ zcn8L-VVvY~odh8;nU2TqI1c>t1RO-d1cs-y^-aGW$Z$}|#F1hparg}Y zb$>#(zsu{31mMhJAKwRHPxM4hviSQ8Oq)`zY_gMK`?FQSn_mp2+tLkFD=dz$%3iS} z_@r<-1F2Yse;dt`X`Ms7)TjBs?sK=b9h!Ynf)eSbjUNk%XQO$VQCkvM-`9L+u=mFf z!t;xhma3W~Ri(o~1DEMp!ookv1|lAgJP-X65GA`K5n*O-Ze0M6DzMWSz4Axr|Midi z>;IV;)yOmxNB@g9rt-FdRE{Ha!p@dIJ;iZ64pY@cJ`863PyG`W=NtiK^CLufo)%>~ zOV5Z~8INBKD>GF*-uFtnQd23mUl1SvWU8Yrl-8YB2{YC!$o?Rlc#WET?L6^Hjvmy6 zVCNjQQzeXU-2^RmrIeCRK@<|4d{Xq&V#=_2cje2Ul(%C)K`rEIp5v{#j`EIITc_S; zl~(P?>%H4)OC?jT!um_Dxcw6OQvg#yV;_7F74~DI|jP(vDj+5!#Ju3UMo>t z?W0u^h`BB=F6Gwt!+o~w3f!Q~rzp)kZkXM7lzgHENICwizi_z3hQ~n&_MMS=NT)b{ z1=wDoZ`nd6u(|TOw}P{K>yCp-ot3P5gTb5Y7%TzBsuFYoYAXG6^S3Ojms3Vc1xJk1 zQWYK^QQdR(XRdzdaIH&Y#8)fq_uEK=D7MW%a6&sl=yf*Bx_r4QWW9&~dWxLNdEXs< zo$BByqjOr>?yTl$)M>W1WDDcBx@MQTw+dzP5|GzyF@}A14AwnR$$jzTfSLIY~=ws`_!qsE--(EE3WZ;pIMuZz6NycDj zZ#-UZeiIqTXQWF0q1nu6L}?ubQ>UlC>n;)}2a^llM@BdNmot+g=C$r~Yo~(&e^qY| z|C_RW&b1kk%*)}?hdWoLj;@UV}z=d91lzLiZseU_ATcf zE5Pq;+I3n9mv=z3f+W4;<4dTaP|61eEf>}q<@OMrd>Uez{EbIfUPWRb*o*Vrk~|)2 zIMbH$V)|dy`#%wbjo8pps`jXU`M>@lH8fGmw*pTLzqO*3yUwk6_l!QJ>ej_a%DJFK zrx11Sj5#lW1sojy;^|1jtM(j$Ew=+h-l_*ZU;R6nJM655_0DcAWM3_-JKEZVTCYaz z7-Rf;SP*aL?=sN{#rz&$=@;pJlh!5q#?Q=&o|gTu;$~7}9;EHIP22z51%RE~whTDq zVk~hOp06{$Z8O?N!>`~j2mq@bPU1L-Jd4#q2X!-<0rU5Vf6AVDV3GjT;^quZ8XUNu z>3P1ho$WQ!MnfwPTEdV6g;a2hUd-GQKj`7aZYk=V^>qshCZVYEVznWWH=r3wi*Jv2``!pB zOIZAMo$t+Y((t4cpzTUMKCPd*GVh z#iFGoC`HyaCn$4TrcU6WYO!xlbmLXy1Ljf+n&_3r{GNbgZOqRnl95E^?D>#Ys}SQI z)yEZq;3HaKT}bzvWiZO3jN*xTso5}C4}zT%04?IXSD`!8g@YzVSyw>e%dt@w(H#-u zQmqfh9wvr*8zn|ZDxZ1epfQ_+)^oKVYQPLaSJ%gzy!U9RWB1NdvvqiNL@W;?^HE?q zCMk~kJO!ugr4}tAdlLKAl}S%Brppyv@#m-YNYm!&K;EbTyA$DI{jAYnGG2O~zRXcO2zxBc$jdz;j-(NU|;O7xW4*{{T(veq|I zI%JQ3FZaKBz^Zn`RSWM_v{u*;a*vi-!pSjJy1^*reXK9c)Az1@sg+egTJH7{LDBJ+z^>}P`ieg)=Kj9-oN-JMKFi7HN}44W+nt+NKvV0${~9%R~<6;4oE3d-_~ zrOl^$b?`Z#uKRM{^{FiE3HsXe^QzW89fQwXq-3ab5HAQ!%4Mv z)ke&u;^mx?bOHs8zjGKicz|c{pBhL;5+mVHyW+u^^>z`~$dA)Q=>P&+w{a6dJ~RJh zcwyPFcBUQ~K#1fA>2+GSiUR9$#h673oL3`a64gja<-Os_nN2=>U&4M+3z$u4t7DI9 z@Y2zq!P}EF$DF|uI`t0_6YZ1-hNavOS$dU&8QHoNqft%`rJ?4ZLhl5K+=B6{i8DSs z8TxL0foB){hJRl~<<`Pe(GOY6Z<{!hI|8KuT2ygVm?`YSDOi%w`;L#>Gm&FN? z5pImXk~X6g<<-m1@gvB<^xUD!DMmh~%1U}GrfT39;Fv>(RxULkD&50Xum^2sGh~jB z7Zf;qeNi?vWG3?RIWX zmz$k7V0ZT_TF(Js9HGjV#7`|i>#{0aH|Xn^u){SYDx6pNqVL8AZtZaOvE0a@6Vmn- z3|y2+D+@S?Q?MH<9#n9L{jn|jwwN1C@S&y0TGj*{XtDzySeQq2#ZPNznw<3;Uo;Oq zm1||&?^j-^14RYCO_j9hvA{>869fXw6x5*)*EeoydK4%r{DaI|dvraEKy(g9Mm;fc z?0fS5nM6-X{#cxIb>4cT~Fr{Q_)QNwiwc?++~( zYpO3^GO776@G745EcFf%uY;<3O(HA6yGM`YGT>I^3{*>a1cy~caH34_SLS!4NGwSi zKcQP`&@^gx4A&%#3V^Ee!bXIU!`#}voNUz7c|a<;U(4@i?bmOJQt!d{3__p2PrkU- zHXvs5qOJoy%CF$&I`t#CR7#PsND0JiFm{fp#*eiyh;O>OE{PdSWYV%;m;JZ+2$7U) z{Dw#sqtY;All9B~8RG1_OZ>E-&8f%^F0{4b>WmslPzKZQm_5zojZ<5t4Loz8PPy-K zDSp#Kp7+W;@{HmITRc{WCf@9g7c$FkXZmwz@3pt1zZ72k2EZyZ8tYi2e{_r5Fp{|T z@6WZ^d-bvj$b*(K|NiN>(^~S{^Zqf4TCqUaPfE727+`=07sQQ@N~!IK+u|_-i7OC_ z=#Z(axBe7kaF)&b{VO9d49J^OJSTi@kf0|ginRQF=~iZPdmF67o$bSvy zo)JBJVgbd`^Cf-U-At)<1^&+_WGgJGvdf;ph`g(<_>i^B+JU_9^gFb}&>nN5@E=2F z{lV>=;KQ*fp|KO&=W+AZ_Q%8ZL7EdqAM)Mtx)7p4%O*b*KElki>?eVbxQcq43B2}d z?S*IMTPZxb8O0?c8y^<+4_y+Gtr`dnIJ1vynNLW6wN6ph*{7p3Emhr|GysBkHvMGHq}Zl+o0#oc`47gJcvkMe zd6y=7Rr*3tcBynAUGGXo$<@4f>-e&c6b5lBl+yULv zt@47Tn_4#>ey0h0`Rw;k8L!{9QemUz1R5&TUd-6#V8tzIUg_mXUA5!h@cG{FacRO( za1?9v3Ymdi$v0E=>Xo4XKUB3djgE?#U_J5UqtjD3WsjX&RXckMNy0k(1%emp0q3i# zIlR?!4!YKWQx)`QPGz56kGaBBHNv8ociO?_ zULiy&cgh2khg8&DSm&pp_v97TKy38O9TK~m(_B8CxP9Bq$LTT)@cpxt}KANNQ! zE0J5~Z=hM~h(%=^rahR=v>qIc5Zs6QQOj?!O9GE)Xnd#ha_j{l^Xbe?H11uhyNpl;kpC6LKRN9@+`Ud2 z2-%LYt$XlZ{fqNN#?f}K@zZ0r-PIDhchVPZshGyY8!j#xQH%m2-+e!hwtd$+K8=FjOMyGUCENn1QiGZ_>#O2n&^=bpr&*N>D!(VqE{SH=Pjm^3t z^DnyKn;H{yN}t>U`qObLFzE6Y16IxB-)P=Mk#286!#AZZUNG!e+qIYGaCg;g(qyB- zHXcLA5i!IgVfq=$B)RW>FhAu-04Mx--*K~Qb1+59ynzF!x$cP>dv@(;^0LaU(tCnN zNYaIU9_xM~o-jh=wqNO}?Mr$(C@eOI+9`EJ#F|&LV7W!u1xkVo4Q~*WFa37bDl@k* zAkCLTyMG!h>qF%|MsK>{mKSAg&rjgwO^S4wq(yUzg7dE#Y*o&qQvazZ>peP>El$Qr zO?HyseCu0_B-F$9nt)(=CW?H)qgrK3NU(pp* zuE8yb?{fZSefzPOob>G)pR3?TqTduko$=uwo4IQ%L9gsD}(-)VC+tj zD5EmwU%|UeeJD7NJd&lXR~NE-`<+T3kZ2#=y1G}C?RS-$ZLDZLG^Hq+jCbg_fsM1J ztb?Nk3=I_x$7LFX&Buxta#?Ho{9p9F=2ic5Lt9hWnb!YqntzDL%fIhD8y^Ib-rJOk zUi|Q1``sGl0?zNYRLh8=Q30R|p;_r5t*~ z%d31S^Ydgac9Gu5^ANv$;CkO`@aZJ{_nU}KnO?zgvM(}F$mn_F`ZA{J)6Td*c4rK) zA4KkM@e6lttpQ{;+b{gBg{>m!Qi~@ZAFBE^`xsJtX5T~Np#UTp-~Mx{OtI~+A(iR< z!BUMInjitjr-+{P)puSxEtl<_8Ua~L>nwxiRwH;^BdDw3nvE(Tw!HTZWiGR~5~514rHV8I#2-mig77RhlL#1-Xw+xn2Edt7=0j7@>QtnZzQi^~+Z z%W8n&Q^QYHy`wA`&e>YDi#xGr0c07}F0C?2i^#KLxB|*<^@Rc(6q&-~spmxvz*Lq! z|E3ubc)@zUSICRwuhm8FuA_J@zvJDjE#5pwBDXVu8nh~YP?6kJF7_4H0`#q1p*vJzf*4ka;~5u6t|>%2?b`eK zjr$jTWLs&Ex>*Rk^66q_SVvb(qF$ZH>NB4E4)4*2%_ta!Ob~gU=7&@57B#Qj(trE# z!}zcCN5)IrEA8R>4I!YRaCWt>1O0EumBuIks6YGd6sA6p=ZhP-yG&SaMv?T&+SU9q z8#1;%LF@eczZf%cg~v)=b+sApu*-Ta%n@uH-vFzAEIBihD;P>Rd$pX{8x?dHZaEOu z%`=;aEdmh!1eD6)hDnCP^zD)IxQWJwS)UQ5?b;6u^zzSn9;A@_?!@OZCWo(lrr}Wo zHSGwike2AhPp8}VU7VJ_et245^?O88#i@^^toJY5{6lPut{X z4A)q`6#wv=Yn#fD+5LeLJz3Y5p8w=?1p6mr2_25R%;o9(ubbAt0R-6%jsqWDHklMw z)Q$RSOJ=aItMpyL9A7{geOIg@^meca?Ej%0gW~o?Y4YsSghW$-QFL)1HJwAh}eY_8(m+)wxi=Rre(x%Id6aXVS@I8H6SW-t#+0c+XhQ z=)LPlq^t6u_aw(vp?;o^X)Ovci>U*Gx}vKiHDbb_0&h91=^frr=9sSsp{Riou3LC( z!eR~LNqkt1plV>rDbQu6ENhE&^YMDwqmmM0YvJc7GEZx^h7%BmGr%R7W}wQWCqDL^ zZSiFmqHdEOhze(t;;<)r`+Mpi& z)^zI+|J_wZB_#B6{*z)lKlz|1`ClT6n|_w~YE?m%34E&L9;*2R3QDgrQF%jU0&Tu< z@$SzZQ=^>X!g)IabEB7*Pb@tRHU)j!BcMVzlT|8sUcjV7qqd;M7HO-)9m<%jfg9;Z zc5h9zzE(cCRKlJ6yzq&kq2aSKm4h3xo=sX)NMZAMY+8iQIw? zQVDM`g56()w`%;RE^fd9C!^G(aF})t%=4k2PUSUW*>bVyKJ%DeBK#wHhIzDD<%P1^ zZMa%!U@x`RQ|OVu4du%_K867*)+dN@P)sg z6u;jTDRSJP5)88xD0x%V+N9P_A1c`TQ?6U*uPa?don^U2tzDIuLFkER4L zo>!4$hQBRs-=ft)BDL!I2~r3vB}iy3Eab^TxNkyB=FfV}^7u9VKeb?_)V{SWXSS%H zelzy7MufDLsY&Yk8D0jqhP^Fo6MWfqVWZ&o&HUkt7oLkhei$Lafg6TKo}c*Md`2%n zbCvpi!km7@=d&}C$eyL|cXP1~m3(2qB%&)J>$M)MUzCE=bU6BHpsK1u*V_1aIP)7Q zu`heMuy644(IX}~Rddm|8PE9)nH$a{PEGyhXew-V(xg{QN-I9gNP>|#5JkAvqaExx z^~U^R>UGJL{K+ie<^Cm^jpg6~-E`~n&kn--(UF^sM?80 zrpLuy9c})kk!(3aLA+Yn$Bn)OotC0cQ!g>IIj?yEf`-ia$;eEVUmp^GyG&OEA5&$F z&}UShmbAO3EeWi&THQCP@c`jgN#UGh#WEkTn)GVsC;dgGTed4jLLY7Rg!7nZcjk=Y z*0g||%eYd)WB)18Pbs#C#vYqUFlmr=Z)W{4Xl|k0%m~L`@Je2mY`r#$tG_|bpG09YuTi;a#G}x9WDvh{I@h34?l0bS7Jc?9-WN^_oU1P z4u={?M9L#jWxz20*jmnzF?yV4?xu*e87>4O5!o3jaUx+NG{SomV*!?5F=-a5AY%OVld6#ZEMTu(-u0XqULb zZ``wm%9tBlV|}83`{vj(wckPC=SoN#w=sHRCoAmdXMO~XU_u#BT-AGhE9q8fDan1M z^37)Ym9qQwFQ%(|Z+AVmy6%3QB2luvJLNL@TYp*D{qa-O`6)lO5Bi!pC@%J0w89om z^z>($FdP@aRj)ke19h7;&&=?xeIBf=y#&*UM-z%fU0fyiF=>z5-U+iA^a)fdEW(bM+&-brwPP~Y~9xq&xN~=jucz-@KeKwUz%^rAA^JKwxm~*(Cw{-^PDINS-Z;fb)j@54wt6X(8`dLP z1YP@7>GEnEh68SyZ&o|9d z>9Upp61HU1rYMhBIzb_#4HMa8@KC9{2M$`sjS{<+sl5{Qs)@{uR{X%A<&c*#|9XS% zx9)YW6~dM>NW!w}#*TVV&n4(BIpcJE@{DEk&iQKb$F`CSmO)DK=>ga`oiw>k)YW#T zv-a5{s9w`6NYb<$CcEXb6zqbkuHMC!-^cX!`-2h5rG-~7VnL)<{)9l>{Nto;6~PyC-194Nm73DaIHeXueDHkUs-_t3~=3B*@d)8pBHc5A2lo%an=QMPQK z>(d|9d%Jin$9=RquHQw&F9&(eo-;3Naen%^BxwF#)c$scdulfmEToBkr~7H-g%|DQ zeO-XPhhBj}vHy$T+Zq3!X$#*y?`n3uqo=AqwhnipH)poDTNeXFMTeqR^3d@ngYR#z zB{~`R?0&p`e53ejWdfPj!oW#5vD=lNi2T501hrqgswVv_Kln${z5C zV$ybck}OIeUhK(7MtOa3aLRv9lU!gdw3cM>X0=rS@h{rNPm#EKb;Yw+fXBN(mHr+h zXKVMdq58hp8>Q20PE-ZwbKdHPM_1n!Vk4JDlh4j#<8eW}O8$K5!vRP191V8_bFM)3 ze6vws!-M%r@u%8ZZby#G)S&Am@B=HlEB(=k%AIoW*@|+)1V_h()7pqI4CU;YV&nET zO^O#=g14I7*04%_cv|o$=!M_En2FhCzXNZTmJ5)Bjk9dtl`n&Hdg zx=$zW>#csR3iJ@Agd&axMEFQYeDid6dcq1+PieeBMTxd#p;6&0~C* z!30YKa)fhacWmm({#4?EEskwMPPA(I)(N_gl+6*91RQ6DOrm@5Y*jgjI(A3J z06LfMkzevgy5Z@MIB6HszqWOx_U@0czJ&!;VL~`L->~%RE-G&D;<_rN!TCyj{|E7( zey3&_i=%{?3ja%(>M^iXHqtb%_GVKZ$X~d)x)YiMc{II-8t;oscmEkSr9h4Qx7ot- zO5IRlf_qoPwg4i_w*a9#XJ?75UpBMv;0vs|xE>@b+&i$dwMO?+$P=GqIG+tl z-@D!P1jU;*^sQfmxyGB!)LGZ;8x_>S>TNpV$8h!iPM7Vk!o;f@yG*xlin`Aa(hp0W zS~r#dfjfjFM9j8Ye*H3SY*gY0xA43VFs>ZURI-d|AIJ-SD64k+^bo{StF(DxG1;uO z%6a&aYkTqv(Z5%69p`?2Tr}5yu?1eZa~R zJ5slr<#aIE_C*FXGU{8DC?fljV@Unc!)cu}XMs{(44blshswD?pu^WD2jBAREloYh zNA<3JDg_#HFOvMfLSWK6Qwm7MS`eJ-Q9iJ2yh`U`WC^AVs(07yzuw%%zjVa$rQn;& z)hO+xy4A5q0?MC0>+pLf)K%wuMqv;1g{$2NY_L7MWaet0&~|H~9$DD8*nK?hu`1r- z{E1#k+kl|4h}S8wHI+-8y2%eI@&!W&fk?3EkKfxKS}i|4e=pemJ+AboH2-Hk-^$;u z($W_*qCY9w{A{l6H?pR9C-g)KfO*ON!k5Yjm?oTPXS-%(Uo!u@@F#=3>)Da^g;R24 znh*|Cr3gI)>qXiZ*>QCWaWqldUHU`3B?pT!8GM$>t9DPNJ9@-wHu#udWc_!L=~7?i zE!NuhkKn(1>%Zjcek;ogF4?~`zLn_a-}x#bSx9{P;h6C9P1(#Bxw#69-G_hKU3lB` zU*EdRd@meh-XQIKl16|~ny<`QLbiv}OO~UoIc$z^_^qRRwf>miib86}(A{C=zgKUg zayC2H5)-2TG>-|6$l6;P`qKRel84pf_d%l6S}L2fxQ>GL;!MP9f}C-X@+Wo4UD~-_s)v)N#xur7Ytn zasmO*FFiWEhWe~ODgGapv)E1X|GTu?WECrDc9q-s1Cz7ug`TN4N} zZ3Hty1qCoPJt*xbwB$yqb2~*|?9ZQ{v68SCJNMg@UF$w1ZrYX{?ca63lVhL; zfk9?aRlNe@OXB=B0ufEtbQhOfwhse0rar;O6;CWj2yZQNF3gZ)y=tjAd+(fMPWjel zrOjmh%nUTc5S*%_d_3T8D5zkD&ev?$arl2&4%jQd{_SoYfa8~UhE-v(d(0{5l@A?r zr_*6z;BhCrTARhn+;h*pdf0vOw(46?TaSj4DCACv*69aX62l1qMLeD`<{ojNTIYN9 zp#9})ZTLtlGf#Kq*Q^IvM6oHplf?cy<1woPgP`9^AHRF$9RDdh0MEfK)|Pxa2yGa0 zx|!@}(=oT5In2!SMwxVSPn&zN;`}Cy>gly>l1Hx}`|flIl@{HL9KTetGgcF|?~Kf& zb-t!&mz(^yKTG<7XT@lhK`#~04ZU<~bxWg?0^e$K=~b0WK~?b6*=+Z9?0`Z#gpT882B1bYu_dM04W5brV9HI@hg$fKGRe2 zm13mxpq9qOGke7VqeC}lzZ4ZO-AIoArvuaY{%5Uj)+5Y+m#K8BO8l2{P ztE3O5bbAHv|H{(0lM$@Mpws!gVi)x9O&(^w`ch$9heTcf-yrs%+zp?n%Fa5TG5e$HL9KQ6dnpuj4%{d%!8x?ru)r?mUi|U4 zlnN&L&SSs&E2U21^sHTCY-Szt8+xoClQ{dWQ)#4y45veo-gi6eoW_3iN1O@wiF)6A zo&AvRR3`FYUTj(|HM3(E;hr5Q+KS(6U#$ znXw3u3C@>OFYY90Zqq!me6<)uXaiAbeG58re>a`;z)3no<0nf^pIV7)v7VgE3#e8? zbwg)-R08Dw$M;6gRw0g;Yta7uKfeQVD4Y0~oCP>!+xe%SxsJP*$MRS5vFPH%w~{^H(`Xj zZo;Sov|d+alwoJAk%YAG_kR%j&@Ss|<^0Oie;|L<4wf_Ow=SyV*O;>}s9a;Or07-Y zF>4{d_gYCHmttv5N0X2=QbGXUnMb^^D%?%>r9pm%MAVYcfoJF1G&oUHKW*~a419$ zr|0~KqU-DK3r%r*b=|yrJ|P_uRMP!=>$s?AS^N4e_l6xRqXc?NbIIpa^gVus#NL3q zVoWzsT@I`Z*!s%%PsQ^n#!y+mzw=8IUi`D$!r$BVJvu*~E}OMgmwFoG;?YFeHsxr| z0wL0<_%~D}P!LJ|Qg#TBsn7np9{=LLWIpQ)e09iJ`+Uob|34DR@E^bxd~|Dq<9{rf zS!KSqc!j-4q~F=X6XV0a(B@{SH;0cXAhY}C3;NM38UHDhr~A7)R_&5omv+#lcU#K) z7JKTCuiN%~g^`<9??l>;MewQ6j16Ye-44{6>Ue=to75t4@wB{=6J_7Vh#gr3-Ebq_ z3iMyQt|Z)bHZ?~$Y{sp{O7#fDZD;vbY<%OFGOu+aUZ1Sc1>Evb%CMXh#63#ieE!6l8_HA<0-F zJvnJ*&1En}0t(yx5+&&WX`4-+FkP;_eaP_1_UvDcrFXJz_WIhz1J7Q1g|WwZ>*RR^ zJac>FM-M)lhSoFQ_4;7(@E!>E$3if6en0MdnP+f!A$jRD0}hM&xt;kb7O^nwS2zW^DZ~vAf?K$;xs`@N35F9PSmOHoIwmhMB_OxOtEC#eV zAT`_|z29<-=kmQKY85O{&NHlUoAXs{Pp{yR9gz0$o?7C)!4#oMg8J;D-_nuqGDHaH z&Zhd|g$ZBt-`0`l9qDCN9jU`981{?!LcQ(hnaOtmY!~m;-UgMHQd_-5oeF^}heC)l zV9hy%atu}exb2lp3pz%l`psTs?K!-K3*m5-yjk*tIG!x&56wuqsEZ;zQQW^Ex76}PgU5lj16OS7m3fx zXLg<%L~S!TA)X(wT6^$&>ff5&RIrm?P7$F8CN>jPmp%1?O^m+x%=cJDZld2=Q=wWI zp2Wi`@>*p>Flt+krP(Tt$tGzES}~Se{3@ttxp%Qy7nQu6sB^UV*~&U-Y|! zhz9LZh>snGSn!%SCkpB>kA7E^nX^eX?;n(zsPGF zgH}5t5E_4!bFLSjPbTWZsJ>l&Fb;dT#D1i7U*eCGx&L&dW5Lsq5Bo7MiRwqv;SRuG zFD{B?1&wDa)l^MCF(Nn9>UVf5f7(~%(d>aH>tvYLV}*PST3vVqAJEws&3a#X`}?b- z>QS#MRRc^zz8h`U&A|#7KYO4p@-ZmM-Wl3@E=K;K-$P|kWT{GC8+9YP5jbOC##roj zb+X8X62aTVEQu<@p3s7SO3hAF3wdtE53wrhK z$-RK9^QOM;Ie?1@9}lUad;zqy5&f+T*FK8XCwc^8ws}?p@81 zjU5r{uL!ue5>t|Ehf0OeT{v&vCSD|L#bBj{5kzee3VR0+W7& zhZ7Q()x2wvNd?w+bV+`uSzMCQ*L*7$$BPZ^r^G1J^(Ar(*Z-Ey*0|sf-I~8!pGIcM znW?V2Q~~KKlttWzGvweW9itf3Q5#RP_zcuCzJpH18`zQQv50=V)zKmaf5*&YTIYI; zHQ>gm1-ImWNrfP85n#u|6UoNc!OTOJA?Gxfe@MU}-u1+jN)zADUGgmU19#x5n+?1M zxh0OiM#f4qavwf-n@g*59Z~Hqf4SbHz$P@p<349#M(AqrT<~?}vE+_iG9%LX>`e0M z+|Wh4-n=K85e%AU!I?b7QWuY`o<6+rDbxFWCy$m!c|0lGXW;$~NPGS>#9GW%VE;w4 zfY7r`o6Le2Ct^A|OQ*iaTrPxfCk80(AgZmG2YSK?k!41Qa&%KelQpZYixa(iu69u5 z=0Xg0Pq{P2(39OtRU4&p%Z}gX!DRTa@K9$}=foMdHT^5I<0=w19r(*7&BEFxa=}#@ zAOCUu+P^neY9ys1PP0aZQNGko``b{9Lt)A-!8$7L_4{@9$03>CBjCP_V8FjTeo%|d z@#Dv)Nw*c6z4s!!$Bm#siJaWfOO->s6@T+f`}&JlsWx5I7L~A8Z=Sp(qXGiT<5aEt z`bBaS{XG79u3D?k-LP#D6Bk1@ZCCJyr&Uzb-~UFEAqV>N>{dd&kx;$+(VmMZaT!vu z4V$W11r}GT?`ZIw$gF)kE8zT3kM;^a?}~T$!}D#Ak;d&>r=9WwZtr3j);GKK?1Dl6 zR#!`;V47HLzuzJ)pn9!e8PmwZ%yV9f&TEsgykiV^CsAR0#8@PofS9#sZO474-7RX$ zF%fg?>5&d_^96mMSWX&oS~=knY%H%X`>?W(0>I77m~TLC&?lt7jR;` z9LDr8l$uh`|7|_m7RspLJo%4rqDr=K|HP|mTa<<9TVMZ^dh0`)!rgGzU}7>eOxP5$ z3V0u`(TbhtFCYLsH#d!}xO8M?I8UgVs9|E~}pZEfeExhL+?DrizsmSKrdh z|Ha?M12c7_)1b-t&1`eqE!HjLjm`6`LU(LG*Z!qAeEpQc5m#_<$h+Wv=dVt_&+XAy z*PrUEAGMdpVK`o?s|!wtm*nrhUuse~Ccu zcm$LcjOf>YaGCYMlCAYz4oaHd2B2h&EXyH7c9ioo&K$fKfsgrY!1ZnJWyoGVgpdK- znKMEcbwR1`91A!B7nfi@-~IjIS=#tx2}cy<-^Nst;BnWU8cxGC3$o>1??^zv))TRo z2`Wd9Ii#*qHGa+gRG{a?)G!@qQ_x=}({$lKPGMZjs=}%JLM_J@mc5{PMX#+;-`Ui~ zGC_7g&~yPqi>lSe>gyj0!5^r!$r@{-!ohy!RmHIKvreR~A}vmMxfV*l{6b zFD1iW>xb$raya{~5|D2;4~g_|&r|!*VC1;Cx%Kq9fn1S1=MU1=|LwdharIs(4Xo zDHtB$2RNrp)md9k(2CR?;DV~pK0*y$74tF+}ERlW$H-5f_^Ic-$PE5(uY6-}BJ87<@t<3pR3QW+t*eRwDBQhioh6 z`|S(Ro$LVOh;NHmfyqpp+IjG<0j0BI_bm??uS=QCKGo;iObDwdjiAiEFh zTaN%3O^P}_mXo$fH-~(xKn(B~sDvOviH0vK0dC-pD9BSqtiT1p`4`HcV%Uxn@}yQO zAb`)5Dv=W%0cW~p$E-+v9D`*Zlt{xEkNGISiV%Aib z0Q*%EfzGQ07=^IEt(1*DDN;TCLpj*+Y{QY!ftENIxC0QfjRMz`&a=VY>r{_M!V(bz zom;WCRCl^(jO&V7*9Bt|@2RMlg1Y*lrx#WiE@!TLLvKfwK$8XB9>eJFg0-WzKXWje#;=wMJ#Um~W^`=0J@PS8q*f<3EjiBHMiV(?ZPG+)5Xu=5xIBKKd*&(vpZuMvpGFsOm z=9ID`fU2eJ$Alsv0|u@l=P<%Pl2ZsfDz;l>zepZJxN@$8^!*lY-kFZ%OvhH0M$r5g@80l+#d@+8D=R`HoWEv z^@>~%<67*@=V!sX)(Sn46I*idn5wEBoG1=|Xf#L51Rx9;{UieXIym20cOFUc+U)p3uQO=-~AGgUchhn2%q6a z)X?mftA*XYdu`IzEW=`uG?3SmAD;P{(7|fa%j+8Srd48Je+E0(FrWO8mKb#LF^F~) z1RlN(+wAC|uWrzXfcJFViDc!>R#@HsC+Pao01%y z*lrZmZ4|VV3ZCm#!=im*$60_=14^^%ZL35)83#wgp0D-mHchE{12x{WevmuVOR5ey zYL5c%LNe3@Y(%AkRxKfza;m2~tN=MyfDG~@@jXT`%=W#lC0SYvWK+ z2M`*+I|=1j}S7oh;K%FQvaUW9<)66#_|zPn(`yck>r$(lP7Q4L zyOl?WzTFb6HO3DiDR57fG7+YdrHCCQNc7<`?AlKN#%m;7?eANJP+*Vc`Cv{1!2q&} z)=ikIq7Rfdzl)b3azKqX+TT^t(hz7GUlaX3E@xjd@B&R-=j|q#% z*}9g;&lG_EgHsB@dpwIIKRw?hdYH-C!?8E045tAT-av|IKJ= zu4Wh>l3CcUjC*~~$fy>nt0EXiLeXI#)#&jq)79YM=V$Lsq9-erPXOIs`cFV6xZ z1*(4V+jQfTLK>F~+Bs*9CyYX7CmsDUFoNyWFP}6Lj;0O~5b`l75bcINV7~N6hyk{5 zkF*E4o}SOZ$?;@3;3~Pie%X1WWxpBdkOSh9H7I`rGnZ0#F=<_)lk%I$SUyRJ5Bf!v z|NL^l{^Nci&%VCtU_Ur%JDu8Gcv1f$wN{w#9yY8?Z9SNc+PpgN+nuB2)su~?164hY z33mNp$!u-ui*6&~bX!gk3HSa#s=k3e6J_Z(wr$(V#I|kQHYc`i+cqb*ZQFKcZswf5 z&-dIv@S?g4YgN_irck`g+{TSq&rP?T?q?G<Q@6gGaqrRpfp2ArrS*A*y>Keqf}O>OOl zg|F(O_{vghq5KN39gA%>cbeeYj=ZO>0nxnNJwkMI>NKO{P}H@nGqwH&e%Jczfotb? zOY4@;$CY3UJ=N=Y?Yrd1bMO$q^63j!h9NmU=}Bz&$=09WE1#HCM$bCXiKS>T!#ys; zNOiRfb#~fqM-R8pEK0QnrWNy`8l%rd`{oBS7P_<`|Ng{_>%3l zy9eu$%k6sf9fI9x)7HH4+a9KO-Wy?`E6+W6E013%?F0M#mo@-*UfMq{GkM`~jB{hU zQ=?TrV+Jl!El~BXOk$-{n~@>*gazZpD!%tMYU^cE;P7lmd>_>AfWg`O2O``G6`jg7 z&3f80BTSK#3Hpd(U6+<28SF*rAykUl&`q*@!`?sNMz-w}9m{N#6-c#SZnL3k9SSb> z4mW_Io<{>bF8u3CzmiMWz(2`$uO=zk5SkE!SN)-8ap?h&|2in9HLxp3jX}Cu5tf5!iL04n~RK@Y&6F z+7ved4W>>zIZ^lu4MuALfLtvizHcdskI{;co3_ulyYmXZ71L;q9@&m1-i4_f)U zv+I7}va6#9_Z;&594M82aG$m*%N`8hcMeO%p^I1Yo+ zx6jzR&lAwkZ3#R=b#v_4);{f-)50Fr08v}OfA)X)8t^$M;D?UCN_`<`0@JtKlKSp8 zx~0BQ9nX?P*F%@wyQqYB{T|uqaGgMi7~b{qX>iuGz(Q3P0k=KZRf!dz~Ie`$_1gsFJ!YJFCiB3*FY3^cVP8l~|{* z%c*N2auoRf)dDEXwX$upg`I3gy)<(U_U`VpJ~j>TsnTIn{Gh0Q{QYsA$=x=4&e-us z$j$rxcVp~V#!TmV%C`(_m(>ZFPWYGLpxMqF*@nRWz&cI!9!Y1~4J%90$N)lddG{dY*l7j@zT|S)>m3_CL5`a;+q{XVr`UE3 zE3R(dHhMP8*Ms{nxoB;_X;)jx3&AgkUw6d6UE*NZpSvS4SHhu_lU0HPv=oxvQ@$+E zaaZ@PvLWJ4sJ~#%b!JfZ39fadA(0(dENAvQ7(@PBcl^C1Tucmj+N<$(2E6}^pp$-I zxNV+oo{4Kz!}~s>(L4isiYo2F(z;~ya#+p@7S6r)S*P!DP>nJJ>$G(m^ai~siSuV| z=v$}Uk{ER&3|qOLNy@zAoA|&lJh#1P;M3YqX_d=_Ge7sY<$fJ+yJV(6J8I`-)^L8< zIu$yL4k0*WoI0E_b*m4bAdoe!sD3hlq@eu1p)VOZXvMl*=`N?_Y)_-*nU#i*C8__W z_c8QBu$`bqk}v(OLiby#Eb^+l-^GRF^AFBi8;?;FSPCmu0-pue^JFq>v zfG6j8+)m1UHRf`rz5M8T#oW2?I>DWE8@gB=Q=*2as9P0siYi z?4Me>?ze~9ZpZg+Q^At0`zv&!YY%`}Y@e_1=@3vk-(|_i2}PIti00}W;%*7q)J$NYtFBw7K(OO<$`!=d4$DJXFWK`t{rvX%zfOB?9sTYx}XI4|p^rCM$J~yGO zRDMw6p?`pN+a`v(*NbcW66ZPQKW;CT0r)}uZz<{1R)u3p7@Ewl!t$W=eU3*sy3q_v zNS>bz!o2!7yya5)tW1JuC;OcOx|fz_5%MgBY~MG51Z?{u=Vi53nq0uSw9}JN?j+Xt z)5xN!_4;D__C32Nk_-IzYM&1|kRQw8aKL7qkZ@qt8|ST zPo(#KTpJ?1?7weuU@jy58{v7J|9rjR!`L{Mv>3O_d*!`Il<9j`WuT?iq_HG^Ac}+OshRW4sK&r{$*@#c)nEyRK#nC;B zOBMW?k+3aGGUWJ`i6_;A|jN?E#`MpZ=4+_xd0vis5-ne6~`WF^ZVAk;^(v1!LZqeq^?zV2Rtt+VZ zH`~=wL8aQe9lzhU)n7i5E7#UtJeiRpO;0mSA>@v?W_$k||NSxAPPV;lamhyg`Y1#6QwjQvq@t%W=9@yqC4^mI(q+i(0 z+;EKc>|VOqw@g+}Vn{CWUvqM;3CMYI=IaW$D+9*Tr`7bbf;~<|#}S0<`PSJ6l+r%f z?-?%>i7cdqJzC(%9QikR1q8dOw!6L2_;14rXkob{a~Q7?essI>Y6-c9sXt^db(?qb z+&-s7_JtUJ1E020jJ9uiu3C1NAt5t-&9MuMR_>!4zCT_EAzUodY;)!33hcZtLzIP& zL^PiOY`4)rv~$X^mDesfk02W6Y+IbM z!zSQUGBF41yLyztk3ht$AH|ciA+R@RDSGG&eekh=!i>DP1yL3F{dT>U;InTF^8!t@s1-> zRPS__1KuAG0(nHfk>A%o`4H(oVo4tM-#focWbhaH#!C;18~sBR(qV#K+KJ;lx9+fx zG^}w>LwTyu`nTR&=FVS36%wemFxbNd8oh=D*Co`Q_g72qu18^20daEvr|8)Y!*svx z1OM}>l-qTDUTH(T(G&f)Sbn!MYdZWb6Ke%e{c}1d(C637F@{%bTKB0l5p(A*?w5~< zCu-IZGovxP46S@1UoZmI&pE-h(V4e7e%Ktlux_6;447Zo>07TnkWioi!cUL=J;FNu z96kC*d~{1{Ya>`G2`q?ZL9T$ksJtL+4ME7Vs#n`PZ}Gtr^O-~`#%eHbLD~WxaTVF= zU2$W0+^qDKH6INnRz8FMHfYqm3>8KZWEbq zZ&W3aK2v4^UbloqS==7;?j7IQoUK>7VNL(bTv^ZUos-zk>sA+E2f-0S_VYUsGSwtDTpU8?oQ-BdO`#-74UKwT;{5JBJh0cp`YgEr} z9f2;84)c!A9DIG)0-LY0_(Nsv6Fy>ur-SC2^Wvu)Y;0Y7ijYtxFQuYf4irrFJVzu;?x$bs@nWX(% zScZ-~8^2BMcJ=L*L+H`>zqC(+S*^w9etsYEx$c=+YYY^_JZTvE4lQF=HSMkwCrYu` zyzQy*_pMn)_ecY&3q1>mpjLY#TyJRdiPgIyVg~mF!KH49Xye4>1|Z>1WfczQtXTsN zfom0>CwZ0T(M-jyClrwF;p|}lG2mYgyzLbO1D;O%rxOzs)<0QUECldl!scbbyX8@b zRVxRX2M!iB#LMBfNg&~TkES)1$9t$tyTL8qyv?p4+13iap|2JE@N7qPaQIAjMJ z)QZ3Zy`f%9nOW87`wM8kA2@zO)PaIAn7S+k8ol*JlxoCSB?rZj>+ux8Cm5}>-0+~x z#qXu*-rQgSVu{5n1?IH=o1&n{{k%_YXXsYhM0nhf`x@bMd0y-vQddD`uJ&Pl z?hnI}OJ#LUcFJ9jlF@}ysM$^ee>VNnk$4lS@{q`GKUry*qOT6=giF0ud7Wk{Nqv0} z$){GXDw6EG&7) z5#IWg#(Rp1^jhpH5)BnvcQxJM6icg{c5(xw%@nfA8k?u{3NMGPV$YVoVe@|i4j8c5 z1iugH_{In2IU3oRXQ%CO({E;`#VOsk%K>RVBm-82$;~JGK&vYhF(dQZ#fKTv1cc$E zCGJDeK2G*a%whK~0>!}9=2O|mN-9IBI;cGa;*Y$*-5BjdTAyaKwq(;Iu*X;|2*S0*g){(7<_9iY=?&6+*_5Qi%!R!w$ z*V~i1H|NCT)DESxcwaTJij`M;$eAr^nv}8rbdE1Ih-tW>O0)G5>v`n_$bZ9P@kF@ zr7U!s(_q%-uttE+zh~+{TsF5V5D_XT)iOjNC|$)vd|A!Yj})l{j5iwg&IUbDB&`; z#T2%Fcx+0n3w}{(zN|PAG;(DrW$Bd}6PBeghBeDkiMPfqSdE?j=kvZ3C;eYLrLO#p zvHY*0Oosiw`dWS!_O0W#chtUjtTjw^2t+q1zruLHt@@FCP~AY8Ar4f$kC70u5@R#F z?rF;CT1f-gpa-I;B` zB>+n8DYFAiPA<4HAul>3p19Qh)o^VDv>yZCI!wZb9Mx9pw1iR3Wf3RFC8}Qqc;git|EOQe2HzUXM zKtUgHhazA_7>4?18K?Hro1k@pD-`3^D;X^C27%-Zi%qZ*`lFoNpRVGxvkdl@B@pE( zZi`0@QWGwH$^QRnh4{aykzLUf{PiF6S?l-yhQS_50{k#?bafVPN}s@%2&lNPsf$jN z)*QmPO84ia^#a>Q``!+2hC}DBWzGKj$;{l>6%~TM;jDwSNJsM}Mf5VkG1g6U>@9sw z5|4RE_BF&Rkz!Zfr+Z*FLw8yZTD*m!1R`;hEg&R>3Gy2WGA0)wl3JT}4Eump^tW*7 z5=|5pWwd}~etc?QfZm+l(i+D`wS4UIsp8~YTog@htH~6h5Z_TX5+p^0+aM4*Y`ZT@I?HOhel6osqW@O(=7{^IsOye^0y` zHXE((x7wKLMH*0UE1dj7SvmaD`ivL|u!KAbNJ1Nv4or36%8$H|Y$iJZnl%BWFE*V- zSC-1hlHw)g3W7}%D6&^U9S(}VuXs=UBOEM&(QTfjC|lwnTQ>xn4PX(KIJBM_VyxPk z97)xvJ7^JvMdR*FlPb(b-VV}a_a>6L+5?U2{@!Q~!yz z;$6OUVRVA)&)vz}y_tNam;k2qL&6U7Sa2W*n+-_>K2Db|&P6-npa3n$J+j#pfz?U@ zrE;TWl1=!mqZR`B|F1-W{&OOJhq1H&2>aXVmYCK{~TE zVQw={i00!kBayj7*I-5_wCs;V(DOa+;ejG=(;~EkTbO1R3NGb(8Zc7N=!IRr50^cT z2L12HzY|s#PjWBWnD7z24dy+cr5ZGtcn`EEQTaTxMYh#LbUGO-nM>m#8fYq_$HLD9 z(`ADC^DFLPKQ&da1VYv9bxH%WS6ln#hZkpCA<>_05MDM`I)fn;7ZhrlSeL9>wmG`1 z~vsh^;BipX=@g^ZNJB^$oG z9kBd9wL}0ta<{fVMnVb)y1@H72IZ9z$jYd^9bVoAcw1Rf-w$g4=LmM=|F*?$N1b?y z{>@!zFf>>Ac6*#`^}l+~>%U!PXRCF1=;X_4B%x(|<5UH&%qiT06&m>2|A*Npyyrtb z--Xo12>&98F7D+3hF6Bcva0wIwfobDO|bO?<$bx9Lee66!mvSs)_u802QdE9DJO7G zWtu*lg*6U@f+;K~pG9LfPG;}CP z2x2Z)h?vu~EkDJIby8YESlY0TYH?Id%{~Q+j43LrKxCX)OMdxNmVHAH=x~`ID<0{Z zqGzCD7S@?{o>lcQfSqjxT;5Fpbmgjz&U%aMQ(|CjI zI@iz1_>{sK^c%s`l_i$5m8Fg0!!neG(3qX$X`dMU2BqEO zl-3{N75+oRdBN*`$pR59Ubs=7#8vcoMnkdtRioaN&YD2}LY@0uz&`-5?OH5(;-m+r zXR^w}X^>?1%WDkH!R0*BK5q++VibCZOwExuF(*e87RB_fFco zJP&wwCZ#k&W;t%d)uri;PQ$5qWORC2>*#g!*St&uMLh9%@!0ywq?4Y@ALP6 z4Q=5o*2dw^xiQ~}aQj{M3lBv;1xHxZLy5T~Yh?rs1YPn{)S*yJNZc_yb~TQM%<1JM zLP|=UC3Rzf7kH>O#<>L}#-7w!2eCqU1ja?B96D+*g7t$1%FF?mxWVZy*o4hL=q8dL zvNYyn|Bp8ZHT(-y{loAL{=J<64EnzLSMJs~yD6Qaby2JzbbYQzgw_dqr~#;#?^%2` z@Ct|FEq#8N@P2Eqf;R0YgK~E;g8Zi978ZM$_V&l*xTyUkXT)?qt0n?ew@ov`ij9U5 z1c(}d@e)({S1a5{2RRmb^55WZ0$%+UFx95(v=OJnBsA$hNO*o>*0KdJTbB}4l!;cP zJ(+^{qzsP~A9)GhpeLOH-p)$`4PycIy#cH^z@cRQWZ>n zDr-zVu^*rVCV{eiH&&xZ?+= zP|rT>PM>X~39bETX069@Ng;*hSzGE)A#Hw*bP<=~ zG=qo&^_XxfnwI7Ub4`WVqY$iB&*Dc@{tnJGS+MuQmT*;ztOk|fpg5vn7kVGn8ufY!ppGg&%Q zgy4qr(+MTU)E^6S&g^7^1z;j-#$`6lHki^*!o@?y(d+HqQgH!wKd}hH3Kinyehhy} ziU`o!i6m(yca#s>VgR?&n(uS$@guXCuTKT9Hajr98ro0_$yi1!wJX+Uh7n>{(W606 zt;+rqBCOJB0RoQ^j{Oe6&}WL*k^3heEpIDeZmBLbP5|A3@K$v0kRV$``434yD= z66UdrUq2dirE_qTjkWL?@Dnof`2Crzrn4tSmrv`e5Us{sZx9nbdG&q_O;Oe?Q73Q- zj|M~|K$}J%_CZLwHFsb>MO%mqT&2vaG-Sf2bV;F}v)*I6(p=Zd?FO?sgLnAI;(b=o zo=@ZHY^Lu)Nwa%NrYqZ&In`iKST)z8EdXzWXy)JAia)X73i(GEig7X-^ z&~!vT@7FkxhX#*j4^PGWN8wgn;Ef`im5n-&C4*dt1Onbjj^aH*K7Wg%*KI4P{ZP66%i#ZVN#nR{qvMbNMBr@p<`8Bmq&qMYKwYd6Mvy)ks-u44{(< z2sq%Z&wqk4v<+yTa|$S*#WU1`Ci|B$D+T-XCrPN_VTq~`x~i6D_oW#Kk-+^)X*4w` zzx1Qv0ecHDI^77>(`*s*$R^Vlt7jtug`0VY%=`VR0P20@5f7+a#dVk{<~I#McEDo% z9B9*q)zpl}{eS2Ia61mxN>Bu>Iy50Q%KAC^-AY!BSz<9V#B(v)C>&IwCag$Y6R-S6 z{M4r_VW$IhAiW7PJkCdui2uc~79rQvmQQRpH=vpRYv)vmkjDY?CJhREnzV)(V&P=^ zhIMGborMoIkBc80I`7b)3@xbBvYrB0*+`f1091Ek)|p`e8GMlt2uZ)vIl?a@?2uGn z{YV1CmESDR7_H+R0|~>Vm#Vbiib^qH0FaO>1J>hp3_Mbhs>xHKgWOV4#tM-nF+dKP z4(7*-AHtX_e|xf9Q`bdV<=)RR`*7Di~AdGPgLO#f5`gc_9{qXut$}vR4WH4>|cKnqPvzlBE~$)2^>bA zA}b6!yP1QR;x>C{&K-}5Bjk+kG=wGL-1%Xl0Y8m+oMS!!*#eQ6oZWw4m$%K4s9&RVx6&0&9|IG&n!UPIwk3+JiHrz{oMgl85R|hwU4V_Z zW#vU|L<_gloSpXs;~S%Tta~<|r-HBnDK~mPPEnHRl|2G%B1!ep!N!<4k(o>7`I`Qf z%S>iKR2l9L^G~|_Ce0Wb{ti;`x-cFxI{qE${EKG9$iJsJ_5alUDtvZk=I82H-I0m0 zZMOys1iebQQG9Igl2lBhl6FuF()*pPm&;mb%_fS!wrC;FpmN$hHXRt8E^bWzw&-Pe zKJD`12-bqx1E21P_KRGvwXS#tm)Q(yjk$JsX5k+2v($3jPA;QngGZU`#g9a{+HPBF1aa2jbrv60vEeQ43 zVm_fgg3+*+zXaZbtwezeirO-EB`9ljiOYG^W(M}Pi7A&W&^0vyt;KYkB=1m}M)w#1 ze{;&|@LE^E_;M+(hy*Cb+X}Z`*0a0jxw0E5r7A)iP%v?j*Dc;R z8>F#Z%w@N;c3hcdtwPPA&D;}RAR@C0(P5&{{pl+5-9AfBU$F`_da%ihgoFMj#UZrJ z-)yqd?`Ll5n4?ZIZ=*&ThexU{Q?}2lo^{J93dVYhLZV7Yy1Ma_;4e=c+bOlz2#Dcz zh4x!!v3a_u0k$1d$sMweI(Z|zE$AI~F`Ax!ka1~^o8GR(ARP`iIQN$zPpPRbj3G7` z5MVhRO}=O+vSc5!+4vvqeJ>270AkB^x!dy6G}i7zfo7z_&?R5u-HWg`^;MiobKUWy zJ`z3q4K?c!)PVpF4@8?i$U)8#>v-S1QXTSCPNDSd$p1CbwHW?3(F2MM{vAUS8|HtD zbi?X%MSgZx&f8*aeROFw`9uxWQF%zJ7{rLgsbC`Z-g6<9I=2d^(W^j!_#1HM2BQ`5Lcj z5a{kmXf17_=!$%>9=?|76d))}AXbYdb&I+QRYHsNHA$XsA?SGw@KYVR?5`E zt;M$_7=T;hYzawbo1}cqEC6XUL0y`k67`*xfn__htODmsd>5J$MMRP?HiZVr3Z>yx z$J4!n!?k6pXn5eH*oGWSN-U%a;P$%%Sub| z0U)-Fj8kh~QEWPOX3Y1Ke9OsMhOK#E2+u8Ydyls827xf(((%MLv2u>QY z>OR5bW~N;8S~tZBoU}Ys(hyCKw!e3(?`?xIw=GniufWy6W1eYwdOoU)&nEAdv%{iu>KgxEQ4iUg&~9ZF{ohql1!Zl z5a@lG4h;ypo4M))0JIc>V7w{4HwC{fElK}6ta)D$9@P)4Qr$}gb>^2~$hH@rOK|H1 zayYh$P@FdJ&wykWTu52DE=o_I4kjS6xa^Qo7w2#$f{pLO0GNS9AlaXntrR33y9IF0DG^6^! zoynlOi5mTy$Np@C3IeaHVFC{+71A<>5ct3#cD~db3Q5s z(VHjC363GNS5Bj}EuL;>2!xp4FJj7jI_RyJNi*MGIWAaOzB~|6sV$x(uhc8@w%^%@ zO!p~zR|dfEjp_wM7iTXhkG(4~jaeRA+Bpr|{Aq;=OA=GXDu>W2NdXtSF=QoniN-TX zOPifW!-_`?irJCPpD+#t2iqeF1zfS**j)q-l=D_ayJkrgoTb!&>Z$&xK_Z-EG)}`N zZGy?unxyzchwV@*WV>Po&JjjVZzX#=aR!uWjE|dP%{l(S3p@%k;nDpmw||Qt>($AO z!;z>n=M#*?3kmpa)gmWtyL9@bK(bx(i(}f6QUvo(o_=(gi%+ZcpXWM*C-ZG4+M``OTr7_BjIpt@_@`2`p4|>ao=)aIlT**5#Qh=6|lc$7>;|c zvDRxV2FEDTNjf-~f`p9$UYY@5B!?>899VN=k&sMDpiKc|=jhzsL%AmqQ}iVH7KUMX zF%y4?TqFRdBuJWuV~me@WNL6Du+5di;S@>s;7|VPE?|fn$~BbU#+c3KI+WRLOthub zn7LE-xNP!pWSg~`We7g9WNq{gRK~)E?3GS-W~{1lj~z@p^eMr(a`pD3ls;Hk?RRfc z5rP1?A-4bzouV;#xM&H@lNRGtGhx7s&Flm2{uMG9JWT&DD5DY|wIIxohUr-Prr$2c zIbp@zMjrl9UPc2Kc$Bl+)sA<&9cZ8Zz(HeQ@??w|5ZDuS<^8wSNNY4>ClG{pPVBOS z8eGvVY_M@tDNZ|t|Mu$odJCic?{@nOt^38=|3mcYGt_-A1EC+E@12`8qW(cm9Rn{-b%-Hm*n{BR0sY#xG9m%!+(W ztK*o)5Zo*TGfrN3XF3Qy%`Qy+YLjq}HHOt`wVxIzpoIs@;9Fn9b;? z6AoJT+umNxK)YRbeimg3NX1A3R*?!-V`7e!<7<VcXBH;cm*@ zuhV+D%v3+|BY8kW0D~)jD}g!1S?wcPn9I#%xash;Gf)7Gl$eXA2%v&t+VHfnT&C=> z33}wA&7?g+8iz7_bzHwn1?4ogP(?$PA7O_;%REz+jRNJGpmo{HCieB`DCvkluQgOi za7~%gnw<2+<)v}b+)+^zo+pFnBCH4VAd4COw81_RB`}s5i5wPDLrZh2d=^!ns3vlV zRVklt^Gr-#3_5Uolx{86LVY5WpKK$?OqZ0AAqQMJla(4It@N#4eZmbuDqlDU{Z>Y&`{T2IP#&yr$x-y6~k|( z7>Dd?a+`?CR=5OIVUTcfbX(7fz2R(FJ<;^*I4}4CKE-1tx=7xjDzPkuS{PX0g!`%c z2(s6dINqO_1tu=Cmicg^H^$|26;csNDgyNf^%0srz;WMGEa|rvGFwNC_%=E?>nZ_> z%6)kXVHHyAtOy$6eL9`ehjCqjVuckL4)k*5_?c2B%oMSkSH4naxRG++KCxvBs6T2)8vjYqK7dc%jUdDX3sx~_` zWHy+@0x^WGP${aVlIO66kKMTXO#1e z-v_JDQX+mh`0QOonj(D^FqBCi&W9m|#=HWUlIB6@`+?t&HS;(v zoKOta6BMD4Sw)MW%IkG$S(U&$p({jWuGk@@QDiiwDIfldg$XSPcI@c2Jvs`+`+XdC z`WfQT86t26`3@G0 zv7XT>kh|i!kxSTPpM-3P6j^%%J^bKk=y|l7PERjC!I4JufuF0OyQ)7Rf2YsCMzIRs z>0yW`g!bhpB#)=VLeAxUKtLy;$yh8nr-G3%!tA#MV-bxe3Bqj;Cx9h@b?PiAAkMJf zxdQ(*oe1}0w?#GC?_`!vTQfe{Y(dp2nkn$lnQOn2q-;C6>gj=i`@1a z34u4kTzPS^ z@8{5z&heP@miXp42CXBIz`060H4vvg(2!f7dc3?}@8+d2H5UswRWc|uRM`Zmsr~+b zUaWTEQaa``w|`%{NXZXI1V}0N=Ok3?4Rt1Cutk>e(QZPBHo`h5Ncj$BCN>lE_NK2&^D+aU&#LTce6Atm z$XFto9|D*z*PFSylwD8=~4^W9UhM%Zi0YP1TS^iGk1es?*`% zh%~b6yoCm=m!s9a$-jVQWVnSqrY`Fzv%L=xTGD_(@&kzviclj{=*%sM#OYAq5&hYP zm-`i2+9F>C^nxS&@Th70fI}X2peYfl^g&^C-p2=jEGA#n#}%xIc->gt^WYj<3wVm01+bjf&d*yevhqhw6vGWI}Ud;32MZ$T_dDCa#k!+{I*b1jtfMT-{KX9?^g) zic)D^KhHo=n4mhjuV7Czl_BPl*+V+Uj&kT2l7bzccVO#V0(2^-rZPCTCV>|%phT0& zE5+%J|Ds>tk7&9wv!;m(o=5-eWaCX4em@9ZfJA|_UolZI6i|jcgwt*zWpgV+b+q{z zE^%0r8eL!)zFytcgr~BeAP54@oE6w-;cE%4^q%iC&+!qk-wM}*616_LD&%AWzj)?d)-M^W2(8SM)zMKl?N84nKC0a$f@zPdS$+LfBjp99Z z4_qUqX9R194Y%|D*Ow$!x%-XX2@o+h5{L& zUM}kLzuzL;PcGRU9~YnlG125$%POtd1!`))eN!0%m{nLX=s-gWH>0zbH(Fu|gi*9r zfRT?>=rce`r?9?biGENm+$ACeZ4i-ST1Qh}>)V`7(LW7gPc0>pq?Jhu2>9u~X0L#P zV^~6#3OyXaRtREH#o>E7sNrSrzk_a9KrIcx-0PUa5t9{;15%{To^7%oj~zb10|Krk zSG=6OqSz3r-t_VTAZ0UF!=CkWYuM$HcxM!ogvp#A&1;4_uWT5I3FquWEUz`3!E6Pp zdxbi&LWiqgksee{^A!Y$5M-LWj{XzFo~I+H=0RU({@S|U{B*^KMZo@d>`r$!k^zmW z0m5p03O_%lKrZ->KXVivM%X3X44n)E;f|Vg~6)5nZj`5%19MAgjc!2#Z^7@vy&@(WnUu( zDF$PohP{FZ>)_RM%M2>-p zXI9)*7|V6Ws}oG7UCXNHdzFK@31w`@@|}6UU+;(xco0_#=fb-b*G7)ChzIylNJa)j z37uD-0#{Y?ENQwdfMG}dzzThG`5ZW)3fVDo*<%~>9NtiV8nAo`(YvgGID83+_1PIh zJ`-Rt)q9Chbm)iVsNsmJj?qUl;v$}gG@;er;Cv%1YqQGl#>B)}Y*(&f43FH@?h3RG zcQ&XjoU94V1cf1-DLRcx=y1xE*mS51FX8o52B-kHdgsyv_dM!Uns}zLg(pB5Y28r7c*V*$C{htanUE2}oHqX{ zB$e5=?C3&DQFI_envU_e^-ag+BX>)F;1VD=ClU@|n!NT`5~#w8KioFUJ#hnVbJRUJ7!obg%$O?%$d*5HR?yri| zAkvl5My+oqI0;cJ*y693wHLx3g_y=|l!|{yNz}(poyD!?R)`k;fYL>rmEh##u{Xw{ z77Ah^1tVINk?J!Rw}&ce6-fI=NEa|5s?J~x8Dyl_Q(XyWz8IKU7#4&Ne%4@Y>Wvd?!9?M5edUB{jezb>5IHn3BWM`Rr>-&B`+ zy!{0hnINEmOKIp|YXO%Y&QE9g0mK33I)r4HA9ff9q>~1mzXD}zfSQN`MZz3q(}v0! z-@Q*uS@$BDf!7=vm?n4xyk{>kEqVG2aWom~t@(i(qg?#*m+Q#=^i`#TV9ZyO1J6by z`@H+o@f@sE;^Za?x+MYqGwZADJP-TK#Q_2h4Fhgpl1uoP@%W~51(DSZP>^A`(;%Ar zF}om91Fs;PB=kB}@-<9NoEBhB_p}_US5V0!BhrJ;Ysx@6O?MG`lv%Z=3+$#1`b@(9 z*CnyHVEj7++;nVZ%=PbhHWu)zXBr;v++fj@;|EC8Nc&$7DmHVEG8G^cn+w6-q5@l^{!${D*F_8 zSxYd&8LKxvzx0%ra077l)#PgVg5qI%#Va!_TJabT*J|>g&{iKGA9T6@WYv7 zP(`?jkDQWwPu>YYAB6-xSFP-`9{kulwLS2Kfzx3ho#R4DNt_^u*k(qA-nx{vvE?LwR>p{*CUw^H0qp}B0WU;+LtsPST=mCr}{SBJ@z$_0S2 zwKuLA1nY|iel@vq1O8Qm3ULx?AHBU~i{^e~yg{`L--y3_siZoBIK<@_M7=Z-{V}8r zqAP0i^RSu_TpcjQM!nXQTLxF+GICtv#s*7dgQX4er$=8Q-aEw7C8thXkQM4d-jwgnxuJl7pD42b@g|nXz%LQ zfs5_1YFOQtyaxMmKKot`;{3$1QT=`cgGz@KhONL|n@)HH{%ve<54IAp7oj%O2Cbc) zHRrcjy;FEl4;FAZDR(6mnP8Hbxu#{mv`sWjSjzAvf&)Ot>B+txJ&RC?5U?~L9epi= zHn!W7wphqlt>RPMg@p@#=!@b1aRH$5&ud*{vfX_w!>qaRPtb!bCyKA8JSdex**p`Vj}z9#!!)w(!GCC&+0)IiwQn zrBh8Wk#?MuMcU%;_h88z-70w~E9QGRG6ljoTob1e`8LJJN;<9%Ua~Aav4z9yO5DC- zG+OhmUtJch#^I5ueTFW%Oh1L$XG;|u+(frgKb>(_eqhNhTJet6mHLoM6A+=nfA7R} zxko(Sz_GqTEyo{4J_yUA&M>yYZ6y&bIqq=hxg=TB7RGzfAyE_!%HC1YC13m#TSc9) z_3x4ApSqnW+kc8#4FFMp{}r0#KM@Wgqn@87%`XisP1xoA5`Ro&zoYQvp_ybXb8Jf( zL|tT&ZpgeEjYQrFM_pX*K;;hSgoW=qx+cgk$wSDif9Fg^ywGzSL zZVT_8@zIQa_fg|VA0Xy6+%ax5eMPj0nyQpR^}Qk_N^OF6a-Bf_CIlXb3rI)M&~`i& zkaHxJRuhA>sO42Gb26$D$c?I(anPDXmrN-5wbRMTpKq)HM(bLN$oYirf`6@&PZ~uY zM;5;uHjysWV?1`_;Wxdh4?EeuE(SXJ+@(v1T?gH>680qsiS!MWXi)-P^VaoCRV@rF`5V6pgkG>u3NBbn8MA0)B~^(A|JEy>Pgp5ATMxDhiM z*CI97W3aR-yFRz}O;?$UMU3ctc2*}BS!}E}7n;cnJ}Dam+<6vJv4|55l)aN<##~J2 z(dV%34|b|>v%{f}uQmXRu}lYJ^HTUxjj3 za>6>XPyz?|V1f`2Vob7Fph$62)&nyY|LmnVUjoM@juWE9&=rGO5p~LNiqWpD9C2ni zL*RyE1GDL0Tap*M;ESMh{K&@O*74|RF5wPw>XS7VLrv^hY)g}YW4)RNPsE@RjFqgA z3DmqY-OmJnyi9N0nrv?gjlXF^|9K9QDChWvE^937o_J#P6Pd`DGZ>7;akZRc#3a}d zR@)Bd3*c(|plXwxQqR#hCv+~D;DtMaM`DOl)Dkp;$A*+)`_!rJN~W*AFWnY#*aH5B z;TA}fhjJjRT!_5f&@r78YD{jU+sYj$Xh|SuWFT(SWKwn7%h&D|cF4}QCj;_Lbrc%L zH|6dl+v!a{W7~s~Q(@?GzT;rVWc>FyqZ5*E#IAZmE^AWeI?x%%kX;pdPDyF{|&V-?@X%r7DVjlAs>hW}kFhP=Q zk}X`a2e?7J_A_DrlzAo2hwkq^?i^9)z(}j~Qjpf;1wZ_TI0fcvaAV-I%FkNOMe?1){rdNi<~JzZ-4 z_u%K|z2O)?JIj@G47#c$+Wt0dCoM~87FoG$OG8BuI0dhMh^y*%Puz_PVu~Th@J+KV zewUOol=An#JQb8c6P|xM@OiRwXubbiP1x5fA#tkUxLB=J`VX!rHs|}&Y=UGvaSV8 zO$@<8Xzep_F8;`5gIk@H=|*mmNaKG~mG1PC=fsywYp;NEYRvs_nqIm~uv1K`aB;vi@8 zmu~tt`E?Rmmk%h@kinu}g}%q9HM^%y<~N}bmbpS!!d&0wfY!GJurK@CM6rxa%w|Q< zG3BatPq;rPvD@ejLG=%j54K#(;{jokkdI4mxEu7ISP+x@!8jx+^xZYhlRIR~*gLT- z2SWO$=e_&gWR@OU9xwY+j2cJCvGrhw@0TqJc+F{o-t4ya38^mBSthP&v~of4X9SZx zMaDB`TJS}dNGwgNWJ8>?n+KpAXvtn7+RXddKql`d*f=Qd1$iZ75tb6J!9A>KfM1O0 zz-q?aryof9>%9SqS}yO#h{>^vW5v=|AvP+v=h3HDpsJUfN8T*=e`^BBjZiCSdc!74Uf%%;AdT(yFK+7f9r_vi#a zSlz>wxB4Tvc(xy?)-4QcdF$$UxMdIgo`vy|wDG?6bdvd?L?0umu3NcDfug&xm96kh z19GzTLiaQQZPX5Iz=S+7Nez4DB$CBAGkpBnN@u=1p~GUE)o&*R6_+qNqPkTOZ=Csv z0mpKc&uGD#Oiir=)8hB>67$nw0AikgBUAiRe2)0ZZdqhY=8IF$KWb>fh+?SWXY9U0 z`Sd89IJc{+&5NXF-)4pD1+pQ0M(|eDv_>`uLCL0bOnW9v)dL_s zxH_wFB06OGY6{r$2#^XW~I=~3lluhmNC+sfi#I|wQR$VTN*IX&Xt3a)$@MtCq@zXWDo&$8?+`f`QpSG-Cb^V(UL8)q)j- z)|=WXMOMOiSGAp*2}8JfRMV~e~Q2}9~C3l3eBES znG8no)1BLW02VkZgEg>mgd+Y9o=h^G`Mhl&dr=f+e!^4S(En8YMZ?@Ow$w5$t;Ah^ zdW2Ht8Ba=+=9a6|=7r8lKFiO6b9Swro}p3gB*=Nb@|1BxWn1$GH= z$+t9V1TpG)7*wi^A5+6&LqZBFq^H#3Ar-dU(U$1-EGwLQBp}zx(4$lWh6!nh1krDD zv^;yz0|7J|+zAZvXEx|nTo8U}haH~5SP1<^iI@AiwP9Zj9(ISY4kqmaBbtX)(qY-S zN=i*feeBGxtwvLy`=KJYp50lfr=t?^;ymL4-kV4Rx_f&ew=Mpqx-7J8&Ov|Oi84`h3@XF6J8&fQO(U9&$ zwr`e2g@}!o88ZzR1%FP+6w8$}(a(|1orsa*{g?D|`}4uLAmw?&L~f5oj5>e$MIOL5 zr+9sa1x!Vecm_!PnW}0fdFU^2YO_?HY>x6-`MSX~7rv%l8YEjvHy3#+;IqT;$6XxO zbHtI~k#)ig|4}Rjn9L6gd2bsXbdNy{k*o?mP!@2?usQuovB3_9LdkK1aVM39n>_(w z0#U=SUob$oL)Iv{4!?LTNSjdu$E6^g2)TP>J)*&cKeS_)8q9bPOX4)7b;^sYh4l+b zuq#*Iz9uup--hb&3fWC!3TVoxe0R`vy#Z0tv)G+(tvKFhk0!P~Q%exL4uQW>K1yO; zpgnx>vBM_6?x(%ABA?sAA|)Rc^k;oaXq5C&kJL-QIgC;01aW-sMKLnbY%krjOL`b| za$`35PD8#_6`A!RSV4Zj=9jXbtL8AQkc@tuBY@K+jmE$+u_37$VhG{r+n-%or_PZW zu<$%S_|QTC1`JX>g38D6F@{oB)qZEIXX}z(sr!r)40W(pwkc6d1mn-trNSDoXQz2H zTWc{|&+~mz+=o1-b;GPWQgumk^Jyq*8uzt)QB=v38)r{$S+Yp>Nzz33Up6q(ZQTLK z)9>2LBvZWq3_H1HOSCeuzkR*aGea*PG#w<^pC;&Y>X)2LI4afF9+_-UOH97EE9VCc zjnCGP=)*_%Y{KHoMM7VRURN9xSG+b$#vMKKR#M_9+UJw%wg2?KTYw-=#ff1=cP&`k z6{GHHs&@Muh6*EQlxv5QSFmK)H8Jt<8hmA&6%16Z5*|7EPLtR{=F=@15(#H#EJRB) zMXGjrnST)qCSTlk4r&u6X3qT9q2HM+4qS@77mCHmL_rrD-IFCH{<*S3H->9fuYzcr z1^E?M`NrL4I*=lV@0)O#&2sR$z)JilytV&pczS}IhkGx(&hHn8G8abwSE52M30yHZ z`91JwOMFA~_lL-}ts(BL6wBF#I?>mS-q{G zO;L_s$_(jJmbC0RgzI9bK6o_ZqjpGC6V)qBZg``c*F=`u{_69#60~LpfS+?m8!P__ z1DPftd>Zc(oRH&n9izW}YLXrU*)6@zbT&E?s=A*4^pbhOHw#sXsqvd^yOBeqv(A-G%8@$VhrfeaAy;xlJe2GFak5TX!%7U<7c*5lQaJl9WF)%TW^SaaQ9H(t)u`kp<@ydOb1 zoE66nP=`>FB(Y>_dUEw-xpS_yxknez+>Tmq_;tW`ZrAIB@M(ji|8hd!zO`zemSJON zP`su}Umq1P`xqcZtVhc(p~ES}zsq3Hf+3@H%#d-oRYr7KnDb&+V6s~MeWrOIg0wY7 zXlp!=KTe)uU2O7U5azw)Eqxdpvv>8AGM5rZZ4*V9lW3xSl>pyN8hr$Q0q@#b%ILCn1(jK%ot#^KqCU7)>maij=EKOtW%;UB&tY!T% z5WIR~a<0ryiDA3FNZ3G~F;2AZTa2jU?6i5`DQjBtEc_a0(ldCL8Oxl8e+Qq?vcO4# z?C4gvuvyohS*5K`d}2y+IeO&*?={3EUg%pR%FN2$Uj1 zE(|nT!~-r#B(V@gU7?yB?=i0bwJyv^p8(-wH!%P0Q5yALhf5Y*oKXfufQixi z3%E-@`;`@I;P=n!$uKe$MD>?VoG(Vdm+RF1lTFA6q{l9ZT6n3=DeR_+OUQ_Vg$0mH zcpDWva4MkS#o^Rh0rp+lEJZL76Gfx(2{l)1W;pj3oI-X$sUj%6X2ya`4`>a+GB_S5EYC;U5YxI! z_$U{$&1)>rFZSZQ=uU3X4$p_y|4LyCQq6ZF6&u;{ZqzzbXNQJy1OiU8-c8UE#3%m>ZV;gt$Aw!VW2I-rT7iFxLM4(m{rd&T_aRb_!Tfo!9!kD%@B5V1Hm0gcCE`s~HY-2V7Yb{Ju0?spSZYRo z9=DwkwF+$B4qR#j4>?y&I-4e|Z&tyz15`%4+*ZWn+n_<2j%M^&)CVniTWCkWZ_Koc z!1{@x)~yKm6sCop^wZLdSV#M`5zrcQT|v=d>5{Ie+b3|O1lRU`VWw}r4|m?)Af}{O z1WWho9HQ$OwoVF)9qyK?zf+zEIE>`KUHpn-6K1y@M)V0kWMHd$#cg%vLhffo^l2Si z1if!iXA0oW)*F$n^6d%o3sud+8;oDs$PZrTb_Us>xn`A|+12k>g>_5us`0)0E)3Xm zQ|!!IJSoSi8%s6$B8h*Bjcb|L;S>BuCGw~>7u0l}6G%nAQ^*$oNKGUw&?=SE0i^8X zRp_8%Um(-Ixk5f#X|76R3r2>j9qxU}wB!vpm|>=JT8Nk6DPc1p!>kD=YcTrp0ksLi zx6Q;(*%%cg#~`y?>Z?4!7s-8dDCJe48EoB4FrjuqNw=->eAR=8-$lQj{^R#z1r`4% zVB=up|KIeC`cF=WlGF0vG8P^uz**8Tp6AUjPnp(%WPNIoFlX`a*DarVr-*-ja~AVH@_@@hjkW3DdmdC{3reM3-}pHHq{$p?0p*WE7t4*w{f;&!~C=58zzS7*5G{g z8|60oHqv*$S}w9-UlteWYc>|&ER3QeTZUc(e?PB{l7$N8woQhpHV!J7*bsh+90sW1 z$K@1S&9^<+P_BofLD!;-k5}@F`}fPZ2DRYf!ji^EMLOSdx^T{Df!Cv&Aczu2Pe97?DHZ!}0C)u47nH`K-#zXv_Rr;K&Hj{0{?ZQ)!H9o8a)8z^?5ofi(o_PwD z#4@rcV=S2n9vNgu88lqbriTZqf^lCSd7X3>gcHyOBR#vmXP@+U;#PmoUZo!O5`<;p&O(rGx{*ChZ zfHL76Y=)8vlhlnF=*j2p#DG60u?L1WA5OO6$={j1|A13keAr56Ekgrg%vDemkQKcX zx?}74={B;r==WQ}K{hnL&yF^@2C1*JKQXmCMdKWc+E#rHcKgndNns0H$zi&ZJ`cBb z@}xz@&1+|@b$YLBsMeBcx}bN@K~6M$x^^OM9Jkb*G+LTxXe99JLNw)#cRkJpB<#76 zNMyDm`!>D~2IgiAOIPJiMGEGw3*~f=ZHP&8!(EVVU3nP`NI? zW{e$S35Y&j1|isn^G#!-j(QDN!%5+y$?T6~stfegB|{qty$5kj+j4Vp$-LLrC+VNV z)IZ}URRw%;hBS{7()Scr2Y&OV@1dR_4ItBOq}H${Ho?gDu{XXRgOkwtO(|CdA4Z?} z!uPpxuewyXTFG#2ncC&HU<|Tp0%cZRGaWR1B1r7B!QlGW*p6-vvVntPFw~@f!582cnygr_dh#cgvL%uT-%iq+ypl+%2* zye-xa7i*Zxp$Vyp;QBfoyZdo=GUoA{zhEy_S{W46!(IlRx6HfF^QJ#fwc;o~XZx?S zO3oh=$Y*3Achv1AYk$kPA_+#CLW@kx3JC;VA$FA5ASYX2k?tR*A>%Eub#6|H7=PRQ z{m5VV{Hd_bb*0Wk)*AUjUB&fkkLG|h{kF}O?@=qqP~iXLgL?Q6`%u8qYaQr+`Jns& zXHQ)>MK2wHc9x{_urA=Y=kEaWZRP)JzvvfJ`}`tYc)5?c$0@`Vg|_W`??C$txkF2RcIF^5I9ys@HTBl>Dh@I9|Is4#RNRpH)HnUJueVI^iR2`qzHf?Y)gG!@n-nH4K^!BHbrG)0u`()ZB#sx4@*HxXD&AkmAZsL zIYsydUNRazYXSWZr6+w^4JpH71qut1d)22I;g%G`=~Xz5{@d_B{l^qhx~GwGVBzlNO2w9OgpX@Q-#jRl?Xt zIzs9xNPU1)y3|ffT0%4}2Mm0%!({a0ce9Oqgd%>Fe(qx>!j^HbCTX8N_xPhE7Tu^Y zrCH%A6ph;=8V`;6NF!*(otj`Pl!Lb~Cr~^@8O1ZVsI=jf!s>CneCL0cY&lm(&M1vR z#QY$2qL~&wWpsxL>B98VaRWGFZfE8?v$t$8O6UKk52gu?;(4PS&mUqJesMfDm~3)} z&3fVV1>-;?tCo6`A-C)Zdb|Gy1(Sr#$`JnRJvNa3OP9rShZ-PqkcZ`Xs;(}%UB9uf z)BLVD=loh}5kqu0RO@{)1F=s~X~CO3gHcJk$3GZ(L$9lnY3)&&doe#(37@GOM};Zc zaC@#eDv99%W!RBqL#oo>%$en1`IBSmwJd6J_GuBfaw_mVtEFnartNR&gi2~g>fw>+ zH@=wF%wS$9WcCQht(86d1{E7^9V^AO<(k4)xKw14RwV$1>NUA|B~gVS5~^)v1nNO} z?{Al}&+&mo&s+o=BqlOW!(#<^+1}oxfKRl_lJEQK1ap=t?N?P>ocS$9_v4nobEG06 z`^Bwzrr+87dHsvcJ$$6^hfsM?Br&JFHIGy`T<+^im^&wZE!P=a8i|@V#FWiDAO-^+ zu5jm5NmUO_7pm(u?n(b)k1LX+d~HpoqKbyqHG=DrI1s~}n z3i|+Tsk6~vI< z8JU{ZC!seD-{RRR)Z5Jpb+Ew`->UkZ+B-^NV(%jQ`UM2i7tGW}zAHi%Fg8FPm!V7y zAH0=S0)18~1oP?Fl$R_yJ@>I@;A}$-GE>TaxrVcgBvg7|5mgSx`|$n#yXsGf(iMC1 zHo#vMnh;Ke7CuYDfC|fEq@%&m;9_7@<+_*eMI$E?kKI)-;|Pu52(3u3FV?9(-WfU??qkyCLk%9U0UC(?~RYtc#)rLOsdz&K9& zWD9Te_!Pk1i5Aq#igXQW^R9dEWx@=@fwZbjUWEy^gaVT_FO7H^RJA%oIkaemf0 zPH?$sr<4psQ_;CJ)z2MN!tZ`2pd-8W4z;dwVA;^REyMtrlfs_L=Rm2FPoO5?n__9N zEq@7;BQU>nca?PJ@f>YnC~^w}sVXuxYK9*crU=k$hh{gs;aw^cgo;AUI5HLo$9A3P z!a90yP zqJ*=S?iqy_NjlyAYBc}jQtD*HG1<5p?vghyT0ne&1L|O`4F`$zghU4nFfNiRV?1>N zM3RSGu%nHp@s|yW-N*i z;&-AWm+T;!32ZpuNpx?F)h$QPV(DU4e*yTjI|bQD2~zl)h8YN~wlLG8iN~YKG(*h2 z%wPly%!f%Z>trv>OIsk2D4@SW6hLLI;96RLYAO$bs;o^yAuj?pS3<7O4)T)&L{!K1 zO$@}pPn(oYw+UM0$CbXZsM{J9R`JND*8DhH`hak^q^7MgzhnFiEsGg|TRzehJ6rSF znvGCU_8mv;@@L_#amrO-9|HIiXUVgTluW<+t45Vh6SX5xfWZMtDktFzA|%B!GjXB| zXHv77i=*y3&JigTBWNFlpUH%+Ds1Oz(fBLjYuGTR!!ALktN?9)UK@3?T*xoK+uOVC zqXxI9@wn}WjVar;L7D0f(zXw|89op_&=yTdgNMuQ6;Q0J(ToYo?y93+XZMI5O29z) zDxoHN{KGtQS#3}TqwS(QT!<@K3&ZYmtj$@2%eotzu9Rzr95(TkFny1Y$5z3<%l8RN z`+;-k3yYCqzdQyDAYAP3hk1B_WA)JTzRBnx-Hj#w;r1(J{j{Y=8H?23=VEx~rX(82 zF=CA;q?*A!5(I@-09s~9wQw}51`!vhAr1y46j*dB%Hh%+0HyT)vKYQ4)c(B{fta%E zTD__<5`n03H=OEHBK>p zyBiLR9wu`;lCMc6DtbA#2Tf}OzDh7W{C}w-`ui2dB#XvZhLgXM0znY6!AhkG*r8~N`5>MV? zHytg@lT0gw@{m?%J3`kgn}&A>D?)e4RMDUN9(%w+Rl0lblcYH3ofLtQdY%Gs8r$|-igB%$be7f`| z?$fMQGRb>#5J^$CY6B|bbd&+E7Beuo#{cD26C#j`>Po%jh8&MnKtF6`m1zqi(4+{? zU3@6!3jo+Jtuue^MP0YR%Y=1kv(P`=mWtUN6%$+$_>yfcT=#AK?#7(@8j;z!J|@;Ulmz?Qyx$HXrv^p{8O>jDxTY#xK&BPWhPn9|l?JnO7J7 zrW#6TqGX2ysLGySV7km;>xSdHaAdNp&;R;TfHzN`E6jM*ACs2)!9G*|Qn8S_K~jnG zrN3)wu(BBcqi4j=#qiNvK2)XX)HP~N%|(jUw9%>CX#%iJYK=;HD1o`fRa>^~D*DL4 z5qe0WsAsrv>T{A^TEZ9cBV<6a{#oju!uI1;9zYTbS?ZxatXzsi+KljXVVnepk*sVC z8YH-#coLl%tp)Re$mnRmV$u&zbHm8S3i9fo_iff&39Y15*))!;T2LTPa!aQ?nvf<1siAwvc^>eltD>=1%I0F0Kf0LzW2JPzg$&-1p z9)|i36=P|Y8u?NfzM9zcoijXXyOPdCyypn zCR~P8CFZ2U?%xO%$@UbnEPV<@5{#ZsS5Jp520^s;IRUKm!7K5r3{ofGyae%?;_R}~ zMD*m=8Y})cJz9DbUhYn`Ht~#?HluT$KwFp**@h2hb&Mx7i{K4Wu_!5e`uy8^E_C)= z{8x*gmzEZnmvlbQmPoFo>Lf`Ju@fQDhOV(07`3dfiOps5QiwkmCG`0{BRWCXY9qe+ z+dO!#?A-JBmR>J6EZC|kf=Unj!GiyNNTnU^rljO%;3nPXyUoo_op@?A`2`GI7E+x4 zeNRJg3(95I$vN~ohf&1|o~UH4kc4#uYm>8NK0%UpEy|lOvhYZY{I|0zAuESMMb9#G z`r9*?8iz=f3y&2@krVPb=bcZl?~B6SanzSCZRot=6|BewaI45tc+*Nqw8X7CEnBDn zk_yDU8~NI$!Fd<^S!Fa;9GSiayAa2vh9e;XBiM?Oe|m-IfpLW~h>71I?NB?uDFy>A zG+s6sqyhxHR4aL^TyL1{@Y__;U%CK8!+d5)!Ym3z2%zrY+0=$k*^$Il>O+bVVHNwZ zhDwOL*&wACOOc?QexYOvGMcQt|1iHvQjeer_ggz&=yZNShgfS$valtFQ9%pwn);xO zx%+ZTVaXFY&7r001|ws6Y~4#wkS2NJLFZa3J80}}oCQ?nvB`v&INJGFd3MBw25DKKtR^m| zamL*=?Z_#ed7XDoQ$p_$G12{2M#Y4quY%!R!uYR-?C!`k=V5}03)}>SRrB!@&O3~J zD`UzcFpTgvMn)Gc0(`ll(3r}kFTlQgFPsj`>)m&P8|2w&aH%1i!ElG|{gst2Lv^WB z(B$q*QUORRhw-*jbX{#mQaoOx%KUOcdNf;AO%l+f`T}ozl4f*}<7X;kDxa1aWm|g} z%roNImYUwC#87;K1Z>=ekC4ewW*PRM>&BBqSYR*~;!((LD=u+B&|-rbHU404|Ka|f zY`lKcH1Fl*J45-J(ujwNc$LrkYe2UT)hW+d<6Y`|JBQwCDU8<+`KW)N#>Zm~$t4Xb zhOlXwC;_sWj9!mQdlF~uS2i~KPOcnA4kJ%a9JdzIUqipi&M&O5%U%;6`0k@13Z=7F z0;G2`w7D=b%^EIH0R6ak8OlY>6IWeFcFPBA%eO2%2VV_H}` zK9aE)tXnV;Hz(-6(K0aBH8s|hMAqS0-_S{#elz_>XZlV^-onUm*gWWU_Z8Ca`FVl9NGeLsGP6#{C>)lq$0EVyF5i)4J|*wnkBoOx~XS4LEgG*963s zxs`=|%uYZOPd^Frgfsr+I(aFFPNki9)1e+xvn(Eb4UJmEl%?mMy7s+&BNnPSR$w96 z1OgoIk)8g6UC+uV7o+=y?tqB_3A(9`XJdByss;mI8xo$!#9YRc-_#TXFKo(=Mk{0G zfHEuX;$6-rg{#(sybRyj4!B*0~6m)0oQit$WvW$4{auCk!F^KXMCX@@P07TVmgy{EmR`# z!{iM9lKP2-w@V_K7_82nmxO?aqa6z5@&$h+-jHNz9{d$Amq8jGdsJYQt=%Z`V7N*d6eM%=6r=^ORi zlNua9+h*VsccTry1W-!PDJh^X4h}ZPh%|-E5>H+RL9~`F-kimwfxPAxNjS6`a18|r&oT!#-N;^3(y(HDnvx`l0?})3QQ%gv9bJ9vV@xN|q+lf% zIspN=MCl+kF8-M8Wd%tdFZ;Grk(DtmY2g(HC_*#lYTr-CEeY8d@^5^xH|Vxe=D^(NfE2kWC~ai z%b5Poj(zM|K2L1@=7ir#xjfR);l|Ntr;U8@(k8ZOB2%SZL8gGl2q>E9IIPmzQ03^~ zGBc&osmPP+SwgH0&KfoJxUE?sGS`1@R7qy2hEz}kHGm;MG+11jX9eik&Z9NT2MjIf z`h-;we=)ShaG$rpTW*YxJq%^=qL60cGG0PJX5&7BjpL6*NpZiYS8O<3#oq+E>v)dv zJF`5ZZa36cKckbFQ- zSRI7}atDh;Ki$|QDQFXo+ZSCgHd#S3o5;!(uxemrcopfw#=%NN>8bx}J67s}=_$gF z|D{EQ|3?%rgyH@+BWN2?6HiELa>vk|&Kmn@YFzK&;->~cK}V%OJn9N$nvhcnK7>2a zzEDA+h91~7a640HGmth(Gv{p|tvi4*e(wn=jP`*T+@w&OWdFAab{cPY__U6}b zeer|@)Gx+u+N|*HV*chQGy4-ThKqjYx9$VC0 z;_btnSH7K6b@GluU$=EsnCy*p5fQ6JY=$pbiVWH%$>St`N|i2^UOkQJrS7kBBOB_G z1|zWRI?i==pL3W^>JaIQ@P~p^a?-a`WYcE>(P$N_9kad6ma_LtcbVZZWphDpmGAICQ)IQ-t0~uK2JJprUE&C zZNt~k(UGMVn8tF^v{}6ptEGf8k&eV`j=mktY{f)lKDEK`#)0D{rQQ?=a-JRdhnCD% zjqN$WEw3ok4Zg+P|2X%PiisFBmPJ;i^OZRM@&A83-WMGFlhbP(!1ZZh^nm-lqp-QX z;z7k11HOoyR%CbPfzJn%L(G5>p!9_mI-e|KPUKZzFGd2Sm!gbvpaGJgmy9#stO&eP zFOcY{>!DXB-&Z9vXrJ9&iZF37K>XiMe$N=(MmR-FV3hlLF60e(X?ZqKgcXNxeN*ep zhKBKnDi;@Z5#G%bs@INO9hd2IL6y%x^eu*7ZGTGVOHx1>BdDd;3^a=lp$_?qMnWq5 z_KL>)8|63TR_gq?3kU(?X4Z&)AsRL*&W_&#f+d;G(|(tk+*l;gNdxX)JC9eQlm6X( z+kW!eU&E8>i0$92PdeSF;h7LhueRs54-o!C zI-`(X-*sK-|NckFl{Cr|`y&%v+nUr5Vlephmz!IwbEb0(%L}Ne&5^Tr!u#5f+@Qdg z_nNvZ$5^#|wQcb0btiwZHtyG|Jn=s+1qqSpa9_t@mMQ~Wd1UW6vlxGd0#u!wlYFj~ z>ieAMhJ7X@TS@r;v{i_`pA^hvGWv`&L+8JkD?1umd!_PA_bgFZ(yq?r?p`g67(a?O zMt56^E!99-6cAz4N8Yi6IuR4pBnla|!qNlfY8!ppK0+&8ldzlIhiE%6VySyrh>uUJ zaT&R%;cBkuYX1-6mBU8sjw~u~YEGk{J)(=`Q*2PXzR#d@S`%NZTq;AR15N`^s-_c~ zfcq2V7|aURzXJ>~*SF@kUmBNaA?C`s)$)K1EHx^)wCKdsJ!Pt&dvOcDC3+a}rATcV z@!2X8{1jMbNGEHcwMNw~??vEY76EB7Vi&VnC9#>=1!PXBkXE=S`}%+Uw=Xat;tZcM zgqL(P0(Do+iy9l}YV=WP-ljLaZ4}PcMsp{l4VaggmYk@I9iY13q4i%i#irI!B<71F zofYfToHNi|i2O}qDRgbv-3i5hQuz1B?9)FJZ+p!7^zZ(IAC3C8A8THw~(2* zg4(%T3jaIRJufBqJ?J7(= zq9oxCmB#x-7-;E_L&R#MW+H@~eCwS5ai$@nBJw5Zwn{#x4o3XdUWRq0K|Jj>o(T3TJ5 zD^n&XDc7L-*Jqi?QCs}Ft?H|Lrn}88Onzsdj8_S}ICWgYhdQL&YQ+v5sC!%Hn@prV zBjwAIi?Hac4HBsqfyyDDy-$IZrud>DPUOe{14q2F z5~QSMT(tZrI@t=BcyD-|A(=*AO6tL$$G~SreaFPg4)qgvh(}D@RI4R4Xh^?f1g_tz ze4xum!zL8eR4c`~d7b~Rf)p_&4tBPl<{C`s=Sm>z+uH7cqRtPPZ#VWiRm!8qJKoa z$5JRCbiZPqcST-nULAA4*wRcTMdvF=*{_Y)EMsFk-WlW*mYq!U-L8p=&XAg5OZZL7 zg0THks+cwsPbcFR(fcQj?%U?(qgAvJS4R1Nzrr_9+`BL3FK@4^PPipLYplJXv!Jo0 z)7t5R@A8gF+@|9F$W=#R1V?jxwhID*3{EebtP|Zgah66YEha^uF4LY7x_L&tjl!6( z)7&OE9e3*OqgFGn2r6j6E_KkuJmHE?1W^1;7Zv&dqvbikRc>Xmm^G2FcZ`>j{nP$e zOIJ4`0#8-QV*&eZ!_w)NA~C$A^nbyFuZgHo7I+_3=I{CdQ*{EF(abG z!}zTVh@ycj%agvGlf{3b6@~p`cJGcHN65r+-N7+-m1x3Ob#J0mB?J7mR4R3^mR!x( zDdIG<3-Im}%5n5c<6-qQ*>Rc14+G*!+%^g+d@VX-4 zb#(E~VDNAbo1Aa+Ab>Nap)xuzzDs7Q;I_xRkuHERSdy@b0@M7-P}h@(m0Nm0_c_XL z*t!owOW%SI4HQ|`LeuprG$_I+-QUW^=cQ)~V^c*3gM+QYm5unWu15T2w6YZ}=f$2sUoLLI2<@`%=kL&_CHw!j41fz!-_e@=T4Z ztrQo}Cy(FPrYN+WiElhvfFU`Z6_cD$Yv;lMo%d~_G_V^CyE7{MAoJ=Kl3EM-WIjMj z)PVJ?-zNksS=9e1n2{lP^`hk#Xx0%zbuD z7g$5u&1A_lv_JgGh8T`>E^isA8V#kS(404ZN4o>}`yPo*IY&$q$FpcMq|iy}6{#q# z>?na$#NM&Y9w@9;uXa%gcQG)27#k=Akc5(bEahShZJ6QM%GL0By@(>~r@JJ4cuV9n zBf@Q{(5@Hxe|G+yDA|t4iQ>fZ)X3PF^Es!u-*XLbSO3c2wdvF0E-95BNIP5If!#T!DoR(aRVUrWcEM#u&z)QxE_ z@%v5t;p7z{6M6>il=s`Qzy(D}R!KW`-pQ4x%lf)H)< zT0nr+s!wWTXF)UQ>OBQ=3=7A9I~i7H?%YX;JV~(-2>wBn?1IHOC9e8ee=fd@*bao~ zGtAvDBv16ow5Kc`Ewdpm5Dmo^)L}!t!~aR5XGdU2Le(ZEMgw|C9pLG1@GY?l zv!v+(l8wmUvhX0*m#Bw3Z;YG14>Fu+EsW#V81aXC$28Pf-@!%Pk?ttL?noj=9lCFfj*n}tl6udw9KEfQ#)?K-UF#cNd z?L+u$sX)wrFdly`{UVJ;b_Sl>baZ|U*x)-my&Rt2wGboJsbp4!63qGe`SNc9joX$6Qe5#RO~v`6>*Fgh{M@=BK99>(Sthb2}E+@umAB+z(#0;1?|yPlS#wS>D%!V9U^8A7iur4fHYEiO2)7Y}v{J_~bNvxB zV-Q0;n<0lwTjT&K6_~)MJ+!5@#hjuf9UCjJU{6nI!vz?DL>~ULxIZyzsi8N@2s#3Y z1U%)Z*h3kzeLQ54fjhc%CJBM9Dt9bF+7^ZwDV~04nb9pNwq1wr$}E`(`EHk`4_}Jy zJ3DF&nan%j^nQXk{XA&0DmVfP$`Xpw5{eQ!Ou#zr{)+6&!~9!Kbms_tNNQRN+}@h5 z;pXq>U68>X#fx%&|W3_Vd^Cis}=2a>zjDFw%Q5s?}a1>Rv<3l40GxXJL+L z0Nc=k4#I#1-I&D1)qlf)5~Y!1#(+Xfjh2+mPg#bo$Ji+fQxmAq{}(R7BK{GawusC^ z{!NCxNWOC#t%?Q3ne9S%3}Vs{cx~dEcq~iVO_>ceqS1!CpGO_3A4^fWfd)d&qI05A z#b0(wXCw$#Rvkg`y!e?ap%T9Nj&(v3JD0-)RyijdDyO+`1)M2!yk2T%ZzXAwscS8Z zpjwK-b2{H_pf^JDMg^BD*+35)q1qpGIYuB*4w!Oufem?NbtWVY^(u&p6tOUiUoe~@ zXcrZEIzY%UGNvo&%ajANacN8*0TBqybPrM;nSfg?50z$D3Aklr*NmYy!r#>d)Mf&o zAi#1$L>dbiaWPk}L9f;&V_CHEjWo7VO{@5>M{ACru2>;D;059wUu}onQ8fwAP=sXc zr)ko2>&LRir#?yR;t7gHA`sEdEBz&A5aHu8wq`^z(npJu2J581eURRt2OsMFCP{$3b%^2*Nyw%=`THIHX?Y;*dY5!dF5;nq zrLDMi3c9!r5Jo7Vz#b+Bo3Hw+O4xv#A}bEo(%RbkcwAg`Pj7Zp6>NmEyPLJU|a{(dAH8(|4a`G5og#St-*?iDAQtAG zNF=!WMCLibiCAOf)DK!Y2U!JJP5X%qUx5eDTDRON@Q!kf(?SKEi5Kicj3VK=d!+ee z?r-RUD=`b(#M^%Rc*sSdY()zN_nGDLmHfHVe>Be+o6qww!*dKc;In}H z@1SiOyM&nl+2gO}VGSzB-&YF#q|`#nq@Ss-K#PO~$1JBIMSd31f6Eg+VAv?ZHr`6X z7(*&o%m?n7sv3ZKW}1Bi(5qPD|ERgzBxDjs5c~h91%-9_{E1IcKbgdTo0P(gAZiyX z3gPmp5MBJ%Sq3SCV-Gx|GLk95I7icvSauL=(97D(1_bb*z zLFwj@3Vj-SgU2gxeP63t1j!o^pQ`HQ2V02_<166}Auz&F;1mwiLfs3mf&Z}O7!DQ)1a zuZGhNmmU*Y)YzyGmpqJYzZzHoIT*e-0$nY!MDWYm-wP@!3Qc4I;kd%{ScdrJWs*8{V7(U_l<-IRpwxRB`dnoY!s#*Kh#8cI05HB zWJ(aj3Nqufps1LapKqL$u4KI~Wns}uOLoYzTtFn#)6kAiumE3+k1N3$9#_OK*=6pA zPr1deO!APbfJaiP_P=ZYvp>ne|B!E#qLM!t><@|9_6JnFbCW)^ljqHggM>Q}vna$` z^eyN+4vDb4-vO_KGFCGNh)KNfh^#jr>6M@2MAcW4b1W(PO_U-S5cDHNf_zkb z#7$9FK}$X33%Z z8a9N&xGuls0*-re4E>*bpv-Lj;sm2m^>6fvBn5zT31Q$xnNEllqh?cy5s^M}HKu%6Ra($w9Kl!FgJJ$RnZEES|jw-hT8;dXUl`_3>K#I0F7B zt^Us(0rMwq!?3CU{ufE;BMlvP(6mZE%nX$Q<(cl)ZtNJ>nbrSUp+pI*9q)pDq=xVxfKnQn)X4<_M8^#Ij~9xwyxsj~eLkkMs&gq4ZxsZPd|*J;3xz=q8GbF3glM7|uny8nBKiJ87XTzA&3DV|cQu~l*U4kThe6Eq zOIbxFxEYP-bc>VX54G6$1M=(~B}R)B0EG4oA}?c@k`J>I^NgE>1y0=_(}K(=z6kP_ zqMuDYT|QiL0lR6{aXXSNaoj}VaE}iq@k&A-6Q|%39Xl4eqdoy*$sT!(qS!v0h$02B zt_paM4+8lC-izB81e(~FEhJeDz(7n(n9pfxdFgzz#uu;uQ-lHuWHrL?y*=j>QMuM= zBt}<9%juk{H5YxtitqDTR87qrQpL0|XAN>mR_Yh$;!MU5S_(spNl%kdM1RJqgYKcM z3r)irm*;TgspZ$n{)Gb=WFRfOJ-kSYp8sYpd=Z}ux8S`lE&Ux)Jz_%wSc^VNb!ow| zjR|A`NEqGe*jx=;rdzIBo2`-d4Th)lz{~!P$25RQH`YZSvXmVHaN%r^uN~7QmO{ZY zfP9SL7!ICD|C1Aa3H~%n+LK+cw6kQG$QwJJ9Csl!R*qf71wOXqMqZ$Z-`HV~@5sb@ zKF~u|S$mBYq^e1hb+vr#5#yp#@iN*FX)Ic}CZLcMc|NvIRcq3+sHadYRuqdtRE?!Q*3~0wz;QDDMaF5?_pV>D$z6{LX!I%_HW$l z2H^d%YYc_Xng4^dWV4_mW>(Alq|SV7MO7uZ7U|J3Yw0cYl!M>|XOI1>zQw_PA_h}} z1SV@L?_SI5Oo`wEtb`H z#3-tj|vux~l877WsL7wp~25-g~}Aj>?I z)ko$)i^iyHVeyBQ{hM%gd7-w9keKJ^XV!k6TDjrQq)41#>7T_LE>bipr>Z-c_w56H z$6>K(sxC{6IB_v(;QYXhk4BgHBpp_Y`u{=-4UnVss$!IR{;j9}K0fV<+R;QgUn)yV#BsG9*eL&F z@plNoOa)5D*+46JKybCoOkGSfrk>M2qa83*yyNuDn;-Fx8IM)X(Nw00p{gprzXjk7 zf|S9B^!5$-xziPZoPOE4XgO`yE+p7T63|4>)%!uPr0m-6R>0(fHoRdXm8s)h)^*qa ze<*+s2y7}v`By$mChCPplQHWElP6|K8c8~qiT=-QVg9RfZ*vhMG*Yo(thpu;K}(hq zIKeoOBxEQf!dYr`Q=)W#{*f7f@;<+j|8xl>KVGTSzFfV3cE&$WgFxPg)(*-1)TiMR z%;C06Ge&jkT14cB4hn)U zQL(>wFUJ<_M{CP~;*6jVwm(AJ&$Lhxq^}eQ2^!Y3e(|j4E$sObT%x3=BUUU`R1jj! zUaH#|I&W;oRHsjZRp5j`KRET(qPXM9^M=E5BrgR8W78NsPLCAVit6TpJjZA$)|BonK6w(~FAn~9bT_#CI|Keo%V|c-BuiJC z<&iizu)N>(X!{3=;jToPz^bF8MrzRcTe3-Z=Cc3plU^*~bvr%PY;HL1fAT_&Fi<1Y zzAFkT-C98bPU=%}Ptvi~e&NObvfreeIJCa&DebeE^ZHwK5RfS%UMite1J4brrx(Mq zF#G?9g0Du7mu8v`d5a6yJd5*;ZTo*-g);eBBxNj9CE zjTCewmptHQn`isd^b1VN9%CY>zE_@xYOl$U<-Q$x09+}V8!i;dGD9AuVd^&@w(=>x zEf`9_uR#&oKaqsd0F^Ub(SruP_94$tO-<2TPF6RXaMJQ>~XFU7QlBy`HoMJGg!WOk#Agh4SB2OvVG|Fc^5lL5daozbNY( z;_q5ReZ2HyC_o(@@D*csOGfXJ-)+5qFK% zm4yYqQqsoG?z|AFF|JxuKPIZ$M|X#i2erk*{2Hs!RkU01!|r#LAc@qHSv(p9f@y*d+1lEyF2Asau;z$Zf;N5bAKU<| z0j91#&`=lA8)D$@Zs9JXad_g@`9S7fPv8q1os9ED-7Hw>XT;oHhlbx1s$dK=sS|J7-;>P zgROWxD9Pn}U?%XC^)(-_0Oh_d$O3{^xNB>#7p;aYLzi&wohzc|m7YW7gK}?&pMw z&79(t%A7|b&C1s_;gbM3pVABKS(QbvpP{VYW4}Dh5xKOOy2040gD3kPwwR{^dX{{( z3%WGuFIr2W|3h*jWTJxp#aS_sAWaqwBDuX&?ECi6s*^XzgrPT#2W6g>rtI{nv|ZrK zwc4zD)QYfXZXv*sTvxgqG2^3%WTXoz(7`xmO&(z-A?APhWG|fW%F2o(iBwrhMR)IB zX6CzgCEZ#Hm)~wbt=k(Uhr1Pw@{n}}Sbi)?DcFO3dY_6w$9E|ysimaf&CSQ{k4cV> ziBGTm_X99+M-DF}nKI}#81Ko9=r*nR)-Xl-MCLS5ddQ3v1R1h3woim}Kf|QE68)eM zs8A6*rSa-;ksHE_!idRjS7E9-bwrg4{WCDcZ}ffebggJEd+oD``hO%$=Mg~xsoFD$ z>uX0+fY5`jsB5gQu5w(@uFVVauo*ceXz>~aOcwPRS9zmVy8vn6&CSmf(br*>LoA-qHfEvC1z4mq)S|uB1b{2~0EMm94x|t1% zFsW{*fUUB4cY#UrtATsP*wrTdF{4D00hpo9(Lz=`!e^%oR%{Q(0dgjRBr%_R}nU*e64x zdBH3f#HK*E>1K6PwQC8E3UY{t$h9l<>2gWz&sg-N|{Wn;|n)$yC+K~ zUR~F=1|I8#25X*TmF!=8HrzP8W*kDmI<8HEDIdfw>{c7k$7al|#fmsWjyOc|Ux$bc zq+bxwazsIv`YYrDmJAV?!_Y>vuV$=&-SH)s#FEJ0B*An{C(8y!V3?^c>KLRsos=F4 zE<8~g!2W-V0J&2Sl>{y{Ec}dScK5i^NYEk?FuOud;rk_ehX0f}c2Wafi4}$WD`szS z|0R7AOV&FQJ~WF;r)%*r(|Ln@y#r>=p;cLJB3yUqyt15K)%t_mG-ce6pXQzsQhC^` zCpc5Fit2@^9FJD}Tpoh3D_S~8z6|ly#&9xBcx#jmTGIp%iG{1gjBx)0<`+qw-<(%F zahNtm4-c1kDDT+}c|K)57c^2=#MzC}XpH{7s#Ii@NmP#nZ)pZLum|(r>a=lmC^9KD zY2(jM;wHQOg#{+if_iWmN>oEseC%Z5v>!A?qUTJG0)i`sq7$n06tUc2&<|JLeXX1W zSiZ+Yoi4(tN()li4^SE>g(mFPWh>z6)09Kg1CRd)k$Rzl&yNsFXY;%NeF+5ycnunU zZ%AXk>C2w|kEkzwHPuCoNf0WB{4wWv{22f)p5X!@I<2L%N$13@d(^x%HaSq~3U^(iignV<|* zI%J3=Yo>?OC3`IL+FwDoev2V*x|-O967-toJiC7AqHnui6Ta7(`x>VBk7MA8k29p? zwuQzT3dKdN_TFP!#!?TR&aUd&eIX4uws|r#iY|*Ykn+7I$kkttREhlP6h zos%qtNOvZ!-K-~A16C*mD#9zRG&0X>J8Q0|Na%wzI+HI^4f%n`#iw1NYk%4QMv!C? z(H|OK(0Y#n1hNzdwxZhZ6E(6oe5&gn2aGYA8)i>jPOXzxN3WOzq>XxsVzgqY-O5YN zv@b+QV57u%C)O8>x+9~XUq#5EN8-OP`y1#-ZT%U&5p5#SO)NRy9H$WDBAE>6|50UF z#6M~~FOZVfUmOAucAK#GdOds^GR_X#JOpWsnt1#T1tZsfKrH7}dp8IbV_!8o;E9L* zK2fk43MK!CBZmXAL<2f$*!ZPrV8(CIfxW*|tCLkW7UvhFQ`i7#1iy9*imIE?*uGG#%{|XL0eU`Ay>Pu^dDRK}m z@I^-Znh2T6Ch>FD*wsfH9x*%Mf?xo@#;Z6 z(POWLAK2{?6FJ>59(kbYE*~3`>H`~2qUlq7H8({&Me#F1JI+L6O5iQdjZE(1<&CTQ zLtW@R5KYZ67w4DRJ$A-v<=@EmU#Nd6!Jwi>!oi(NYq9d=5h_4avMktSJWT8myPzOQ zsERi@;aidl@2CST=lX>XCu33mD`*WuobGcY3mBp>ij* z0uT*lCYA?gR^|#TD@r%0wG@Mrb&Lav zbS!g5vFOuOWi+>-v2|n(tXnGJ<>{-0N(uE?B#Aqk8kenKOt;5BGy%CLQZS1)3ay6$ zzNA_$ZHyBdFU>;=nY6U*_w-OARd`+%MF_LIAwD`IFc{CH)!c_%Jp3^*tS5HYP zz{i_rVPP(^QarASOEu9|3TNx!=v-f6b$)#ATUMbp1gjeL)x%S{a)8y$@pz-o4@RkF zYle<+e*&t$sZNpaK{E%o@kuq#CqG&#sGtHC*R=tIT)%Imi>0B~OLo(T|DJ@3v@{Lv zzpNj|EB2@7LhP#qz)}C8m;`2!pN#93h<23epK>v_CCe#+5zDH4CFM-Ug3$wSUWQ%Zmkj+COka~ zs6x7;h^CgNCk--HaAJVx8iv`hM{*V5VvuFBnaR5?7wt)>v0a}#Z&Z{?dU`NYCa`Z| zQS&q`IppS67o)(SKcxz~5yTd*1G6Ih8g1fiZ2p<5Fe1g^eWSMB{Wzurl4K|Lkn8IH zWl1J4i8!6=X9WS+o*!Y{?s9Yv9e)jxN>ujQ9uAE6z7%c{spwOvjGGqNxfGwRsG@3p zj90!0r)j|w6x2(9L&<|M0!XQHc@GO{USUT?!4q|#?|hQMV1(n?w6Jb7#JOdialK`G zjs^OQC$Mg5xl*Zky{)ghhPg%YaHJDY6BkK^MSK2ej{w}-j~MMDWCw|zauL}C?BEpJ z2TN;vcA}l!14LHU{RBVWjwCDa+r-Ji#*zPI34wq=Jc(dyzWiUtdBg|w81q1vunz{_ z&?k;|b3D#QJz3MC8B6+0C?oF}>BC08S>Zqo8B6}EC1Jaa{wq0jKZLZwe9uC*x6Zoi zEzZmSB(0gdvlX$;L)NN`)O^al`hc|{CC0&{Y+B6bYxpf~l9x2-#LH47e#1GfVvp3?ao=+6Z1%Gg)}8y8UBJ zog#U#piw1hANDT}6d}QlxB>CkPX6HB3W`*>&#?{Cii{BC>X;i- zkKTOVEc|?2vY13MtjTS#-iTliKELv1#~z~)wZniye(J@OgsX-{GPSVk&p0Govj*-| zW}BjkK2Us8ATBC+*_lt?@#(@gMbefHa>xI{5 zw85KyCZhPtN20G5bW^m*B;q9wOi*| zJUC$Ox~W9~jWb5MtViTHmKv8@atE!90qQK`N=nI&&FEKG^2imhw&iPqojbUx;=Kct z1GRjiX3*!&gu2a!#Bl-l2k-c_3Y@RY9?*X%bJXZE8Pjzd_Y2r9n9|C%4*ySL>?#t_ zoxA@KS;t&}{-^ap_YoM$MegUGWuuvMld8B z@|EgOD@r`-)lP|X<{JYVGTBwyNPEOa?q^zv0ul3FT%CWhQ0kn$bATi3z zb#12U?ypl1Vxtr1Ow1z)q3g4!Z!=o5i}B)6O#;h>PWYo+?F*q2t9t^Cc^ zbxUprWgNJsz>-IcO1Co5H|50o-0gnB#Q1wXVMJN$tZ?|p^Biv(oS^f^n}5{P9;xnj zyU}RpQ$8(1U&4v8h)?Cj8G-tb6T~jn<0B6lrw2wjNf#9M)`TQ@7LYo?{NvjCROk0o z$lzqmv_tf30~5%Zafhg}gw=Ui*M6(P9mcnxe&2aA3{}x)WUtg@gqG~`sy(XI38=bp zppC8lNO=0-zR^V{oDx|6RxR#wr^e`xqX7fB^=i!Z7Nr_F^s)&TaCw+vIdES>S+m-& zc)Y#Lu1g==pN{gKHSOQDv&Td%mWHm>uvn(ebM}3CRa8`N$yO=8bb7GS2}CDrBo2y^ zC6k?;o~Tr+QB!Bq=o}5Ib&o_dTR?BL(cy{|Y;LEi&Y}p+vo769Ly>k#vHYBgP)N6a z#iw_kvtIg@z(OQ}1iLJ6z?c;2N+HD3I(U}P$4QQgApUm{L>5dK=TEm`FH@oXUvZST z7_sZS!G{QNPm|F;VZql?tVLzEh&~At@m>O%;E^057-WLBUB65W`e^6H=UWMAPl3KWPgd+s^5YK2$ zf3zHe1A9*1e*Ff)U&I{M@?+f^|7Q(Q2wq+(8bK3E9I#Jd$RVYLy7p{r!axQj>XXE< zC(0eP=m^u@9QQ4jB&E^QSxlcQp3bB6#E+`%3cSdp>~JQDp3zY=ey(6;K0fvBZ!F04 zWPiG)|AY=1>z{^yb0$)x$p2o_Ekf#g*G7u&N~^YDe{QM1(UW~X3HpKm{d+at!cnfY zn>u~k%#ok_((Ec!FdAp0qT*^RI1d(s-a`vD_B2Rfy-j6OAWRNNQoa5*zm|l+hwns2 z^DtVvO_GAwM%(#~W>d^;0yGK|u;!}2doddQ!?s}w{fnTkhzRM%h-MJ#)@I8$p6uny z)SwrL z)o)wKS@1DF=O+*7z6uVn=ulH4>4qTt8oP+oMsm}HxOaVN{U6q=~RGP#;K22N1 zu1bOyd4Qq9f7iOn1sKmKpUkslTYi80E2+AXrA?oikWyl#x@toFSF;nV*|EsRvnC&= zc7Hu;A#ST|e1CChjzH9E)rg455{!nq|FfyD{F$}QdxxHS`KRE?5`fv-r|jnV9&~d+ z%r66#Dj_n2hPeu7Ib~pQ)so-hK6FF6@yD8gXkvZkJkVWBw8rrvC@SjX6r_h&S=7f+ z!0rVF*8Jh|9U4jaIvDD^^6u{k&dvddm3r8ENR}_}fJs%wQB| zI7NoXidi^XJB8!(1Ez^(>$v6=;6w-9_khjC_eOLrwu*f4x4bBJ>-myI0?rtRu@riS zVEO`-bmLu<1vn;?2nHCIU$c*~2%d(sQYHlctQ=bh|x=YeZ%uAN4Jgnz6D?vDzXATY}MYCtBc>S zdU0zO=3niyADGhd9n^M*D7j_XXqjRa)O6g8jP7_~qOd)Bz+Ez$J=Zmi9(Vq>xkg}< zImDAOO4E6E&$>B8!{eGctiyJFyKJO+jH;s5e7BxIWX)sauGO@qEB7k{N%ZX_(Z1@q zdK*36tL_+``Yl%i9Vc)N@!Ps{t_KZJvpRAA%GV66Tcf@FFx9dU0`Y08JcS^MbPlRD zhrH&7dtLnW>ujp5{P4JDD3%$^I6j-K-dM{tTfC#;6mXpwVN{|FV_w()z-!T;xsHey zg$mSvO02FSp3iFM$G0Z*aSuaEd-X3cu4e4_A73%s40Q`#hpEMHaUQ-1VVP~YBGRK$ zytaB|5o}|AMUt`SICDkdzMBF>X1CHVC?pP>u_SVhOTioNXWQ_#P0O>g2&J4g)Yi*= zvQ;xUEB~&Ux%ryQE+(U~rNcWMD~;Rzyq?Oaw_(`*@E`_1&ty7E@frvx<+Rdqj~;i-IOobV4_k&%#~m9z)DIUHl4H6ZSFfG?GGms zm0*ZVMkhEfnT$pZBV=cLz{8BdzD;M-jBN8{f^1azh%PGIzeegShn^rIV!F}N^MPvM zNlpl6a_jHdr7-@=BD;#{Lhr}KX%N-Fq9E#x#oNY23Iek*3K)+kW`gspXJfDJKA2``|Pf%xE#;Aoxu{UAp!rfFQ zF{QLKdDFUzSE$#Yj8IkvTxGD^1K!@qSwA8kp|0tzOV5XDl?O!Q8}X2$p=%anqy~z> za>W8C97zm<6(*Or;en%QIv###GJ9InHrXTVh1NEBrf64g^a%Lx|8}{q__`i-f@Q|A zivLGm|IGXPq_-p4^_3uvrco3qEdam(BdV(x)OW5zv#iC0LKZU@U&v=8;{ZcYsjrP? zKI3iOQgnCpw|Vv!Cl>1Hm2U41=wYXC&4*c^8;{;cmKRmMZS)2#{7&O(^Z1)zEE%0I zx*@#{lQW%5Y>K>1yL(wTJw`u2oU>t(exwr7TfN^;u)Sk5Y~Rf($mqCC_Y*LBzPtpF z@wiNKIc2wOxRokbcmQT>vOjk|%T=p;)Rw$EJr5?cqt9;kM>{_KalLntgT40za5Zh( z@3p<(?{%`hx8D&sw}|+37hh(j-$y1m&e_anhzygdv~C`~*X{~=bdX?d-pyt^?)oQf zs{0QVNMzJnoz8D*JDzIoC*9xg7x@(cV#4!PFi+iP$E$V6a>143pqBIJ`+zr>oBI8v9#`s@btGfVf@eYl zQtw{z-xiZA`nRi>ZMEg)Qi6|bR*$lUM|IiOVhDvt@DesLLAA>;T2V|Nle_y{a_+S8 z_HRQ3*J@F&{6D!qGT^kVIfnP9>n| z=`%>~^2r`npn%aPk@;-{OD>>vx26FwClLq&Pumtx<2z~D3gDtx)2gR~u&tZ=(P~|2 zn5n)E-1Q>xcys|n=75l9*!Hj=W)L!Wmw?|@2Y&S8e%Ma&-QAFXi{^eg@_Ido+&R_x zd^s-6o@nzWkw~Iat20F^>pJGON|gL;IjwNtM}_`bTHfGb8p0aj`SFbI;51Vqhx@Vx zd3ZCRF3)s@;;h}&EjSu5ORv}CLccUS+o}(KJ~5Bg=3u$i0Wk@puFKRHs}0ILRxsFe zUUg^CM1G)!mcuVqCnmKjdG3TWUWdxa-Ro9R)w4rpbuQSvpF&xrE>y%zV+g^KMW84q z^{*xbOZsOD%=U){R_Q-c)e8>1HW1N9sF820gU+?{Y-&c7JQbOcM1XvMB#v>lGkZ(~tnp|<`ckA8$YTf$d758Hc z^6K$G!^ZQ8Ft`s5k_{7asb_now9*n&;&sx%VtiA_sW}1XJvC37!wy?K(mJ z^t^5RXUjTIZ-);Xk3P1#Q55cBl~ixyMhM({c{Xj&1-0ymfbLyQTL7edIm1kU3}w>q zp-C{DR-T7ik;b|@fE4!rxcV4YT9L$iy|r1i*e=on!9{N3ro|dElBt^ z*TT$#%Fd6Seu)eBD__7RU(SaraZliN++?%8*&42{tW0vgS`N~3A?Xu3q)m>DAbhj! zf@7VjoRa3aP*l-LClPF`HqdBTbxMyk88fvuA6U3krM-XJjUKgR6gL@9t6yhX^--Z(eYNGVV$^6)v)AotL3z@9Py!|>~#{K?Q_Mv_(9yJdo_04@p{2% zKQQog?sj|LXx;Lhw6WE!F_gUD4f=u6eHa)p12!71nnLh%0)ms|yAPXyyzxnj1zn4( ztV<_U|G0o42maMQZH^!kppTc-_nUvsV%yer9OO~CDaJ`BXS}F*|9JmVXh;&uM(4YW z;W|>h$1#kKo6Ryq@kEW$<@*sS584Z*4!7$I!~v;=ha9da4*isw5I_-o^1b4r;#5G(Zel6ml-} z)FSb8q||?^g$l2*t}wmDyxYQgJJD;T5oGn$%m}?nUh7k4_e+E+lVt`6%4!~cV~K|z z`EQ(JgVZA$;O1kH?FXuHD zNTpVTQtH97rYrCTEdG>#&GC_oU-vX&w&MmB{otbYJgAeh0#N@tHAy8E9~L{`_5s~q zQ6~T!Nw;LUt$MzV=GwL|>QHd#8^E@C8@KTg=!ocieK(5Y2@$-=_H3g8J5>R+Oxs7L z>*D+mD|a_}%GB8&jwTfs6(#ntEq&M*j+e@TDG^{z0BAtNhT@3pVF^w`JK)PiwFs`` zMTiZ@{_OeI`wy`QGJhe;_zHmUi(-upL7?Me${o`7tM41i6;WLJsWs{)p>X7ndZREA`tnt!od|ax^{`Grf z-e>KtGzYHJlHLvolD zl4W&jx~lsQcg0OR#vKHbv*~d=4vFh=)bV<0Wo6|KNZ~!}V^YQ~q0CU6uWRyGG9 zZ-O3oRlOI@hd6x=F4sF;PKBV8JA<&lY9>3M-uw|bk0`?1D~+wVDM<>JbA@=lx{oxn zoyO9*EY=+tg@SJZ))3cga5p&^k`lZ3%a+sEnZ96L1kdvGhE;eC@~y7q@2vv4@Ce*b zIi;bRw{iUsu>51`CTP4I)zbn%G0|RkdGb1ED8!o=UzIv*YfA-_f@jKJYld-P*1~z? zO`P!gAHl+*0LQk_o9A3tCMz5LG9lplsN;ql4pOS&3TDG)Pz&>$38^l+MoG`Tqx(gT z(o1Ftr}S6n5&a{X#-)(`tMUN6qud}(m=mWtzE5q@ZAQ0W_qsu`aC{t6T5QjTd{qtX z)DLv~tcsCwmB2QRudaJ}NR$9T$$8@WxLIP;^ATHz!|GymE=B9(Zy{Qy7CrpGySv}k?PakuC=$79ZmX&n{=FCX^DaF56NM=N-+o=E z@^d{u8TaQ9-yp6#Y{5_bhtgZG>cfrKUrd~MuG0Yda5eIi?acs}>n21d>}5g2Dl`*2 z%iEWPCqt)=2SkHvi|?fUapN?Z=M$46yc~Uk`IKU=Ob2VMoG(&_ldcvsWN(_q-u0}j=8JE1@JF5psHZp`z%H+vO$ z3Z{lg+aZ{IjfiuR=6YJ*$hs18P`w8-Afd!-fQ&L`fbT*z0q|eEjU-a;xkoC&11Jc< z$uY6^G#4+E^px##Lq0~JYXhNpJ|yFH(|oAya%SfSb>nW5NnDDiT&ec@WDsrD@q91@ zQl0(rB%;H;uQ=^^3^*Ze@_8GqMcsww@}g?GR5Yn#$=DnvRWR`y~6Lei-(^Ouf=$)vO1r)fIa0lZchGk3b(jv zcbM7vd|#Qlm#Ct3f6mx>Uc;~ZePGi2TVgYA8rjTXM^a_m?(p7Ypsc`a}JcpBvK16*P2ffZ|ImV5IM zjr(Q03R?ROpl+3^)uPdMrLo%|9HiUeCy&F>a?(b=MsZy1B^l@w(o_BR-GOcM&>w(B z6mUPQ9>h_t_rb_=JxH}`Tt@>#+i!}gI!9g+tc%zh@(XIVKa8s7sL^%314IjqYZ)BK z=liAiC!iKDEG-?)@wL62l%||@-cE2_8?9MXw&T?v7o-P)(7yl(*dl;6Kpp_ph$>}r z7Si<>T_W z{+Q+YfY}y-;F$=3^4-*Ca7H6hEIdUh$?takr1wB<5`>U*iV}I$R`lMPmV^CAwX(9! zRUAlF5~2s^^f(myBW39Xnms>Y2;5W1;rNky5NYaA!i18FhN0y` z-}}a*qRuwl&-W=_0^>X0ZX_!4-0w`H-CeptZ(0JloHnVOZMSPhyp0RLk9*yCH($6t z#zCE7oj8fG?I%>~&4ZNbiM*3$xyUQ?TehBS9!?;Me0#>^OnI{Gx9?=TOy+snehlPs z52voO1x8?4!T%kWNU5wFfHa8Ubr;X0Wk;OE!3M(s9!}*X$#vVh32JxGytq?6)NFt7 zwwGaV6l8Efk{-gTJ@N96`+;#6h_1z8b>gO)3XF9MTT)u$Rz>e@yXH9gi3d7R>Gh5s z0q=ddx|z`P-DwF22AxDYWsBl9QK2(P35Klw{eeT%s8OucQnw@fw+GTrvLjDqq9mML zl6%PVNz|9Dlan8?68gfpz1;{rEmbWo5Hh_6zrLG0)-#iO9*2{amzMVuhncA2sy&er z$<>W#G}qVyFJ`fI@!GP;_{)=+?1mO*#RbB+V1gwnNN(|c4rY7YY>VJ5dvCrY4r#kz zL_Km0J#j0ui<;s-PeHo6G=MmMzV9ddyG{l3{Cum<2De+nzctzhQ8vR1uZJhia3N?= z^?5_}Zu*wD?XrP>OR!z4gy*y&GZ_m>hU;>LPY-209i{KVmF51?49B_zbP7fXM;G}h z(dBaCec8m=V5|4?GHXQ4r(y9Lm^?Li{9Ei)mjet@!?Homa=T|H|MKHFj8fL2%j?yu zotcX-O2jypyL)h0hdUIF_wGLbg2#7Ja_9nm@Vh&#S8+~#H^l6y$2JI-PF>9$&@O9z z*kJE?4jcN^6%Tqm6zbHT57SVB2 zk~rFSyEs(fR(ApPHyl#11lyCK3ScUZaoeU;9*%d8IJ*dCshza41D1`F})xWmsHG(=F~WxVsbFAvi&U z1qkjE+}+*XA$ZW>5Zv9}ox$DRxs&%h=iI;hff?xD)m>}Vs#R6&c&)jkc|q)xf|j-8 zT)il;69K5exA$dMlm(16&x_)X2SJY|qFMi86-4sbJ!n|M>drB=Y~OC^8qfpKJNIGp z;(mj|e<^HSgXh^~Ap#Y*VYh?4RJ2!CoVCv*+V(m!YBL`||M{-0c>Q%UV~7=&WfrW@cQy#i^uCZEs#Y5|rRu(0mM>R0+K=?P!gyY{z{; zfyQ9GdwW<1K-`Qt9@tPAf;hVX7VdS*)z|h~4GY6TB3DctGqM^xa2+-VBCrDx1Q9$b zuNBtU=-u$Vh~7hFzFPTq%1nNJ7@OrZBfY3&+7Q?R%9JOCK3;WP*Y7|~)$g)047@gH zh!#HRpAgraOnrdL^7DI}H<86XiHK83B}E4uWFVUvG$E zm2s1;=_y4^yRJz~1aMaS>cC+jibKR@#dZIRINACBOwi-?d}MlQ7*o-d(YUz<4HCOx z3aUD816Gefz_vPyfZp7A56Wh?I(Z$QCKJ_$ey=SRYnrCYFfZ*=hK>UEwtmMa{4DGC zKF@Jf2m8D!_h~>2x{w0WS&d{irqv6kCqnup`%{vxF_y9l;q1vi%>||;3abUZpo9Fc z1+l+Lr@ujz@H|V5&KI?R3XL*QhbSts5NKik*rWJHP0nKu#?0fnxHVD~NY}9gxQ97j zErhrn4Z`NndN9oWKVpGaS$HJb(Va!kN8zh%ye=!lNGO#Wv!6g6FSz>`dR|B3;dU~u zwrz;G`QP<;pGFhBp0_O$rXtE)dva$T=8-U>97VJgq(SDRUVKLHP)9piQ zj@O7eTd#lk%s%x^Q}UQVKVvX~i-@Oay z94Y1qZl`uWHoyKTDNj^0Y$DzC$syLR22(@1hxpliHNei5J-s|nrDA4P4(s0zTP+P4 zMyE`@quB!vv0PtKF?vQ?H3U$JAn5{K)(UO5&3onUtJ4%wJeN(EqA$m0R*mP6h$Oz+ zP+YaMxR2=G+dH84oe3tby}}JkMeC{be-e zs7$r$eVX?GuJ)1cZ4GevIC=Jg_4aiG>b-_0yD!RQYmv+>GCBg@v66`#JiFI@F&8d z&)%&`C@afmFvzfC;cD#H#MEm}iM7A9i1HOwuzvhh{i-I{#k@(5P}deo`J?ZfbZJXL zls~Bm&$n=md402Z&aPaFFA(_;O`AQ4*>1G)slHpMzo6egU%Xf$9OM?S{&kNt(U!fq@0Ila~?La{c zAZC_oXy3b+s(XJJAnEgTb3=I3@UcdH3kp$2sJ_en@YHPItA>wO>rU@`@p}+biB~~% z+kWE4d6noBeC-CTUr%OSbUyf;){|y}cmi~&A|r*T(&aLfcaa<0w*$t9HTMS``PYCU zZM$jcGgVK8Z(vRvvSF6A``9q)YTpX3RsN`KjsCaKiWRXdX z8ZBaBA0W~MZcI8~8t5^x(BpN!e4y)jr!vKEny%6|xsn1c+xAE7d(m>moS5bBnX^W2 zqH9TFcPLi=mvZ-!kIP0NXmc<^ot|$#~^coGg>Z=B5B3=TqLlo#nI;%V@ucb<6)NLkY*D z4P8uB56SnhfoYqzD3A9|a=(KI86PKA(a_^~Aekr4CPuaaIcAd%enCHT5KQv=@0to;;~Cvucg$J_lX>E%ioz9hER^ta%1BM$3h`6U(zNU`+F`6Qm z$z#7ihIdZyH7W3sC|tbMFG0ei|Dk^9*3ZYYI>EX6JId0WcN-%oYZ2Bs`dd&5 zCYs;*sYY#fO)VO01=*zF!|P3Jr){JCj=3jf8w^BMbS$G77*I}MKMV-Hwy>cJ6pnSa z;qiVRjnuXG;-=^*s#i%nSDPC(&Rts*h%!Z`)8d+hPCJ8^jk{j`q5PZ_ia$*~E97-y z^|EalECJl6!uCc}He?(~vT6(ChmtJk>!$hzH@z)Dh8F>iU1+<7hZ?Cmbl5y=;gZ#b z$qNpx0U5ZL$mP`5)+liNJCsC&)cO5!D|AxGP0;KV|L)xL!e=MS)^A{+Ro#L#5}lC4 zP4y*dRz4X=U@V*S{&L$t3QuJ;d@b9`a8UT-IGFI{hdrI;5iwnzp;;PXEB55v1UWrg z>xS@h_`pj*#|LD|N=b3%pUbP@KlSg9AnfMytf-+lS2uT&Mqk%l}NNk4Os)dx*E8}m=)tA)fG+gX{_dRe zcmP=mbw_i~^IY+_Aclo6`YXDRKa$DcL3tBNB)|s5d6e@CTh*Fa`#(tc2Y6i%(*!xr zDM}8@P=P_;J8Cvq@wzM_xXSr0fiP-dFg~fzU76m_wV%ckY_{7S61E=bg1Vz!u@5D( z?+%koHRSAN9V86YtIcv*!DDU(*YlW`&*g_V-x%*eH1lsB^fy_oTb~K-4v_>k2&$|R zvxnka@BVg#SWfEIiW9A&hf(VLqN#U3fHmk+aE92I3F%M z(QN7sZZ+;3hzVO|grfbTrh1i4*QJd>kC)vxQGEgRF;}9U<54*3k{e z`x=6v{4YHMB}gVJ$tC!Q--ww5;ZzdpZaNaU{ngtF9+TxhkN0$oYfGppr3i;=p_wEY zAD6Yae_>jL1T6X9jGK4iuYTI34|D~q;&%R2)YOwM@H&*Q-UK$0Qfu1!vlfzKq^ex_$x-sdYE-qhF-Z9#xSxOdV{*e4OkG9$trtQ@D`i7O{TucXes_Rh4QXCZ zJ7rCux*>-TtB%o1YM-yHLKM|;aX6L`J-&DswwNL~LA&%t`ze!*&U>fhMElMDy6eTs z!;3W%`<6L9JRage>06Io-1?#)d7<)F@*w=JEGkj4l2| z+s631Wv$Z_i_1mN_Yh0Wpc|S-Ab2XoY|5HbAVUZ4$K9h*hz)uqHsdu9)9vSDT4j$l z`@zVa9-9YsqFOmZMigQ$sKMz6#uLYZBhx5c)}A@d@z#xaB37>;H%3xqONYU;T~9It z!JJS29BzG<;O1$`j)Sqjr{RO~*?g|@fiP;l& z)Dec$H}(Y0K4kSMDgW)K!}T5@CBqDZgbOwZU7%L3E@-!zyLXOe`4)4ODzr|>>p)3} zgUz50Rxh;4PPyv?yY>P$82ppnvQS1?6&VPGo!@(gcg#$XO4LLLf>bV3;75O%ru*Wh{gOKl2{R*!Sf87U#@$fdTafA~ zinn~?H|&9C!4=C;yNT4pi5kivG^O25(pi&VAU2ZOk00&!uq1Bx=M@c|2Etd!O{B6g zT}_`+tqs65umo&wD^yOzI&S>y^F3qD2`nZ1S1|j5CNO6i1kb zjmRILdSxPsmAHSEGx`*MdQo?z1;F8?%W!k* zkWGCev4;+6 zq3n=m7OQDuP7BHjH?!$B-clrDe$*Q8m9w?=XM@s)korgHnFRYC+elx9VRiJ*wNDlcJaoMc)jkDAChxWUMS*8fK zBZg8MlFP@IM5|#nr3p7UeY9u#R6TM+o+>SLw+E`Lxgb+x&DZ@58;(eDr8=+vh%cci z+%{{!9aATw%@kAvOFxz4Pf4`D!bhfVlKLn9ip|7knOygO=pOrNrwv9l`xa)e(lG75 zcRbG%1+L+R^?Fk?7_`x73*4rb+ieR{5OQqbH&l8#7kcgfPUt(qyRU8d1LR~MyDko-pux8 zDe*Ziv}Yo?=!i>7mg`sD-T3Q|rB#vG2n^WzIZk52Xr8mk**Yc^5v2@?{07nL2!&Cp zUUiMmb6wNK#VCmSMznQ(lol0fH6t>17S?5VIg8L_>7y5aQXx}>hM z6~w*;6VvW*aj3xydDtvi=2R8km1ZTk`tZCLT@uKy^&HQaAtidzNjK;aJO?CHI~^&- zmib?4g7vmPtb2|O7vm1H)yhBRn16KV&{JHu1T_AVyq+#eIe%geiTW{JJNeo3I72!3 zngk{G^+qxC_VOm@T~@wUuhyhK$z>PgLQaRT#*U0Ec5|znNA8fvUIbmlzOiz>0B(Zt zk;!7VMJ^-@*+S~5KE}@tRTP=^MCx>@;|Bj2LyU?rf5ExUycmxjzsew(BpJV_Jm7f`e zO+YmfPRCt^?N4&)TrvQ(t1R5!vT05`h*a)7nCVRfEE^0W_mURmN2ji+-4??Cu?6{=TZFHQ3ndwMb8 zS$1)y!&2iScR}?wgM(YKH494+VA5SKnfzHDlfx?K7>y>0e|;o`mr=HwreK!xCv7Qi z@#SJqe*Hl$UD02ZIQMP&pZ0}ga){%mZ#Y1ev&8=e_#G_xYs`qoP+J8U{M8HUH+*52 z=`c)5Zv0<2<^H4RW3*GJLT8%=BrkuUMRk9e78|%=q_C74{?LhezIop8WVfRcc#hnG z>Tu}aQxFgUH+N&yms1peyj5PAG=`NG)o~u@u7-Zn;$C0MGUc^5hj@i2MX*7w+wHmG zT=d@2+-aJs@La^n@jTDhVDEfrCt1l$klyioZuRwCgluBnj+ew?&<^X=6^Q-(uol(} zp2=v(5afoXe^6+T-}KSZ2_X0k^^y&$ciOnITdlWL$fQn~+25v3FJVQOk;HYp{*e)f zJ%wmg_=dWTSDf?yBJ{42$?J0&cma=gb&xYDPB_18*=a+FV+VDDI4Kax+jrqDv^SAX z%@HpHWrq>uh459gfq{`R`3ECmB@3uUQyiK9dxF_&182&{)}VeH_H%0L)ZbShn!CQ2^vpKzmJnskfKA-a>WqVHW5Y|TR z#chGqVZ-7$0S>DGpK6!0&sJJ}A|p#Vu!Fgt@2|xNdg*SDm%r$l+RMw!xozSf&n|pz zWqfQ0ki0Z=ygYiG7k!9s5kaipymVrtZLwp<4U4RYH(PcnX@5#p*Pkgo+Wzfr4b$vq zPyq}>0iO)$)B13FRfVN*y?H(J+nxdwg^&Zac$|5!z4{#;A^592!eJb)u{0n1(}YDk zJMX0OEt|q*n-VNN1P2sq3jkOc|R0K=tv}4Qp&_EWNLnuM{UsFXOpmzPape1H>h|zuHRc1*6{xHr zB=F-nDb~ctc$Y!5?XW1L^X}ZYXsZX#(KP7ciet@=@@(_Ar`K^v2O`Kbv;RDi-|aB> z*Y*rTLG6YHg&szIq#tiYr}Ciu6jb2NAc-#_;=EMq-O+5bBTc3{%Yh@|G#Wh1-rn12 zDuXV>0Sp7<&IDW%GQL=4*AvF3JHtrZ?TpMe%O@tpy}dnL+o5q3(a^|1I~S*m21xNr z|LRspP@Re#@j|xQ2ZCE}>7XiiC^5OkZZ>ET(hHy1=W)x{_hq%A>s8w57WyLE_u~b4 zNzxyVSrg$O<5vtJ(5%G3npi`&%dKx541&J0DBi|CNE@2*6*r z5sG$KSnza=lLAMdTt-gS5s*8 zuQuO~J~Y}bIB#uN>eRp1hzVFW)Q-G7Wx5X{=M(6b)GAYtXLN#{EU22=zP+?RxEot= z0noZ27XlUVSz5Ma_MUL`)@qDGzcttb$#j_ya$K{U_BSa=-;s->@d9hHUC#1?kC#b& zEhbDhPpv3e6wqr;#J`J5?vLj%M;I`9K4h;~$Y{y948Tq>eKDp{$nrQYp@2ok_2cCA zBEB+xX0sY%;b9_uDFeQC3SA!5xn67tJ^S?a!xlG#X}yPVCDUGwlvU|EZ9!YF)`=WV z`Z^4{`absl-exfAqb{IFs6E7PM14HO#?LseyxAB?A)Qc9U4~3R?EX!EEPQouFCwXNe>Oh^ zoGI|`lbo1J%~#08*Mb=cF?9d$yWp_(0h^^XPI*u;925vEZBUpxpoupINPsNmt*+f| zblT<^oK554N}U#)wK}x7*VCIx`-x_!CGB!Qt5b~!B0eXt^N;MeJDtvWcC7PfU)QYn zJ?2JR$!zeZWdl55_-wn@L+c07Ry@6LpJ0QUIx$42Y5%zFDwH62p+e)~-go0^C+8z9 zg6B#9{q?Ch+rwfqTQWRznPjp-OXI!&+Gf3#Gj+BA2Kj#P>2Vh19_hq_%zx@vMZx#t z->ST(O@=06dWOxZLl0N3Q@g*jJfTcBdY{2%gIN`f5t3sE1N{BReU>_Q2jY9bHJY8a z@OFMIp`+fpPp(BFMNKrZ;~499?&W46AP>{EXcHfTvRW#(43%0g{T4)he6je+j{AIf zLW4<2rcd(QFA!tQ;oW|a=mDgD_d^~VXa=7k!G#X@iYL9wdA}{LF&ad{6D4WP`FK45 zDIt6&vjwa}UZqJqtv-IT3-zp8tXT-ObeG^Cm~0W2l$MsJFlyyFjURLN%+3qs_A3{@ z8;_@nTCqE87hxES`b$zt;?GsbLg3l3Zr~<$-6k$$F9wUbi3dqKK2Lc|O*@}v^1W7@ z4;Ls~P~!Q!k0de)x~~wa$o!vhCl?j9%LfwqUs=7dKbc=nm@`=#@L||#AeMKOXUGe@ zf@>%O@*w{9Bi}ps7BGv{N3N1^B(2FGO+k?|Qr{{rHt#`G<4R$D_hqTp;iaQOC81JV zF4Ud;yKPAl)OJX;`XmFS}orQ6JKD1&ALZ^2#tR)VC=J* zv~;B2>vpkz(|HR4DF3F+Y;w9UXO|!310$^Jjw7iR`1ZQ(1HQ9Z?amN&f6cDw!0)o70<(6&~8v4U1IqsFRj`rx?e|Ekj)Pz~lA5n6pII=*z zN)8B5NNyXJ!)>zni&ZN|urJRsLdaA0SHcX(DwMyyU2)vN)ul&LmUzNo^U$*}h_VW! zB8qgYPi=>N517$_rO|c0t{SuI@!6YK8*3b~tWrRitC1pUb$*=S^9dNGIB$ES>4Ag| z0B(3U7KAp9epD&Adk@MF${~wa$$40x@|xNLYjltK3B4SRH?9Q~iex%YRkZ=- zfUhQH3995G9d9s~4YuDJY*zn@=f6&aB7v#nG$B!*_i5|nO%Ja7IDe{}eN+Gimj9yj zqhksdnUE45qc;aK1}&kVN)kgao7UVhL2QA~GJQcT1}%--b_1k*emHNP{VOkl$Do!v zG0k+Bu%9iGQoC@}R@X1OV(5+;%aRNYBGXrgFy)tiIGR;MuU*cnv2JxC;+ytIe!^h+ z7MdSBeNbLYE}!UN8id*X>sNWX>`c>$!Q5{}sVdES1lUhwS$sG&RBP@BSsa!J$vyy5IKMDH5#eB)wRohOTKh$K)Z4m}6ATc}O zqkyy`N^2m)wxij6mKw<^8pCC=QYXNoe`b6S&F-X+XL*MWYg7*vleOH~d0DX5RJ2bv zF&-w1k+rbPcXnGSi1xe9^0ZF?G&Z4|4TBQG5xjQzek3!Tt1j`5bQxD$%=@UkJZunj8GOz zJM|Zoh9%Sy2en7kkwMJDrxoO@&(t}HCL-IAmh{;S($(!oNN?X=`hSUzAV_s$?+~enbJtO^Gso$atz;NH%`#3fXqj)ibqizILuVUQA@Q zGBuw@z@B$DwzcJ4A$8F~gRC~*_s4-74f7iGoHGH~2SEmVr7FcayyqYe*q=GTyZN-i zdbO(aiaII%JX7Gk^ZhXtnC1SQ>W&^h6!!`Oen6+uijadr*EM)KI~6Dt9aM7pioqJ> z#x;|0GwPxHdD0AQE z#Ii{*4?5o&B`{tHdg+47Mudnxpc+eE52tMIjyYc*X354txVR>*1=;0dgQD1rS-Xj+ zfrErr@8!8(P{3+xvl93@(!J*EcK&VT#}n4!rG*8svNs?YaqM7`Wipc&(U&LE?J`${ zfXiyBY4}PPJp`7-2iG?Q51yf2dy{y{X0|qGAhpak)JQXZsFlTN2xY6w|IODvi_t3k z=%PKeGlfy6#p$@t;qEv+{r5?xeJ#v;81O9y*x-Dk_kPS{jmwMWdC&i_hDc2x?kjNL z6w2oP?D=?udc#idlF|{3_bw&d@bj!z-85-u{TET>96&4z!!*v-LbByB4_Dm2`KJ^_ zJ{%?WGg=syC}&9pR5!(-9qe4)P>S7}y|4o=DaeHSthN=Y7aUtubVfV*X_p_y_82<$Y^xbKZ{J2wKk@fnq zU6`uvaV<>~jVFMw3vKrU>?+f7Q17XpJ_8%am**1Gpz;-yzOvKSpJcT`be`fP8q05t670*NFRTiJCNO6$$ zb-^%rq!LSSN`$8x?eS0)( z+wo+Aw6E~_sDR|-aS)VAlGuRs)Rkh#ax8d<4 zxhDkIx)I84)`0t>MCs!&>jnt*x=%FOTZ16-a!@$2K+Ddr0Gelz_&H37$EypC-=Kou zl++SAU)GgqgcK?tEiayIpA~vp|LaZ)+^23a$mCYmF>WmrGTuA#FKB4pyKagJRw;00 z0+IM}QHJvE6<{UMqA2IKm<-S}1S-n^m!^<@pn)*Xt8eW0zr!c~sz0nq{eYUwC`Zyr z<;e*ltV)J(YpZ5IxfH%@vh5ZNv%|PK8~&OKf|%`+Fpoh_lR#A&@mZ=jmU-!3*Sxb( zTPa@8#yXepIP~GI7(mab(~(T+hJ_ew?OaN4$iva|5YKGM@fAGiWNN}iqL4tcU8p+( z1y2yP58y-R`DbvX50tv1+x?CvORvThpm4*Vf`y&NP>NqyjJX!ClB$#UGE z=)Y$Jtb*=DXxcN-;70z7wdR|J3cUk^>RNUEmgkejEjIm3f*QN6Jgk(QAcOn~-4|qI z&-+7GtHW7f5k^gINvG^F`dVNIY$lga8OrN7+X^A!tkzms zQdUl1R1NgmTHqy_z$w!azsIW&Fk+b@2w{kYYDm)P?(UsfCPSU=l*!>OGgqwD zOBE!qFE;{6Bb{q(%}baV?6WA`t1+E@Vep~gAV3BSM!}UTcMADG>{*lYz3L%GI3!5%gDJ#ygzXu}PuPyH|b)<#3 zz-e5Rhs`%(qjiZoU;NqNLC>`X=11c*%6w&NdCZfpk_+I4K~o^YUa^OnIZ341X@YLv z{+ecA9cr)5LgR?b`xM9GDlCk*s$p=s*)1X92pjaR#r5L-dX#yG1A6Cbs@3(P3RI2+ ze!M^QL*rTT@PPXefFxz){8$Cp6XqbK?F;Pm^&Q@Zq=gT+uYrJyNQKZXR zVzm7sI$28I$b2{_j-zvXziP{@*I{_E(LU7-f+~Z^d^WVeb5qxDA1SMr0!0Mg9G~Z5 zwv#lbKIn$M zw0GDqx}Y!)MTV43+O^yZ)4=LPN@V*at{^ZF;S-dp=eMxz1TtY1& zHQsw%>hF3r*iK#-a~3Ha{eI8VvSlsP=IP&EK4>R6G|~*0ss7%j%XGRUd8o6n%7NDC z5Fje!0x|K*Jg^J^wFZB(Si_y&6JJQGjoq(bI>WWx&DU)yK>Fco1|;@mnYCW)B(A*E z=(G;Bj0hjR7S1J91z(b%g_o}5@@pEZSw3%_yJXCCo|H#2krTF%PGrj1?dlN?N)FEV z<41ao-)sm0mzfL%^;}SmDhy#}ivkd~*u=7l4@OFp7QR^?tl@X*8Nb zG6&%ToaOZ*5RlWYH5Tu4eN^o8RHpZ}0#p!9B&g~HKhmf*6}ke|0@^>a-O&Bm3?IWX zY@Zj>rj3FyYU1ywAOTs<3qM6cas`_Aga66ZGcKc0YcM??NPHfai*xYIn|^P%0-MhQ z5QhYMpdS6+c8gtoF1ukv0w}P3Tn;8-0O93B+o&%uByO8oA8nAu2-V#`3~qlb0^7DS zxB%FCzD|R&iV>D<#7!6etG*7cnR{USwSzHlM_EGF>n)rDsy1O~3=1K)%||A!wMA^cHhE{y-2Qo- z5%xP_mvwxwS)aj3NYU zR{(!v5)Yn#xIdl|Nw0$4zb}lrwio-k)YOF_K=QhwROi;GT6!QJ!8q1kb~yIERm(14O4+!^$Q>{3q&eb;tHKb+}ZKwdj3xTZ-y6}FsUTP zC=$n9>3_V!AzWWhBSQTX1IKvnb$D{!@@ZX3I6Sl|MxIqb96XHjsoYQ@y69f1#$iMw zvNlz#;oty3%~_z?b=0OJbnUanU6T0f!3?3tS8!9tTYMqmHf+^4woZ9>#sa7yHhKsq z2#4I348$WWAUvId@fKzrH8MV-pmyL*kls5tkxBNtAAI9+`@EcHJ{mF_)S=IwU-J-L zV!@2NT07g!aHydHp*UM=w0M!z=j0|74YhZ|cv!F$urbpS0Wx^XmF^!m0)^D?hkNcu_bascn?h^PmRzaGQx1Q%T@Mpn`VWa!$rJHasJHOP1kp2f zLil=?Js29Gua2*(caRn>L>*mZ)Jbc5@2NbpYe+}o3KIJA)WHuDY`&c;tD5i$!^KiC zJ}Wkb1WIbUyFVGlz9Y^oRO)CcDo&TFF%t%DZizIQ#%3!Byzjohz1X|)rMU6gUsnIJ zbp5z*B(2nK$LIHcz76lpXx8gAqV9M^KY!}U(te9QyBM|l-0HRG;M8ENk+U@6F*jvJ zM()>Guk&&*l5W&1z{D8v5XAzj+tP;B+8x=U94=PrdA-@Q23@S9URlojEAikg`0EW#6Z18 zgCV<7WOV$5K2x?w|49^Silrx)?%z8i7lkZ02GRL&e6I4JUWlMjFdteI+=RqUV3eMY z?}Thp0TDV!VJtLmwqqCa${$OJTKN4%*8&aGSPG0xC+Z5=MV-3~cR87idV-}w_rZjB zHGt5!8rzSUcAvP%FterK29XmLyCHCxw(0FAJ1{PW^J}ggj~cXw=gA zG(cQ7il5&LLFWexT}NG-dTa=m=clh)u;+FD7M44$;4rh z(YS!$t#pnp+58{fqC_dMpa$_GXE`~MV85=hY%9b*k1vd`jg5}b?SEtE zBsG*0MN84Q2-}nN72=p^_5l~5SbGQSA5FmBrcZ>x>2?{%@*JZ~GRHUogYCDZA$I_P zs2<@6^}7P2GS((dNoS6R_^CS#W^_3NmZF}CGL9QEz>D6sLyh6g46jOpO`?<+vYqrI z<@iEUoPzz&@LA9f3VT7#*^>zhYW_E3tRNWdO};7`b={`)>re<>#IU2kql=a6P?)@n zVRvPwX3PFmym;*7v4D^8!)%Pi__pf!WokOy{(uJJaeW+&jk%gegOvsyNoA|k(Vx0t zv;H{LF6{X@%9rrFpR+ech&drQ76hQtLn`MLYpZz$;*V!o5{RB<_YjE^JiELmPrTKT zh7dg;@5RuUQ=$#Ve+kI~0H(JCuCYkE;We#Oxnn^PqH%Uk3Ub5{c)W!K0Dc=%+FFA! z1}btp=hm`^M1B93+3-S2=t<;slS)666TL**{X7t(l@={)v^rDtgpO`|yI2*~+HUgw z0EN$c=HP+>*!KK_;p2VwdsP06n?H;tzpT*0V+H{(-HI^9oOY;L=jZlF`jzYm3zv*4utKYDjCKWQ?B_`N2@9{?O&+6px0N zs?a=hG*z?LZqQK)r=ahFpPH|)=9mV&AVKtNceN4>$M8WHKt2)LSqa+yl-+S;f7gjr z-qq{=b`~E>)&f7X!7RpY+C}Qn2jIyh3&;C2 z$KhuS0nm1f@E_+iV~M$>rW_rs!K& z{m;VibFeV&PV(pAI2VckgV3NcLrCDSxcBgnTlh4Tw9(45pCYgg8%>KV5b6pBHGB%O z&|9azI(lmgcY8r8aOBEn5;7E>(v&rRuk^4Mx!)@l6VH7o`=+9@-eBFExrS{JNDsv2 zqH;BVr04kE&5flSiE=7b4KmBgmLy7U-M^_X1i7d|M3bKqQXN41VcfRoR1rz(RFNX4 zwNRE$=ZPb3s@8Lt3*=fw3~!|4`IL{%X7o#_oz~qH4M(~|#)lEZNWq?+dkV$*yb4yk zmBoJ%rmXfzx=S|sN`aMErsb4?mR0YtHc5V8TaZ1XPZQffn`9@f{K!@m}L&XNpyZKg~~Rn?{2SI4hqz13AgAz%Rk z3p~>Twe-~sBZk>c5kB1eX9P{P=ERX_`2OLGT=hMih$mV8u zASlGvTm$9UBoeD94ZYk0{0CTL!uOjY<@sQi-6@paxZ8VCJAVZi@BA{hEL;EfIbZbO z>3aUJqQXQt*C%h^tpC|KC1J2mtX@E~8AZJ=<6+kE`hTo;l+M)_iXdv*ayv2Mb5%BQmcMTT7#^xV#`-6)Ag^bn4=x4h`wDi(?_n`@5gVX-j zU_8DcRs?3C5k!F4QjfH>Q@nDf1c?aQbl(?qSFkT*8UQyv8=fnVkS}85yyXH%=oZt#p7F5&5Pc?`!6WA6yhPsb4yC1WNu9Cspra;{^+=w@ zVJR2EBFq+{{u#o-R!T~db^aKz61QNq%_Gj*_VqtRis`>13ECBl4}ovU|Ln>&1bFsj zC8yl0W-;&{oJ?s~jWt)v@K0-CqVMf!16+)IWRM%j&@N;oLOQP0T8zWr3+;@e+EhVY z<`mm@1LEL(Uo}HBgFopD4n8Ipr)Vev_loak*YP|(tN&_CU7dSm!gf(zXw^h0BrMAL zx>h}?bUZThl>sbnRy1G2X+~5>$cD_2yY_*22NEhLHX>r`JiH5upQzwew(lmXGb2S! zqus!9%&LwOkX{ZRw>+^x3hv-WC6aUnJ$q;lDSLwEBZJz3ej1S!Gq?*UzDUF*Ep8YP zHt{-x*VDn(Db}sTBAes5W(t^l=@If2!Nix zpS!GRNZP=s317=}a6!aAe_B=g1|Z6^JO-V<(-zit1-1^cUYMS_m;3Y#=Rd~mf5&DW zQGimsBco!@f5OEA8vIpzEOfIhgn4>_i!5+F^Mzyo!ClE;&%7bW6jjWe4|}|P0+qsN z)$%Vx7$B;@dQYU&;^aSaH|PqUl?+<^F3(@~y&<8NpsH$h(Bx|SH}!+gK{MT-qKdr( zyH#D;h?&g4X}Fn_`saLSi{!Yi*D&+AVho&3ZmygXY6{g6SjH7*kI}o0Z^#84ZgFpP zrZ?A_{j0oB)fcq?_Li**hLBIEDYocg8k}FEZUJcn-H0ZPErY=0vnN z#On)GR=r>NE{vO;TyMa(3$ofm5_VN^g%%eHzY~k4s5jIW5ykoyiEV88-0X{(v3qAG zp~@a*wxP%G^|V96OFzl>Ug8^Oe)8UuZbSs8^{H2klv|!}gLKyz1hH9jlD?a@{+{G< zxeSbfQl}7LCEq2=C<@4MJf061t7rcH=|EIk^Ld1fezbIM+icsG;9@|>SdEl|Q6da7 zi~ef0fzi$WxMp%)0C)iR7q_$haO2FH8jw~aVkF}D*t-i{7ZGl|K8^+-I|cvoQF!aJ z%~#)?ZnMN$j!i>9HAszX+AE5pCSJS3|_?oe*zbnf@+6HY$AXf$Xvxmy#mVwA$yP^$w5 z&*AFGMR~KubL-K6x`kb`*BWc+=gvuC$$|1~;dioXTcAIPnshb;$~Ro?EqHeylu^OL z@q%`!g4qJ@A(-6j2y3{-uWxuF%cB#^G1mh|j%Q_tl8yJ%R1_<9Dz9*<2O}kbaiYX# zqpg3dH}tZ*`>WD5*6enH>5L9!E=?i>$Fh0DT-;&h%8ha)cdC{B-0bOM>05X0Y+uH1 zTXSQ&9{V)MjI8MGV_#uQaCTcp;jv;%Jf>+Id0qv`0k9y3hk!3wRQ>a1lf&I#LlFch z|Aw;_>kToQwK)tR^pdF)_5nE_3G}!73!!g1?e#?=3dvg+^$_k4N~*I?dZ>Pb5Y3Dn zHcBV9cKbySqfZ zk`C;VhW7nnVg>)wIsW{TV37-;Yj&;0nf|ssv;*;xsW)9f8=G<}j%s?*D!cR7)%+LV z=FvGzD8f2Fzo4m|#d#$F#CH8ujS;L@7{9p2SOgW#0hD}v_(3z2MbB_9s`;k@dZK%A zCsQ7aSg18cuw)qM1mo0Z=QgJd$r&b7t8k@%!vGYSzNm2aa|uFO-{t;&=+6VU`CuIo zR~G)OFTIKV1|IVs6ZV*7om7sx>4j(6v3vQ6blzH5`lcL`=0|+jY^e$8_8=tysT{C5 z*;A2!`mo)fmUQWjrtpL|iG#8VnXj6>WiLKs8U^CcOrH|_B_PC?Z{A%#SBvW6P(tU( z(kctF{9>3Lb6+osO0JDoYw5YP>dG?xcz#a9)8qOqg^f6OKBfLVYZBDSQ1XgPuu46>TnA*J9kI{{wflzK>m z-AK0aBLmL?6RJ~`l2d-VGHCH3L~@(VXD4)8PSVFp= zw2JuP&e~!BzmXt9SQV}7LN_OF*;rZ31m9u%`!Aw!{&xMrhWjD|(+Iy$QoL%M{Vqgu zfA2f8ztADNbBa^mFrAhDbI_W6#I~~WBm%^zf-bbKFTD-PB*cQ8U94@gcE$Zas3fJt zb}(>IWpwo5X3~^g2LW^ho{f1`rQ6npi@k>5i|Ei-sz(Hr zW@mIjDsbPM~C zchP^Z!xTn5k4fHFn~(<2Fvc}sL`htP@Yv~VctSbMx9)7$80}4;L>#Bh{ZQ`XCnf6$zPhuB#a_`^xcH|EceL*lfUG4o0|x{M$?^)_W=FL}|1o zndm7gFCXg#)W6gxaiYqR$dOy--KfOXwv3-6K9w4696?WQjwx$vuNBFiU^8m&$_f!e zL(Gmsrbr=WM4bqjJ@#}Yaj15OlQmUEAE21Ib?>K3;(s@T_VUYvSCU9skF}Y5PaiUmI2r z+M!J(sSpgeHMaN|2L!ump6FIPZ)wfHpAbg@HT15e%-^hl@xiaftVme|VQ7zn2R9QP zC8&mA81_X$d!ATn$z1JsCzOzUW%s(v$#eVXNQ!tAG_W-hd#x@TV^~I5Vvhb3ia#_Q zN|Opre-njVFjTXRJm~jp_30p>k@>MMfCy(s>}%N`D!96OLyMKUyKs+58b2HwAx0F^ zN1`I)4I&P*K16czh=0Eiv<=orO{gBSZ#w-T)A;uyMw!`v*>R9}tsjVAfdb1n{-?U# zCPrG0m+LpN|C{%fG6r6-05@A$w64ooe0l-)@_Mb)a~pz_?9Rxj)xFXfgQEiU!Y_*n z3#KpsKeoO)DC+NfAC?7{5LmifI+f0)ySuwvQ9@9bPD$x*q&q~qr8^{5q(!6^_`UG0 ze!hSFW|(2vVc_0-&OLR{d7h-wy8qUh8{1LQ9!0>Ap^SIi;Qp{2>Ge%fBcc~CtM|FM zYV6pM1pVM9CdZR!nA;l()J=V!1@sS8@|PT{8DcHLG!Z^gB|&Mx6p`H`V+G^Rd&1u+ z-f>XS_hj$KxD{^*;Fvf9fxYiioIYd@x76g2g#U2QL2MZWc zQ_rbbJc8XSD}@H4p|Y|QV5aEARpk53 zEfR8U-pAd|!zS^JG&9bDBY|A2djJbvTeTjoua7%lKAaLNy)#^h8$*?EV?C|eP?-5e zbKiV?b~^Om#jl{vW#q6XP$l?7J*6HCmA@o=L=%MVUG^*aJcWvhCN!%Vc8m#f330#G zT3b3)*z;i(^m+SM0#JEL83d|F=}R;BNzfSzqL?IN&Wo$lceOv!iS4JU4ET@rf>Zsu zcU|E=U)pMsg7ncpTRL>lexOI(JW$+-r&&G5Ijrd2s3zlkezF55h(?lE9gMPcp%oN9rL5Q8ZE}1@3k&{)4(AYRPk3T^ZkWqc@UW}qBKT6Bo{(zt zZNlH$3gE8@5f>!lRkb&0&A zK}&p1XB-7=pZY2IbA56v`I~#`A;SY)66r!yIv_P^Q)o$>?_++AtSTO5(B8&T6H-y# zekOe)Q#5cmkM149buHzN>scBYLUgzmU5ukR8VJtm!l5=8{`>y}XyZshTooe9HJG(d zNoC=y=&i~v0AQ5E53Q7+Cea3F~AFouH#A$*$jf?(OY$IU^_icR$ zT#A2KP1M1G(C~{Ho-?zaX;qQ5j`YHnxN0t1*+Z4jAqe$9B$6H*G{woFe4Ik?tjH{{``gOfpJEL}m@OEk1p#PwEyl$9jqw_tB#bX zcwG6fK92xzPd`^RA;}@-Aq%)mC5U^Kl(?FWtF#so!F{ckgASbN&;!sTe;;!>wv3d7s}KJQnvX*E5)MR3k4Zef0ZoWL>)UQ{_k+=f)ET$YKN@7mwrY5 zM6_8X)>4-FnBC_(GbNa^_k%Qt`(;^dOSHl*yO2NruDJ(*8c$5OJgxvAffqUafmwh=#g5mtz`aF7kGJ+sr7(gd-AaDLb%4suv|)CZG+wGxjDb z8;ukISN@MGwn_sv#3vC=O-28Grp^i7F#8mr{h}@&q}tDU#`V)S=pp^(>UajVeJt(G zrx{WS8d_8Z)V`jhy1je!?ciuwAokOt%5v{};h}*pb7w3?3~bQ73Fwp^(CWG5Q9PvI&jRnOgJ<9?+^2%En&I>n@rx^_BcDJJ`I6Vos})~;)6gzXOVd5kje7uw{d|>QvF`Giwf_Dag~=7Oe)vdd8W}W)75;D;n(MQnATvk;v5T z<7v-uSV!W!kwq?HUwuw{kR=I;!61&J5Zs>VV|4tqw&#ZbC}-5c5X7b`YbS^Hp5l}+ zrUB|7eV{a_1!^sB5n}H`sbrC14at*CpX-iEbNb^_J0-!6|xh7A&6j*@#lnw zUVu-WNi$yVmk|FdFP!DPMrObBaRN`DU~8xAn1@(Xf}UawEx%%REI;_44T?iRYj0VTXI_CQ`<;N$MuS`CT6{n(-a5%Nkr*a3-lyOVJ zOlSsEHki+>KcUY{{@o4?Do+MnCwVfV%F;h-NDY}?oA}_}Sv!NkDdmCYL47riGI2%c z(!7%#w9eD;f_P#}_8!$I4F z7xHzk+HA9-Q1jnj{N!>yI#^4u|6QgtGI-4^4LR2r$r;Q(8DO_GfgU}Llhwp_v98V| z?hhOG^go$DkL7tTM$Ug5guSSSctg7wUo+hB(8H)Z-A6cGaA76=jj;%1y8K%i1r&7A zZxFnEGdfY}=};eqFLSMhB72&4#jfOGQjRaat$6xzTaz<7SwZx9vsXIL0Q+hG4yJQl zF+Nha`)my)5?np|Y?3sMwky`DSkkdbOjNFMx`xlN1+pWh)$%RNG5Zn7uMgUa+%rq~ zT~Cy34MpS)jN3nCdL!gj=Wz>|zJ=Eu7ye0F+-WGg{$rBPC-iAo-)B~iHahyzr|njz=BG+7_K)yDN|=4;AxXr8m2DvTXQPw5fNuG{dj=VH)0~lg{!G{Ocb~YDV%K zrHcq){4)geqzVWy5wS%xjFPI%LuQvP5^z-{GDEuc(e4BWLxEd{h5TrZ+8aK3CAw>S zluhQM@SiiH_JZu_KwEnajsEx%prCj;UOBj19k!krqcz3cQ#~1gqi_EG=fd+?6>2os zbe7huQl9YbSoB~O&HG0JATK0Byw)9d81e}cJj~+LwS&4-8P^SS)wf$taFaPSeNnb5 za4DcBSl!cj_~al!B~n4jO2hiGYg}-~nG;Q0b{3wAOQhLyvO`IMkA~!{Cq7NWVCZuq z_PsKUB>L7&T2*O`+05Sox`7-_*RGyJ7nud*6CR+iI|xp^>R0c+nxMm`L<)$w3s1Xy z_X(|8YV?b(q<4`l?>_?(30tEBa-GjNMuq5qLd{t;@T1%K%+~?4dz0HL%Ri_Y**`>J z1dF=F#=XaRsde)aMNg%&QRsV>BO@TjTyvZ*e(er6>Y)^y4tp)GZ|45mNe7(8H>(AN z6%>)bc!YgwCgT-6qY)@tm0`m(Z$=&`AEDA8`WD)uzj?1j_K|;J@Q@FK4n0<%i2=P{ zUbz&4RvT(ehj6+Ni?b`m@+{14Q{N~hYPm)}D=n>Sj6@i%dVvQfv8`)WWh=m?tjB?N zQ!G;Ns;5@`A(#7)V&=Z%VJGpQDV)UsR++g@*w*vr;u2k}kX(F0x7ROR zm<6aLJPw-6*<4FX8vnAu(`kBwZLh^dw4Un-5^J)|4(eL-Z}8|K{MS%rE{$RU#${ zwVz(7lsql`!B3B6beE zS!asB+-*$WqDRNV;02}|dxNjsfY8t#hdNPN!m_;%DUaBi)FnWb`&QVYX2rvD% zjUakOL-UYy-Eil`xQ0>ERARI%yyyeZj=bV)CeSkixO*pbkHN8vE!G^cw0qmarkC$& z-|eRwx8bMXS<~iNUF6$07xaz8_fn&kM%SG9#O1cs60lC5XS*jF|TCLG!}i6g4ul*!}vzhTT*)vFbuLOqD03dbS{*&T|8QAkp(gQ z{v6>n{c%SS`c>ZwQ`t9K2f1~>Z_a;$%Wlb?_)na`Wm4aO6pDPJI)0bIK=x4uCw>fi z#FC?3XNB#Is3~gvU;83ZYKp5aPhwP*nilSCKr2f;Q*chMr0KKZ4%j~1?F$&v+9z+p zQK3(KfOO=JjD;<2I_;*In_2jfe!{-lrsO>?=7?+HHGKgwUcqaH8Og}rz4_zFS-*K` zxJZ=&k;w|E$myEx%eg<3`)po^7k@>i$dlxPoeC#=tsG{KKI9dflY%IO2!2GoN+SIx` zm|B8FN_vh&z!DMp2bDB-+}qD=wCYr_Ds2DJExP!J8X>>XIILIePtVIz+oBLe*T%bo z**PkzjL!nV6?McG-`C>5sYZSmhqq3E%v;IY=qsHm-zGI2jr<8(fF1`BqNoQA5t9?y z-zW>lP8T;C4V#UbYL@`bJP`Q+%XkQQiB$|!N^?-r=|Xc%%zf~X%Y_ryKjt^Z+u1No zq>U~ci!(}{=BjUzz0CN%qd0Sgfc4-cyq`fBm?Ridp6jHkorGaA(SIO-lMfSe5sJ7n z3r**S4zTcOLc1cCnegJqz;qbLE_Cx#jkj-1-3>7!Aha}#}6r<2)|V@WViY4 z(Eb~2nZ=pJBhu_uYW?RUfg`iUwl@v1ys|4<#oZH|K4H}Nu9KYA2mH*F09d!`%2$L8 z`ivn?2;d3nX|$VaaH#hce_UTC-p!lJvgAMScbDTi)<2P#SSvaRTUBK&DLtL(4v=tX z!bA&_4FSb|>XiW7{baOCF;*IR0|H_Cp#ORn+PL<-U3a}3BxlkY;&`?@HBcXmu%e&( z^xC@Tpbl{xkJy^2m-~TLOJ*yRWfM=51(8fic9yD?_O?#lTi705{-|&+?mwU2E_Ltb zkJvSU5bjR^w=4;3qB|jktU+jeC#&t4H&t*mk>I!V@;%ljbsfK5i8>DC^Fp)UO9w3+ zH*-oRlI3^XhGu-N0_F&bpMFvKC;L3<-N-fP$qOF-jFVrXx{kUZQ#D0(p*~&Q2~u!o z`>6cYvAd^{o*nE-p$nDY?;ftvq$w~RYO}j+ayS#ZcXL*LK$1X5=sCwd?_3$du0oet zfGA7h=hmR~PtUq(a`j~JKN=PPN8XzYnWX`Xhm@?B%cY~S^mlK7NxZzzcS7zc9kKri1|0$)WjH!~o!;u3k*bm2EOYGHkr>Fcw8-p} zelAyjp3{I@hi^0M_nf7f#bvJ1Ddk{jBrmDBzh91GF7d0+lSRk;;>ewRc|CwkjD9 zEoV;*WvG9yBR4KnxR673UKqk+B~sl8myAgH)X5TLlLOVm*)XwCf7JHd5?YQr4i&dF zJbTKLPASR!_1jQ9DQPLSQsnex@FY!OpX9b?zT ztsRE16TUpN8ic$nPDQ^gBT}k37l`(fviVp43VD5RH+R0}?)|l!+@RCP!auvAq_5p$ z+m?0%>Yq+6Q8T}zWnvKe%2dn@S)PqBH+=B4=Wu|m%4w#pv80pX#kx3U#hOu!vxmp0 zD2}#LrLW^aH!!u)(?Z+9aT9>*Y5w}ze#ljr_$9|hw{{HuIhDZi#fxwJSKs_Egk#8f zC7+MX@_!x6aodpxaL@ckOSKTYb8dg}XHaHP^1B{vz>%zWCDZtB-Y?aQC!a3XkBe-l z78voky=J4=)7JL?h1OG2>RHO+N0euf7i~n#m0|^L!K4D>#=;npw7`5lC*jd{ z;csbPK$tT5l24eH@qx!dG@QvW21J;lF#;scz}5;u&5yDmZug7a3Z``-vHvD`#g5;_ z3x^V`^?duQguFDW8lUF>8o`0u2=u+ zNnrAZVWSejvCaxT4-D)uDIP5X@JBC88gzztOJW4i7jJ9l$ZCa=cL01F;cp+G#+5F= zy9gmNjr}9@;SJCNp2jog^qFEjz*~xK131bgZu+K(XqLEcrRD5i9GYeKB%%#0rR?yMY zLsGjA_$)Pqt$czQVZ4`H`W4s3MF|=%(UofD2h~dNe9ip2X+%3ViVcy*@7art8bc*P zl0aYP{ak{7hTAvv{vJ)NxgS>j2^2$Me6J=AEgvh-r|Z_yL1$jJ$KVOt@q@2}u29mk zqIFfCh^Xx7j@0an%~AQBnuT5meOpJ#vh_O#G;<6$e^lMfd-LjWxe3u`{A2644&j@w zZmFB=v!B&J#vQb0clPzno~_^9A*|%wJ&Dz3-kZqB0obME|`!3r5ER&P>(Ff0$0jYa*M@O<3BOhO&coc+w4u{5^i0a%q`$8yMEK@~WkV~;1s6EGsh_C`f6By{*0olF(b zry+7;pL;_Q;XWb{ZFTK!n-Kr`E9#3G`Qj^tq%|9xn-3qtFrWK9OiZaih_b~3|X!^qeeQ&GmIe>Cke0H9dw zw7_94?6s>o{|@u(V_sL>G%epd|K_Rh@v@BXRZpTa2-m%PW7v(Fq*w1@T6#ktJZDXy z6n=hCW6>K&C@63Ibd1-eP5S807eGJA?YjK_3H1Rfz0R?!6f3#54sdcw1-@s2cQ%8s zDIS*B(yCKKj=gl?J8vCXKrbO1Bn<0*0tAHLvAI5fNlCSerIS|e7V6a|BA9Y>u{}#n zBaM+mD$`L2%fKJlp))d+EAQ$lP`GcW_%H6*qq{d2{(cLM6Mwwm*#pG8Hwf71JT6^Y zkb_)mCq(I|cTf_CVlIpYuW)UAS=dvC@L!~fcK#9vtK~#nI!+5+CgykWASSaq>*4*x zlEB3ZhZznNAvz0|xmkh8Ep@OII4GEM^&Q>!wV#hCHI(Zx(L0X1E1{Qr(D5M>(p@g+ zMfJY9pXZ|AKWiNu!hC;hvje8c8|`7N)xQj9wk(eUY$FVUH*adkZ#9Ax;2J6l5KZHd=KvDCBEYbw|E|`3=_Hm>kB5 z3V6zw1>R3)+k%0qCa?se~t8(Rr`6yA8$0 z$>JIq$LAxkeqMZg^}U}6SeceLdlxcdrSn?DZb7VArpPn&AhNlN)g6EgrjHd=MKHb2 zG%uY7@EH2Rf{53>KgI0Mlte8^mQQQeR2sbhP1iC)z#8dKt4{rl8kAya{%-!88gybk{~1i(yWyRPChHe**Y%NJO> z%I-!>*dxZ1RhOl@?z|^xIZFFjiB4?Z)UisE4ozc4G~_uhSCknaae0mV84dM>MAh)c zik%)kn;nSBILOcS%<1q{1JCoglOm2wb}?f~g<@qBwLjpNS4}wOt27FFI=0;S$cnC! zg@xtfpsWw=!8xH6o%^p#j}{P;L5qEb+1GlbDfa;dHnZA%w;ZuS@~56*&kvgqj|FD| zpdgi~j18=)BBd0U^$r1;tgFZ`K`26DIPy`$g8~T_zXaM|p>Jlt z_y(6l&cqxU840FM@8sKU1Pnu$&F0Amj&I9Ftb_&0fVzADAfExKMy$gXfGB0S=OAKJ zl*?tA2ggHJGL}+Qq)5QbVn_XCgQpv4Y0v8h0xW>|`(x*;JIJtNx0EEg*g!PNW4}xH zOTP`$D1}7R>Z-Ms+QjS@bDE<6`dfQs1F&hj3^1I%9rglHX{}vNg4nB-ZEctcwusY< z7$fV%PZVY?AAPwh(|MH6`XA;AU>)Mv!CNjEL%L)v|47Y%gewT)V5n|#TUXD=@H*q`ZE8t#@Eo2Ynkd3}WLcF=`R^Ai2I~U|UWR1zFK)1#T-H&1SNJ@yLMUDzt=MhU4hGgeTWQMe#Wrko z^f_FpZo0GlGJ3hu+sn(Fs&ZY8F<$>A<8->HAp$Q<2bc`h>-1Xjs%pJv`REA6#VVev z!EOAZ9Xdz$CZie>e3co(ENu3D^u_xpXT`Y`YbEm4V02~fcc8^1S7?niE0;U3MQl=W zDy{{y?g+;VKZ4RkFm~f^Ast%&NJ~HId-IUK$yUSXb0Iv$iu`d0wERJO4o<7r&j17v z^Q*fp49>s>P&m+JN&yVy1L_iUx+TMN3_G5UdV`crP_9dTG*^C+^16Vvs-`Vg;LzL< zAB}-E{TncLadq;AsqJ%uQrojb6xwkYVCvB9_GFj(rM~d1=HC?V}GRUtDv3#kh6{i=rY8$uZzIBiv z--GsGhq`w&K*}MnrHjiMOl;K~GWr3ZAv8!{q|B%}!OF6_z}H8)UVy`Sxn0oo%T67D zg%zl7x>l{WE+*gC%1+y6D%9#CEbjt1x_niYEH;N{nR=_5&v`vF(6|+$ZRx&-v!$5Zr69~uc7rdhBQ8C`$$w_ z)nl^REk{-+rfa?4E|$aO9W^{kFp88g#M7t`z!HM2pggcDS%}Kuk=U<&Nlfj!*bspQ zx1T&th~n_upu&c4p`l_^7@s4%MDg6izcRSsew#=IKshG2ftjdh10>&6b6j^AxX;F# z*Sz-+Z@+vI*v_+?C@^e7aQprx#q4D9lNu@I@0^PVbP}@jQdBm@hQ`kS%vK^jQXUDO znIW=VxWgCO>VIJ2*~EO(+hwC1Zx{I%;&CT+! zbs-Zn3H>~%7j0}okpU(~WMmRu<|~!}oE#Oxi2Qun0@)bfvpGFGfY;P~BcRD;dAUg5 z3LH{T2$?1?XnqwXJ%{|*u-!O*L$xwP0rFWfBHeUv~G;K0C- ze0^i|6dZ=jofC~X^s=N{)wXfQP0#6Hq8|zKh5+5#-ivSlw9^4)z+9ZJXpq{^5ABHC z_&Zc%?CJ+>DFXEm9?`^4;Y&5;8oak{4Bwl1=d5w6F`NpPn*z}M5*?I|A`tA>>mFs8 zxkkl>U}a=mkb*YT1NMpAE_QCe59TaRyPNRT2|xFtzb}jVa&dXuYz-s%VuGipDrT{_qd*!yOxcwOT+nSwDX-cX9(9Agj|aN zY%1hFoutM+BqU9b+epq*Ln@yc%j}=AP80{0{!;cEd?=|SY z(JNb}Xd39~EWg!}jM1$!Mb36zZR>VIjJW-Bv)K}`LW}Z%JgLT&6cZqmm#owmSNasM zuAyNO#p`7nAOHZ|mSph_aPaQNkSjOoIFt=~Aj;hQyvX5w!RO2Gviy$7#5>P|4^2R} z!G3D1enPx=7i(VtAdtVuT_%0ZFlI#=ZT**U9GEMOHdt&=SR&9mTy*qBln^0ad|pB; zsMLOBPOY@19sTzy0_cYb54hle8m>}i|DVMGgxWzHkTdLq7x*l1_~nUKcwhU;rgM6D zp-DBe=P;z#IauLqQ@Q@FIFK2-@^no>nm+Xaxu4CL?c5&ku>EYo3;r4%ZQY}_dbvxF zkCM$*Ce+|&nb8b!aS|Hl@40!l6vC8q1#*DuVo`~)Ju_+^NS8sI&fs@FkW!^A|p}SElo;^}9-TSrK}+Gx<)BQ|%0ZO7}dRw^?hKPq{c) zUd@%8pj{^Y9j+!wAee>c`J476Wht-kQK+#S@NX!yzY_zut4|z-NLyu%(lNvaX~&EoEdoNrHJ^HEhop2c_3C7<$^ei1Ez)=2DdaqC(4)BWw!M*nx`D^0GV zeyJS6b-gqxn$vYfD%gsg26bQ=feb{9b9rs;$rc&csAFSEC_T+XUox>hgh3H+$KeoQ zemgnYH*zB+Kq|vMsJFu4@y|CG`}2+EVTemssF(t8?bM{))<$2{6GYaWJOdP5EtSh+ zX}V~CzWVVf10oI(hdtdZ)!3h}Ty5}}0oXj6oR=Ow-Ig|CX5T7fQPrZw*7%pD(wqN7yVDJsgpQE@EkOXH&!+1V z#^;XRxb4jZRr(yy29ENjHqe7PrKFP=9T_Y1TX7%sB5fRaSb120w^30r1)c-tn*jay zZtL4B5u7n%*E=0g)cP4MVmPI>fqZV?JYDfAmpu>IQsG$t&)FZGNin^ ztTVm4+^Iv|d_bV_(0H$v8L5lqxav`Lb?5M3?up zM0Ej($Xe>hN%_y&3}kke>KU6v0$mnd2a9^OWa@#iV+Yq>@#o zU9JvLIt1w(JU5H;k9Mv>ez9^Z^n9sy`*oC*Si4Mey(Hi4=F5|ps>^-50o|Vng;iu4 zJZahFxpD(-_!{IjWPyl8!)k`1+G=aQ($h*3fC~JS>O6x_4*4vPl*xI_5T+)6qpj#+ z6m0RqGG7W+2;z9t_AGmFnEb(9wpwJJQTxHP(xzH~FdHoMkxk_H@dlrbMy^@(W49V5 z6;dJ?CNUSn63Mq$=Hc1W1^M}=a%V!{`|zq;x6@7E`xbZ}E;mwAj{UL102%|C`J*Ol z`z02w^em!OdBW5Yf!EUUq|o%@|A`R>1>QPNN&Y2nk7ROn7kD4(h-u~!)@G3vaG!PU z4((#tBEH_S9j6iZy#)YOO)p}cR+^oJtv6u5NFHD>kiXn2D-r#15Pxe}_Nmj8{E55P z;y@H?vGv(<6jZbT@^K6zPRhwnaaB8F!ILBsD(T`Aqc;twJGvSMXB)lPKlkc^ysSSe zh5UmmS|}nbEh;vp<7~q{*xqHipk`1aVbK+1gtA)C#AuZMqWc*HrdH91r6#4GumDsM_>y5wz6d4fqsEC-g^8vSG z-ZR|D-8ZxmSt7Y_H)9Lkad=bmqz19GyJFWeQPP^!@)geIyAD9?^%@7aiR5VE>c$=$MdE8} zo6}G3u4iY|Dh-;kl#cT0l2@7!7r!6;;5<+`iVHY0+072uDc5_!bN3lQ=gTZ zhHjl($Juyh>yNh=I!dgmBk-JV;h8m)x>Qwo^2WY5x6g^T;DfEtiL#g3RR{CQMdTwS zg2!LEinNM5e0I2WJ!mbyR+(UnogTM6APeR&#rTO${_CP};p=!7(dNuOA~$^$%--w@ zkaLsWvkXq`jUpRwF%S4b#npuFaCi1j-zR*b>P$JAVxi2FEn&(b7quDtvFgavagXVZ z>^A&U(qZ{gQ6dBtWz0q#-{;>VgzANYp#OB21RbNxkU@9;k7#IN=G z?8t1kk94<}L~er~)#qm4S_gZ5fU0hMD0?(u)>dmgK1rNIzSlV1N2ZQch<->5%Fx^`aUP6|y&(Tcit8wKANED&Y!O~-!lO)#a_UR6cPYb zj24{*kX!+QA+JRb*gD2i0GzLuBgR+qw+l~FHei6kvlN8F@mY9h*_g7VPPFPLoi1f! zFlK$^AL1!>@E+=YiS@=ir!nPEdi@Ly-jIoliv$y<{`jh0Zz|0$SBE=a?k1uI5>JBL z3>89stsnjg(D)y_d^NA`Zh4*N2YlHJSviHzSXfvAesL|LcEU`8s)Me4O<*_{3i$!W z)Ox-rT8YPdf}&Yh>uOGRAVA>F&XMMTm5?i7IhK{r))CADQBaZU37zDY!aHfEjyzrM zlZJ&3^(Z|n)nrHhnCHG!ltLkF9$;Ye%|w^a^^ZZ14PFt#{i^TU*;y z6_t<8M6p>F91<$yFn)(mL}d14_~Cs<>j&yv%72=|!IHaq(qXH$FJyW$~<$SM3br8m#fZn{}Zr9U`>!_6z@L zA56K_?oi~d^ff$jP+yqN9lPPh5$Ibg4zhtxzScZ0OIpFtH*NrEp1UKh9MHXn^riHW zoIg=Q4^Kx$JNr|1@uIQga>+neJ_~c*0;fsOflF$gmY)_zj_63+oT1mUmwl)0jNv;U z)2hZb2l_k~+Z%^2gb&KxC8^Qr56IeiJ@V^WsN1Uq=oBwa9(CqN%Tdqbn#euKyB*|` z|M?lG>z9V4*Q-a*6_p)>TMEdbDjxh5~T?4PQ1(b3DDUS3s2i#f>?SAde zK^^^iL!WM>!F8?}2s-=pln}4Cn!ST(tlQ=-q(p3EGzg*cm}^5b zLS!;TC(cgPtf7q47hhlxx$33A&qza#8028F_UY4pT@pWmUg)Dn9uaf3Mmiau%nd=X zm%8M37L^Zaf6Y+n+|N}>(&bcJfnDp}e)dSkpSYrlcQT#OSA!Gj7d0^a(+gtsiFfuW&44L#@}8woE+DRdaQb7p2z)a;9!kk_-vKQt9UwkTGomKbu$O>t zIFf+@eqz^&cY(64Uwl?y}dQ<_ivx2Xka-$Vvai?DEo z5GLJ^eE(M9(q@SDfey|%=VjMLklh8h3v5i2ZS0-kTf#hdQiH3Qzj8Y~#Md6KjHXHlD_oV+1SO`A69Tspd(Q;?42e!rg9J{MZijP*D#8em2!#=Hd zk2VnJQ0q7IcK&pjQ9wJzSrp|e*t+RD$w|;BlqSu9m6(T?P>-!H)ygWILg}?jw5IbO zGJRml&-y4i-6D@12i6i#(`%MN6=4UC&9Q6_rC{8E;Qu@I#10;!6>eI}B6UCH9+rZF z1hH_5V%!8G3nNnB;E22(j`XIZFE1da)4}^+^%HjAs(MZSudDMa7N+`=Xht=hF8=Ww zW);C~kMM&8cjh`?P^4fz&J}jPqdp@JSFCZ?j*823$@o(_Gmr0sMPlmT*S;E1$5dQ- zrR_12-YRV9)qZfUcleTxMV-6ICD@+#zGcL^5wU!|K>Z<@5SaOeQOY^0XW3p>y z)sjXaAKO!N8;M|*n0?o8Q2wh;z%+^k5OoZJMJvnyHne}nLcG19a(#MG5HO5%B9ps# z!zk!z#FQWM0h#vJeis%*8;1d&zF{gf;w18ao5hD8zuYa_r+db(LS`p#EQo*rLr{>G z(EivSta`vgMcTE^-{j@|>dQxXcz>}Dh=uDlR_$4xo+-jjIQ{~bHfxo7S7J?8=E!$qz{tw1NQC0w~?TA8Y{*Bmqd_2vHl-B~LD zoFk92^J@9opK9qZ0AW$P!#j&BIiL_1sFY$W_g0_t)&kT3lFEeh>gj#Y5y%&n-Y{`US7lD0CvQSOp8eNeF%x%|P>RgY342$VzBLnvd4p z6}*U(*o{pq*09IlCr>y^j3da9ESGr&4%gZUUkMCla7VXu`H>y{ zpov^Z7K{V81y1iNZ&0Wu-XSWYha8=S6`f_8fm^V;<=2pe^nl?7Q^%m0h94dP=oD{}W_!zMrk^aq-0CTGBU@)z@ z19@OHq}+$LT3#897`EQ_=9#3TnZ~F`kJCtm-3uk2eiSB^-f*)v4$Du*8x4 zRI4+EFB;=k;F6v9n+cD{uHQr2(g{o+C)C>J(QRBQ%nV5Qyb*qZN5a60ix=310x_Hy zuLJotAqtKKahxXmNO2ZDvQjZ0|L8}m|oi@BG5-%srr^fg7l#+9?2N)+F|Kv z5(nFEWR0to-FDK?53PV}0{*ku3PMCrg(Ny{Vnu0sYX{{-v$kXB>ts6~78?NNZ!zvB>`3!xBcnLb?*LF=1iLW%u z8d~qY6N>B4r{fy4fAl2X@nmpxKAr= zJalr*SumcwjTpm~apw8o4Oa(GdLUAP|5G1C=`Jj2sn?KM;}~!$PR4HI=ddf-9EKh} z#lkr#Fl2fJMckJT5bc^T%V@L@Aud}->yx1%a@t^ORYBt7gb~6nluJ9~Qhd<7de9eR)&P&;|6E`dnhj^I<@~xo3KJf{PG^3+{`rBEwbflhSY!CJ zt!0R;6s7<@Ju3qbQMtm%R3S|WL>~!MCpS;3U8g~Tqd@lFqiwD!-Dq!j^ZSkzb{R|C zX0r-f1d&dnz|)<$?izKsuT`s=Ru(-I>&A!qpkpEB>zaMxd-ns{rhsYtUuE#MKGccm+3%bA9J8apXta$41!t4K2kUl zJEvmYt3TWqYVOwN+pmrQwFiOq*QGEcK%*chE%RuKuJoDrg!1xi!nG4Bxm>yt|GVqD zQ3yvZ=8erL1X6!55L*-i310v=5-G0YkJueV?qm?nyYPy0Ir%P5=`KE!m}}bIGUI7rQIfUQ{wGv&7pDZqm}y0G$uY1L9oWA_woj1I-_- z!4)((kD@TwlL$Z2OKJ`g(I^*42Px4+&Uyi*AX5~lTzdSUOqdV2>DNEwN?1&W8=vLJ zN8K+rfPgMH4Wr5Op+@6&ct9blgi$b89PRXr)dI#1;_ogJeg7+e#*5p|l^$(n1_ zeaCBPz>C7;q4-TcIsi^sz1zB}&Eu`DPYkN5rJt1qTU&h$mH^6wZV5PCv*V$|B#p2z z>(HoBO*Yz+2D~8#oD|2e%{4F^{kZMBFvsL=Ks&A0fX>gy#T5u2B>##O2az4Pj}jPI z(f@QlS*nZ2>OywcXo4K2B8K`F4W~z(G{FavuVbQ45kvh+^fYn^u)Hp9NLQ@JGy6&H zFIIwp9Zf$< zy1IJ3yHmXCd2KLUCN+ss2A%u)H*Z{rf@3OEqy*`n*%#af?`ew_tuIpcodYcm_T^hW zgB;fM72{q;6<+kXLsPua-;?6)1jzh5o-=Cq@0MHQcfTG3DDt^4RkmB)xtYJ`P~6=U zP*;rO&C*8Q;HByp6s^Z*ut#E|op7E$JgHvvIvI3p8a@kjjaE1>5mIMz=aQB z_K=8)aI*POLlSNX`{KT}bQQnWmk$od{Na=f?dkb#5fP`W~H z$WTKo*~!Vb3+uwv%Z*AzkO>n3>+3*l3DxIV#^QJF3{_HL`_=ETG>JH_BvwZx z_WvJUR~40Kwk&Zc!QCymLy+L^?oMz>aDux_a3=(Jcjw1lg1ftWaPF5mXJ*dJePXQ- z77tr`S65ec_aed!haeLnRp)GN2{hO+(9@>yJJU7^A)T!ItXATKa3KnsB4nhql7J?E z_8+6*-@)O1bUAsrhR@C>9km93S1T#mghPnQ?Y+{Z=W!>fgowq2=k;=1eQG>BJmRBU zU(?c}T{f1m-b)WOuCpA~|N4qlf7QzCC=V>l;n z^11OxAO>oS0tUv1sv!PIkT1UMgE64a5#3Eu3m2UJ1g4A}13+5f1`ET5diz}+U0q#G z{NVL??S3e;))FG^YInam0p^th(}!Aak6EYK1RpP|bK$YkA8*cr2tdr^n;XAQFrYp;(@>D*jQf$mZtA_1|4umDv!-(nPMZoOX8xg`n1BYNcD^OK25Q%fAm_}mZOcLU71Q2 zO07>lrAn3lKNsPz7aU0x*mfi+guZ`(PpgFBait^YnqWWrLJ9Oa6ve>-`1S%H z4w}Jwo!}{aC0f?NGo3d}UXLUB{WUSG zIpYK?%sc61b)>Ko(u zK<5E4Zhd`}eC|%>`iLMVM*2X7fqVJeqz0FlmJzAs6daTb8X6ixej6M+m$N133lMlx zLRtpIPwV-LsKRu?qGLtnsay{cry>n0XK*XTlzE885oswD5^tnl6hseZI8JSvaDu-f z9e^?_;Ig)b9ow%LzW)7D(9gF@-oHF4ixl^!u$J5;gP0MkCQ|y{_UHfR7g_JYd1^`( zK>n8MStE&cb=_NJYkPHd?BeDoCUurTkwUZ=J(?fTn?Uo!E;qcX=_(IX&cYY}$U$S! z31%Hagb$0Prlt3PKgn{s#4^6fwvS_fdsLscg!e(B(B(_6%jp{88oV7^FW`*<=KCqC zt7pcg{Sii%4AWBCoGDIYc&8`OccZ?>OMy{I37fTzmFLood>CWCG5*R>)*KUUo)2Zo zgW|Ln=!MRCt2rr&r;R?08|WL_DAY|I_$P0@d-`aOtm(ogM#4eq{ItHDA4SY=7ZVa9 zVJuAM%HaC+INo;7kByzYvy;I0bmu$o-`MyYUEx!;+i_AaB76Ys`@_Z3!cs*0+!;qo z{kFk2EGqd741E0fq|DdpqgnIu%+uvU1$A}KYwZ;F!#$vlv3_&h-EqIcL?$;rA?7L| z^^GXuahjhq8jQ*0JDwMcb`PeQwMGsk#6CcQ%(Yzai>WC_()2E$#ey-1>T*wHD_~9# z`xggXllp!~-0Q)T`71X3;U#Pkz-xqKBd_A2Q6j0xeFBr-VxxVomuI5HOi{t{MY6Qo zjLM2*<;q90dn6e3rIq>f1uFGCt^?tfl$6Hh<(cQ+%ftNo#nyR$WNnn9lM2o?iPhKF zpyR7$Z7Tll z6dZGRd;eBSW*LSv(#Z@L-O01`iw=Zqg)BZ?Az|0W)O(r%-X zf1X@HFF0VNF9Dx}DlN2xfbX_{0D1LTJqJe>hp?cp4|@}us00hkw{PFD%~>zrWy79o zNY>IFMqr19nA(Kg+1nHPhT2T;0fQHj5#BjCILXR%LS(qGoGuulD~j#Q|9Indq31Y7 z;9^Wsgw0nMw`YIBFw%jJOFymIIFS6UZOF@b@dG#XbR#vg1mO>0`m>~eqV89KxPfkM zuU}yP=Mxvyfno>qmBMdj7y%g8*9kKTsPMB|q-${IUO`5Jd^0Y?P&iF+h3$=+U1-QsY1txo**R~X5^$TaEdG%4Z|osos@ zAS`g_uB`UdlFo=Xl7DK%I6X``_k@Tji;6-mAA-x5+qsbWkON^_L{nF_&E^3CNAEo{ zGA^ry;xYidvQWOgkG^8v#DG^%OBMcg;qZBcp7o(P0>e?2gaKQ-5**yckTYI?PO;2s z0D4J<)DRq@?ga@-jyg*WJs)ju66I+MoVjtoaJ=Jdl3pVWk;olH)v>zqHhq+!h2{v+ zYKx5Z?bTHvwGA@#2T@<09-ync5zw!dw&dHy+U90?UL4Gn+rtA1E+OvNZuV2x!Oe{m z$&xrCuNXKXAujZH#40|_$;l{|#CK&aFhRXAiV=v}bA6v+cC+tA5@oDS`{4e@TE1}X z5&+!I=3aBQVf*`IR0dDk*&t_7~y%GzhT*^nwN+`J1{MJ;1a0=1gugQ{0Ji zEamLv)aKRy<-7d%651c1+>REi-&EtBShlyf&pOZ77=Q3SJl=!c$<598mzJ~_vB-v32rGowrjFiffLTEYT7fJ5hg zw`;BE{*WafUtW3Jre7E9OTRd==e4x9YW3UuS#%R2Z-N_fCW{t**~3b42xXTtTxb7K zD3 zV_UJ@>YlH!@7ZcS0d|UbW@hHMRr~qO_ST7?yau|Ou4hP|AG99>1FE#Vnr z(7fo>j$HxRX{C^sTY`_)+8jzJH*r?gg^e>};iee^*1Ffss*3 zS{r}AUPC{V)47JR{O)A+ZSZsSv2@~P7x)$+c)z>7OS=4#kzx4J`93fZ>f7Pe!!GvE zxAXh3!k=D@Ownb;0DfhL@Y~~fnr+jj^FX5dPRz7zbN@v}$sXP&0I6p^THwyYNGUd? z71ou(cOfkP18Jah$fzOTZSQBw{CvPR;D;^milep4WheM1m+qa6rKzUnwYSndeo2_S zROj|M^ufx?YJqgR*s}N3X8(+jJow!gbkxgH6$M`olltdxSE9~pw;(@A&vq|Q-uvF( zUW{anfDo*~o>3uwuWZV~Ji*k97LTK_f%rH^KgWPN<@jGGzc${(p*YFw*UgW96WS5R zONJ#MFZ*~hJ4zjYXo3t4kN;tEPgoGu@RL7ET?Se2B$~wroV~S$FW<4Co%lYAM@J9m z4Cf^MGQe;1$81RJ{87 zNdneaLGPcvhJvCAJ+^7!8s}Bv`8uxZEL7*tNvW# z6lb4yz@gbxU)9&`;kXtC_x>aEc~y46x7S*)&78hHs(k6h-2vnY?9eQ;u_WnXX~4y#&m; z**Q})lh>P(B`Ng&p43Ti{tglqJ(XIwV|A`pa0^WXI9PFz3lrlw;|32Hfj5k3fh$g6 z-NIOUqi~&y=cf!bWMb;0ljd59Xd=kH%^nHQw)U29$Kvv#b{0x1I`J_v_wxomB2$2V zyQH)f_B{cZ)N6+~2>e6XZ09o)l&<+i#_3|ahvrC9RzPxhM8ps~t4To$hHxZXdrBfz z?sG;?k~W5sgoL7-SzBM^-JP5#%L2YXzhqoTI0%!=iRDB7LyJ70LMFG8zZ`E|T-?q4 zWUj>fg$~KiNGsZ=Z4kK^VCaxZW)fkLlEP;C@WI2w0~Qt5)4}9AuM3D#H&uhJ>l%I!qNlrCJE#!> z`Q0Hf6~>udA~+cCsTyCCDtr+EVnINSQ{xUcLwsg4EH!;U*Gz)@;W8TO0UkeQ^7qruGiY3mj)`K38qp5z7Fx|Wa zJfwhl&cWWL!)!)sXCOEBV?3?{f z7{?cFL7xb%RCZTXr(($D>ur_izo)PY9*W?~aVYjDK&}6mkE9avjZ!YbFeMXk&-R3r zW)oG8mgSRLLZA*Z&47`}$A!iNC4O&~v4E#Rjdh^3v1F0WTL&4nA=Iox{D96Tz>y|d z-+Gik8WBV>^kBT6!8+yb-8I=STJd755=SEDIUi{b#z5j_j7+zyC5D8r4FgRF8Fz0L`OFLPIAH5mv>%4YcZRTJ?#)P49@;?3$aG;r(% zHA>v>VFby;L)Z6|dX4kb@^Q^*X5b{#TD&1F8yvH-h6?)AAv$;#M}ngg_EIx9v0fEAZQ3^&l?0&l9NBWEhX$En7lZj z-XEVA4N||b)~H&%eD`tjycrsR4}-6v(5h3@ECrm#);C4JSUVPTk-W%D-5wG(y!FYs z;Av_rUM%G%^!XA!DGTfBZqfnYl6_yaSl?}%Aj}Q%MR)B4zGhhs8MlP%-HuJ1%Yaj!P!TZJ2y*vQxIk2JG)nx}W3lai- za=M=s;C;Qkxw^W$B;s&T6&1Zd`spJrM|JlCc+q1L;q}!Ku^9ZXseFlNIq}-cvZzT4 zBv`7SzDQ#pMs}Vk0zO z3l=2SDAa{CNd#V;^j0p%mA5VU#A%urK`Awb^FB?9J`f>RnW9@cZ7_^u$dW~hKB53J zUT@0E_p_=h4O0@$Okd8Y1kCXV!2_u+O%(s~{kj9y?8k)48*?`bU7k8TQ%9h8QQIB{e&gSWHx(ns$^P=O1)nucAz(tTTg3FQQYhqG@?ns-qni}+I%!VOc z*Ms091eeqKIPX_j1!zeKVrXjG29YO#?&F<$$)WoG6HKhRshPDhC}km~Sf!w*+$nNa zNUNTPYjn5<6GeNM2di6hi0`DHSft`Be1z^ttrzpS3@?$TH&uP6KQGS)>{w(b?GjgLq1h?O=poF&mrGyckFzCAQ; zxp@>ff_#2yv|U!VojzK%-`rQD+uGj7%l){)cJ+wwaD9!Z63XIs8~`(nCe~~FIlNI` zYr3)bhLRJC`*>2gq?wz8)qHV^lcXFSoPoShWjF(V>pP-T#?NAr@esoanEGE%BT%IB z<;+NL(`R89&w?}WTy%XI5#IRV`0!w1ncN*<4`&3^7AVn`;R75ve#dO6YrsBvSm(WX zJu?7q6}rmy$7v=3+^Z^_`t9!EDEv&fS1plnwP8=T^YrS^{nQU;I~T2*282~_4-57m z4}V`X12G&KA1_p)v*RU+L2GH@GY5OLffsVWQv|so_QwH6Jflk;+cN|iX=%{%)Wk%x z+`w@$qd=!%Cyr~IA8$ctCvg+~;IQ8|Edm09*Nf-o0i70_DDu2eYGW6H{Qz_q4frsr zS#ofhY?Kc~l%pqsWhZowD9$%sl&&1Gn$Yht4;}}SGsgIg#ffqffk_3jYzy6VwPdjW z$)Led6(t1olXDqE|5MQW>s(aXLJfMtJw}g`2@og zVE5=$+p+E=#d?n3uC6C;6i$2ccr(~aI_)GRLV^%GrkOrJ%(xkNS9Dqi7tK@}81U&M zR(Vr%bMKxLz7Yx7=P0`5mF78t%*Q>>rEw(5AYjNR+ky|$HLQ`n;pnL>)b3SGp~y?W zI)2c%HlHB$_4PNbK<2x*7@CzSrKNP-UD9J5;3?qXuO<43in*IS2TBv{ zQoUpWT5||tKd9*OYHZC~i&HF*`jr2@S)jW=?8I_s$O6RQxC2B17nAC<3HkJ4(uYF& zPT5yxBh=myBBTHan=D~??uZYMrA$k8mgTfpbG)@`55df>-;S{`F^g&8zvwo;&g1ug zKmS!~Tgeakc!Pg4ZnGfXB&&v#l1)llefrrcUprIa@iBI?rGNhEmB2X!60{G5u+vrB zjllE|0xu_y-&P&pVzSADqY0o9!&K=AKiLVUa~>&{4wIv!PLhDu#L~EwLj|t|eKj@B zB8n*sg46n;8QJ=KsYa+hfHZQ zNgz1G!Srqgljy?VMAPadc4BQ^!U(zziAlS34HWn<7l6cDRmPyj=7HG9MqQ3I@Sz{S zvM5|lRdtQk`EmJtbZB(Y1ft4zf2sZ$_C0PUsY3#;`3jHTkPX`R(um`X#iMty;)m1C zDj)0y70vue3zPM|onL~ZYVz9Jp6$`8c6W9L?@@woIVSk8^WxTb2*#*&;!FJz7T_Sv zyv;^++oF6|Fs!tRuX?*#m+6-DTC`&wQ~e5DQW!qxZJ{*oN0g84kPb*f228s9LvHln zCs4r$TAv-S1Uc3cW#nr|L^np$uA`XA$&J-AeJV7lQPZTmKS*9|H~(ESTY591$%RQk zqwjNl+pocMURvpd2{HH{pYx>nL#F5Hu7+)@lZ>iK0^^W^z*AjOk)#Ig)qlA&pmTM7 zW!^VocLrfN13~mO1>BMo{=JT9Sq6 z!HngRGTU!LAdP)m<>MNc``qZ0>d0!V?c2U1eh2@=z6p5@~+OFzht4AJrvb~+(CAJAz<`l`Kp3)0-7(z7=HG8rRl#-}iZ32SqD z7|686u&tc*{*FpXO(J}a%Ye+_FQe`!1{kGS2x#%^8Uj+WY0e7@9XT3Zp1u{6+N zaJxNR`(ff_u^FFVa5VkNRI)03owU?ss3!k**6hg1f!s;I)aUVVGPZ{&9$ALV=e4b( zmK&t9d9X1dF(=oVg!`X5{HC6Dx12Q0D1OFqg32yb}`3HWf`-15VNoi2k?4~=3 zk=Y$GvuWW93COAt1*yuXm!kp%EB~XQSM4sHrDZh#ToL9oWgq3a&%M2aL%8e<;oHed z=UZ0fQ0`E&%GA`fo2vBv)00fDoY)^yq#s{=$f~lW#C@-M-*nVHS0uQ|)``XERhZ8W z5fPC|Usf5x)y=(kKxeK|q&N^y;@mS_8)k>Y18VenG^P38%;(7l1}rL;(nb4o{vn#! z6v8kCxK+(IK<@xZc)!Zhgi=*LzF(n2f0<%C_QePF%A;c;_t{#Fa^UtsT;Tt^8EH%aTQTW*&e!md z{3(VEG{;v}pJsvzrL{ODOq(C}tC?MjN9%Zqe`-rs0!XkK3|ba`DVcKR=H)ppm9ycw zNwS$3v_I~{$5TZU@a-jY9EV59q@DpC2~uChDL$0G|Lx)KelTJ2s9OIpR@rf{tM$v= zv6G&WM(OI-4&nvU)5|LnqPSQCz&{C|H0HAk0y4Q79bX>V<#jN#Ve-^yEK*oqrjUB@ zmS>dBV3tSwK61FLt9Nqb$T^{%GUZD~YMDPm_iLh%m}yxJYWXJ$+0Z&m>R3*Qpj+MQ zXc7m__xFZD1$`IpN0*ZmCzb~sO)XbX$y2|DM?hzt{$zi~~-)@-TbWr#WJ~NPs3qXWPMks@rr*{u` zcel23=n0a#R=xLEDRX}MDT6fSUDWSRxX<6?vD(Z*zV~^2z}Be9J7gkdQt(>h*MK$f zv5b`Bx(ep(^_ z&w~K|B=JWf@J93*@t+C|BSN3|>FC=luGmRKkkQNKtY%Hg57QPb{LUs_N;iT|%9Z zNcdl&p>~wh?p$OsbGfmXX$vCwk+#n0 zyI@sgk&USlwqz@1#oEW>DHa1ZEQJfhqV`^7rt!Do7%Hdi*8YnLEs{F$r%7Ja#$`MR z?}EewvLC!_blz4T7j+X)zY8Y!Az}@DvnZP;%6PS#PN^24e>7UJXlLrYcW7-m%`)Ng z&H)O!Wz8+!OE_; zYN@{kbAlo$S;vHf+HjheB(LP9#0!tGEwcVo?C<=prem~`haq*KBZ=5JDa9Pwdsdh_ zmg=peg;K|;FmM;e>rb{n$QNBPbWyOeGju#0A?^<0S0FmO0+21BpfF#vecszjzx`6@ZWC5-+K{qrhK1cU%Febe zCrpc&t!L1dXlxM3p2C?KIO&8n)q#+E*dmWWDaY}ir-hT*b$fQc5}&0z(79XmDWH~U zp<#EzxwGtL()Stz0s=zS+7@?7Inh>X58J>yHSq^!>E&wU`mg+2S!F!FZ)a`n!v}ZO zr+~#d!4ubf^Z?(KAk^QurD0wff&8yb#?LGo?ATM0O_rBz^iEn>7|mLa$@r&bm1i=BU=?-MmLjIHD#58jV?g zCNGB2La(&}Q@&F4%Qx*$)iLhQqDMmmAD)C$hLN!_4johHI}NJpfJ3Aq3B)iWsRD@! z`;&O$XeNwJ75Pd9@P6(5d{%fxh<%n~|K#<1&3wu|up5+ecd$lY5%BmGVG0i&efGD1 zcvMtuxK?Ix523f_V?f!OPymcv2A5JlJB9n%Gk3nGs!3j4@lKspeh2o9>9x&SzA6~^ zf7E*X5NOkMIC@qo#n1qV1>8l~wr0SXv(tZ5957U7T!?WhI(WqYS@>mfLF@bgv)KU1 zNQTM!2xpU*9LT^OG)u|)joW*4g@J*A;)9{yXue-kFhX-LReQ$5o zgy9T7pDdrR-b74YR`7S8*52>bON&a3Z<0&X{AJll4PRmVpE*+C@s4Sex-9f~hU&9< z;*#IjwryF5Y8q_4LJv{+?c_+&|F9z*#PAK25Is3V(;iwHUK$skQbs^JZB?|hI1jot z(T?Fq3L3Wwl}+QKsgX zaxkc5OnQ$2SljYDv$a<aM`SNKL{?nR9E->E@vQ+>rI5A$- z!0PAWK}WB0{7075wR~hUaId|EW89oQPl$8^*6hT#Lse2da*MDml(biK0-B#@%qo+$ z=9TN5U(#+tkQ^Kw>!VLgi_KvYdVrh0bV->@L}<}mF_9hpI)qXtfe3Tr)2(c)%NKmb z?b%WdHOHX9z@jVDQD2hVsH_Sc7v`ig_Q=0d%E?jGjaLL;$fuFmpvh#?TqG1>iTv;V z^rk=39|*hl{#k3zKtc33_%9-o*(Luh^$&7Eag;%J*e3-G)~ksD%-*?LH*m5U*~(?} z70$B)mXfH6!NKR#Mu9&En}9j%Ez9r46cgyL$ah^fOoYVB!l#5C^dV5zb$BP;l)%8i zK+){&6_T}s9dF~DDN5Vd#mU9)(g89V8gXWJkQP+jJ%q_WUOziNOPRRQJ6QxV)MS{8 z02)OW0gZyvYwnv`-Fdm<3Muw)X3Khtk*zDEc8obk#S=`e5E^jEc8HXg>7NS@I}jo8 z?})N6R&BZx6hfShDYudES3{ZH<+)eEUB1tE=hP$ zL$UHb#p-z7vz|TbkH)VA4mKM%_d`$gZir^R_3~L4ERjVD#H8|Uq>vDv!gNkfP89w} zs=~a4PFoKz3`xlPugw@ikF zNr}S{eqb+^qJF0BBm1xD@J}Hzjq(pRr?=(h#QP770q86e(3~K1LlcaD|3U_s7AEN^ zWDF_%0w6_BAlXJ5J4|EUO-k1Hl-Gzb@OWX+liJ(k<>B#u4D*BlqkVafa1-lu8g0*R z94_wo0T002#PZ|y#-H8;(8R=U6&qP33JOf>s`~mBSq+*_>2qHedab1F?d88#407J! zb86e&I)7AMvf3f|@k96J6Kw=7UhA#@%Y?D^7~iydFR&?P_aJ#A*~{S^=~h6Y(`$1= z_B$obylRBGg~^?A^3dJ2bQP@yeufP;l+MdYr3VG4z1< zt+_tQnd+TSxJ`_W$qjuBR8cQEYuccCG!V{jlugZWq6tgB>uY10d%lsW)Nl9AwZ0D9 z${}O4HOk3pscCDdDJej>Gpw}PsOYqVl9=sVmGqUsgz?RCB`(G8paprI+I zQ!hg+3C7QBE*3B9p>m5H6cj79KH}a6DsY!0?o@_hLy5%{In#+ww>o6d=?a zzQxXSa^q=F>Uu_?f0kW4{Ny*j5`dyLM;(mpdAJNAIq7h*m1WIk#G1yqlScr7^@BK@ zR4hs{Z$Q{g4u?UpVRBsj`2NNJ0iW}WBEudKO)sy|GN1v5e>m{VdzbSyhEn6lnM0t* zZafQebW%dn0XDzqebtP$AAF@|a~wD0!LW|5vO;wWD`&8Qnu?yPnzEM4Y>F^_Jaz`{ zhKjo0nfB?{_SWuJ;OEFB@Q&2ExU~3cy8-FBdtTV}scf}YGlW0}cAW)vzcWWs9@d#? zKP^M!M6p)cRyLHb|G82q;K&g75 zfT;RLWoOAmf?c)0zpy-A*oNSOS66LaT>|CpIn$3JW_KGz`A|i5BOL;Brukw2tNx== z@69L%=xh8a-&8+a-Anm*nG9a8@hi_OgsT}=bUWT+x*#)nvueDO4iUs?6ykHc&hVV8 ztvdfDhmkR=xK>14N2?#jccVsr)q!5H3%s$7F{tc_`y6$fbLr6!GnT?f17Kb_V)5`I zC+%=vIK$Oz1*x*N#2U}{L*_q=uwXy1%Kl(g;~0V%U1?kSxb7{Z9vj^_5y8ZHWJ0GW zA$AeYsM`dxl+WPt3ZE_!wM?DF+$^rIuZM<)rbl?vdw6)bq#zmI#6Y;5L=Z3#K#3{9 z6V{kf-Ag<^CdX9{-8BV`krh%2fL7c3mnpZ3qUQgK1uX{iFi zK??{S_^ts=!ySViMex$E5ygp5cgJob!AMhTM?eQe1FutLy<)5lpX=FV8>NW4iuz-3 zxsD97UAv#?3}#-jCBws5nU4*vnZIuviM^NyhK9nDoenO3)!jG|sZm~CU8_B0a0A_T zzAV4`Q`+gy)CqX$Q+)|yHWsJqd#N=v;5h@X~K&8Z|c?PYgAL6DUUa?7&#{;7mR(O(yE@}+0gTTGeoTK>Kc2^ z`}!2*3+lH}u8i=FuN}TfuEkR+2}UdG?(POU_By0W5P3n7?EV_lX`l!tLW}>P@I5MQ zS1K5#YTlp0o#CRu?@H+GwdouJ77AW8tC<8OTXv8HR=&r}=qpCWzKj;$O-g}Ex=H-% zHKm)0Zu8-vIEn*7>(h*zgqr`)=8J+lPz({tEYbsid2^DI1>5jJ!5anoRlMcZdHH2o zM1>R%Oq8mNiiy!GL&3N*jJj_a{W$~cKI`qMo0~P^2od4?ZNL2@7pp8x6Ng7LW)!w2 zfDK-4cH9cQ%qGo#fPNTI$l!85Vy_&T9*Lh;CNt12r#6*7{C;^kYHzdw0=LPLPqX4W z-7&k?M=YzJL_z0b#Cb;`orVE!PqvsB#F5xX}EDiHH`8s(u90AW_}gp*XE$De7LMbgOWrT|vV_>=RfpM#O7u#>v?Z%(o{N zT2^U|lI911XECV#T3(g}{sOz=c};;ky--vb;_efW%x1A6DDP-IbDPUmF74WKe7zb- zU#Rc{ms!xYCm|uht`b@@?xsv_MBS(x9+g%%-B<`6;zylJb`{O7lGd?UR$a}BvO4WN z6UWEgf{MA~SAN)uU-Y3{S5trwP}laTBNt_G>}KY$hB6UhJY37(GCmj57DB{+~W+*p2gA4i1EdzhAF^3Yf# zlwqT7l(fh@Q8CfLw>tNr%4jpvA7##^>Tl0LbNG>zq(~U3dx6V)@?ks!_e0E-7R4)k z(B?kJ;8g>Hbz*F4R|+O3W@lOKgh&;|q+(L6n4?1t5FHNpw(tmXow+HS9Sgr_EP{2v zYtS()cX6ut#@PgZffE<>oeq!80vy+Rst;m}qX2g6&UTb;;n z6`M;%Jn-%dB;z|)Gd`sOtRH4~zpb$;Fk8)-)xZDU3&7<@dg>9~dA8sqwcV9iatI9m z3z41O0lTBGL1a>#a*&_PrPJx_PL&GcU7~KW`5TUwcy7)NmgBK3)!NVnF!J13a&H$t zo}1pc6N0iB1ODX7{~g?I)()v9r8UDB{+xepcT$C~2#HwqX54f}__m0+C{7T;xW*U~HUNY>L&<#fr`wh5dIZ`r`(M z_@nWQhWPgr9NeTI3}?dxEa5^oJkKx~jZ=LNa}9_pMOI+UgrPB4;e747^KGfGPfU$e zlb`je{ry~-R$-bI%O1dACZyv8d?lCo?yG=Em|R;cox9W+?Y3R7;rjjUmAB*Y8|!s! zMjRd?X5T}G1^QwdFQZ*SoTBqnQAGtK<6LtAb0vuYC|Wk?9UgmDL&f;&ETKjB%|{3C z1gmYs=gX;K^^}b5ouiYZbNy5Ubrr48hMRMXD3FN~EP`PANpsB9i0BX1)9yIl5-Skx zPVl@QtqtiWH%J!CQhFx(h{>EkhF8+dN|VGd2?==C=GSMV+e8+i3zY`%LQqudIv{yN zhT5siy;g^BTNB4Y(UL4d~%6Bs;GN1ql3!+PJaP8Ux z+ECFvU~PML8hSf~KM%a##49vf&o^B194rA8l%yklU%G7@_w{RPGNT?Kv3`e)92OPj z?CAJet;Y<$gvZrwt~ORg$;jv^ab}Ry#G290SdpPu(9ErTm`DR=ub#I03J(D9zma3x zc1QKp;*$)N)L5S8E8iLLWS58mHknPC51X}z3wOT@J9qXs%W3;lJ}6HNw_*9dr#u*f zkdo3+(bs&@ml8`=Jvqx(F+<9r-c1Q#OScm}tOpC4p9>e4D=)~ireP|Jql5RKY{@Gv z{`uNLLdpB`x;Z^tla?~@NI7$0b=pKy7LYYmb>;Vxo20!k@T=F|(Ejg;2ILdle?U;w zx}YlY-#H;OCWJ{CRHXl}MO5*478c=Zcoh3Fve;j;*(oJ1baduNGegV~mDVo?rlz|+ z(Vy&rB!g~RTM>2!P!@+*w*&y&LjDN7%ah|_K84`BG7Z%koDjbgz=x zb^a0D`SS=m$Tp=kxXdc8WWfWHi{@D8Op+L!VfIeWXU`j89TR~H_&(7n=QU-<#clAG zk39jq;c19#Q7C?^$wQTKf!%$Q!Wx$z{wI+uLT_%N!gW z@^&RAWrmHM03~A_Jb}UbqXHk7+Z6^Eqi(YUC19@nEne?QQ#*XusAD)*TvGqwMV|K? z`jAR=sU3=?qfiQ5{5UgOFdp%kxXe+o>U`(0l9t+QTNw(}fby31HsfjvJ+jR$8z z!lHDCypfyM2$aAppy#gQ#8Lfmmi>D=ec{0V3XK0Fenx=`M5m^~@vnqPM?n>C*+QR_ zs9FRY0|gT1{2m@*8gG)@FG)B#$v9*?8yrWYsy?ZDS(4+qTlZN)BAo-KJpmJjN~!Dt zolxgd5@vF|N>gMU5cm8|a~VX0jR@#Im0`lEs;U*_9XXRLCQT`Rk8hRfg<~~O^?0ZG= znmC(LBBU{4m>s}OD!Z+nQ`DcDVLWdlk zqneUVDW(#6%|Usgk`Rwsgk%j7R5@YJdY3rKIOk~eg5FXIA%h|k4vR|8|4uZig)t_y zU61$MY1_9wuNLjuddK7-M6BT8z$3IOB%hHvhT-rQm%|p*2!x+fSkjy_F?*|*=K?gv zY2QWHy}f#Lfvt>mj~XMLej|0(%SqF$WI4{(T=H~GAAjEztIS6e zxLxf?QB4AKL8c28Mgoz;)aUV9;Nwa2Q>6>C>Z+Q`*y%|y8h(7Nl0CKw^&4LPj^mcS z_`L!7*Nw3F)ZX@+q+_>8k#%rKRasM2nItWJe)6e49iNC$T3*EX-~>bnn59*b?POc) z`t9$VSXMc){X~rJ3izqHL(8(dsb(E4DaQwo$Gh;et^Gqs#_g@eI#EXWfiEi=Ns}}= zeETU$C?_mlRM6x|(S=JQ!>-2EO}q;)JvQ2P)$IRhm(Kn&TXpsUCeT0R8<1vzNT5t2 z2o2eVngWYaYgAFiiF@^ML%Fh{iD7whzt}keuf^Lk;G_zTO7?)@?Ise!+E(D3qbpRW5DP6fh$Vmj$Eytm_?m}Mq27A?!IMc;{P zV&pU((V7+Z_h@)`DCJ`68-?ZHDp52HHqAtZ!_SO9>V@rlNVo~RyPrAiONeK9=wdjK zlv(b%IT||2|AmCZt8nhJy+!&f89%y1rULSrxAp)i8R(+-U0)@+N_stb z|6RrR>c+dB-#c56=-$<&pGqPuc(ZHSX}tEuuH6`1B_12o7lJfkD=>k~ptQVbkAM&A ziN(RsIvknQN}BGP$>iZ1N*cjZYyATOb`b*>a*c&=eE|ds>KS)C^^k+4@ERH~S=MzI zr@SHECqEe3-WYK>S`2Imgv07k-S9|4A#gdZYKK02+s@)rxsOrJhuhKoQ_hk86}nj4 zwhMLO{Qc@c7H%L@ORrr8M}WPtQ}+Fma3V7v|E!E4;FFvwKHJ6BCKo(Ft`MzyoGe^^ zym)&$7uai>ELQn2@hG3Hm^97&j-Kpi``O4xDuLY`7bqMiHqz~>4Ciatnwek z`*-BtE6rb*hx7znd3c#T577HF8XrQ)rFuyrQ!3Z4OqLa=G`Qp4H*=%bAKcx2oDKM1 z1l8e!Lj`+K7Lr|6(@^EjQ}_*^861Q7w3GPQf9r~OW9wigtEu}goLEjAF2Rv<^L`l< za(Ml*siCFz5K&sc0;dhzkmj+k!+&<0LHOAfQ(5Ysp|k1H>(CG5_q+6lmz&I)4EGFP zg+jf-(fNBb=ZHuj+a(!B`JMN;O?MOoneF=5%Wu1&b6Yv@G|E&O6A{RngYa~E#cf$m zU9f$jUs&kiLXyI%QXlBC{ze7>Z;1jf!dLL6%Cd;{U#yA~1V%3&sN{_W(Pt*yYpaZw zi~DmNT#-nIG&{;*ZB4)P?XHxl67V-;O#nG_yaxj?R>A9`hPdI&%L}Mu7547@_4m)y z2rSP+31q|`T_vU1%*^bvnSp^qpbfQjM*^GCv!-RjG@VA-UR|7>lU-X(jG|Wg;;G@p z8sElPRsuWs0N(;#Q+kyZ0bD3;sqc?Fa*1zX-b&K?M-7P5;QfB9_qqN3kUy&T<5c?> zd!TceqYy~b#hovmknu5)$B~=OE7W%qrGv_&v3y?V3R96SO}tTVXGyMA;n2RxF(ZJA zSS~MCYUqQK9UQ-xwvm}YyVqa-4#W7a?w5CU61P*nZ^Bv-F{ieZC>3qX)qVhGLt*ZzaxU9FPo?Ud+{!YRw1Mp6u^zCY~ZbI4mwnMm)4?)~(7SE>=whNC;DsqK zKgaPHbYKC7KQObhDyx%3&&|y^9Z_U-R6Fj=_n$A-my*t23gd~`a6OwlsN)5?BMi|F zKDcf-xcS1Nr6vIcAc4B+^ryuU)5X5%h^mkemwZHRM6&3IAO+=-aHmMg79apa`IvD(-^HCzOlsHmbPw!<% zWy16J_8XP$8h&MGjpyMOF#D?BW^t;g2k_YQf!2Y>!5tA;-ftU_wX*tpJqRl`&goYD z`nVm=M(&BH z>tKkJ`Ehg7I$eQ3F^eV#TSdgJ@wG(x7M-yS(zNKu@ObMz(v=8xJy-H&U+T=8Y)k#V zG|O&H92E}Pfs6ZA1|&FseIQ*w$Vw?*ZRO`|T1FQ!E^rEnC(QU1rlXL{G<8>!bG3c# zviq|W<|ar@LZ;%tYu`fS@TgEIx1~v$S1DBJLAfLSjvs3(PkidenOf7_EHE{84#$R; zlp=PRL<|X6D(CK-Jy@1yMsUIOd&?#Zb~pmz^PnvuVE%#kvV)?s@)h7Uq}RU+s1T=5 z>%6FzAo>g&aU568=AAqaI70n%$-WcKYB!cAfR6)>9}+wly1RNrtdsGIWxweCNKV$s z=+)^Sn8zj{w6dnOHN5MUs!a_YXJA}#+F_zb63$YSVJN2$c+j}u_I*Kfub;IMCP#;H z?tE~hVK&-kv7GV~t+^eK?D{D1>5-B6ePuk2x%IJ?LP3Vin0&}7Rm}VJ`#BWg2?%9*#$`g0BfP(^1biw9@%`DrzCGFG%{cDxakzR_ zTvF!#X;j~GCXl_BW6Kb{dSu5q`<~%y@FT}OFe|jhQ0afp_BGt#6FcWR$^RCH*x|ri zE`$bYGTuP_dVMHRhUr#l-I9>0Ri;h>w6!&|fCJYboh$?MGoS3(vwt^k<9cdR(pQIa zqXBu+8;rH*}#fjtU0i{;OEUh5G zk_R$<{kF#~DuCveDm%$UA51$Tf03P)F=I6Vk@vPSKP0TUjXX~v= zV|*XjU1FHV#2=cFF3t9+D*(A&qcVeWpH9ro=qovX?*7z}SA~GEo+gp*RYI_6`X$ks zk;uWuN4HoCo|rkXv4*(|N5AJ1;Nnj%c$x4#%wJI=tCf0-a$1nR?sPIU@5b_pDeCx# z{|Wr5QT<5s&0-Xg_bthtD*if>L?u2@(s7Q(I0^5@?4!=N1%QFm z?cAum5=JaS@m3PvWDRK>LiT7xdr}u+iQI}UxW~ugN^~H4+D$lX=%Vpe?bM>_aN&pC zkI&>{T_p405d}?QpheOYFsZ2|1^sBY^37R+FkSBnbcf1s6?}9Az7Wdo>JgG!G7S!n zCf{yC4C|w)nCBAZVT@}dg5h2s49weT`+(&Uk2!PrZl0)al#yR+iwP zmvSNLk~l$vG&S2fva;OnX7jUTfquotm-JpjCe2w-*P|^O&77jjRG%WO&?SG@p{=8) z^`dUTQZD4jZM3h%&{FSDRMT`ShAwcK7Dct_t7(O0^)CilR*3e(eXvro)8W6RA)ITBP;zrq*`5BAYja-%G>P1hATn;K0u z2vAnO;yE@rf`8=KIe)p)6dP_aSgIf!_IjdnUNU2@M-T!NvG*%(mU5&;UBElPnj}zk zc;4aPfobf;%TXOxJ2cCp)lnzDp+YShkN@*2ZPIM8SOZ!tr9CDx(77!^2Z#5ktzZveYbjR0y2vi-Kh=`UhR+ob{<_J3qs!VC0`nx~ zE)j6y9M|m|^BnLV3HcM~a|t>gHR zSwZRB&&4qz*QS`=Kfn>Au|5<{pa>Se6CR89Dam5EHn$K(uoPJCI(nyZHC0fx&S`-Y zjl=KpIso7?_SSFOy4+$EGQ5h4EdRiswXINEeS%AZV71W-0>KlWSTiLg1}O$%Z-J+i zr5~GN9L)0?#L~)<^KvtYmV13MC#q62)YIR7eYD2%A`o)2jjGQjI0pk|<6fCO9`cFi zP-?2pk~=^U^@cyxY-r3K|6m8KpDh_lL(k}A?fc~#LezTvQ8}C{n8bfrN&oQ0`hd<# zEcR;lvHsVwU&zN-x9*Gd`6AZ)^Xr5%({wD-$4i5FN0M7d8IeML%S!FHPmx+qb@Lox^ zxx14U+^9w$T{m`F2vi5xm};((#H!-rZ0h8OVcF;ppFP;s?w6*IU2%pHPVk|~IVtQR z(BgC=8$O9)k~bThN9$BAAE(xLHjsfedsr$;?XOGJ%(OuF1@32bVt6~_ix_XOf`yNR z`nlv@xh6<0eY1OG8ncU8P6pQt_dnFmwMYpiT}yk^weEPnbSVp?V_Z;y-3R`x=??Au zFu{{KZm6T}Ta3HodebwUN4rbM&Gj|>0_Hll|N8>KMbsu0u5Wdf^n6uO)0o+Y5!ihZ z&wwM)L!=idALtxlgt%B&pro#%NT-!5-xOKerZsnzp+BD@*$1}{>nBF0_yxz?&csv? z@spUW^`$VSxnfVbWok@AqI87Gi5LU2FWOd~7W}eaBifVmLFZ;ew#0@Ap4%>{<-*o3 zj&t-VjxGHS)an9)?zzs_J>9)kT6Vitk!+%jWv#A2d4{7)N9@0KDy&!;Ib+&iz zY%A9Jf`q&8&8aHB(8&ay$#R`oBX~C5fn7*%t|QWa`5tt1iH4nDh-y`*r9gYqF9qKS zb3X{$gf*|UslL2NqeRk&{HRQWcJSB1X~#|5Xl3kW-e)9mL>C;}fxvPidk(r7o-hzA zhUvC43|-(&c)15bqKd=!d?l<*m6nY6HM?!Z#Fog@|4RucJwVt};J*8L&A+UWAS?30 z02mnfG7JuaUutCM@sSw#BQZ1**g+B!MI@=B*f=o1tP1qL??`pD99wpDLu~XJ5a}EN z%)+HnU^TA+^;s$w9}5mZsNLLwP)V z8;~4kz0Ycju5DMac{tz=?P!I*%GATls?q>t@ZNZB%=}^cYj3GQlEBFgiKoQ-%*M>t ztiw#0bcShExnFY4iO^F<&Qd7Szf9lcoYiu`ahnuk%FW<(FVfgR)j=>vldN3_p`j-u zmqgP(Lb_x*7FqEf=p1O}yJM+=tD{t*!J$SjJ>k@G@u!8MW-e>&i8E-6G3sf96qMk*oWtP6>%X{HQC8@+ZgkIAHpj_uEI*@+)_EB&72> z`NbQPokFQnS6^V=6=>5;`-Htnj0Mx-w13S(J_0;g{JF-p&I}mw-SbKN_A^}sJ{um7 zux?gb+DehPwqZLx_uKDo)dY{tRoej&?g5&o3Z-hsuh(;G7tQJTbN6TqSi+#5 z1tJRb_;pncdUHgcCAUYNlJVJb6=Wt&R%;1jc8KnhKQ_v_=Bgi}X58yw_v)g% zz+VUH{0$8c@7362J?_}ozU$5pt{7qy9`7FByhbR$a=lu6sp{SjN(*d1;fZ~AC8ND; z%Jc4?zSzomM^Q&mRD#0r18siRa`P72?bjNM!SHlOs{B_MRAh6+S8)6b(dNpXY}JLe z{iD*oY@S!-$@Xh%i^0LfG)2)p*$0lK*yhum?{w0I*vj`d zw$^Cx$4A%#VmRm+q_kf4dh1uqVmwcVGIYHNHy5`~i>DY`MMoC8W7Ock5xyX9iw$M+ zZP$Qd2gbFt^&dNw?Qqk)%byB(dsq}O4CYR z@18D(7VS%fTooT+wr67QWOB77k&y#KRTY1r8!m+D&Oov8Mu+)*G8IQW(h|wyqs0P)kox^|Z^39!< z$#jQu5;n?t=ruaM=`8z9LCJL2dfuiz<%l#M`~ofy?NWhJ4eV(w))nAnJhUvlXy}V& zWR}@2x^f~ELr|g5jj7u}X34A%M`+4}PD~g}I$52*FT5Ywv_3jG%Fvj&bUvmu_Q~p0 zYSs3Oq7UQNMF@RoYLN&Z%h1?Zh1Nje@UZyH7>Dy~o7q;PagQr9h%D#<8s9U!{4)os z>tlqv?*?NBvBm4RgoJ_{>&@?c`-N#%+h1P-Iy=3Kq%)R(49b{Iri8zWIu;&FQIwUd z2+26fii3OW1h2mvALqX-JuZb>Q3hkIxj#hS;4}0E8Np)a!q|)FSKTAu!OalYG;bZf zgK0XqR`*-01w|IjPiZ*5!d$vi6)okjEAJ#>?tsayt|%MVd}LsqA-qxNi=JDPB269B ztn~;onh*D-aj-d@YXwrlTbJ*CEh#7|cXuGRA-_Oc=vP}Tq^c6nvfa&OEs&Hz83qp9 zfjA0x$PKhW6j&vat_j~?>kaNyZyA&MHvnBVMxes)QcVGcZL2Rvt%kafXX z#B(v(doW=h<;l`^x4hhvg2a=aC{6ZUeg@D6_g*~UJBI=g^b4iyC@RE|KQR=dNxuKR zw`D(H!_105+H<Ry%w>lugB1(6i)!x}vS9Hbh-Z>4zw} z{0*eCp&%_m>DN&^~vo450JopAj<$F!( z=OdNJ&d^bj?m2byEhED(-;a(Ls6tb;wi~)e=@zvfb8LykK-?{zlCUNq0(ulm_g2vr z3bMAbNwarQBwthev)2?N$fn zd!l(5DSp zA;*}P;CGFvqwoI|ys=za&`DrGfu^k6#$og~=ub)5m;& zU>MvnQxG_2O8lbxL4#((ExECX0lptJrmMSI*cKwFmERmT+MQ26hqvVB+KnXgR>0z7 zjbu8loJDipYziRpY!Rj4Ca zQIDZd5<>EHiM^cggYxNV5)sUz&7UY;L)`AS%Q^1}9-QF#Fv*OXnrnm>eJJjct((B2 zx2bg#{uy%tZD&*4u%6UbVY+Tk7=H%~-<^d;Oqxyn@dxqXuh49HXJnGa3;=vW+`{6o~9_tcHA+ zU#I>=U6Vgj`9)VovnQQ=yDy~U^+O6bf4 z9=!~7LnHb$8~*f{6j3(DTqN*4Nz+zqejUt`e~PN8D9fnZq8{%wKAcVT>hb!TUJ@=k zO!m>y%k{gfhwXy;X5}cm&9`21CaqTISNjDL;|7gk>7(V(nZ`9Fgew#UEkO^KqnTH$ zL3t~iGL0RKYbknj4LAdnI{3Hkia1rQCpn@5MvkTjfgc=&Xc$3VnbB-~6}!NKk174; zKBWH@fuN>4LY)A(?;jg*()|9Xr{a(gZqT(qeP4)sfehJTm_QZ{)IAU3I*J4q`It;P zZOECyu<02lTvflyQ^kaLw5b#0!(JyDODqn_VoD0Cs=sC?xRkZ55*6D@e;65R#vf%< zKZ2N{^arF?FVDw6wI6pU=74(RZMyo#;)__<&B28U9gk+Pcurq_Zl0>Bs%WXIX5xS0}Yl2o4ls~mqjE-xi(JcwPZu6rn zMfhbj!oDQCHiVPh7AOSPuqELb={ke!1q32_z&Rz$=5iO3u=LYp6do-M7|G{YLPFFu zfTooEf*C7{Bt})hXpSJW;NLQ22W&P^@+g^S)X-RiNJ9{dwND0|0NCjZ5xI`ZAh>tZDIz&WcjAQWeyd3c}8MQMJuxyaiBaMZdFyP5QKT zx&j#^lPXRF39Y@xU_qztgC;M}V;dV`7@EUaAV-jL)Su%h)SDy!lQ6gg{@u(+vK{95i_p#3kuX)X2#&STKQ`$z zd2xnhmy0-Z~(RkvOm17BM&$X~?|!P57@brK~uy zc*SzLt}NQTx2WkLGgGaQE4-asM*S)TeF7c?;6+JGMJeN=7xWmZKe4IHuaF}q2aiD% zFHGCn%6`I>n$DcW{Q1=L_Au}^rEqueahC~d$f5E8&D1MX>UgGqd#sY-c`-;g{5`?+ z?KR0;v)M*B{VEjYdSiIM_UWRlQPc1#E&doXB^uEpi*Y(7?zw5;O<6F`+eKVWey!EX z^TC|=VcVzGuO_;^;dHTtWs!I%d_lCibG4_!-l~7Ejgg6Wv2`!0>V>DNs65;M*J^kD zO%auE^G3Bo1z&_6ZHCv{%!$@#xg*IHq^$QcOC>Rstcki0qcZwuXqR=0oj>U1Ja-D| z*ICUkV*m?EHED?SRHh#HTQAVi5YZV{d&6|yl4=L%S?b7=Dz=4GE=Nx?(B+Kp_5(-h zJbeRPh$>pRxh>Te*7=*=9+WHBh!C*xNcaGME@T3}-as&TH!&YbQP4#Y4m`XGmVFU; zfgTV0GVdZ-Ke0!hlzQCd0^{d~OeASHb_H0HSWi0~lwlY2Rl^Yopa z8?$NiWMdmzBF+ijCYzHJ0_IGQ4WSb$Do%Lpz@#%(4u`RWkv~S1-yEP+h1t{QXLg5s z){>k(!|V(KbyhC5_`6Y<+u4yTh{MQ^)$exdbSx?VD*IK>af(LLVyzV=_oQK9FaDtn- z^OdV8PBI8q{9bi?9i`TaLGx|j9_iog4n8{bw4atFDOZ$ops^->PB!bm!OnW_Bj<5G zKd{@$o@y{>S~D5@S&q`?`24!i)FHoEq|J3z=G<|POV>xd@olv3L|b?kK_2twWDOI~ z0+q4;ZPTB;Pl1}3&3Y=|Rf^QWB@CsSUMCEls$*M+Fd#2suQ$96AswdxN1dyOO@rOL zrXnb!7f#>|f3>ubZgg5YDZDxDibdxjQN!O*$X+aJ9>*xPj0)p) zK~{>&9d0M$X&07-dkR+QWyura{@J+(Z&zBMm#H_JWg>RLDfD1^FWQ@)nt{zRqQl(n zGF=x#-TRI|z~(GgUS$)!5S47w*yQ!}&1>iKHdv=>ra`$0ye5r-4>ySEMOjqha5aAQ#$<=pCSI7bp2tKs#D zUk}phM9j{xT`uy~4dAet}-&I%(ofO9Ma>?Z?O*Vw#vG@Ts*V%#E z2<~p8r|I#9oNjqVA2pOp+-#3nr+#=m0UQk*!c@T+c%epr*1X*lFVn|4qJ2CSwh zws47NSDUN-<&(_DTM-h^0~jf9RA2=a$gt1+4~e)7WztwdEU1NXY>4MGMHBZO_wD^# zUW0nv&m;TX%d0^u3v?WqxT&+ZZ@2PDtXYc0p&PGr(YS#u*gk<678OwO55u?jn+r+( zR4;EH=l9a-m0dqIf4qblRM&(;pqml}b(2g204_7HuSfWvHaaH@*3Ug>b%smP{R8{G zoX>oxahp%-2iO|OYSKUs3!eI&D2`YqRTy|9o=O*mi|KoPkpz+T=b0yWHob0$Q`Itju)SNy-Xm- zXg815BF*eV`Cdi?Eud*hxY&IQbd~;7QB!Gx@*T3Vlb-GPze-x~>oXQGi3r)CG0A~k zJM0IaVq)!&>6ga5q`VXaV4wtz1neq+OJbl4pF4_=aegvfvbmJ&g<^AUfBBjE=zP_} zE#H|>Dc569Zhp`6GL_ugTaKpp?5Lyg+SlKA>Tjd-WBye>OczpzqFRoBD3|eT&FSk- zp@i{Cp0MK?b}beI_>e65FQJ5U)kRJdgGrS__48`@RtHD?urSNLxhLvtVf06_s{xFy zg-&LqJEx`l)y9ynLb}Vw#_h@2FN%HAjICU=qTQ6nGNj?<>bBhu4rJzL73H%UqJm1w zd<1m0w*U_7rVvTtnXm<#SCTxD$R6wGs}ka+JdtQ9IE;L0*WAQ>b?GLoAh@sb_4Wj> zcJJ=s>fVK``B-Uw>egX4o112x#fD_H3JXyI&9cDr&xRW0p*N%gs;Ys z{gZzU4p+N`i+DfCDS{RcIMEk3MP8P1loJk^-A`H4QL-ER%eoa9vV^=s}=ZEbtq>xl))=jm0vUsDTS1Cf1*n}HRI zoC2-#!5c4EM{DHo#u0G|yBZNQaN@E(>9p7HF5L6ZleM_m>>{B%MO9SvRs_!bzpwJ2 zgi(~VdFxWsi4u-FbiX@vObCoAPn=35khR|D(2L79;OqAIOmVPp=N7AGjn3yT6cS2} z$2mpnt3W(iCvo2*eIQq%P(dLxkQxWX^OKkJI@oSNPQs^xw`U#u5 z;!Mw_1&sg8yWAQ}R|;Fw0uxhSQ$4u;)!&KFKi72Z@5Q|=sTuvdxGn(J6+$BIBm3&t zcWF};^RUxp1U>urZp3|=zR*mGDHVr3n12-At zPshsk*?Kcb8>hVbHr)Z+ZLv_VR4{k%y4^N!*2DBq5S6SrG2S@mWbxHdcI`ZG7H%z6 z$NKR*81;q%eC>nve+266H=F)w)PK@mbh{i4)Nu)T+nuk?A@9G%Auw-Sv3;1UmUg9I z%%Era8VAGphANTv;B}LKuKxPfa_MOMd#^oo@R)4C5dWC(aQjG;MtP(;r{z?B#b?R2 zck#!m;JMMd+#xFWZXpYt08@g!K%-@B1Y{LE7)ZAFL?rbcgfvEzKB*G!KM(~HO&L_O zMhPeL%sT*lDwR*e_kCpjPP&9Y&GkkcR zpl`OcdG!hDQqo9#|6X4`Y?B|VItwOM_Q5rZ0_W;`nbj>lF;~NZIWhluIwvj)YFort z7BskJMo2U-<0IwQpGWw*-=W|Hd8({5(cUw>&FvV;U=!_QihVY)g<=$_UVz7(Hr{+G zCYv{RcAlDJ*C3KQ)^i~OMi_xj(MDSt1GP)s@_<_2v}T+y~E< zTNlzt5$(84UmeWl^S)ef@Y|iy&q=>bC~n+i-0^Rjl~qwJoUS0FW!BD5Mjcq^^wE|* z)?CGWhn6%xm&M`MB1E0-A{6bKR!H0~1Tav+3)cLsEI)MOYaTk!M*1!L2*mP277?I* zO=iKujOWqUgX;BR-v|IRPuL4{bC#%Xd!(SI467)5a%JqW^S27O9YKNf zII@5rt*Jr7d!ZcOS#Pl1Q1fFu+Vdi4;{JI3I`lp>I@aL0`tt2e-kTKST zx(czu-Jq$|U?U~g3H9@C_}jx8`Rn(e{P9vHoCkEC7mpK7N33I{jp&#?%S6le+4guu z_0{B1hUM!qc2+;>BFO)))F3K=Sz|B`ddw{D-$=_>2dS|T?$Fi6+I@=Fr+5NMFX)ax z+Xb$Nv0~Bxp1}7#fsZ5Rcot963^#d^2;wN_XXm3hKwy|wzz6~YgHp4@t6Xp3B+cDn zE<6fyz(2cWl+4X&zV8?nC5rN^L+_jX85QT+;WQU zBo0e>9YG_;Bc+!s)9ykT?FC0U7E+|GO^HNrbtDdlwE<0*>9_Ecj{!7Q>ds7Zb2_RtSmUD<4T7njYa{#@+B@GE&61?la_ z+pnRK&UMwsrk0K8lL}G zB5tA_dX(0da0_yyWd1D^tpHMYQ!BI0&DW;llY96gR+pmvF{WfxZD5gs#QbWW*pi3O zd^>kB3ZmfNUAV}=pm%=$OUZ+)zZM=A1=?S{Vj_@5z5T~kW}#9uv`OkMk_IBPRBWd$ zYp}&dw8c>dE46Zz=2$xfFU1OG>+WIWZ*s|B-f2kz4x*ZkH}B4lrGzyP-oh>5B6k)a zd!G`mS-oMK3EPtZfu^b`%UUdqh{G10nZO-Pmq2X10pfYp5g)EozBBK-d-Wnw%`e?%oV1}m5VL{)mwBsBR zKS!|x{BpO%TtV(Zv(3riydOUyZ*qW2SdBII&#F_SX?7nRyNQy9B^OKCXv(gxo*YR& za3b04_viIu`ByI2A zNb*w@ZkJw9=h2QOTzZ*#eg9#K3a-YtU0sT7!e9PDxA?0S=8Ia&j8+~;cD;oV>JxKW z6K7NIdP9p^Yi@nW(h3TWnrGv++|5T9J*X<4gEHF9&4K)cC1|Gg@$BCa+A)EzkJ7hM1jVJ@;0d8G9zW+GE zdy~z5gTzK;a}qwHOaz5C4IVaHl=k!lSCELk`3L;=sK`Am{y)%yEm}6E!)h^w=%3%% zQeC(7E{^aoZ!?d$ZEpfAVQ@qd_$i&m?Ie6TgG*WE5n6XWd8Zg`6gFVNDq; zx|3yreS%Dm2-0C&Xgor|#}MLK-cxk`ZxtP%a>2erT0wKyKl zGZnccHK8x*xi)d+ z6*dv+s>ki#4rj2r39H+gXsWql=pi1l2=?iHp{Etqm1B2C{7_WO)S49r8?5wiQx_uL7<7eB{(`PTJS88xU?_EJ;oa9{>hOiO?jf^mPI!XYrsl4-%DDOQ|rr=#&6K&vK= zopj%1uB6zQOn4Cba-x3Vj!S(IRRt~aA7~VV3Te_DgBtA{_$SJOobl#9k7%^t4O8-( z-z~ces3OV*$mvwQeR<~kom*w=0}YliDMw0Iz|S#YoKJO+*;U?6&8X2dZTlerT$a^| zdZ!&+5c~{f$~R(V-Jz;pUq433*$7bxL2y{J0lWMBe+#cR)5jBjMpza8aAiZg7wom% z+k+CaOZ$cY3C=;4FBTj81?m4Hehg z0;e@;n8HE1A$yaI9+<(@8fwFm4h2VqbMe@j`@I|7T*qz*IAxu%6hvu=R3)EKfG2{# zEueyIvm>KIdcz7B{)GN=ZQQY9)wC12O}?q5G1(a0CR1v<)l3#vKfWifKN~S=UG)@q zxe*cfClBQVq)7V&!KJ(gsPg_%nviIpagS=mKv_T)IXK~Glm3$a_`h5LMDwtRwtO=F zuf?~1i*%y&0Rh*)7s?lr4EY!K$>#RO-H|AtqxJyis^eLikY9rEw~A8z6>|R87e#mX zsYc6yW^BtApU?VF7Rj>1WXsEtm&eqW=r8XUvqNow;I-`fLP`JVGJ&Aa#sD(hquLif zC5@Q=_&cmYF&d2ePClmIr@i``2a@G)4cMPel82`V|Au)2aKgzztp74@Q9fXE3CbtV zuZ3bEWu1u80gKU41aeh-;{U1*0Sk=53?^mT@v^kF@-e|}L#?(wby^Fd2n#D+P7bZC zrt=nP!k$cZsjio0tV)QsVc?D$4Pq3&2+}`@k`Lr z_=@3i3=Rn!} z?{_R*as z*c43jxk)`mP%K|Oq(L`SJ>RT>+&W=9YI|Agf_W2xaJ};(DIBvZNfh*7iGu+tz;Wm< zg#M=KsK@}&mN|6qBVrp5bZ8QCjj9a@b#B3R;OdeXWdg?VQK41S^(xR2j?D7|Jo0HF z6Y;jK6v5B-notG6TN^O)SG1pA$99CA6Keo$HZk4QblHr4XLa=27z<1y7v^V3VWMd7 zM2cOBAHJmtO_KgqM^Q*{Rp~E?ZzLH0UfHaQ>8uzWw`wcsW=s6c93THH8bB|n8RGlL zeS=o&n~$`{Hz4Ztg0Q{5ym zyki<&L#>CeIzi=z)nngHj&+T34~x8%50M`^r2ds5K^}}v zSxAIidO7@`&!|uJ8F#vnTLF_!rnaZ3mVxY>{7wMJe8RKbC?7y3(ftL#>NA%y3%IL& z9S&c_aJ&>eyD8R>gOng7TBTUapSg$Xb8VkwzP91`IOIC6Ndx1;1;vt0OupBaM5D&L z1}iW>?96Flvm^EZg2RTw=8o;ZxaA7`lbTFIg5R@P5+#D*$Pk|fZijfPHttft3QZz9 zr|(x9Md_MuC*qSq;rZKN{$w&Brz{Tg#vUmDxv>DBQKy*9fNE)ov%97aZ)?Or__X3` z6}m&-2w2hS>Hq_AfT;|@RTbeG_4{^!s@&LN!bF!W&%`{p!i;@|L|U7|WGjOlGL(7g z(rmez{`ZoZ7NUG=x{&a3Z{B`PNi85Jm9c{6cXl*!^Y%rX&}{%NRL zrK>h*1r!K%v?CNKrOXoiE#yvI^W97>+MyK39Pu7u$6p^ONv}VLowA**h|HT~4Wn6J;;NDhG%HC?)x{&)(B%Lt~D;7IHQt zw|Vx!T(~yE7*ctEv^eFy@@+wJHPKZZlol;$6T+YoGw8vv6|>Y;7*kXk#t!1s|3F-Y zN@jJ2nWtBYIq}=1G0v=-LK^e(*k09Zn zFC26Md|n|`qqW@Zy{l-W(Fb2{0Lyw582aSoLxS@@sDKjOU7|sNb{xmVSS`Kwh&GX% zMe6)u)^FN)j<})o3}aFZ`fEn(VH#ZsiyxI%gMM`m7&f@9Jd`$@#C>N_*O(~9<7WO5 zVMF}qR2kR7OhugXN^U*$7+n@}T&bu>|IJ=8xc5+Qj5%#MZR@}EWJwrg5KDJ`svu4L=L^-6IiH|N5F`!{_>h44c)o-8T({zbsIh!{FwGOrdY>fLG|E#; z)H~)AB^zofv=_3@OSF@+gmvWd%4-Qm7zUH1G>ZlmEt~GvOEM4Lqz{%87LdD_tn)&WfuZ5}p<7{a zu+fNr{iqM|WFX0ZCJo9=ROop%9PrtdrLaHX0h{pQV%EJA=IYS4Lm{XAecC!M@n~Zs z*(PScjewMk>@7cm${(MmG;ygx-Z6qrz}!^Pk@#w|69dbped>{+JHK7|W2)`Wa4WLm z9+oR9B+?5yT~s9I50D{%0Rv!kMCaaV3w*iZ02uTtcf%~Efd(!)Rvj;8*ES&f3s;t! zX%=^zw<9zDeOWLd*6;W!^v5^CEpiHZM0rD)`lb0hMj99L>X=#mA;xUe5fto{e_ae{0m(H$!Hg` ziab8#aYd)!AQl#fVIVTF&W4^9VyIdiwCt9oK1h(AC{lbRX=GgnLN=pC^s#r&d6h+W zdqkVcx2NCg!_?ErvK#9BNT|jB`prpra4YmFcmm-+n<;#W$T6zOSWvZy(71@O&8)aR zE13LAM)MVoKMufL4`{Z2m>B`U+v@N7*_2b7q0&Y|E-LBE(`;HC|Gn z>p`IiryjwSETlPZ1Rfs?QS`(arJIxZ(Y@n#IDxT@$093hfTg77Rp`3>Ck(R{m{Szaun zE}@|$TcYx00z50z8oj0Fb;-*-Pjtd&<={x?e5}#>BLXQ#+#Kh zGv3D?4-w$Qf@V|MR-9wev7r=ell=#V$>sn&8!=0TSfm=o`!c$$j(yBw4TfDQs4UuS z`ie~EHM2Q5aUv^=KWA?rfGzW936?0V#t2+3d0V~l4 z+rKK}eKiqkx<1kU0QTKK_LC=UD;i#k5-@4)jus2mS?4LpGuNf36A!C;la2x)0Mg_6 z?fG$^W)NpNWPcHaWN%b8gi5rMrpM5OY#gNBg5<~pK9)BDs>tH3suwW@VVB${Q}v}O zFBoufZea_vRpNF5mAL9CNe7epN7GqN=j0!MXv?=McxO$gCjHUCh+kY z-Mtb+(>^bgi)gZ#p5>L5MrW8A3Nyxsn$4AjurgE`!DhB3d;xoaAV2Rx*i`ozeV$>P z&eNF(cF%{0UqY6LIgwUiky5J6H2tLE_-zsSSm19nsd{^Qw#)H8M~ZE5PQ}9sQ4kt# z#|z)8E2{_^;X+WU1vG8DCHnt~51?B>7tC*u&;9>AzV8symolgmyRHDB18Xw%Hmfgx zWqf3`65J5k=<~k@t|p>07%`>HV{=XY_B1q48l55D@8_4Q+7AKH2UIR-2Z15~oX3?d zu;8aNJ$27Froq8)DfBxRXYpWOR*LmS#VoDB#3j76z~rwT0=pQVZ^~M0Hk0|~YhW6kOnB0%8Vh+C(Dh5ZI z%E!?JjBxUDucX`|KL)@57U~HGv`|l(9OjaL#6AThis?P;1#$LkL8|_Tc(-6*0(ahR z)`-+z?%rx*wXc1UeMMj+n1R|qO5vN2>T}k!XkJ9BKLJye^wkX;cGAl0YmeQaN?X&U z&zRDUW+(=NC60RKr*no+UzDY=`bGu7!~&d)V^Zytn}ywDfxJl>Jg+m#crQBIsBFk$ zE-5L5|2TL)GPLH^l4FgrC&Lv)rKAbk%+(_G33e)TsVH_xPn5Y1GfUGml&^i$E{vY1 zsU&VWnlL!fXzxJ4R{mvOZt#C!cmi}7asxpA=gtTq8DRYIylo*mm$x>5h`?8pM7}EI7X9KP0Mcw}Ah*f{P1=W`aRo6fE2ZJ1gjglNw%!Hmn z4ZfbUMU)M0*NFceUd(kG`1v4lmz09s=-<+c4` zwbXwq+#Qt%|+;s%8^WB0CK&gXv@YE%55mUI7;puzq}4DY!gz( zj@Anxi)OT?r0zRc62!5FeY9h>g#)ES^FY%5n^R#c)-0^J(ZGM-RSc4+^7DJ91Eph; zso~6H^r}aQ%0u(#kYsx)8Dc^CWyR4r#=n)iD1rNL#dptSF&F>y7Bw(~`O+zG$ag1X z;shDONA)nQJ_QZOmhbvqNI|; zvFbNA#G)Rlb-%c;wNc~p%T&uSmzo0lU|h-)N1z{6C``WQgW@{yCF5xp=0_Tl-Yj<+oqwt@UKmJA~Kfc1K{Drbb zfdE!${?)E3$qbudsrZ-@R2XEo8-2Ns{G{9l?wgG*G$_!P=Zwd~U-7v`;O%^5@i|I=TRO_b~{KM_CUKbkP;1h6U;wDAiVmvYcp zEf-4R@g=?*29h2cFww^>T_OaR^6+G4Yovg3fPdUBNpjaz^xc_iJ85^y{g5HuuI~rB7o62fby&lzHl(&{&}=0 ztgj!Qs~M?IB5X%mh1tzuARz?9(3@ih}3 zJXl1g$_i5y55$w3vVBT5W?EU*r=q9c6CD-BI$%3x+X;8s+hWLVRVt~GNNVf|2@BkO zQ#rU(Wg}-5*#8>{LrEEObX4@1>Q~ci=ip?p2kNNe2HiS`C@`=8t7=6h0Gczkvoy?q zWEaDi3v3=rnLa`GD*oj&7I#=4Pfp-hJ)X2T%+LHqZeNUG<~qT+iTCQYi=!8s{zHO# z+(D2@&&VM4ABmZ;0VYEeprdtzLr5wcvaivER&tecopY>nTn!s@?G=IUm_(MffG^Ar ze7sG#Np~%=4Jv+g$UxjG1PF|uvV^)t)UqZn6w_$pqpp{;Le{4q=l-5z>R!bE$JSeh z#kDQbqG*8NZow@H?(QBe1b26LZ)n^C1cGaT0KwheU4py2yTe;KXJ?=L-TVIVHGjJ2 znyY5bs!^jxG4~iV{MU{(KIz-rzD{_E#wvQ$CHPw!pkgEou-jUyyhz zE-Yj*D|H)x5BK;tT+00b+{d~50Y(*nPhtm*@yBfEW3QH%l$O;VJ4|eJ(}|7n`WI12 zn9^-M4Adih@G9jBsZ6E(iP9&lPd=ubUsdWeU3E^y+0Dolx7zpZ*0E*^RvdOjU*P1S>PV~Z1iLJ&D;t$fk33FMlE zjQx!NR*@HlR_82$eqw;`k`q_(47k&N9*eNxvjSxe;Lw0)(EO%5n>PkZr12#QCYlyL zB5;T*E6EhRgpSs(yg}gPNAX!McqNN!QVjg}^7?<{D2#Crw3v7XpTqfC23WzG;hgc& zH+ES`8NeP_AW+_pF*Y>xm1-DUe~hQ9v~C{NgvxfeA=SMq zZp|gg)>Lz@hV_8H&TC@-43tCmhK*^kt}-zyZ>0Oh)4H}S?bPhQO1m42ARwP`p7}06 zx2V;&J|Fd}cnzXbvf2F`snOGnHz)~5^Btjm%dNpF7RuJJVF@@eb4CwLA^Q=J?JZ}M zhnOPqn?{jQ=H@?DssECGzWyQo9EVjj|8;}!pjhC@?E@KdHzCm%+DH=+F(^8B>eTTO z;eWy#$Kf%aWOgrMDoO0b%E8=8nEof3%Ibm_I)Bh3oM?C-TA(%E?`TlS%;YpN8=nz% zQl3q7+gp%PH&b28MPx|M6q#J3gOxOXCbF2r4(6vgOVx(iPxbQR17H2blpMHL5Jce) zr%V#E^nZN{OvTh5%QZ_(A&t{LHeMm6YAyT%uTrb3bjbBNKJv5TP;I4uJp1Pj3CID; zp5}kd5Fn`q{duHq>58R5CIq2HrFFq=@Fb$sU-!tmV;S?gaObv6vj)f7-$1iU7B>lYR7m?OypSh zfd{1`ZwI@($W;C=g^SKX)I-TMDYR{l6yp%hF2!cGws!pZ!{#*S5o8Kk`?qOR2)H?}!efQTbmDw8)6Qwx33ObnF`IRoXIXRO9y%u|w} zr@xbKC9I+Wo5G(3S-<(5B5~P}w;wRnZ@8EyW@nmRsKMB;G*VD!ZtLU|U6#X&5y2t+ z4w>6SHn)-tuk_$!7Wt<>c2%~TxV5nVyuK`cgfqmYjOxXGUH5`QKXD4t^gsQtB>;c@(Cmk3q%uYBessBW7>U#tPBFka*ld2X zvlP!6z+6wJ+#5W}$Qn*UMyQU5(Ce?okR7@5F8y_{U0JSN=*erel3i%ZJp@~dMrNj! zq;Xa5-h!*PWqe4ZZFWEwkN7GagzZ;UeRuqs`R9)k9E7XQGb4> z(BCLS7m5X2@qwV2VxVE+mNcsSz<|aDje7KVo^%`c2we4Bl$>AOT6Yq@kKs^OlM-eJfrT7)JeCP3(!@`nftodb_qIq2jV`UehH1GxT z&`7G`AQwvuC-|SURv8170Qx&aMnC?=<*=Zb1cmHc8@Q~-THkPK1WKv*V`u8jbNS_k z#yV^SES$=RG4eVM3jv3Z_3TIXC-2had+o_W)y7aiJwq?5bi`}K%ok5xnv(GOh)Ybf z1j)h>U;E~TMu@U&@^33YK+@`2F_SJ91?aWAopS-cNlGCAck=~$3up)Y0P<$? zg2K{YSx%fk@4=QvQ27m>v80dCGdJN>oE>My|IHcbsoL$NWy09W6k_E3!myFdf4NIl z#Vi6a#y}5#|bIH+=+0AQ@3=TYv0F9pGqd>{S4WN9|-3mW>hIh zRKss6|KX`PK6A{tN~$#RFuFi9K-FImr_B8X_Z35Y?AL za`NB$DJi5RkO6pO{|lQ3`i*{zc?l*(Irc)6+*L?Q+9j82qU?4R_B`#xc*81PD_A)` z&`NSi15N^-rMEjSUzxcIXoA zY-Na!&=(KXDV?=y7!PxuM?}*@<=QN#BCQn<3fy=^l*WJOP?b>DmrfDs%1?%69~uL@M1 z{u8-KWd;3Nyof&5KP*asw1Os(JXw}E_i;_c$e?O%?1-(c=aZdiIdrE&n!Cc7zpjr2 z))i4~G(%KMSN^U9UjopToB@Q$yh~v zIr~fJ7tD7QUpf{ZfA)eAl~28&7AQtVgtPX|;k%#5>8z?uEty}Km8e< zuNt=lpV6J1e6OUQ^n9f)G&ihect3;n;by077)1<3@o}HO1lgQw<~QqiV|(!V@mADyqBBTs zBo%QC+!d1HX^GGGjQFn2c z?!vD!fBPiKajf$^xqC3CbJn$arCnvdzBFIe=zId@;axHmP2{_wprG40RcW`m>b&(jHu_jAb;L;Qo>flz+C{IW>v3I!LxuDfmFUYw!x(5r1auRjHsI@N_PtS6}Ad4guf%(3_en z@Pw8Dds)`_P*}8fufLf$t5z13QfmXY^xl}FCoXLD<-o}T5fU@O(sPP5FY*hQyt7aK z5%RAaqXY3OBMqPBFo-;JDFpu~tv6?d#0T3rQ4hCUahXvL$-W4J_l;|G+O_D^?ikUW zE1q=bIuQ>n-$C)S!cNb`jP&o02>!(bNWrR@F}n2s2Vmew3^VM8ruKlJcIZN^5!xLZ z+VBy{w*Kao3e_kY#J~IG8$qZ=zZMm+gCdCqvliVpo733%V68(0DjCK-I6S_13c^9f zp)Q+YcI=&OJd9c#v4p&VQ9XJIg1z;5abo@=E;f?p5oA~6SCCf9Eew79cvhc!2E*MkRWS2^UDGy@y14#PVKL5WyG)eAmXpe7NK$)aC^` zAIX~bXB2SW!4ng1M-A(9>T;RY;Wk6DMk#-J+X{a-M5L|#6#sY{O=H7`e_u^=YHoB= z2RfK6dB6C(C&FwrsZ_HVQ^2a9tj_J!OTfu(j5u51Y4eL{!0RDYR|K!ujx1s(vrVIi zVZ~uIcLUgO-z0>>=6O$Sg;>Y`GU5DMchv`W1203qg1yE+x~k*3`HR zGj_ap!JbArA-{hzmaGXtf^4cO9;EM#pfhAob4FnB_3+CV0?kDN)R2sH&=Nq|Qi;9>=_! zzN{&?FjK&_TI|_3`Sw<=Lzf3(_y4?cg(^MO;gkNlP0|+lyB&gR^y7)U{{t0BHN zxW12^zQ|(-({{dh6OupnZY~VOZ)cV*M~~+D8xdpj6Bq@{UrG5mwSA9|G^!lEZU@Is z-42v5BNzZpwe!5gGRe*y6BF2bSf)>*@1^&@<%q)uowl~!q`R$(PaSEnGP8u|3mWxa z)vbUQaeIaKi<4P7z0W$5n2nx|52IOm)27LMs^hqG5Y(a zF~-MH(L}wEv8c0R%DeJ54k4%@!KS*BJ-?Bl3)4~e>ivuJmT zCEk%@7|iyTl0dDxq5vqcNt@B}uvEu{f4YKpb%SRrQZ8O zojMHk1il#DM_Y}Bl)%Q$7LBbgzZ~y(QP=LB01CILf*1Cil*?YuucLLXlU#frWNmlb zqU_VtKwG0esO>3XWqWO1w5;WZ;3lmy@conjJ>t$tDz2{pE~Bn?t@V9J036-GpAJWT z%{kiUn}W~`E>N#*U;giBxTBef(APr3*^UHmS#0zo=w?oShOfz#I8(O_gk zhBRESa{)j?fI*YZqmqsXn=8ctP#U|z)$?{(xU!H*q(jdAlzWTY_OzXRa8L%i7JgbS zoW5_pd9WfBoD1DTEbSfZDHI8c2~!foR$l8?Z`n=ec*RsV%XUpQZlqyW(;oL zHsr6QvRzar7*VlBlG_PvlLp^;bM*%Nzx_NtQs{qv+_`?fc-9lCTNq4Kqi^tVD9hky zhQtl<9pk?h5e*ogq{si_c$@Af1VLM|3OX#gzxO~!POMnDn^a(|3!1Np?I_k@rTs`t z;`@X^`#Y8@f+ms#HQ8h+BbnE2^r@&+Oi%vS*3kR&=kZgm)hAla#X8^DaBN==UnE@q zyU?CogDOqG*RE)eE_=v~a`aT{N+GpPdYMGh3o-E?Oy8$Tzd0ii^BSJ?YJp#`ArPIUx=y&wbK6rCnVgq9wa4r8-vK!;zaVBo6RAx3v zZK=k5Qpv6dr%FBP!x}2uOQLFNWemMmoof2&5RuX#<+rWk)(;5zi8J6;;3r9oa>= z`X&B|*SMT^^B2K&;+Z-1N z!Tt2IY(}hA)qL(#csTlWjQM&ggLv9#*Kz#SDE00BOv&QjL=Jky&-f@}bV7nA*%clV zx%k}abr%LV45_ke&oz5g!PYvn5((0JN@ab3I==pC zaqtp`>f3bH)y3XNxEmojd^=Q2#P4LNIDq}t$S4r-8%3!cw*C+=`Ub)7&CTzq;qQ#n zc1nz}C~U;OAM}ToV>Qyz)3X)@;lv}@RMEU#^(n>2OHDHhz5?S??($liFSgUt(murH zKWMB+EEFZOW{FwBaNvNxpU&4jZvIlr10RPp_F#81>R8XC7=lzQ_!sC94h${)1Fy=; zLa9EZ+?tWoSS=gd#!+!__5KS~i~V7&m@DJ`1@h-5g-q!`!HHhIkbIqld{>s!Vbf@X z9{b+26W}qD=wT@c+;75B4%$QJxYcHK*wldrAGDd?bGcsar6$tp-v1tYk0fy?yLNfI zVw91_kX~T{P`gViYZ6(%xXr53(PAdFEx6j^TWF-fEn*7#X4sRN0B+zBk$A7JY(;fz1l_ zuA!l2d$fK;Ybxj> zSmbYEKF?cmcpo87;wN?J=StblC%JC)W zVCn;m?R;eo(3WFuV?J^q9oJnA6`?*gJNx(xg_wuUbE_X=vv0i!5!QX9dqB8n;+Xh} z(&lV$7yl=liWS8L2M*~+t&jtq?{1iM!N$K?@YY~Wmoz>Sy+BK34>ohiel8LIY{11d zuv2O9x>KNv_#%3@oMIC9sdt!wdjvJ?cpvEJUv^tYPTuT`7JOKgT<-{6FfNPyT`y(M zk){@r*{e7bY`s*Co@48CdC__^4cD~QPx3-r*4$_@*=w^{+iO(sC;9k$Fuuv{baZ5- z<)!gZt+}3jBg#A*36BXavue?<3E3$^aVkHNzUu%{U7EJj-e4$!KB)$2Qeg|{8v>&= zDHw)jAG-N)Y8_ag07LhQfRDSjpvDkoMowd1Z!wO5*BC!VaI{eD_2FzsEW%KOTrvxG zBYTWAUCbN-HEFUtV=E=D=Ho=wZ_*HzvVLDeI1~by&B(imyNR7>{Thg-K*pupo|osx zOhMllK*HxEE&cLYM{m}8=U2`Z_#BuaoQgJI?H4$n@_H`!oM;X^Fw#sC^JJ;Mvf*=H zoG2}WIR$t`h9%JO~f41b$Uj=9-BinZtE-<~gcKM>my zAg?z2bUr{M*ZSg5m$GmKw!Kv;Yp%V^BV@Y1KY&#;K!Qfy6!Ccu7i^oe*2J7^@j?Zq zV(T_;?I3K@%)O^3zV7`>LOg+=$zZT6AA(9k%xwshAltVrGc+jq_1(O61{Od~x-pPa z{SlYyRrzVT*8B1eZf~4$TWo!Ij7nx}gFQtaN+z!xF_?)rbXT{{d+S>%_j37tXX5EA z`Bo<2wYN=5yAOiw`8Ny!A!Kl;(w?Upz9;NBJ0y^RI=elsiL>(qxYLW>%T$^s4H#b%A{x5Z<^hNOiXLMhXjx?iHlp1i8?eP8Ea-FJ9V zhpIkxLsDeat#~3*+ybYj%N1cR>WBMymN+7A6fxBy+nW1r%Pq>gIYFnH;ou15RDmVu z$_-=bff!)XwnE?V_}0~5h8>Y0R~(1v=Q!Uc;#og#X1>6q)++jyv|rNdaw#bVxD44W zA<~lQ&oCM`ZEY<%EFPbG8_0sD`oF!zP(Cqf@4e#?38Tv)QK$1+oGHQUYT8}#VoXaM z)9eQ8)Sa~4t^l2{b4PC!>k%(P)&Xe~VFNN%&pBXR+E=#)6EW3hpiB#oAvEOMP<8@PWAq_RefH4Ymsw52X_Va6{j`ZjLnSK+7>?RI|EH z={~h}wu|8^UwR|)y|$Y@;82M@cbD1kWaHx#$c{^+1$$T7`M9VRGQ4{tY1b<37GhHq z4Y3P=gs=0#eyiXckPGGPpG@W#={;S(_3k)%KH^V7F*Tsk^*P2?4_kiN*FxX$xw2fA zwClM9N3Reu>C`EYG?u6HI6QS@vzE2z{}K;I1TvTaWE`B4&Q~D^TyIlASQKh95HH#K zNlLXmiOGW8avhhht-SoIAH7)masp=_bpprk%n0+aVkX2=%OYAxvWP)dj^6F91)rC~ znut8A)xLWk5=Bahp} zHFK!`XRa6q^cC9{kxOTzE!=$d$ae&iC7##Q5x9)NC`9ka8zQ{SDuMH?xy!M#c6|c? zwy7Eue2sr>3oLNh>{V6nC(dR@q5LI4f9RoG|K9ZNi%BGSKL}#!kIhoq+bCO$g16q89B8^b1Yt&2aMMw4wm@F5?s+ z)e*bscJlt?$7aV9?Z^|O$KaF&){4tMZ{c?_6CUT~7+fUmuXxF8mzB=vhVadoCtm%9 zUxN|ngpHQ)@Mo5N&(@^vhAG@{i*`nOwv!jC%HzBZ?1i-d|zkVUcNTBR+0|>=n7#H2tg(qwug*0{o#HdYyC+@ zSJkvAwmmwhSoRI#u@D6%aaYiUkwjOu;cg!vBK1IM}KZ`>Pi0B*L_QmD+|9c}(Lm#dr!Vu84px?}%9sej#Chh9K+ z8Jhm*!9hxnWE2igIJ=q7vc}yr;rimX-h)SH0l_-0S_$1XWu4u!>&D&rW1F*}#1ue5 ziBlJt6M9SLb8&Hm5u7@g-q99h*B6HNyw_t=4`Ed{tLuY}qlQ?fd-bw#`?R!ieswhO z1PDzDOXF#+VFbhDa9Qmf0T;#wuW4e9nU$jOotb=MZ{P5p;6U(3SG|RShYn^}A_y`htxAKDNotMH<)kz;bz40yajxt~QaoT2 zyze5)_|8ZK?>+1dF5fq334Zw=`d+xK^@3o=i$uWtE;kI5sk7p)mvD3{1JFz=W7JgD zRdqXgXA0Z$c=9-z-`8|glbM!LO*S4on->)S$#90APMSAtR#o|yC~5U*gmk7j5@Bk3 zz<)reG0-Qg@qhzu&p4Vj&b?dTWIMvA+JhDe*It7yt3aa*R#2sgzi!E!HL}2RbgERR zt#vwCT?ET(!XNf5lgGz%o^_04)J8~AQIM#8u3WL0jXdghMUU5NC-UJT;t`cts&?If zM3il0^Qn}rFv@rU5{S0A=t!O5pgvpZC<$xZfciPRhq-e3H{QtFCx}XOXL8zRCraDj zyyHbPEA<+@r5UvEI0;p3 z_cC?We|W*=P9x&{F4Yz*N8;Vw{utj*XZyL<>I28qVx67W^|V^WT}P|W)v_cICw4@PZ@glU0N{hbda+M6EdBR}C4W}tf%IId6D{h=8r+oXWEJ7j2uBF~7^Zg@!@MrUM zBAi`RgQ|^B<>V}?P>dGz2?sp*F%i3=dMp`Bq|gBn>h$fb_M|tFF@gz``CSC!No}dC zj3l&)kwpBi4{R1R?*zQzZL8o^=}=K(?-Svt*7HnR(IDxb4dViy#h2l5Q9m8!L)*cc zT0BOGVqqi0gHRQNgJqECpnAi3EiSr;5($SDfSzXn2H0ib7GM4DM?&0|o2)6}&2Npj zSnF_p8F|nlSxi%#aS-MTF7}R@tSQpz{CXFc$!~>ngmlL1v@`qy9Zu?9%~q`}IyFp> z7QwHcu|m}fc9eXQz-sy>dvm(@dOjL06O5A1;$f-8;t<@Wf7P&?*(E^G;snfCMz@*> zg;q5obpBAxU1cUI<}$J${`DhTXmz=T`M9fSplmk7&v~-@B9WVC0*oL2Dj&R3@IIv3 z;*g;J4Qn7Vn4pd)D&%MWseB4S{ZbIhqZ~Yvbuk&uyQ@JIeit^~l};xGXso@ke(P@9 zJ|)KE^EIxzPiS*1*GLXT%z~C_vf#Nsn#OG6AoV?D>(d=N)5=U)o-#{{VL^Jw1aYrF zYaz&agb7wjpAZKin9yrNj=S|Ub;i_qR;*G5Ug!D#Ss*eqZ}*FKNkJj6$mqVm#Pz({ zGl=*N*Tei;U)@ZHC*s?SVSZ9$A=(p{}TuJ_QTRmfQUx1PDancdl5oHL`L zp>(S50(1)3Qy0YSt(`&VUZuvfNEi1I4!7u>Z!oNB=_4FxplRkQ zI_XY}CMn0@K_ee{wQ?!1(Ii}Us22)J}5H%m5!#5cI$y)#Qz^aUxp7!}FUtqd- z_<8HD==DIYEu3l~rz z2U~3zAh$SJLy3yfTP=2#fGBCEbj!9l@@hwRwadP45b&)0?7sN7rJ>*Pj|k8OuHW=u zT_DMJQ^7rW2$AAL(m60GD|NMKXC%QA$AT@A5KKTPLY}6UuFuxBj;Nh|fU%;J;6<-( zsQA}UmTN%$#OBk9%L-4gWK_aNRDPjx9)+uKUk1vasHELDWJ)otHZDE<#*r5>o&-e%U9{|2SSWhnxr&MPlNIT*gpzzQY~5PyO>@w zZfy-7OD>Z}@Bp0{){q&-Uy)Z}rNMc?@aO^rAYv)&ooO1ZP0k7O2e+$MslMQeV1l z@RE6hEP`RpaXypcmeRQYM7yi;`fyOUV*APTwsqWMU=5jmq3RN~C)%>O--3h9!=S**q@M0EeZc%;jx}uI3?R1;DMxUuS8f?HP)9mi! z`o(W59JfDh`A*7l8qm@T(tnIX7!L@Kct-OCxDbYDcxkkV*xgRujPrGNddBWgVso#m zQSpfsOq`s9?cRKDcB;Jso{5@|tEHxwsBhb2FJxV={lL&g3P$Wa7MaQ_(yYI-vkgx( z_yaz@*}WM>9^~BallLCV)X??Zf}fGOJ2DgG&62X7AtWKOUI=a9XU&7-j6Uo66pXZ= zm?>znVJt{#K|EO^j~lUpJX!1dp;SZn4O>MZ3`3l%km>(8g90$wS5!Y0)-}HT^~&Ht z^l=9QXM(_MS1TI-Ysx)iczkAbNOTUtQkN_-HC-un0|GN=`{_PF;4~r+)#6=H3?@ke zvA2iWR6!mT$ky-F!^zTpS{gtPhBl=u(ZQ`6}~d-C(w=nEo1;c$AecU>-Mt?arBRnVWRBMS`u`k=Mi+1bh8ICO)Y zFRw;zT*j0a}}4HWC3uSqUR-T4~)~(>>eIIa_tJ{#7VAS30VnN zGo>+UH2Zl^Tbm3v^WZB&hc0m$_^z)m^!0^7FM6PR$Uwf^r?+$R_loM@h*c5m#iRiQ z1cne+aIhH1KCHymx{fHKQ>r)G_d-bziT0pN&*i|md>tXf!Oy4#C>RlB9#0E z<5E1S*Q2r+b`_%1e5BK7J+jSWHtpkS9^rr>lobcJz!g!CD7;AX%1|w-c@Kvaf7n z@(Kwp;B>p}^C&eyNJ2yVv78vXvt<9bIs{o}^ET!S4RfK8KhzIH2Y36Q=;Vn9g9BA+ z_E2DWA5zmu^WV<@a-adNAZMZfHHrXzh`3p^5N)AVphOH-LL#Hi z%KaSNB8iS$=_j6mC+NgQ(DO@Qw4I+H30^duj#`V?b!QoZ+v&^*gV$|rgTVP%zsCIq z?cOCz5iH&3r#?Y`3w+hgQqbeE8!Y;a{ArxmdS}p=beSo%*8GboKq!oe%MVf{iD=GF zca?95tk&yFYX6e4*}TgI&yTGrXs}QzTJT&oqHTfOy$xIBu8xUd|vt4fLVCVon z+c$NC$^mqf`1#lfuTd1M_jTA35(iI#(QBt$T7z_%Wa?_N)2ZQyi2_+t!V4Qk%#!5S zr8utuQ1N_>OtY?;Gs|#lNFE`GKWTce&3j2olQU} zI*AINO&YH+@hvRm(Y3xwguPs?5QXPk5xj2eUBsfk6I*B_`4*HpC|3bq zU})n#kmO!zp`>!!RVW9+j&?#M-fi}CPK!Gb3CkH5Ly_xxbf`SvAlkF|DF z8ICV;6nH$@!bd!d1U%pC?oQ-LFltx7zvHdXg`vV{aefaKKtlDDc(AU=f6+zqvX(PK z@hpe~Y$>C$%{sd0Am3Qa@ z@=Gul(1&I);V2TyuEUo0uSNm{f*%CXl>I-s5K*PTV;ME{gip#=vn7xEo4r3oL-esqnq^gmY`n9IOy?`j2txWWE3Ia2pA5fl z18l`a0*3fRBGG)`%|HeInqv3Q=gBE43kyuC(Q>H*-FCUn{QAfx1;yT5vu!8SRchy1 zU_4JgRnU>QkGY10Y;3Qc*aiD(Yrtx#kgh2mMSjiYp%aOEcww~pJ^8tgWmCKq}F zHUEsoIIls5;M0etY>d|H8-gbsY@C~;=}U8qP`8z6x6@9$O?$zuF?F!dbRl|INMa%hB7+MA*kp_^fU!3wwj&A3{ZpdkjnaKa zc;P3y^)XnoqTP-c)|ML64#5A zwXwXFJD25%2emxBjEy}V1JcNf%dpH@VO*Xrr{~X2FK<}T*O?i{(zvTGOf-C9-@sAd zTwRzHVBz34Y~Ju1QbvXra(P^Ew)tQ=rB67Q7id&1sc+6zY?i<2N}$(@ddIJ+t$l>L z1M37aSz**?m~}pHmU- zbaHZfZu*{>DEcOGvDQYb*`2vn88n4RtE!GA+e&wai^^}DQO19#pD7SK0OzS6dQxEO zA0r-tlRW=Z`ZvH$Hirc-*|wC@WmKNb)t+$#4I!+~c=RZdV)~Aj6SlW914Vkzo#bu2 ziIAV@I5a{RL28UlT97cVrxQd8$kBWkXMtgu@~X<6p=2>_pebG*Vh(FAC^w%cW&v9q zUW3lS2B)2e*gLz?^{xx)3U-8;-R>|$R>;K>1Bzf00r$P1iMk85K3=;F>8Qbru39MX zhm$Id^e3*5oW@`TJl%}2`CoC?iD}jI0n=XR#B({(*3q!o4@K|V)ngaLYjr!-Ri`0^ zsnur9(_5D$;wmR8>RG#JZ+%96zQ6@>@yXcf@^omGA3WV(l32%>+?bA};eX#JZR6)} zM$e1*8f=8|dtA4xM_funPFSl>6fQo!?q9yP6f=-uNmamgZv3~mF-8aueLNYhzRr1n zkzi=ybS+WBv#5GfI7C9fZ0#7G3?o+RrDhD$C@6*E_jWoD+>BtTCZ2OI(fEgcMI%g% zQ28R!kz*Uk)ByL>pV8d!Bf$g4YGm%b50jBXIk?ml+X@fJ7&`ilZ3$TKdf#p@qI!H- z^c47&zNYBZpV^WsV(B6|<2Tn4$bZX3Ue|p-k&V0J7cNQ)C7@s8kR2nB4;Dzzu zT)*D!)3$lo-S#5l9v*Y3n;Wd(HLNz@gIAWdm^wqMuW=7=gI?w4M-fL7R#rJXD0fHG zBld?#d?+WK9K1I$GvDLW#rZi5r?bLg`nsLWDXM5_XnE}vq zz^9{9QPLs8LeV8B8I}TBjV$)B7USrnIsh=`SQ2wr?3}wM6ZbO!)tqDEP1;FGpT5|A zx8-qZrOwskar?OU&Fy(yn_CSdiP0$4!$CEL4xrw?nw0c8z03nN;!-{S{YIJul1636|x1WZm# zw6qEnWb1o~>XvtW)f1MQw;Fac0y+oyp$m`lMu>=owIrtZCq1K zJf?`-ly{7@t!}F^3_hS{a!>H|Me4`%(UM5MDG?Bz$#xJ{_E35Xz8l)@eYwOTC1gA8_T4Toa z5ntP!%pLcr2sXLZN2{D%oXLYeA%VvWC;9e-ei3AHWN_-*tI8<)G2xfOuZO4PxLpO@ z>%GKTQ4VMGO^1go3vc^u5Sl)@-%pK3m&2KG)K1^0tySig*PzE%zaGLfxq`B?(^-X? zg9xrdD@z6)fCSyU+66zbrIYRcx#^rvC$5>Ln&0fm#vxmR2Qv5gM@ZH0cIkKq4)}mC zCx+jsVFX{K(53*XJzdh9i+P>_;Z$Vd=!c#TLAP)QOgSM8n9dd%Myb(J67=ccL%kR9 z6YZa0a12Wm3VRemD9o{sEF&xLMOZRqP)!4^ufoOP&;Rz)X^QyKGE@iRnEaDjkUAkq zl|eP4J|(AYpiZl9b_Ns3j)%$YracX1>3_*POU&cL`3)li6F6dX+wHMj7#0*4>x?%y2jR>$3qp|$% z!6PM}S4R`|${_e1(MP51=k_~(yq8EGC<0%U?*IcJR1V_iEdsMfd6QTTP5FB~Ov>(N zx?p@IT`QYhLkdep>&T6o_nBB|I5v?u5v#_W(Y3PvC0OqbfO!}2?`;$Zvok&ZxjX(e zZiWJ-JQ2FRzwP|i)T9&?9Fj^8&n{_Vy`StubBK_>xlfe~_dy%WlIriEeMkb&(kP@o z0d%pk_V9a^PNT6>$%EOn3{`Zg2gJB5!s@~W2g*;2{*qJ2IU}j(gy{W4xfn5FHW{|p zC0Iv6UwmxzEYN==1Q9^@p&;KdupA??WM{oYj1H*Q$i4O_L#x<7`y86fx%p+r;_NB; zP0;l}(xYG$3jOf;%-FKV*~hWmJ=5vwo$N>)dK#@1X%)|q_;k*HnLPi0cVvtPhGB(1 z|FiJyTS`%gg?)Vq@u)A0U{#E-=+~k=}?v% zDZS*Cy+quO#-}R{03c-|_f$2#{}8#P`sP2zG`S>H>L zH?<@DVh-IuH{38Vp8nOlp^WMLqt)Aq8Y2Gp9l27HR#7f#hmPyiz;!l~IllykW)%sG zsc4t=EI~daV~hgw1EfU=>aWJC6z16%l#?)cZ~90{J!kh-m#?_9%$5;h9-l$${wYK$zCTPx=<(q(k(x#V`b_l0(jOwk_yqCXb0_{s($lQHMW}my{ON zEL|gq%}_=wET_uh2kq(K@@id-Z<1z=(bMTf?#$%yUBr{~?WQ#x|3%2e{`3ZChq3%s zzLipd`mIAs1gBdEcA;{2srVe>{ESq|74Q-edBs*<*AdX;#DNC?3Ox`^Q1wUJ;#gK_ z78z~%qa~+EUpUmvHEix9Nd-np$^v0tbPVpVVeZJ_so6*2Lvbh_Lh5P-m;)qqn>u%T z9GJ6zacL<^u+;KL9u4L4cjcOBeNf%Qzr}?tWa{g-at43@PumtO&!)ZhKbd4w80Uw8 z8>Rwb|Ix*%QJ7=N=47_@Xc7k%kv^6heE0c{`k_k7A z>HF}gTF(B4l?zE*ZJ}8bG``Tp#$>Ij@zzcy!>y z2FCwu=u}yQs_|7go4`oAWmjA7Q78V`g%}8F)*s)Ym7uJXR7qw0YRu=jzd_A$e{ODo z(eX=1R39vv%m0o29!yWvBTx>7z_H;j9E&^v(?7RGPWnwVj!=kFm}&J9q^ELw$LaL4 zNwREowX=Udc5qO5aPW|EGGRBe@KsZf%w*O!NX(jAg+<()KX}}9YJSO{6ao@n2m%TX z4NTMi&qqH97=Ix)Trn@<#tMZtD9Z@6dpNaLoK$A|4_@#`K|+sjj(T*D_`X4)4u?Uw zxi)^EbL4ZR=yzP|R&IV8Cch+Bkmh0udtD-QbkIX64V#W_-XN;Ie5ik|F~2dR_A}4G=SuwZyY?th zv9oua^*mC`@j^1iO3}v$>gPN^o^Ce&>=dv)z%!7xD~O-r{r#U5s>aVBMAIqMIg%I1 zTTu7!*wM^UA#=claSB`G5;V ztI0_G>@TKeBlK%ws6fmobj4J3iXhzuhdKPLdY>XYbIs;2_^H zqy@0d{^ube;psAk*vQ0kS_v?ws_oQT=eFKIE$ooq!WCSv2gA&cl0rZcQUUs(gzP9) z)YR>hEX?C0rfQ$8AGhix-0G6)Axi}yytM8{I+u;qjt4YSOYA5mt=Ke6P~rjzJ7s6( z`T9j)j-)keu2bLhm37E1NkqC+g1hQ2DB|KEpbn|@E93^^(LXdfUi#?e(5I8e@u-N) zG|pP_P=)+``<75o%H3091E~Mq_i}kCONKKVO=QE&(nSp8Xi=;@%55Ne3S|na64vj7 zxqEYTw?ksnxRN88*^S^uv1VpW*)(G=WB2KtwEddvc<0A&Tyte2#ds>H-_uHlc3#@D z;%3!nS9Rt$iR5d9PQN0Ybl}szwvS7C)yg=M!E5^o&CYl?>g-MbKfc~FuBz?{8;0XiY3Y{kRyqWvJ0%Y#(k0z>q`N~J z5fEvRmTqb3?oR3E*(luq_kBJ*AK}M2ti5LDnrp6^wKnLL+K@8t>TzRI-nUfpPAl*z zwek>?(;9EbYP>iJWIkSvN$Q6Jh>r#Mlr*jCM$tM$M?d<^yV^FjW~q#&DO$Bof_Z~l zT6;CvoG7knaV3S8YxBn9S!c_X-w3_SFRMQ#i=a`Huy~ToM!wV#)N=tVnTezG1=@-@ zl&XTLcZ*~e3CeyFNCiEFbpj>erdsg=Y7ZOA;3wF>a=b56EishDXC`vc+u`$NEjYZQ zC@U$g{KDnC*v3%B3#J}eaJ=loK}h*dnNsaj)GjqD=ATbzfO~ma;*iWdU{& zC}tfru!zzEdqf;l+a$@C^k!kRr+QVDxkSJ68(QVZhRW_w&>dR8@$X~N$R|A^EUq|@ zB>*{5MDDNhQc>Z7RmBC(3S5KJj@^sBu5Ciq;2P6!GSOp3@#_i^K9K6BwqxQx1{)qc zi}ic4YTyNQf45#84Vriz*_cwO6KSHQpc{B=SSKVGR;@4hLs+ChlYDDF?pztAynQ*A zkP8R+iY6(za}{19`pRfqb^c#>FQC9-+O*!6@oraTMD#|Ppq*8y>S*mRv=NXCs2393eI6XrY=en zYdKI}8-5*2^z2$h25x6$d$-Y3zv@c;sxSsVV`vTbKP-a*_1xQo;cQOf^*?7fE?~%g%32il$G1y zL*86*zU6!_;c$gjw+{2WFY~$Nms69*DB^^iRjdQxXQmNnBfHVHo?y8j5jE=It_Sxvn17OhMXV7mXZ4rzTxk80&;`ehcL|4NvxK@=@<(d`G#6%7(Ny6ODoQw%r@ zK-S-U8z}4|{ih@i$YIog!kmAp<@vac;^ zUrjbGd3LwMM!CtZS>ZKp1RELN$d~4D91&fNipa??GO!NM^i#{?)ERvF8XlT&nb7k_ zf$0EqzT#Ti0Rx z9s!R3KTu~bLjW2 z+wn?d5ia}m@8dEedK_WVr)ITgdnQu3`lIv45u=Q_x19w1G`8|=$3ER?k4-Eq$6c0Jk}OUbHGP!?tOGGenu&S zdq4JRm9%Bd%!Krpk7=2Sx$3QuG6+J$)`AWio|@DSGuyLwR8$Nxc`Kad5Oy?#7qD;z z@lEF*6`JHU=X3B93EKOSyr;(XD^#3vU0cCg4bR-XK9vwZBmu#NnCIezkI_`321>d{ zeaW&*ozXLI^1~E;x0K>%9qykAAW8}AC!6S#kZd68C>SHD%Km<7^rk8GjtkK5&r4z0t(n@r#I=rCeBS)=X3i7T2-hg|Al7X~^w5_(I>0Sx4Cc}Hjl-a`<@Kwtgp+=6l7G7hJXI@p6 zXx+YOTDhlS=X={}#e3^2CoqE40-7YjNh4xZq-AFBXiVehJIA%O)xxq;sJk%uwM2-nRbM#&$=eNX1ygkGjjN8}0iDNfrq(PjD*TK9(Xq_y7`gs0~gw zva2zLe9?l>yk#*B8|}|cOrD9@n`fM+I8r2#5hL1}P>lhlxfs^ep#{8h{>dFl)`_sp z7$sh5BjXzOXbt!R0os*Wg)<{~Wo+;l!9&=+pbt&PaVc(c zO2I~YkenY=Sh)2YoZwStiOziqfm>&=D6=3{|B`r8p_RsF7bT_H`yiOXOg2r87_=CY zVNVy{ElvWe@=@M!?Z3+?F#utY<@_kLT1pBQXa3fq)Q4n~@`s}nE!g0Z{=Alx&LB1;q{VPX3Otq_WPR+&**@mWgX!x(Gad7FhX zn{!8jJe%v%u+xW)wi%;eOjeyWl_SfGmLtrm=&4JDR`xwTDy36=;#aE{cJv?0kFt^z zi+8Q_dqlkT&7F@$e-DNWjmoz^fjXcMW;OH!$A(J=uQ)bZZXWcGJc-})zqoj| zUDg}f()R`(7yOT-aQOi)A(z=%^AJ+IsDtj$!_>RP221G)u*M;{GfG?W0ULzYB2-a5 z&!Cp5UB%vz#T*Wkj)GjKEw&54=5p`Q-+hTEY?bnHJ$pn+c}=pZ@^gWCcJ+*n@8*ii zgv(m>C2E&>*FO5F;(VL)Cg>-yRj-k+q)J^w@P?l#)AS=PWMFZ9Vc01FE_thm0T!z` zke%>ByN3k$nT!GzUYnCP{Wol^$4+Y9hsegoSm4!%3?-WSr z>2WNw#lmVrwr&2~v^lZnbSuo_=kbZc^=}-0ubc%Tky^h6@eSkn4rG(UK*gGrl7Cyg zl?w`2S2HAQX*3yCqSs<(ewx4=+=BSe6y8mH_x$lJ0tV(GT~K8Np_(04P2a$aGa{%Z zU%ClVDz1!zUABVufMM%l+pg0{$K$~pG)dy57g3PCbDq7uxwFr;tu7HVhm-T3i_%`# zSLtq>?bPX>Th&Sf>6bTWo+ncR_17``H_OboyC>qhyHPp(JRt$Oi8r7Zjn^0Yfct4l z7!T!dA||_P^?*Y~IaW1mw~MaS4N2X=KfVfvk!b}5o%xQb%Ap40_eYUqX|daUvqM49 zF=}=e=}^CfiFDIR9VRJZ0Oi3=-}Wt1Z7-)(v@dBof4Gqiv7*zNgqcGGy7oPM(_Ah49= zl<_B!kO=L~)l(vyn{rWs>r8IQt1;(Hk*cI> zjQAq_4+yC!GF~BUJ?T>QDa{X|>EA^vHMd0JZfZsg?p2OWL~`XJL0_huPj9&U@W%IN(_QuuB=ryPagVHiM<+^OCQW^I;4uw6F^*pQyso$!E}l z*pMRC--N~~R_kbejz5WB6B}(C=4LI-b*vmFvkB>~z+MBfimNHG>K9+tP&f_qt-Cf6 zBk%T|5npdZiW_d{l8N1q7qgVpIoopu{^a4L-S*<7--QkEaeE}}QsijuHOMNtznmHfE=5ITD zNJ59A7#jVRUf1)riZ4(*AA!r_EU{UOjbApRzUqlv>pm{ZuD4omTm5_k!bmua-8nVb zrcJTmL(tH$R(_xChT&&U&(!8}nLqrI$8h(3J=yD`*a>1ei=eLs1(7;#rN+Bp8Ue5s zrFG`zdCv=-t2ufB_tjlaY%ViWbxCPEKoz|wEm%vqZq&Ct$avvkL2Ip| zBvcSLvgbFabyksZ^boPAiE#j)8nsKWJ>cmmYM>>+}RA;o@^7?+M zLish^nD%|(@Czd?$*YHjFSIGQ`D*z68xH=o=8S5syHdD+lN9w z;-7z(KQq6UJyqk8|+3Cu#d?DPbO?GPP z(hd$F15SozO2c~A*h&OnS#(6}1gM88N5pmT%YXQ(rD5D)i$^dvtp(d9C0UbHpVwFP zfiIQ&y#eitdhv##3AmKW>^9VDvcI+7Iy2(Nb&2vX~WWY=L@s% zUwOJ5a4_4SJ-7`FSOyR|_~0HNA<%oB_lG3f&9~A&20mrQ(2 zZwi&a3e{lVZP@7=`8*_ixaeKgMV(6(k0AY--Mm!N>GE}V>rerEC0;TW@jF?IN>jFF z_q!Oe8sWa|?DqiAP7zu-Ih*5KJ!Va>EBc*=hmzpaR1YsioWhxugOC)I`KS;j$_kS`p(&LxJtS5 z$C*gq_d6Ij6zp-+sc)j@&{$X3ChId*&^_)|DqftOaO znHr0}T|-Tb^d8~ZBB5WYm;W25$&ni^S4gmmo1UV z7tg3&&C1mM&HG#52~I@*ZCb3ccI=Hi&VW?s1bbqO;>qXgpD;DWA_aW?N`}9V*63ey z3+qXOwZe3jpfrFJm%If5)Qu-dtKLP^Lv+n(^sD=1twQGSolsSV|50%`fFCTpdJqu> z1;HaPqm|jqOW$e}Bji=5kl6(ON~Mrez@S0-zcpJ{izG{<-i_%A~BOP_@C^ZmuV{}TE8{YVTIgwMyvoGBQ5 zs`*8W9izulrMKd>lF9jkoGSY3N1N+C0jEuPBPZT-_ItE%+61#rH>aCG0CnXJB!EUn z@|S~cl_+pyV);FtRL6(T{!DWGuz@B&F_!rCjji1f4883+<&cjBZ~u1>@OKAFc94pA z85tQ&q1hKf@=a_N3IFL))BnO*MqKch<_WrSk`$!KDGOCiy9}f}nz)G|B=;CC1!+L7 zUjVc+AN5RhP*Un=Wr~pu-57FL0-trOEgtSvp`z$3-5&}xE*&yT*sp=KPQpS8b|a+zRaWb1Z0O6*{|uDJ;+bvq^^-k?+Cf^6 zxE^r$&~>ks0e@X01Q=I+Q8ugV-%~*G;J)n&;3Jj2(A#km*1R!lJFf*?f=L$n1KT;k zz_QT>r!7qlld}2hULB(ySCUKBX1S=nky3bhILeIr1cPA^S(JZ~a=d=Jq20T}3cXh$1OiGtaA( zH0?y>Mj3ePNcHvAD|I|5QG(j)QI`9o;U-4Nf5R>SW*Uf%#oZ&6$-H_;;x2^g>si6YC#)qf`?Q2ZT;=0B~eBCsg_!aToP0FsY)z+Ul zFn%1?v|bL6{`?pGjDkA^q7rH449*c4Iv)02HhY;PzI5+R0BM{otSfk}hz~at<&DL; zO%`A!RccP1{VQ{_=;sLkz|UnviATV1>VPjbnI zgEnV=mY0n_fZjyCW2L*^4^JS&yYGldqjmWYxyNXKM>4M2vqE}llGEM#hr_+KT+`2Wqj!Cw+oA4Aw6Xy=>+l$qV>u$q0LHF%`W$!#_fIq!j#w#UMmcE?Zl#Nr@=$) zBZJwL6+K`aJ3TqmMk9&{JOSlZ4 z0oKF&LWB;=Je>H8yD{E-LKRycZ z8#D!R3HO~NEm=-}5n{m((@GI51o?C)DFp!y4fu(_Nc9xDXjvJ&q}25W{abh7Ywdib zjSr1PbD@aHr(__d4jiFhc}b?mpr}>9jE3pq-TuM3JJqG`j+7y>juRw~%~-pQG;!$aL1wh#uQDI+NGk@7`)AlFHd`5pBzYz>6g2q0GNifi(dmZ*d-H!6XwWHxp=6p0UZEst< z_D=(DTc;+mNrgKK5oL6>4hyp_P}QlS4~2_DR!ZY_M0)vNOCtQODV&5(evR&T$S;Bx z!dX~Nw@e{SBG}p-$3L{}YF($5Nxx0qAWtc>YAP`zMHS?s*lH z_`?5HrL0QXY{v=Q1j*MQ92+;BUw`DgTd}L6A_IZWs9EAy4)U?Tp!sPhL;Tv+te1YO zra9eh+fqGNr34fQZO<%QC?3rePCaNO`fd2bwW1vVDOenMDbv6PGs-_U47hNjx8Su< z8@MKgJ_KN|TJNg+yTM5t{^-&!=D^9*Nad)yX(sAy)fv^Q?f5g)_pccytw1P>?uc@Y z;gk18gpAO_>hofc{W#)=!&8DD!U|LlvF0z%id{5Y6jU+rU@H=9Z3}51%9dbz@)m_u z(^33!6RWO>|Gel_67Zs{pZn?$O{i)q!6S^5A&bDpVI@%skua^7w393@LY{vCMj7pV zMw(eJZWQnEV#Z5T*>nKLS6W7xB$rN8kFoES)6iAd>@co)t%YQNKGCluHcAa8CKYlt z-|vA24iqy5Nw|=b(P$St>a z71?$=#;ACi?Foo_!j-j}5pDZ@(!VD`dU8MnC&nP#R0IO}DG`O)*>?eehaDUg-zwI)!LRg-~`+ zTDA$4f5am7v}Rje`z73qI?Ij)3aM{)u_^bIwn+rMghNL4Yg1;jps2}s zsl`x^jO4vTyTRAC?7pJ!u^*7*bs=eFDsmqDc`zwRkCe?3&fS(bM-TqdOQ=x?3);^| ziuHT;=W~$8?Z}UT8{xS`%anigGW=d!=;S0Gekel7 zL9a$h#TdV7G`d%)MZ+mZNU+h`SEc7U`bS{(1NoINR?rhllw^<_6*;*X*15xr+I0~q zHQ}xujP=E5Cwk!Lk~(B~hi@t^nUyua<9DxeVSA7scU|at$Nk~cZSCxMA&u7E34vY_F*eI;1OXtC`Ry)2`Xo*$L zzP*+PaP!=-^o7|~?y_HeLO#`@PS5L|3}Tm*-b~oy&F#oGf?CEo+*tK?Dosuw$xVvX zf`Wx`LSdg{vOLs`Kd{MuIq5Xs%?yp}KaHXg6=*#a&PeV&q?r`4jDf%ndftwoffmrf z&^Hi8oURKr@+xf81N`3HIPY>CX_7P_j#oydhFY@C-Y(OHpZ>0m6`B%C16n=$+8(I)pwulyRjdS2vfSB(d{ps?sj>A?r}PM-^}alH23j}bJ@hg_D|2sDtFx=4JwT5!A6Ok$`^BM0l0c?ClF&7}f=jd9YprIms-egCjd-_-JsX zt$T$!)_2l?yj3*}>;D3%=-w&EtB98$7M<#ikFu1-adVvaLSz|BU-@HT_)a+_ms>Sg zi@0ZywZo1MI&gc6sCyYBaDKi2H)p3NjgPE10y+D3IbG+r+v#o*$k~@8E+2Us|BRaq z`x99b8ib2=z)w@6#-QDVbwwfO4LD*ELZm*1tJ75Z&CNj;Sk?AclHkh}mDxu(Nxhy# zHz0&H6|{2w@WyV8Q_eX5x9!kuR2tbaLEqoAiiko5J`~*jze2E$zA5oa5~m0uk?gN|x(&#Qrm(u(y4US{&P zY(+C>`V+#~4yJAdXFCn;L@d52De4+aDwXKM?f}j(jt}N+cXiSi7Hq)n15aLc-?VtGlhQUn=)wKgXxIRDA@t@B2PA8_Q5(MIaymP5S^MLRz&;RR={-)h-wbNbniuarIQ z6ajQ11{9Ssg5(phHw=6+=+4_WRd=g}YC2x~eJ}Vgt$&A7ckK+PpN}BQd#yR8-Sw@f zUsk}Ekt6(-$aQk^c%A!loE{kfXa+^3Z#DyI}j|r9BLYiIbS95Zq3%psY&lxu(~1HGa!*e66QO zTl_sk-5l#8H^TmNE8oq~ql7XoOb>-0?*d@6#f*lvP1aSk0FG`{V0#{PL{RryHxztH z`FOLndvQo@e7}4*a=l&TLfQ7X-&6au3+hqyzu~_Q?Z(!V{(p*M>ieR267@m)oxp&V zcRbi>TRZ9v5<?!n*mh zOxg08Y*q7i*YSz7XO8o?%HR85mj*RkEVI`V1+=L-ID4Ik<+Aek+F}a-@T9F4pv|=k zcHHAj4r!zrVGs3+m&uQCp`x6@GN-Oi+9teYvzcqgmu`>2OL8#r0VsxOADu)H{N+Zs z@bWf6oZafJ2h0%Sg8G$sJs4SgpV$NzC{mzV@CM5BL%B9UB$UX4hR%xoz2Hk6IbLhO z6y5i(P$T_J(qgueS+}0*nJkCTZ)<2&9Q5Ndw8r_{r;b-#LD5)}FUw^m7Vfn~ma3X^ zO*PZ?oVUP0FAP|YLSbsEqSB1YgVR7)7EwdjMy11t$bBsmf%q}U*WLGfwC6oTANoD^ zATtq@zGtu8$_EzSf&4nD@iCJixhfbwJko2E_su((*E&{3cA0!3?F0Pfp;r}VfyOn; ztlBG|$s0=u>J$g~uX<(lx*adTb1v(%-H3UnfqT|xoTAImTR%`%G8a)>T9KyJZj>UD zbEp55>FFvlvVE4?@y$*Fw(~37a{0KTD;oxg3x|*(GJIj7!0D>|GG-bwvQKi~UEjHx zzezyM?!3!-YJ5Tx+g|m{sN-<%=p8FytyS~Ib*Uvw?^yW__hKNFNlpRGJ9Ir>`kCt+ zwfvfW&#l7Q5O z?cqj4`0!Z^Fq^zkV@zSavDCMyMs^OGV^>MS6|vxC{o-5!NMNvMWrz(aoQ$mZb7!+J zE6<@S=!0w`P8E=$mTo5>Lr&5DDT~!T8>|sI?d-(}277g!M+T%_aaO&v|+zH zY_{24R3{iPJKPZyujP&Jb9Z!z=tQ6hnfo2Hn591_*r8_pzD$a6|D3=-(* z3pelz>2!XitfbY5hA24|LxFgrA5D)RP%-JD`SeU1h2*JL^#^$~ioP3J34B*hWE2yJ zJ+nJw$d%cKIbw9q1mlLF(3TJ1b4$9}o29sS_iFHJKjknk{;a8g_3<=jy&>yrv;jYc zmGJe<#jI}fh0XHXMlDg=bN4H9g()7_>Dci)+~&xWXs$!lmQ!K*3UuL(qua>$|Fu7> z06=fqwFrDj**igky)g&DhD5+)bi0(fKUr>Vx zBpnjIQGNKHOyz6y&j*0=y`+Ck2+?LVSxrmf=u@R;P_yRTb$HXmdH_tPIsg+sqM)}R zY8PHGe$b$MYk^3B$h)27{CaeHmQ=pq%|ey!SSF-eG#v)uHIKHnK9ARM2Ee#MuV_ZT z^-tu2R_qz1J{*znwp#N6s;k(q)H1Q7_DgJq&`o5(zixdHf+G!@CbWG?F+mTh4Hl&O zA;<#g*FkWx(&(*J)vT;Jyzb$i7BBn__*tu=YZJ&l494{w0j5@pVku*jszfIe9 zHv!CBP*YRg)R8?<&IYl}p6_7O6ls%MAyiW4>FX9e+VR9Edc<8hvV~s(Dpexm((L{M z*hUtl-%ISX!}G&#sI^mK{-fpi_q6=~SJSYGkqYq4lVjV?e7c1Bfh*e2*QlQplv=+pwlbIw<`FZ#;e)>Mwd+| zpB?f4A4y~+HWGuqmDuvdvSjihfXhTlkYya#5RsJnnZ8vWt3Nc8m+%LHg?B{yz(}rm z25oVLQ^AngrTnBybpEGVWQ`xM&y?mmmCHFl6bM1$gmATD6dj9NkKM^_$cL2j_*?%I zb=km5rgk=dU9aBUlnCj&i0c-%6t3b|WSEHmZ|)No!x@C~h+BdraNn>5)};t*KV_lz zsS?)NtVF`u2ha30w)OdG={M61tHP~Ry{8}3dk$nc7|@tbf^b8WSj0T%GF)~am!Fk$ zohCWnpP+9dWJh77!bxhG1i9sl1QDix|3QfaD6^ijvGm6$6o&TknUPh38Bu)E%tz0w z#)MVXs>XCFdYIr)$A%_SqnRt2R|;PbyCM$FWT3_=+0kGo**LOtX;n$lIBq60Y6!ng zKslUQz1sQMc#th{;WN;9yc8oZ;IL;tzv#N(c$R;6o}1uxIO}!Z-dLZe8*uxyrgf6y zJ_@brVawe`Ur;`9wTI|l*pb(NK@2_iXsjE>O0wY>*Uay zD-n?YIn3v$DQ2rGXY!l00a{_h2F}rUE$IPJQodK=ZxwCn#%TY_KRYqraUariKAbRs z3+iNDju0Zu;WBgIZWyPo40Os_g^`BZ_$wI_b!zOvf4&!$fefd5-yEiIaKXGEH3T-2 zg%q{|_n14#>TMUQHkmQK(Vb+i*$wd&?$k~Yds416Hj|RHXU$Lx?nszZ|RWhwF_!Rsl3c+!;3Xb=C1)h4l8*(Rg z*Zcp8_L2+mf`4_N`@{|<$#Be1p|{yU2p1Z8#p1Z!MT)k8S(D)PGC8C^{L5ZtEKu2O zm)&SWU4D%@JfUR-$BPPnjC2pN*A8!ew?vOd=PXSZaf4UFkeox?H;OO0juFr4B_2&G zX|N)vjBX1Rq&_(Ga@(ydzFvr{npa===0{@?OZN+#(*2^@yuR0&1ho}sYLH1!aDf6u z*{HNJxYYdeJ9Y2@z6 z0PT}}(#Wag_KIH}U@`Xd+{PAZPsUNa^?A8K{wMNIz3(7)$V+_e=tp0fDi zAZ07`qX|6V@-Sqe|+T>XSO|sHyGYT|!1>Vw|s4r0_f4;OmgVMtP1jdP!jX3Re04=wd zrOT5fvbbvzml(V!lM$=RD_JB)Hxbw9C-Fr&qWsfR;P)wm^HgSWp~vJ~DGNmRbVH;1 z^9Q`5hpkwu!h7+daSF$l-`Dn6O?JQ{_^KyP`MkKA#hILmT+N*CsV*7xUV#W|ug#W; zNjQbfUz{=~Xqp^wHe8(e#fR#@)vEw&iT|NPat171 zptq69R!s6umPYagVr@hX-wwjKC=~Br1`_&u`u)F>ZfPcrvE&q$scaTT@>%Q)QH9C$}46* z)))fO*)JH

    CH-C*P?V~?7!WRHeZ-EDJ7v~v7PBc$@N`%)&N^S>FjJg}05UAvz( zWoNVrz5U9|Tdr>+4e>YiH;W@Zc0fh(ul^t)J81K_XvTc>(>-~?dcve5;H4Ge2;&y* zr3B(Ae`sy(>HZTMVoYarr$OS6gKj@o3MUcDW`ilNvD_3tGtZwsu{)~A0 zfs2ND8s?3`6P0L&G!X^l4QAscNC3Jd{&S_Fy*NQp=X&=Q zm~3~d6)BYhAd39U$V~S!o6ztC7`6Fp%@QO~QdHs*rb^oJNxc6CfwhEFas1~>|9lay z2Ovrj67<+{@N``s|FWzufIs_blpuqvuzM#nARL}LfH=+egfiWnFer1zU$iM$Ug$OP ztC%;o?~=P|{7U{#2s9ysM~2;nS>B>n$9Zp)e*bcgLuMPR$0tJCT9koQv@TVxl3B%a zP%;}F2lwD7O!rwSq3UJU!>*0Pee?J}Ald65FsHw2eU`rR9ceP2{f9}-N55wQ<}$#o zMt@B|li+Q{SlidwZAZ91#y58r@Rf0hU5m6ohDQiX9>#D!uO4B=(91;cOXVLo7_P~qlv)4G(uA1jn-AB0p8|x9wfa8OO zf80<&&m4J;wTx9%^J)2DgLord0^75WpYkQ;U-&42fLwjb-s;hTYNWiMj=gp#snCY9 z(iNL}^*YK#rF{pSj2d|arwDIaxZ_)_U>ufsPSq9G!*tyX(#V^h7m|XQz#MPMp{gI{ z&F3!{(>j^jrJYbU)|LN`zulk#J(8PQJB9~~%>YNTB%No)FBqn<3J&$L>fu&V+%k6hSKAZ(Xb!hR#;N$md zmC% zUhszx(;w^zM(l?DdzjDzer21@?fG8v^Yy||LDP-GY)6d7&KEk8(P)`{zn3~!T?2gx z;SWj1)%vGH`Ciysx-9=Tqpsv!iq(R2;1nRW_%+GFuT(85GH0f?aR1)< z=&?R6o@R5gGJn1{9>FN$7DePl6ft4*Q%=sKaoYaSkoAe~&*C7=_h=ce!Dl|n-wUcq zcLb}b=};ygS!szTAegD|7G^E{19GS&{5S3HWvrF(>&c}IESw+QMOQE1E&34uQwnDy zyr-S^o#>2#=lH%QjQED4`#E_;IWdWxMevMI;hfqhVpr&>*pxxxmq#=(IIa_v3{`6AUk^E)5{aV< zQ8LFQj zwlAPb zUX3-ayV~fLd??Von>^tFq!Kv8mwrI17vu^8*xHh8cbRK~)%8I4=G$my&2ozCITz}E z)dKX;0|Yq^=GJBUHQ=Gu?M_od2@v|m-&MLIT|@9&2wle6T9hi0KC_C}xgiCwqZGg| zpcYn7MdGsiW-JovKO>#Mhed(_T>(n5FvZV8!h!*tR{Xiis8XrH(1Nz-mX`VsMPy8( z986F11Xwrp1fwhA&*g5@CjvOa?6^t1s3IQFkM1xp(x2JWsvn=9 zIlEQvzzI(5!yjNcNE1d!j zeg#8bl=)?7Vi7JOScRDHA~UW98$S8~Nr^7ttP}h7owg6_O!Q7VzA6YfHb#t@eV^vr z!DeuDSB;^!L%hxtcIS1zi_>sSeK)-BwSRx?!Yqj}w`cYK*ah+3K##!1p1}3eyiPik zn(f0dCM@33-S{-X&g z34*r+hd;+x%#5icfAP|kAVi|i%;0w~L>s=$p<^>r@ZvK^&x;w(=z8*`=TUDc`wClB#JyFu5-qQY?=}QHcD=63C(8ubm+#w>ch{@u zS67YK+vzvDckX76DKDTTgGtxkr5`|i;N+hdlu6=KW0)Lgw#)M+4NWHPen@9c{;}KS z#XsU#EctU2v^Rt~dqt_vr3?n?P-PIH(5K==pUP`B7&?~0h7U5 zaZfAgxJ1K^xM;4mJYY)4TB5}nL-GD<5(#(n1G(Bxi@C+OChqXp=2RK`18A!P zuP@NJV=#d!TV&Y0kA=mkN;{>q8IG<3kcnG#oZ5Lnkf zB2)#QQJvmacUNs=BIP;vyXMA*ldXqBzTeL8YW;Fk-hIn2SKx?u;BM_m{&wodZn4)A z!@9z2no586Dg1%EHtH=NlQ!LUl61e1^)$5Df>BPFpm{+vKB+zf ze*m_}oIaRY_UAZ>`^~=xAG8`Q;T*6qqW@g_$M#2p1*;Mv_6W4d@RoRE*&On~)@NVa zH;y$b6NrDF)~k7*|C;{DU_By)OPC#8=5ZEBEW&MEnl5vXGkn2K7R(Fc+Rk==5GnG= zDw28@oKo8s^I9|xf+JF=v0q!;al-rj7w|auVi^vH+=J&^gnPTbn@R~h6n`RgzSHqe zuHCi$Y0cE{inFG4(&6g#yLz15?hS^Tg8U!GZ(Bq=z=zvxD9taR0T>IPUAWS*+QYGq zCiJU-$=lE|J>25z$Y#c6cdIE=MtfGFwNBh8UCTqS+yXBCAkxSI%F^e=~x5Lfc zwQlElt*jpdd{?~;8pRaD#_2AOl$5FYi39v~=AeO{>Fb)byPCOTI}b$1sE>K`dfUJ? z!Muc8C)&~;?9Hta(-@I%HGkwA38D(`F7d!Y_J~gQL^Kv5$!iu=PldZ&C2NCAm^_cea8kWH9HZC0IhjwrqFcFN@d6L5`NWBykg5#He%>_V@vutX z?@fQY>EZ8mWv`!n9kkLbsng%4<@uW=fG5VdZ%VWYcB4GdbR|$)ZJy)J1e^IQ0UK0g zL?2!L2tDgs`_~i$girT}ul9IfY}QTjM?1}V7+;C;kF+?=)gVI0G|vVL#&9q8QhRl? z44vqXQU>mf(mw~p-0Xb3yG*zppT7g<(3q@Ir>}quoGZLsFR16QI(2_u>k^x_({ADZ zDiFJ2D#u>2Sc}m0RqY?bC{9%}S#Q1lL1PpHnP8B%bg9A8LojtJZ=i%j=WljM;x4(D z6zmLmmD2!8K?b!87{3M7+Mda5riROH$IrUql(NpVW(%X4Kb4j@_{9;Bl?H+He(a3z zM8uXK(mzRw+dcH(ki)?IgMM%LmEJzz#j$4NCj_x6CnT3JZm@eT09C7Z6SXLZO61R&P z@6?HBLqoobUh2ek0Q1i>KC&0Ajb4y=wVgbvW0s@b+K#d#4HRe}pA|4*NaD#tgI%t% zKvF61wWQ3h*oSQsX4{ss-O~>P+Wik^W<2X_F%2AFam&XfhER zbDgT@n@JXZFWFoux-?XxNTlOa4RXFjF+K+B_WcQilUY`&hY71hA8$_v9{xQ$_}nXp z!ce@rEaz0EytsH^=8-GfyFthTmCL{BH2w2H{qfQ(L8cVP%HL9-h2Kkiwi3glm}cVP z;A?qFDNw7|G9wuk_2foz)}n13h_wYEyc6u8Xs!7r*6^3STVp~i&NMqU5$^{+#Xq~} zwFx)_lb;O&JrUqMEB#c5)vUbTjGx+mZhqMY8kY?KX6x&SQva^Ih6y|-PXhTf_VGD(N<>o!#QA>y^Qe^XLaIEj za!jqHR|ji)XR~H$%?uPZvdHr>Ihlc;i(~ZG`nN@0P=+98BzC1X%k08o@q(W7;cr5Y>nNL^lwUxIV^FhL z=ghVZK@2UUE5se^7z-wu)@p(>5Q|3Q`uC>sedJ^si)po<(G1*jFRlyZD0|M^BMB%0xsbIxx~)BKx^ah1?-gfCiT zPEZ&=Yq@dqRDUWL(Ts)vmtAkSw)dHsaL@Xwj&7mmet=_@d-N(mR>tm2@0nKCo~X_HpCjBJQWzg{5#Qyg;^=% z1qo>F5&z4QeAQCnl_5a2qjh)CHPGu+N!=YBAOTx=f#(kUoZp~B%4^$DPft$JikN6N zkcn$n46k>};h6V_M_8KI0)@K*MRtF?%Gj^vdyMszh2L)~DG)zUefbv6=NvL8b|Vm@ zlt;KsV&EoK!wJ$d9a=@xkid=|VJqT$J#Bt!%ZOg_Wm?0u)u6#OZ_ar9DRnu=R;Rh| z$LnaXfKpj(!{BjGVEg9Iv3-LwbUOpg+xFV&#S%fK_fj@w@VM{qg|cM6d5#sgw8y>e zqXVA*tu6#~1W?j!Wj#*nQ=r;Yq~dN+H?#S=z;`~;qn=6$|6=%UD=3C)+_m_3zt-xS zKGq1>0wI4YrR3$u;X*Z;UWtdQ)@BU!hlj!@Cdp5lbYQ?u8DS!=ZJOU%w3)fPxY4PV zmfI%@pyU4FeS>J@%WWcy{_lCHJ0t#o&N}&G$PFV>bCO61ea25o3)o;E*O}hzt8e+Z z1?{Oc1Am{(ffVXmL2$gjX}B5Q{W?fdhs|a?D^iN#;@tl5`;ZFb;ZN#cjQ8ntai%?# zNNp0BT`Rz4PCJBQxRp1^V90gd0!m&nTF`NcPSY~9A!ybCUBc@;xTGyne-x0#Sb<>I zzH(ejt^y}NKzHeB!eNCc4iTIUxj^NS`CqsPrS=wu1oKo-MOlQ!+ML)QJP=GqH;f_N zlFES4dPK3%5&kwN+^7L?L)+N5qTQhQTxV7(Y5P4#hmsIWoY6b;l@q=_Sz1)_Uf48= zP30gIpa{4bEUYISy(uf`eR^G2339K-&&C{&Bh3fz>XHJ5{SHcripbKYA2IUBcT{F* z8mhf?W`myDja5QVxy#y{0bn@m3C-w~hvsmp4z~6dNrTU+`l<0&%6pnKV-$76jY@1! z%aWjKjUDn0AwL&D5_e#S^1fsZ`A~smO)fH`1GRj&=(T9nZhQ7g2XgURiAPRQ(B;Jk zf)ugg`k9rB9KqM0a!#=I4F9kAvT7Y>t{8?9uqX-s*Tm_{?2gG~O><|E*N0ka4@ zTyb!l=9?5K0k=LifDkhT2d^er0yv*4i~sEKsad}dd!g-Q_LXyQWby6rO@w?n1@N`r z_YDqsBOwi&0n}VSG=Ljx1aLUC9Vv_(m`7=I#S34=4N!=I1aF-3u)Y{ah|U-|Sr@A& z{MW|?W^V@FF8PLA=9H5rZ|F*u-RoJEKQNEmi430O1jHqL4l0c0&RJyw?f+B4cxrGP z69RLNe=Y5nM$wrQ1;ld0d!2JqM0F0^^cJ*Hz9J;LsKg(5-;VIRc%B=wEVQ!^{`kBV z|0bM`+0f2@QrKh#4_#@&lx?nMAG+L)t_3;-i6}SiGT=#ZX{1` zkuu2u-qf%26+)fH&?{NVVseKMQ1X%EzCT=as}~Vv4B9@DYe;N@rl_`MVh@<28s720 zCs9|HdEV_YG2nU4p+-%?(G!X8rJVLR(%5wP?}!;F$xb@%Tw+BSk{PsuR^bbxAn0YY z8U7UmC}8k9&QCjJ+?|(|6U{#@f5uH63ww)pq?*;bkD84B+saiMX)`&Mp4QOrfctDg z&X}VRVaFx{qV84rxaiA)41tpAJXqplB>pZ!Xd6uYn9VW%6hNs z^xGv=cQ#e6Z>TDT!Qd3&exmE)E1D5G|3yTg_KgH_D)h6O%LQ3?jO|dY2nCsz(#ofy z8Mp2=JzyWCLiLNT-$7p;W@KGYYt&s`5T#@~)O!RN@$fN6RRk(&=AR=hIZ2qc;!fIf zM}-~uM!ki-8k|Y9J?SKb|0a@s&N3FynO_~vqk*`{H8+5`DQTNI zGhdS-y+lo4dEhk+>vI?dNX%tToyN~D<_A9=1jkL?D=;c(Ou02}lJFSvgTy7osGjJK zAOOk#1=Pux-`7tDHzt?8I%(d{_&F`4b0Y!?j_v#R2&8jYTW7-jfi!(MX=V=5*T_Bs zvzZsK5J%gzslwl*WX%7bt=qD+!)gW@^gy4E1jO51;Z_dUy%vhG=v&E(R@SzxQBb}Y z7eUu%vFMfYh{WZ(^ zl6Wm~54T&_G%ledIYX3_5bM#b;a19Kx5#sAQC`iW!4Hy%+<_MAx`ubEuL&8NRG9qn zHTz|2`eTj2CKs`Ud2oIzqOw1g)E1tu$F=4t9HBXWns@&9**)|lSMOvx$!8nbPXZDE zSFb&G;{T2V2M~E7$Rsuue-SzyaVH8H)y<$lc`C5w?KCkUIVOoQ*ia&wPx+syLO`eR zfHaAFAwmSEe(UVIsyUqaoS&LUYfHKs?qy(;h6#stEqn1VI?FzPL#Y7b?kON`C6*-b z;92r1CYB7GMk4QQ^@ij&q!+QKp!XOn@JF7`7~U$Io_16-0$P23VgH)55`d>gL4(O% zo$Uxk3^kmR1s$|PVp{Jl@5F1bFH=-<`rb*a8G*PUuu- zGHgwIMR~>P#bLr+bV)*KdbK2@Jz_Gl^t$pF3(2rcoY6tKVPBFe!Yr80NUu5mw%aZ# zvM*8AvTtLzh~aHA{Oac7i4HkGsnv?uC7GxxC%V>cihMMGjx z0Tz)c)P*nWv5_jqq7NUNR_VWM(=0nWl?RKM*9flz+lRh_E&3nR^*t2kDiJpT0hPjC z1*c9C&gA?)LC%>{-W#>6h_zUkNG`1}tdA6NE{&eFjcE$`UQC%wa>)S@y8Wr(>+B~n z3S$8PaGnPep_;RqoA7rNHLpIQlS+J>LcDTlta-56*87+LmNO41lQ-yfslk!gCJC5} z{`LX2*_PCtQ1_mU@Ow57oO*IJqrlYW-y)}z16uHQ6PR1s`ipT0UjI><+UC(?eIk0z zJ?#9*@;)N-JvrT*`XQqL2hH3vBdou8>&{%6-SAk@757ea8}v(uJR z7u>$V$x%YQ&ZXD8@E5f%4=z|#3+LU9e}0W8sycubuXuel{cjWejp5zV)~Z?tDx6g4^=_KhTtz*tJ1w}_(4 zHVG+2cs&xD>pq&7N|F`Ep9Q^WS7ug2#41hXv+a3H7@Hsg9L+WaY=JZgK27O8jlLDXT6SIe|DlP~=A}vW4}|{8v-8CzxuY%V6*CL|>bqR` zUDo9FF7d=s{{#L8bYdxjo60~ICk&EDX6L1JbLDw!TdC)qLi}EN6{Lu{RmN~)SKOkH^>E5=wtZjJ6T48)40AnF0XVAWmWdZnL3cF+WW(@qFz zE@3|$JT6Bfb}$*);gx^tZ}f6RPK-^1F(dihuu>D7C8lOVO6}i6edPa%k-|g06}+F0 z-~o_pCa5&RG+6{GOIe5VHr$`-{qrtM1q|Q&U9PS2O`ypQ!zYf-?GVvFcmICzcQ75D zC*^5;c*rB2EfAPd@zzyni=XDCp7peWO!`x>$;W*98M&~7kxgcDjQ9K|{W*dHj1~aA z6XnKI0q+4FdQmuP^@vvL#$qM2m((SiVJ3gv7=p$x)#nVX0;3Asi!!fHQ&}&5zsf;E zIKK~3sRqSfBZ^c_C0?0&P$)qPcZIVSWvYwg>2J(wj&Ho|GtWH>^=tt3>(N=~Oxy3D zCb+8QAJfLeeYibiwt1l?N0R7Sv@~3J zb%<0uPVnW_wEy$$zv~GCjj&${%k8EPFeW`x8_W1!LLx4tQ5>kv&Dpom5X$F zG5C~TbI+;2PsC?abzlWEn&-*=r+-Z9F*yZmhS)kla~w3VHkQcmUc;ssU_M9POKKpA z^9tDdYbHRY*$=N8qqFbiS@2;Dk9VgW_O+!f8l(pm{)Z<3HuMt5KXGZ0h2{d3 zC+n&ciMg=05}8s$zx^CBI~Sjf&blKo5!|wWiQ=COm80M9Y8*J5Z1>^E>RojVXHjUL zP4WbrHe*35LjeP7^OL;-S_C%UGi9&1;L{bde^ND1%j`K|4;RAEXkDzltzA^^*LqVWLM3XVT7WmB>c;>(k!khp)cX6jH9G`g~7*N1w#Ubjf<6W?i1^R5c z$SZpe2q!={(O-kB{GGo)1n%sc>w=XMYR(mV$JHHasb{J4f^Ki)fRYSR^ zq4wPr9rLyG_#19RyV4QFv%+T6`%+GMSMy=hUv5J5Z{^mbUavG2v%aprR>85JxlUx2 zB%v0Ydx6hkCH#S#P<4e`p16%mRV&;UyT%F;8?1B(;cp2qz%I|V4ImAFeY0~IVY?3g z+`SF~^P=^Uik5@Xdz+7kdCXy;eECBJ`1sYthO9pe9cyV8%5RSN1>ZMQ?fR(+Cd7nH zEJoR`i)c2QiN?~pIao%T7!w*R8s+m^s7|LJ3fRAB(5srXZ3ncyHG`#3SWFwlV(w&L z2E2NX3{~hrO@ zk5@Xui=;nPZD|1;=+J3 z6))7a-g3IulNK$pcqeAs!NU)Ax4Z12SzGgx-kvxUuO1yWt-F%gQ z$@K;={26-DHF`@jt(3)ZZ_ygnEkwVaD-^xhJ%p{`mdEh08hMou)Xh8FwP8K2L6()7 zgUnvd5$j^-t5A%9*=5ytQG~>VcV`S!=%O^=d6$;?65Zt&vpRC$IC;_J@4n6u&uo!r ze(E)W)-j=uun#+!#PdxG4tZymY81xLYUtr5y$#-Uof*N4s-|jqC??fzX6D$5q zOU4AxL(Q`T@c_6f!Wi6+HxMHy!e3U~WsNmt48knNFxvN26b&xI2*AOUpg(!awwvNW z?Hu-zl;}`%2&zQB8ta+4mUc8%0{pLDY%@y79MakK8&9>=xA9?8WbPFUqW8hVG$S6K zm)YwWokbQ!fKge2?6n2VxTUIzoQhB0z^7qtya-wQeF}uB*yB(4rih_S4`LxZd!DocAzhU5GKF3p1+|RP6EN^4@ie3eO5$W%Qf-+=2P;*E9DI-%EqY@Q7;g#Nt9mr3!PR+okM z-xOUm&4h+Jz|9c)=6zZukk-fLU}4Er>hvh5t;DM z?;jqxKgTuXzH1d)3q)>Y{-$Q?_@?jSH254+%x?cI>!+7QS#;fwA2=b0@JB#M#4bGt zr7o>#mweZ#;R>z9464M|rufIT0vSAwg89P;f}>$3g2k*Tk849B1Ox~|T1;5Aj-V)m zLI>yTHxUda3wLKaZX#+*F=ra$DSnt;?Eh3t0Bpc}ZI$#bGBlxoMH^~Z-u&djYQcOn zeSGw$iq)Z*jY;2s<78w&O z_N%`Sbtt#K$bT_r9`E1*ZT2-SrM^zx?AHfxURh_%II+u^csrL`OOd!e8Iu{4eL~e* zL2I7yKtrS`#Ag(Z16syhB|U%4M+@Unt<+o5u>cSqLq<3l#+coCP^8#n$kZjPYwXF=EmA>yZv|>Ju|83fA0Lk4a{%{houTH^7{v`MO+0%6w8u1M`c(x+jN8AFr z$MW+po9CCPK!>-L4<1#x3A0_)Ie5f~wPtoV$O$327!L{NWg@F6R}~lgC_EE^L^h(I zJ2*lBIb zWnEb(u^so?r7#iqQY)j6*5@z_YxrVn`ExoP%~R(|qq$wrnNvxX4U~@-&s;XhQ!zsZ)#uzA@@N{7Gn#$KR6d}qMH@N%BrqVZ1 z9seVc%o9N}*z6_0UGkEWj6>K|{76>3*53MN=%+x5Ph<&M{cJ$lTUdfLGo+6VPR^f| z{%W{AEke57dNxhYj?Iizg{nzSe0*xUEqony`nN1(@DdSG9ryO{cl`$u0LyeJ4N@Un zu9z5)iR}@hu~gv+-43kG3I}U}K=>yh=xWR(her-5vAm@5+|SevFf~JmFgI?MZZ-jrPC0qTBl^^$q9pLI-(pmJ)J=MJ^4Xj(DiKeUF1ziMe^;>vo~ z#U_8r0{MZ_NIP>Id`40e{wKjqU`@jCNg!n?#$4` zqDVSWJqnx9#_34A;C9F{AlgV`I`GfL{fd3+9du^Gf8i+XTPQnn1Bpc}K~U&H^R>`S`017W3R7PC zG1x`O0YZ?rZkI?YVR#xmI|ckSKM_^@B~j0BQ60 zHys@OY@i1p8jIO83g-;AJB9ViI^#?gngKO%eM0Aw}i3p<&Qqf zD5^K8N^nz&hT+Xc5tP<}(Y6Z4&Wz^JVd}HyVihyyPQOKFbdoG^Ou?L?vUt$cY?()q zr;$grzWFJSsBC>Gae<4PWObr_b3@Irmvj!e;S98+I)h=Bp`ZKvno#~Wpje_uqp3e| z+PD2 z+?GKu?oqR?RyCIrI;eZ)!1cV4X%xUijxNg}gWWY6W|21E63{Nz-xPe+wI! z))f`C_u2!s2bYi!EGy?94nIANSkASnE9&~@vWwrc%7rIDB=JYX@t?P<=rzwF0}z7- z&Sd|zF!04u{s{0UZ$ocDxl^JC2)r+Hok;n_DfzTX>eiha_R$a(L z{GH~D1cZn&&p>-{y84beoBgeU&I2iSo%Tz)gp|04h;eBj#`l;x9m9OpmdL8N!(5yK*f|bt#n3ToPbh36}Mnc#=1G zv-c>05;U(|D3qpdEy(T#j!6_HTw^?=lrestc_1*VbSj){yrUWo%nH2OTuqZbml%AoMP!bWQWg~<6C??v&RBN{t zNJAi^1rDowbmYbLn@Kt~rKukqmKW6I_!T-<ZPTeOik*Z1<#CXn#wXkme%SL z{g?t5G2*;;6}230=g=>o6Jr0JCoao-=AmbV-3%%fAAZ;<|PKrVMW1p#DOkcOXg`T`mqe#>b#Nw%Oh!#`X3~o9Pbx`(LYde zyTjyMT=PZqQMu_1WK^bbU;eSC@O?>cu4hElrv#h?czk1m-Pc;Bj;G~k2>8!{TrEZw ztNDG8-LUh$-zVr6TS5~}Cz4M@vX`hOETf)a9|qWk;ecTs28D}(58MMRhaP`^fl2kZ zVTw~C9P~~8U2mxEE;X=vX115$f`Y-nR*P8$8@e8{-Jew96UZGXo4_)^g`U+YTqu8_ z>eIUfkfFMr`-URJmQfaYoGFavIu7iCfK9^-qjbMOY zPsdT13jg1``+=yA#m@dgaKLD zela&kr9>4EUFc+yFlQNM7>u+qGod}0*f`0|#f4J*+~Cc*AfWvx!byiEC9klc#zOt- zt5P({FTO}CMj154`ASWYPw!=mgIk2^ux1<0=&4&LHJzDd6DC#x+&YbM5HrMoul^ zu`G8+vagIDJPOY|e`WO=H1j(CJSj}g^xsVc{zLHFsfams-=oPJhyF-KvwW|ZFzL&e zuDehW)G{1|fA+UePcsa|_T+tF#8OTSNaJVHkAC`)lzLF;!Q5tZK4|z#SwD=s({=xg z%ZO0ydesb$kE6dS=;7Gir*l}77?$joTJ{$u<)#ux8&nnK58qoLp%JH!rtIvS4Ko~~ zdVoC!!1HJ5V_jn2Q%wsI=d?k$Y#JqY7dhokqzmTg{?8uz0SUGLuV){H#2O?;Yd&FX zNQI`b53)E7B6@h^pUVeH`Gr%Go&ZQyfN=!756f?0AV3C`P*gMxn-UFh!tIe_tS62Ax$&$lB!%leSLpHM-uY9l7*d2ZP zT(G%cieUvBQ*ZuH&HsNc|8RyPOhGrsKFvzIw1yT%b2FhOXkCDcE95^28e4n(btIR( zlvmSYoiL;~^=8*ICi3yY_d$p>#jFRRBGs4c@T1|)z5VJvWOB;bw}rK^yT^yM$8n+_ zH_WhW{^sv!+*1jd&Q?a=2yi(w-z0pP?nhOt9)3u5%O!RY-yeUQBi2!0guYcX25Owz zi`@0VSP8RHR~1*VwP7LztxJf1X=Nv+pqb~y4&~07eaqE1&LBSvQF{6gZX1 zyGO(S8l5Q9mm%^g^-Vlz5WhDv)+*%EMnkmU?@gGz6vdfFJZ)<7- zI8F97Ty?1=e3zq5oWQqvLju=iQ%xLqH>;Qkga0gr68+C-4TkZ{FB(JAOkJO{L@bM? zRwWCP%7$@@7}FC^J6kti=n;SIKBE^sQRJY;R6wB#{m347l3@0`zUs`K4R&8Uw4*~Y z33I>2YG~ZgKX5$#4VKCFj=f9K9)_SXCz(e&sYztfQwC2ng!z?2*Wwkd6Jr?Q*09kwBj3>GKp|?dS{jp< z)QXc6=vgFc@i~Vg)geKcfCWkSLzpd2g!C8YOGX+{I zI~sb-u#_i81S$qPd|mjnp3f=~mvWDN&Y44OVn`HWHxE@1MTgH18D)fa_1VNKJrCQx z51Ks0mDK3C5(4usM-L9o(po!*>YpSdXw~h_;}vYXaaZX^KIT&W zW5_{I?%d(`r;15W42$?uIgW~I8eI<&v5q6x&}Jiy z5LUk}H$)#tW6_cYjQsD%-;<&f2X7bV^;;3Jv45vCILo50TNTU5`CU?8%VYX3=#Pk0 z^4HA(dBe!|_~N+! zFh)rBrYklNd6+B4u<8a~&e6&AIO^*ekQ=jj0>RUc|NUSKK=QF--?4xoz;MkBfr~3m!(mYanC3thLdDpLJ4VgMB@A8do5rhZNorH5LlG?b~;Q zW$MlSX9}mu5F~h~SDV$l&vq>hHS0w`g;}nef{&5fL0`-m0Fr=orG4+#;ikIr#b zWw?N@@pzH9|2{L4qtDpavWOgET7*0~3tzzEv?=`HQ#Azg0^B>KQ`SC?4c7F3T+T|~a1o#)V6hXEOX3oVHQWukW)hqKJumkKmvmeC?iNBK2K_x7g7 z)pJFpE)#0#f>_6Ps1NiM8~Khc{uxUXx&=0#co7_U8)88#@S24=LBh;epRju|EG({B zrxcJgt&> z0%rx>@3SI*0xS7Qb8=?J`|w5o-LA?nnk@xQ5JH3>>F}+@p{}JMM3ac?zYQP_@Fjo( z{OT3HF4QN<46e3FeB{*A4a*CBfeFJd0XAY_S#=%#FQ4rt)(;aDL(y`YzczuZS0#-O zODB+`A~vzG-n#;Y!k0z)lbj?Y*;@MCNcQPrZ(s_2#cV1Oaj5)nG2zwK(5IMdG37OV z8HFUw@;TqnE{GWuF}~Q^z)8xdp8!?Av=|SI^g~)?0WogTJh?liBq&q;en4v5svi+> z$Y~MK4OH))Homq*myYgSu2L!UETImi-EDn(-~xF6K~ONFe$hxU&c(&_;Kc=IdB)t} zZ_d7Z`T3x@5O>}XbJUIbrV05MW6h4Q-%=m?MthG3r|xSEU4GOYJRe^~N&nEbL#8x{ z7u|wK56M2_u4;pX(gbJeK!Ge5AKh09qd{Z~E5O4M-BD6iFZ>>}@YcH_r#dM-B5Vd1 z(sH~>(F7{UQA{*Lio641uqSm5D66Rz{h+6@TgL)}7+inim*d!epPwRes!ueF#5cc& zP=^%B8~*nKoEX_#BJw853g9h(kpLA){r)O8XHT9$8<6rbm9AM(JP$rrSu;(Eo;jnK z1z}pj@2$i^B)u@y4hA}w^;*QmmiuB~r6BSpwUrq!eEs|SVXO2wqBWGov>U$7xCwzU z1VXey2TiX&s;Iva`C6fq^vHWI;GE7sfRfxyONYhE$uJ<$$2QNR&_6#c*$nm-Eknxx ziol)l9F*|S)(nY>4#m|=VTfMqPARwTP7#OFn~(nF)p4{2G1jgn$xlbIdEprICumAJ zh%+p9=9ovCI?vriv4zls8ERR#W(vKMf}mH%=)oUO3$0q3@}-K33AF{_#qFcgK*%o@ zVKBp(AW@C_&xe&O=P~^}f|XE1Kgm}#=uN|>(Wo<#%-f{?m|w+HjRYSMq+t*my6)sQ z0%71EY)V4t1^s>dw+33a6+SC1wa+v)x?qxGM4Y2KB8&tB(Yz*tM91|RJXhQ{?4dGr< z|91{RViplWTBd|m*H5q&WV2v0@oJhudRcwyv!m&8kqF)-M zKLrF4%!E${4}~)!r`As{qdKuOmI>@PBA4Ou(3`^9dP}bPG&Cgq@54g*CURvzvzpT6 zkEU_QL`f_tLi?16h9d8*RqAsgnx|05vO-ojzU;u=G~n!lr%@}b)-q$@H1rP?_!L`~ zY**4oPZUCF0Ufd-)ypD&0h308p~9;A5qeX!Q-X6V4~>(;p9c$F)LKGI6uPTj+pLI{ z0h3y!el~S|-!&aliTioy{olho^aa1SP)Y(Xw2>TOs|K)Q76>pY=|!(8-+P*8w_H%Q z;NY3;LZe0uUPKL$*-w5M>Jo*UD_`?t#KFNCO%_Cn8y9@soXnRcDEs0tY~YweU!r&G ziZ-62XDiKZzj}Av6H_`Gp|UyGaWTW+XzzQv6Huj$7Db{mSvYQ<#r9P-ZWZOF%kYpb1HF!Myg{!(^l~!y#HJ!Fm02N$mJxVG3Rc|-5dygix-0bXDZ*-l>=UIHVGSWwaEIM^Oz8l?fRFWE%TE=Y~ z--}IJIQ^9sg;8t`EDgO3d<{MBTO{MjX@{ie`H)%VE`vO%*$7KEMW7sQa9fhMEnkRl z>&i-?IoguE2>=WSREO|el<0u5#uaca5m00){?e|x?#nagastj#%qD922@{4uqW>{4 z9;o-a>*asb^{3d94Q(?aDnfz;3OfuHOO_i7Uy4Dbm7TtQ$QFJvp{J~^Z4`}EYd*@% z#30O~UimIfYXBzEGICe1kMOOguIhA${hC#+BLz~Iqv1rS|8HnnCtZAYBQeKcV;}L@ z%9G^MF+)*^7AtI*tTMa-N7!A-~sXaN^!9KNv->XJ0zgO64 zDcIjvH!)P^%k1@Os#y*3n4_^()F4ZduOY0nz@-J_k*B+z>EV?=UZ4HK*qN_37MLE2 z#|5^Q-|NAqk#)t$d8HOQqHh)rGFc~GYQM}=m$HOwE<>PdIDA|`IE^t?M`&hS>z@~& z27=@s3=_zz{w0QCul&Ux{m)4UzaWr6#3>fKg-D3%UUsBS&|8TYjO0jf_4kCLtTgyY zN@uMqS9i}x6Ti862qjCY0L*`-#B}5y4$W?*1%<)waG3!bRx#)>(7o63*{ASL(I>+$t8E zt<*uIi(%5JFgTA^-Oi&ro005*sthLLw*T;OjaN99PVma)ILHyr<#3vylx&W*6ctx|a z!t#w{o~plkv=@Gq`Ze&%X{qEeqDnjR5Oa~JV7NEW-b@#(z@d?L29xmrq!9R0GAd~=cWTFtHxy0hL7xlG^j_BOn+p~3ev<#qO!j=#3+ol7E2nfpj9BXK?}3!9In zFX*>-=s$kQ;Bz^J%SL(eA}GIm_$KMP7n=&O%hd+kCFeDdinm9C45aa*)q8W5_6uM0 z7ZEwEXKki`D-C*H|M-mYFeP-aQg6gAwOnP{Q=T?78u_lx^Wyg^mCM1ypI;i|S^V5~ zcUQc%W`!SA#i*HBHEfpJ@4JJMMpHTaWRz%pWUNoy%Nx1}qRIH3wg>$lUYt(1!emM; zxgGL2|NL{Xpxdc^A_(II94fx2L-xx}9$OiWYqdQ;1z#1a{2IH8?q7Iq&XTWj z^iHKfZg-;MMaw6*{rP6IgT-D1N;1(kGKqI-9Coh^yRWK^C9?Q@Zq7T}zSmi*EWc>9 z12(nMa>etI3eD|MH>j5?6piib?qjRlk>@Dw+jO4y^NPsVNAvAHbU$Uc=TEk-L~Xq; z_7MhPkgY;0+vlLT`x`Se;Gi^E_51UMT=FGuNaRKdRS+?@xmq6ZnU#q zyNzvJ<32yQjP|;Hb9zkNbme`~*IjEq2718wip%GsU2k=Mg|M}L06G2%I1%sMkKb>L zQSmddCP{GA2}8(N{}Cg-7;RG&b!J9hMsn7?FU#bEpr9hf9AH+czCw+N4#a;W?bu07 zAU|aF!;0l@K70!ak0n6!7L^3{f-Fq9%x%>Ju9Zq6ni+Z^n8d>HcUlQ!QDx(eXB!`D zHmuX7YPl4y%6e0k!*`Ld+1=4d7Pgd*cULE9>rJtJEoM0uJPE>6R!Fc|&bmch+zR2x zoXs9o9)^Vkug$;wXqsrS)!|{Svs+<28eG?>ue^KsW5i zXcDW|Wbx~y&%+(2yb~{P512Yii|#ObEP-H>$-{8v*X^SkW0yo|=##N1t0q_Tp$b7Q z2RQCDN)g7K*S}}Wm&7ypd)y$~D2^3CSxJ>^mmQaFc6uw3xMhrMg>IEU z^glFv_kE^SoYYab_&u|y6Gb%Sw1@8g&}{#B@o7!Ru0DsNn2X6O9DnL)suWM+hfg0Y z3Sm%8m)rhC7cm|wA-~z!zSdx{+^wr^t1AQRp2p3MwY~dwnB>djsS;D9Kc1IIYiOQ^ ztAvb^O7pF5KN|Ygbye@WFEa=F>g{S2NcfQPUmoLi+|6XP)=>84Ig=6a8U-P8B@4Rj z*6(pMlh!(nrE6VqLS8utTrM|0p07Q`p!_~RTEkoUVLq0DM2y#B6x1p@5bg1+Npb3& z@RfyuQe0XeQsWm(m4bQW=!FkIn-tZxcVpTzLO&)hw|L}`eJ)hNaC{4YbRw?+<+He@i#$NFKtCq7xD2!7#-@6Wq z@@QNZ?I0{;f`N}WeR%pDGfAv^+V9>88ux#zsZsvIG5M355~k+OW}VW@;GX5L)wsZKh)igMlx*N!gdsww7Ks;Ki2-e zP&${+Tf0Go&-I}8%8feQywM~uRosq%1qjk_a}WOLh(&H#A<{g>Wck%Hk6clQ-YUam?auibI5O|AjoxcO0$iPVlQ@(xYd??wPTjDlKMIHk zq`XE~dCtFFcc(F^-Zk1aU*F8kE}oTeaqDB|{QQF@Arh|FBqZ z{n`G_EiyU^A&|ssy*~}{6HNE;@N;{FK zsJu)ziPS)cJ>s@qjC7wwwyBeAuvrO+ky$_4|6V&#MbUFt#X2h`^!`dwIxGGRZ|w8Y zT1*Tj3K3VC#@k;`i7FY&e7v(kjiwv6-*8G--XBbIdT;v&!bOwt&-#KMS;aSP5XZla z>Tu;rbGc8Tv$JH@IUzIob)lYZpiz6#&k_^}C#LX2$Qd%S@W z)AOq{^<4s;LIaV(o#Ri&(+$+HnMov` z{ai@<3=M>F0C20=&1sbr80}H?h1NwAttz6rA^Ae2(duLh1#?4aLZ`X+eUoqLdr48i zxyBOVSkQ-fH6{&1@%PjCvJBr+-b^8FGC_oq34L(-Wk*n~SLeDE$oXBWYRhhS<1POG~xlIDPOj~F0e4{1hRJkCF}m0{%V1I>1%U^ zEIt?5kF3ZfuXyx{@s2%^*84kIDi-r2Mh+Gm7?_x>r;9eu_YRr5Gx%Knrhu!_*m{j# zB|@^W;W>Mp3?=7|^-UFG$Kdn*;qLIFabcM)(}Ux#&y!>~@FZs9Sl=0J z-h%2ct$WnR!-KcXZQ!ZuZnC<0a0)!H3NVTurIze@8ETc;Z1Iwj@@<{nkYXoj>2>@e zL~)pii}x#y3^+#1Z@;Tc)ys98+`oUPlB88g4{BnA9pBe~xWV(T?^WiqU97WRa+RxU zh%4WsQ()F~{%NFrGrbTwyYO0RqF0>fdk&};9{Vt-a#F2_7*=#l$stqF_GMy1Ljw77!e z|Kf1Oz7~ihc3H2fq)GpbXavbKi)r*@gdN4a{SNzONy}VgVqRi5X2l`GbS$vDP&yng zynph2KzluK&ThFZJ4GaFTQ4WzO~rK=c5)IDV+Ilpm0r2Mu*B)=#7X+x;@$bYe>^J5 zhU*TE?3Vv&cDUWb$$Xil32(E0P_b30AMOVzjB{ZYR1! z^P3elZ&1A!WU1b@zX=P{<4{(iZnfroSZ^Ds-Q-aCb}HALR8J&yyw;;D3`kn;kv5AZ zrVXFQ^JOVr>0NG`qV!E^9+_yucbtL3xozcPBufZ5T5i0TGdaNV>s%w%6%)U$7hNbW zw>MncC*XBEuWM8l{#@1-Pbm{5_WBDBgx~ShA&iSWJt`b~wSJ^NG$L=q!T()c_*!XP zi#~%2{4Yn{9C2dIKCEBgrY;=pStmmwWaQU!o^%3&3JuZ%R8|-VSTcqAZc9gOylzg6 zGz?1(MhF2ww83}MzBXLN#`&DW<+yuow{DQcm;Th}`*6p6#(yX65euuDuudqwL6Kqepq`8qFJg?=-{KESa!J&RV~#tunv8@!TO*bc$ghyI5hmtCDtg%zx=LN9<=!sn(PQWeM|I_V!J4yt5 zK!&8tZMo5NPmKQ_{R`fdwR&*5)&4>yD{>TyMZl(ELFL5(W`4Z7$@`*^Qq>IjGpbJu zZmvg-7RSMVhp5DQh~$lBr@7rFl>cf_RYY?}zvu4iCtO2&-2x@S0<5JOZS*jOEE1fhSa~~ zW$Sz3%q>^5AyxMwe@i*Epkce%tW;Vpc#UdzA<0a@9Pk6!8Be~jPiNluWQhtpPF|hf zCr9UpCzZyFF5b3#+4d|KdVPPLTJcZp#qNvvsrmt(Y%juIK|^Q}!oRW&Cn2Q^1PoV0P+%qs>tiI@$^ z@v%RM`*JeibGaaX5RHBvnWbHlGhVD_E6vrD4YxI%(NS-z!)Sh)YUeaLLKTQj@axk$ z9ucK1zHM>o_^(~*veTVY{gvOo+a0FF$!+Ioa2eVCORN^Rms6roJ^NA*5pggd_iS&g?xC_%ySnB5p~wUPqsZ*Jz& zxJ|}3rb@M}XPe+q@DFBx;CZXFZQdO5*!o|Q1x2=IP-|%_S`?$KkfEzvgPax!i7d-O zpALeUMfsQV)iSoy-E9D5*8S?RE1bZaHnIGAce;7Erc5Pp>4)2TTkuQ)dJcdmm<OpL~Q2r zEnX(q$F-kBUBJlMLEE=IgUH_D#cJjAOwRyk%5%Iro2BND2n1=7cpP&T@5vM^WFOTC z)9w!Ek4U2Wq7X;Lv^6!|R$5^?nG-M#R;_@(rpv{r!4ta2%r&M^$+_7HR=js+sQ%<-bY4ubcTb&m@kNwDOh(FR!oN4kjI8^|elu3;K7Q;!7j@ zOX~A+v!Q^l3ZD7{O`~6lUdWVmkm>7&vo*yA$hsp+3*w&3&RQ2LCxd#ZQ~dQEl}8F! zk9DDk$MWt*p%PAESFKlvn@1B8++6w0exgB(JVEmpklvX4KqwVu7U`k-C! zNogZxU~uA-*Va>N(KrnNC^(BKO2H`xodn*;`JMUwD>hMW?j9s-vG1ZY#haderqgoU zUJ-a=58(PjnH_w&vxwk*%m%;;``v?h)8$$%_M1f$1lUc{AnmvN-|-ZfEHx35mouH- zsy|UpkcMt|>DT<*Vx$tlj5xN5uH@yP&^N*FD_q`uk=`JIsffN8i0H`Ms~OFK=u?SN zY$2m#b5SmiW~SZb(s!ICq*-Ti0ZfLr^5rrPW~;fu!)g4{JACZ;3i-zRp)WDcqtZBC zi^KrD)6H}#)~F|p%dy+as(f{}dP_E)3$5q5hs0`UsFJ$-vtq3E=QBX}xk|_BbbV&Z zq-ZB{PcOe^IbBZX=k>%BLe?+em`JNM z_+g{#h~N>wJ0D!`vz|o}a!)m!&NKcjTGa{o$!&>ORPYt>AQ4O(FZhVtSMB~zN!AhC9z%WoF9kP- z4mwZ84Lr5JnTIagt#`k`XCns6=NxH)p^e0vMD60l(z4}xufjD9ELkcBhGy6GH&g8b zgVw8&uV3wd&4?dil$oKAl`uIgEjixRxKVXhTYh71{nq|ajJ zs?}8G{Zy9reWm_c{M94E(RzkyPEQ0iJ$)PIN!uM!fL@TDAdDqg?Q5_Zf0N_h)Y;at z+}PRmtv|KP-DO(SxH29-#g{ts{?5%82@~>HG1-eKuX|+$d60jq*Th*EvWrq4S(dlBK6FPW(rX$nJU9Au z-FRH=ePbC{v0Pe9m60kb+`DP`QH|%I%`ragT#*u%pKP;G%_q z*9kvOX7RftJp)a<{jDCI_9sSN6!}i9FzCj87YZVPV*nh`_vs;q9hWl}$28|~^2 zAFkB`r9PnxebS7UZL{Y?w@kruO?|!2eC^#OMfr?@;SGkW^Ob5N(ZQROU0@_Ts=8>t zT*ljk^_(y!u>Q_y?Vpxu4nH;m+ud+)aic>rG38^cd}Dp?iMW5{&D&%)%M1= zlTKr8$;%UXGoE{@WAk!@Nt6ab^p&X{N$$y(Xd5uK2=lqS&)0fm}l19>p8U7muCIrTy+e?OVkRR5aB1FRZQH8~nh#Ba0Xesv%MRyyl2 zWRB`6MV=#gXFuc6WUqmb`Jk#zw|>IVctLVA{ zE{Mh;%&PCo(=Bw+3@*wX@)GVO68unHuk8C$sRAGpgsjg)?ECke7`&TzO#&sPvKM``OJB||i?_sRDoROg}d5O`zjkd9^tiM$~B~h-Z z`{b^&6V;^?_L<2V_YeONz&xQ)A6f=5`L)zv-iz!rGM%w-$S$wgOKJ=YUD7gYKa?Hx z?91V&J-v8~vw1ZyBH4Ey8^=#KEl<7_o!m~_0k)fOJa-?TfO{n{u)3l8E^njPT8c|* z%HK6X9s!j6hkNwlHXZlJ#<8&Rm!Ah3U(>-G- zp8jZD?zL3WlizSlD^WMn;o=YXJbSUHY14@ww>(j2Us_X&BO&NH5J z$I`6WpN94BY2*b6=j47dw(7-@wv;~{402d&8l)Wno&U=G{Ax*4$kXlS@Zk1db?xNLI+I@{L`5ouJ$!!gL?;X?yq2WDe`~q}D+24@XDJOCVc(`iy^iB%r zs!ULpjFSAC++=bSY;O}U{9hn6698vMRaQuZk&EA*Qjz;r=~9`kgXksvJ^=$ml7)JM9M9!alYUhtN;&4fkl^Ei zR%d1)2f{r?jEyON({(yZyqt;25|a7;=y_-4T0-wC?ZjjVlU)cfJ_?rBPs z>RZ2qx97=^jTErWQj>`;3OiCKF+oOHt6oi5?>r+n5q#051Fi&{`@RqX__*WvbQCsi zpdgWphwtb`uQ&y%W=LvxVPi^rK|%CPQiw^ACX1*<+z(@$iSbpux1C1p<^{CfT0df0 zEINCENiUs7m2u6GR5uHa-rer>JdOm3NzAyd_qi)kozy9&#tXcZQc{QB7(~)kTmpIOj_xY%p^~7?x35(xdmtLy$ zr+HF!d{7k1weGVfhKgHIGm(KwxF298wBz>^neW%j&M=m<^z^GC?jytO%PN!pZXnuP zU4Qs!-6uS`TN5!twdFaibrZ!%Pkk_5zTE72$CQWiB09J`QmfACz%zQddf;HbZr!Uw z@`#afxx8{L4&$Tt@6q~gOgTKyvttBwPKFs0^cR(_li`d_^}sOIMgNPYJ914I>+gA! z0$C?e(*fWE7K3)ZMJ`<*ll?>iFlYId6U{eQ;nfjfhuO5=?&Ad;75(nH{N`pA^fg~n z0%H`4y2|gyti`;Kla2pO zdd|PK9-F|jI%atnVNL{-Lc_5xKohs)Z1y~1y-?yWoq9I`+JUn5HMckN4>Z6cnAx4K zzh_W(_D=hT`cq=qhNrPbUy-(NQM&~7izYn2k*+pK2V=T16=7JAaC>3a#s}s%EMBLm z)8F-T$tIS(riY12x>w>HPXn+knSEej;WOT)@uhq^gJezLl4$Z=~ZEv)|)q&K_YZ(8z5szr?-U7;d7Tj%@kt0tEd(nQ_X z0|owszFCcZjWT<;1)yVGY`-dPKkBAYlm^iMg$|h%j`xd2j+2^em`FJ5(N_nN?$>2r zYel=fbha(DYRCK2=acTQFwqhChBQj6Qodn zO5?9dl}T`N3ZHhgtE^9^7Z)YHRMsgiN|2^dkSeB8m$EJB+?T&;lJjE)dJ`Mnc z^8eD@KY+h6E{kG&gb6P+qP(7L{eq=kuuc#+5DBi5fpWE0c|K3nGXphAJBs###9I-? zZCfJh`orZ2=^W8NbXp=lemG3Qzwa4vcPLM3%8YpcIra zXGLMG{*b(sU0*N`ajzr`fBk7qvK%8Yra7;YwrULKgYA3opsp_Fv)f0VvKrAEP;p~!B8lWnzPA?EBytuFQ++Lm0Pvm++S=CUal zklw)sjVgb0xJ{Xo?3fVsLxRsD4_<%M9RmjLpaHMlvxIZFPDO#oNc1nKaZPzErp%@Z z;)XP+{=HjyMDTXKJfd1M*WYpk8N5(I;zjohC`L?aHKTI1d|%g*Ev!<12_YqqK?gN$ zviK>aAJgSXH(c+lQLy%^BO^@^`Hql_Xb3_H<660-C?IuP-GSFg>E@9?ZRp;vE6U&$ zyeLfw)fXo<)u`+g%oQij16vS0MHDUp4CT`(sAgnzh3gQvjE2F#x4ZwFGJiQ!W&G!p zURux;B^4&Njo@-sPI_BHJ5=F4*|qQ_X6(lfFslH;-ZV?@Z{;KG&9Ed!I(D%(ws8Y&50KN?D z@DsLK>iz910S-L* z4BkzB=HA32gpXex%92(!9y=IuN*DNB&_P%7TD!Vwh#_@R#3VMQ_tH+L>b#fwQ!KJ8 z6^RT1f@RLO23U3^ZnXX@JT-110tyL0sP_h)vTOi~6v#W!rTNsYPfD^=mWJIWned@u zmEfPpi7^7`&b=paD?5MA4lxaQff(4CSOfB(c*C`1A$4x4_54&cKPw2~Bw7y_{FHge zCCxBRxJ=F_<92BKPfsJC<&n>kk&&$3N&}4NQ6219ojxSN&~B&VukTGt9pzrCaaNh3 z((~d+(-Y8$f|w~+cw-IAC@0B$aWD`EDpJhYel1! z*(3%Wp_H%@2%0(0b&5zivyvI71U!Y~LW<&}!B)mJM3#gNT)<D0K$MZSS!7MnSnkkH)bww}%uaOVG00q#aa<8QfN4GaH+kW4^a zvu%8wq3N&@FN0c=>F-j^-yv>(oJaI$loZooohq$B?ERso{(397wn^I6byj`HjRN9< zsfn=Z*wZq94&#^5O!+j=E$0mhNe&8hT8=K8MxLJU6BbfX$yAq~rxyqp_M%tPWVx1` zXRQ_Y|2nP3>q-hF)e_8+VE#R`)!(8BMG{C=MM&6pzdV2#6MU>(QJ-ROR<1;y&hjs; z!wCX-!OPyjyp%s5$#cUT?1;U9_0plrAz{H39GCYISJyV7KQyuAhXr(?x`-dtX;_oU zI++Dk`cSnJ1wqshAJ-=$k8IHw46r@O)tUZk zso-Rx!SLwHkMYW>BL(jm%Brxg*5IO|cjKX`NHg*21YKjlrIq^=$+>Y*PmGvU!b`Oy zPb5t56k`>;*+!$25%dd5{n?-|f&BddgGuPepPEoGPO`AyDZKDTR5`szUB9)! z<|`8Ly2d_J7T_}~LH%0D< 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�ND-TUP5a)d#xebY+VF(?Qn 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
    +|hg3_DFE$W`a!s4bWfQ|055{mv|{nzzT6+FTS> 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%0A~YoJS)c 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±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 From 0b7ae2a2106fe47ad080bc066208f6b3632d41b3 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 22:19:49 -0700 Subject: [PATCH 55/67] chore: bump version to 1.5.2 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5f2a226..9123736 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.5.1", + "version": "1.5.2", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5a5971a..7db4f74 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.5.1" +version = "1.5.2" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c27a6ee..26dd0b8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.5.1" +version = "1.5.2" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 85f87f5..eb58d59 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.1", + "version": "1.5.2", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 3fd103eff8f636e9b8a5a26032d6287f9973db67 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 22:27:47 -0700 Subject: [PATCH 56/67] ci: use supported tauri-action input name for updater json tauri-action warns that `uploadUpdaterJson` is not a valid input; the supported flag is `includeUpdaterJson`. No behavior change. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c11cf3b..62e7135 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,7 +186,7 @@ jobs: releaseBody: ${{ steps.release_notes.outputs.body }} releaseDraft: false prerelease: false - uploadUpdaterJson: true + includeUpdaterJson: true updaterJsonPreferNsis: true args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json From 3d7111d280ce719f7e6fd41016dbff0553f34140 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 22:48:19 -0700 Subject: [PATCH 57/67] refactor: drop local quota computation, hu is the only source Remove the session-log based quota pipeline (codex_quota/spark_quota, quota_history, one-time migrations, and their cache files): local logs count tokens, they do not describe subscription limits. quota_api now talks only to the HappyUsage `hu` CLI (bundled binary, auto- install when missing). A failed fetch leaves that provider None instead of falling back to ~/.claude.json caches, persisted snapshots, or the native wham/usage call; the dashboard shows an explicit Unavailable state. The in-memory readings still carry trend points across refreshes. Quota/Scope.quota types are removed from the model and frontend. --- README-zh.md | 170 ++--------- README.md | 170 ++--------- docs/DESIGN-usage-api.md | 66 ++--- src-tauri/src/model.rs | 43 +-- src-tauri/src/parser.rs | 111 +------ src-tauri/src/quota_api.rs | 582 ++----------------------------------- src-tauri/src/store.rs | 443 +--------------------------- src/App.tsx | 30 +- src/data.ts | 10 +- src/i18n.tsx | 4 + 10 files changed, 140 insertions(+), 1489 deletions(-) diff --git a/README-zh.md b/README-zh.md index 2969a39..17667a5 100644 --- a/README-zh.md +++ b/README-zh.md @@ -2,170 +2,52 @@ [English](README.md) · **中文** -**macOS 菜单栏 / Windows 系统托盘工具**,为本机 **AI 编码 Agent**(Pi、Claude Code 与 Codex CLI)提供一个统一仪表盘:**每日 Token 用量、估算花费、按模型 / MCP / Skill 的调用统计、可靠性遥测,以及实时订阅额度**。 +**macOS 菜单栏 / Windows 系统托盘工具**,为本机 **AI 编码 Agent**(Pi、Claude Code、Codex CLI)提供一个统一仪表盘:**Token 用量、估算花费、按模型 / MCP / Skill 的统计,以及实时订阅额度**。 -技术栈:**Tauri 2 + React + TypeScript**(前端)/ **Rust**(数据层)。 +技术栈:**Tauri 2 + React + TypeScript**(前端)+ **Rust**(数据层)。 ![Tokenscope 面板(深色 / 浅色)](docs/screenshot.png) -## 安装 +## 亮点 + +- **一个仪表盘,覆盖所有 Agent** — 过滤 chips(All / Claude / Codex / Pi)在汇总与单 Agent 视图间切换,各自带品牌色主题 +- **Token 与成本分析** — 今天 / 本周 / 本月 + 自定义日期区间;按模型、MCP 调用、Skill 切片(只统计你自己安装的,内置工具全部过滤) +- **实时订阅额度** — Claude(5 小时 + 每周)与 Codex(每周 + Spark)每 5 分钟刷新,Codex status 风格展示(`29% left (resets 05:01 on 9 Aug)`),含消耗速率预测 +- **菜单栏一瞥即知** — 当日 Token 数 + 各 provider 最紧张窗口的紧凑额度摘要(`52.8M · Cl79% · Cx29%`)或完整列表 +- **项目结算** — 按 Git 仓库 / 工作目录归集用量,导出 CSV,不暴露原始路径或对话正文 +- **可靠性与上下文遥测** — 中止任务浪费、工具错误、上下文窗口压力、压缩次数与 reasoning 占比 +- **本地优先、只读** — 就地解析 session 日志;额度来自内置 HappyUsage CLI(不缓存凭据,不用本地日志计算额度) -### macOS:Homebrew(推荐) +## 安装 ```bash +# macOS(推荐) brew install --cask sunchj/tokenscope/tokenscope -``` - -cask 的 `postflight` 已内置 `xattr -cr`,首次直接打开即可,不会弹「Apple 无法验证」。打开一次后注册为登录项,之后每次开机自动在菜单栏运行。 - -升级:`brew update && brew upgrade --cask tokenscope` - -### macOS:下载 .dmg(备用) - -1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_universal.dmg` -2. 拖入「应用程序」 -3. 因为是**未签名 / 未公证**构建,首次打开会被 Gatekeeper 拦截——右键 →「打开」→ 再次确认,或执行 `xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app` - -> 未签名是当前的已知限制;要「双击直开」需 Apple Developer ID 签名 + 公证。 - -### Windows - -1. 从 [Releases](https://github.com/SunChJ/tokenscope-remix/releases) 下载最新的 `Tokenscope_*_x64-setup.exe` -2. 按当前用户安装(无需管理员权限)。SmartScreen 首次会拦截——点 **"更多信息" → "仍要运行"** -3. 系统要求:**Windows 10 1803 及以上 / Windows 11**,需要 WebView2 运行时(Win 11 预装) - -### 更新 - -App 启动时及此后每小时检测新版本,提示条位于左上角 **Tokenscope** 下方;也可用托盘菜单的 **Check for Updates…** 立即检查。Homebrew 用户继续使用 `brew upgrade --cask tokenscope`。 - -## 功能 - -- **Token 仪表盘**:今天 / 本周 / 本月 + 自定义日期区间(含首尾日)。检测到 ≥2 个 Agent 时出现过滤 chips(All / Claude / Codex / Pi)——All 视图**按 Agent** 堆叠用量,切到单 Agent 时整个面板换成其品牌色(Claude 珊瑚橙 / Codex 青绿 / Pi 紫) -- **三类切片**:**按模型**、**按 MCP 调用**、**按 Skill 调用**(只统计你自己安装的 server / skill,内置工具全部过滤) -- **项目结算**:按本地 Git 仓库 / 工作目录归集用量,导出 CSV,不暴露原始路径或对话正文 -- **可靠性遥测**:完成/中止 Turn、工具错误/拒绝,以及中止任务浪费的 Token 与估算成本 -- **Context 健康度**:上下文窗口中位/峰值压力、≥80% 提醒、压缩次数、reasoning Token 占比 -- **额度详情**:**Claude**(5 小时 + 每周)与 **Codex**(每周 + Spark)的实时订阅额度,Codex status 风格展示——`29% left (resets 05:01 on 9 Aug)`,趋势点足够时给出每日消耗速率与预计触顶时间 -- **菜单栏显示**:在菜单栏 Token 数后展示各 provider 最紧张窗口(`52.8M · Cl79% · Cx29%`)、完整窗口列表,或完全关闭 -- 成本甜甜圈(hover 看单模型)、年度活跃热力图 - -## Provider 额度 - -订阅额度通过 **HappyUsage `hu` CLI** 获取,由它负责凭据发现、OAuth 刷新与 provider API 调用(Claude 走 Anthropic OAuth usage API,Codex 走 ChatGPT usage API)。 - -- `hu` 二进制**随应用包内置**(`Resources/bin/hu`),无需系统安装;每次发布由 `src-tauri/bin/build-hu.sh` 下载最新版并打进包内 -- 运行时查找顺序:内置 `hu` → 系统安装的 `hu` → 24h 节流自动安装(brew tap → 官方脚本 → `go install`)→ 原生 Codex usage 调用(Pi / Codex CLI OAuth)→ 缓存快照 -- 额度每 5 分钟在后台线程刷新,最近一次成功快照落盘 `provider_limits.json`,离线 / API 失败时仍可展示 -- Claude 回退到 `~/.claude.json` 的 `cachedUsageUtilization`(Claude Code 运行时更新);Codex 趋势合并 session 日志的 rate-limit 历史 -- Tokenscope **从不缓存凭据**,只缓存额度元数据 - -菜单栏标签:**关闭**(`52.8M`)/ **紧凑**(`52.8M · Cl79% · Cx29%`,各 provider 最紧张窗口)/ **详细**(`52.8M · Cl 5h0/W79 · Cx W29/S31`)。右键托盘菜单另有 **额度详情** 子菜单,列出每个窗口的剩余百分比。 - -## 数据来源(本地优先;session 日志只读) - -| 用途 | 路径 | -|------|------| -| Claude 会话日志(Token / 模型 / 工具调用) | `~/.claude/projects/**/*.jsonl` | -| Codex 会话日志(Token + 离线额度回退) | `~/.codex/sessions/**/*.jsonl`(支持 `CODEX_HOME`) | -| Pi 会话日志(Token / 模型 / 工具 / 遥测) | `~/.pi/agent/sessions/**/*.jsonl`(支持 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR` 与绝对路径 `settings.sessionDir`) | -| Provider 额度(Claude + Codex) | 内置 HappyUsage `hu`(`Resources/bin/hu`);回退:`~/.claude.json` 的 `cachedUsageUtilization`、基于 Pi / Codex CLI OAuth 的原生 usage API | -| Claude 用户 MCP 白名单 | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | -| Codex 用户 MCP 白名单 | 全局及受信任项目 `config.toml` → `[mcp_servers.*]`(`$CODEX_HOME/config.toml`、项目 `.codex/config.toml`) | -| 用户 Skill 白名单 | Claude:`~/.claude/skills/`;Codex:`$CODEX_HOME/skills/`、`~/.agents/skills/` 与项目 `.agents/skills/`;Pi:全局、共享、项目级 Pi Skill 目录及 settings 显式路径 | -| 模型价格 | **主**:[models.dev](https://models.dev/api.json)(裸模型名,匹配 CLI 日志)→ **兜底**:[LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → 内置快照。缓存于 `~/Library/Caches/tokenscope/`,24h 刷新 | - -## 关键处理 - -- **Claude**:按 `message.id` 去重(流式/重试会重复 usage);同一消息跨多行时合并其工具调用,token 只计一次 -- **Codex**:由独立 adapter 将单次响应的精确 `last_token_usage` 与累计 `total_token_usage` 快照交叉校验,quota-only 重复事件和 fork 回放不会被当成新用量;持久化回放使用稳定的 `(turn, 累计快照)` ID 去重,并从包含缓存的 `input_tokens` 中拆出 `cached_input_tokens` / `cache_write_input_tokens`,形成可与 Claude 对比的四类统计口径 -- **Pi**:每条持久化 assistant response 按其精确四类 `usage` 计入;用稳定的树 entry id 去重 `/fork` / `/clone` 复制的历史;日志中的请求成本、reasoning、stop reason、工具错误、compaction 与模型 context window 进入对应遥测 -- **Token 拆分**:`input`(未缓存)/ `cache`(creation + read)/ `output`;UI 默认把 cache 并入 In 显示,并单列「cached %」 -- **计价**:精确名 → 归一化名(去厂商前缀,`.`↔`p`,如 `glm-5.1`⇄`glm-5p1`);models.dev 优先官方裸名价。Pi 有持久化请求成本时优先采用。查不到价格的模型仍计入 Token,UI 标注「暂无定价」 -- **工具分类**:MCP 调用从 `mcp____*` 名称、Codex tool-search 映射或 Codex App 自定义调用归属 server,再按**所属 Agent** 配置过滤;Claude 的 Skill 调用与 Codex / Pi 的 `SKILL.md` 读取按 Agent 的用户/项目 skills 目录匹配;其余忽略 - -> 花费为按公开价格的**估算**;订阅用户应理解为「等效消费价值」。 - -### 四类 Token 与计价公式 - -每条 assistant 消息的 `usage` 给出四个**互斥**的 token 计数(同一 token 不会被重复统计): - -| 阶段 | `usage` 字段 | 含义 | 单价(相对 input) | -|------|-------------|------|------------------| -| **Input**(未缓存) | `input_tokens` | 本轮新发送的提示词 token | 1× | -| **Cache 写入** | `cache_creation_input_tokens` | 写入提示缓存的上下文 | 约 1.25× | -| **Cache 命中**(读) | `cache_read_input_tokens` | 从缓存重放的上下文 | 约 0.1× | -| **Output** | `output_tokens` | 模型生成的 token | 约 5× | - -Pi 以 `usage.input`、`usage.cacheWrite`、`usage.cacheRead`、`usage.output` 持久化相同四类口径;可选的 `usage.reasoning` 已是 output 的子集,不会重复相加。 +# Windows:从 Releases 下载安装 .exe ``` -total = input + cache_creation + cache_read + output -# UI 展示: In = input + cache_creation + cache_read, Out = output, cached % = cache_read / total -cost = input × price.input - + cache_creation × price.cache_creation - + cache_read × price.cache_read # 缓存命中按折扣后的 read 单价计费 - + output × price.output -``` +`.dmg` / `.exe` 直接下载与更新方式见 [Releases](https://github.com/SunChJ/tokenscope-remix/releases)。 + +## 快速上手 -缓存命中**不会**按普通 input 计费,而是用专门(更便宜)的 `cache_read` 单价——这就是重度缓存场景下 token 量很大、花费却不高的原因。UI 只是把 cache 折进「In」做展示,计费始终按四个独立单价。 +1. 启动后,菜单栏 / 托盘出现图标和当日 Token 数 +2. **左键**切换仪表盘;**右键**打开菜单(额度详情、菜单栏显示、Dashboard 快捷键、语言等) +3. **菜单栏显示**控制 Token 数旁的额度摘要:关闭 / 紧凑 / 详细 ## 开发 ```bash pnpm install -pnpm tauri dev # 启动桌面 App(需要 Rust 工具链) -``` - -仅预览前端(用真实数据快照 `public/dev-dashboard.json`): - -```bash -pnpm dev # http://localhost:1420 -# 刷新快照: -cd src-tauri && cargo run --example dump > ../public/dev-dashboard.json -``` - -## 构建与发布 - -```bash -pnpm tauri build # macOS:.app / .dmg;Windows:NSIS .exe → src-tauri/target/release/bundle/ -``` - -构建时会自动下载并内置 `hu` 二进制(`src-tauri/bin/build-hu.sh`)。同步版本号后推送: - -```bash -pnpm version:set 1.5.1 -pnpm version:check -git commit -am "release: v1.5.1" -git push origin main +pnpm tauri dev # 桌面 App(需要 Rust 工具链) +pnpm tauri build # macOS:.app / .dmg;Windows:.exe ``` -版本号变更推送到 `main` 后,GitHub Actions 会校验版本、创建 `vX.Y.Z` 标签,构建并发布 macOS / Windows 安装包、签名更新包与 `latest.json`(需配置 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets)。 +## 文档 -## 结构 - -``` -src/ React 前端 - data.ts 类型 + Tauri 桥 + 主题 + 格式化 - charts.tsx 图表原语(柱状/甜甜圈/sparkline/热力图/分段控件) - App.tsx 主面板 -src-tauri/src/ - store.rs 多源 JSONL 增量摄取(Claude + Codex + Pi 适配器) - parser.rs 按 Scope 聚合(All / 单 Agent;预设周期 + 自定义区间 + 热力图) - pricing.rs models.dev / LiteLLM 价格加载与计价 - quota_api.rs provider 额度:内置 HappyUsage `hu` + 原生 / 自动安装回退 - config.rs 用户 MCP / Skill 白名单(Claude + Codex + Pi) - model.rs 返回给前端的数据结构 - lib.rs Tauri 命令 + 菜单栏托盘 -src-tauri/bin/ build-hu.sh——发布时下载内置的 hu 二进制 -``` - -## Bug 记录 - -开发过程中遇到的典型 bug(现象、根因、解决办法)汇总在 [docs/BUGFIXES.md](docs/BUGFIXES.md)。 +- 数据来源与统计口径:[docs/DESIGN-usage-api.md](docs/DESIGN-usage-api.md)、[docs/DESIGN-pi-adapter.md](docs/DESIGN-pi-adapter.md)、[docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md) +- 已知问题与修复记录:[docs/BUGFIXES.md](docs/BUGFIXES.md) ## 致谢 -本项目源自 [@HduSy](https://github.com/HduSy) 的 [tokenscope](https://github.com/HduSy/tokenscope)——一个非常出色的 macOS 菜单栏 Claude CLI 用量仪表盘。本仓库继承的 Rust 数据摄取/聚合架构与面板设计均出自原作者之手,并沿用原项目的 MIT 协议,在此致以诚挚感谢!🙏 - -Tokenscope Remix 在其基础上扩展为多 Agent 仪表盘(见 [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)),并独立维护:问题与需求请在本仓库提交,不要打扰上游。Provider 额度部分使用 [HappyUsage](https://github.com/SunChJ/happyusage)(`hu`)负责凭据处理与 provider API 调用。 +本项目源自 [@HduSy](https://github.com/HduSy) 的 [tokenscope](https://github.com/HduSy/tokenscope)——继承的 Rust 数据摄取/聚合架构与面板设计均出自原作者之手,本仓库沿用 MIT 协议。Provider 额度使用 [HappyUsage](https://github.com/SunChJ/happyusage)(`hu`)。问题与需求请在本仓库提交。 diff --git a/README.md b/README.md index c82b068..ab91934 100644 --- a/README.md +++ b/README.md @@ -2,170 +2,52 @@ **English** · [中文](README-zh.md) -A **menu-bar / system-tray app for macOS and Windows** that gives you one dashboard for everything your local AI coding agents consumed — **token usage, estimated cost, model / MCP / Skill breakdowns, reliability telemetry, and live provider subscription quota** for Pi, Claude Code, and Codex CLI. +A menu-bar / system-tray app for macOS and Windows that gives you **one dashboard for all your local AI coding agents** — token usage, estimated cost, per-model / MCP / Skill breakdowns, and live subscription quota. -Stack: **Tauri 2 + React + TypeScript** (frontend) / **Rust** (data layer). +Built with **Tauri 2 + React + TypeScript**, backed by a Rust data layer. Works with **Pi, Claude Code, and Codex CLI**. ![Tokenscope panel (dark / light)](docs/screenshot.png) -## Install +## Highlights + +- **One dashboard, every agent** — filter chips (All / Claude / Codex / Pi) switch between aggregated and per-agent views, each with its own accent theme +- **Token & cost analytics** — day / week / month plus custom date ranges; breakdowns by model, MCP calls, and Skills (yours only — built-ins are filtered out) +- **Live subscription quota** — Claude (5-hour + weekly) and Codex (weekly + Spark) refreshed every 5 minutes, shown Codex-status style (`29% left (resets 05:01 on 9 Aug)`), with a burn-rate projection +- **Menu-bar glanceability** — today's tokens plus a compact per-provider quota summary (`52.8M · Cl79% · Cx29%`) or a detailed list +- **Project settlement** — usage grouped by Git repo / working directory, exported as CSV without raw paths or conversation content +- **Reliability & context telemetry** — aborted-turn waste, tool errors, context-window pressure, compaction and reasoning share +- **Local-first and read-only** — parses session logs in place; quota comes from the bundled HappyUsage CLI (no credentials stored, no local log-based quota) -### macOS: Homebrew (recommended) +## Install ```bash +# macOS (recommended) brew install --cask sunchj/tokenscope/tokenscope -``` - -The cask's `postflight` strips the quarantine attribute automatically, so it opens on first launch without the "Apple cannot verify" prompt. After opening it once it registers as a login item and launches in the menu bar on every boot. - -Upgrade: `brew update && brew upgrade --cask tokenscope` - -### macOS: Download the .dmg (fallback) - -1. Download the latest `Tokenscope_*_universal.dmg` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) -2. Drag it into Applications -3. Because the build is **unsigned / unnotarized**, Gatekeeper blocks first launch — right-click → **Open** → confirm, or run `xattr -cr /Applications/Tokenscope.app && open /Applications/Tokenscope.app` - -> Unsigned is a current known limitation; a "double-click to open" experience requires Apple Developer ID signing + notarization. - -### Windows - -1. Download the latest `Tokenscope_*_x64-setup.exe` from [Releases](https://github.com/SunChJ/tokenscope-remix/releases) -2. Install (per-user, no admin). SmartScreen warns on first run — **More info → Run anyway** -3. Requirements: **Windows 10 1803+ / Windows 11** with the WebView2 runtime (preinstalled on Windows 11) - -### Updating - -The app checks for new releases on launch and hourly; the banner sits under **Tokenscope** in the top-left corner, or use **Check for Updates…** in the tray menu. Homebrew users keep using `brew upgrade --cask tokenscope`. - -## Features - -- **Token dashboard**: today / week / month plus an inclusive custom date-range filter. With more than one agent detected, filter chips (All / Claude / Codex / Pi) appear — the All view stacks usage **by agent**, and filtering to one agent re-tints the panel with its accent (Claude coral / Codex teal / Pi violet) -- **Breakdowns**: tokens and cost **by model**, **by MCP call**, **by Skill call** (only servers/skills you installed yourself — built-ins are filtered out) -- **Project settlement**: usage grouped by local Git repo / working directory, exported as CSV without raw paths or conversation content -- **Reliability telemetry**: completed/aborted turns, tool errors/denials, tokens & cost wasted on aborted work -- **Context health**: median/peak context-window pressure, ≥80% warnings, compaction count, reasoning-token share -- **Provider limits**: live subscription quota for **Claude** (5-hour + weekly) and **Codex** (weekly + Spark), shown Codex-status style — `29% left (resets 05:01 on 9 Aug)` — with a burn-rate projection when enough trend points exist -- **Menu Bar Display**: show the tightest window per provider right in the menu bar (`52.8M · Cl79% · Cx29%`), a detailed list, or nothing at all -- Cost donut, year-long activity heatmap - -## Provider quota - -Subscription limits are collected through the **HappyUsage `hu` CLI**, which owns credential discovery, OAuth refresh, and the provider API calls (Claude via the Anthropic OAuth usage API, Codex via the ChatGPT usage API). - -- The `hu` binary **ships inside the app bundle** (`Resources/bin/hu`) — no system install needed; it is rebuilt on every release via `src-tauri/bin/build-hu.sh` -- Runtime lookup: bundled `hu` → system installs → a one-per-24h auto-install (Homebrew tap → install script → `go install`) → native Codex usage call (Pi / Codex CLI OAuth) → cached snapshot -- Quota refreshes every 5 minutes on a background thread; the last successful snapshot is cached (`provider_limits.json`) and survives offline / API failures -- Claude falls back to `~/.claude.json` `cachedUsageUtilization` (updated by Claude Code when it runs); Codex trend merges session-log rate-limit history -- Tokenscope **never caches credentials** — only quota metadata - -Menu-bar label: **Off** (`52.8M`) / **Compact** (`52.8M · Cl79% · Cx29%`, tightest window per provider) / **Detailed** (`52.8M · Cl 5h0/W79 · Cx W29/S31`). The right-click tray menu also has a **Provider Limits** submenu with every window's remaining percentage. - -## Data sources (local-first; session logs are read-only) - -| Purpose | Path | -|---------|------| -| Claude session logs (tokens / model / tool calls) | `~/.claude/projects/**/*.jsonl` | -| Codex session logs (tokens + offline quota fallback) | `~/.codex/sessions/**/*.jsonl` (honors `CODEX_HOME`) | -| Pi session logs (tokens / model / tools / telemetry) | `~/.pi/agent/sessions/**/*.jsonl` (honors `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, and absolute `settings.sessionDir`) | -| Provider quota (Claude + Codex) | Bundled HappyUsage `hu` (`Resources/bin/hu`); fallbacks: `~/.claude.json` `cachedUsageUtilization`, native usage API with Pi / Codex CLI OAuth | -| Claude user MCP whitelist | `~/.claude.json` → `mcpServers` + `projects[*].mcpServers` | -| Codex user MCP whitelist | Global and trusted-project `config.toml` → `[mcp_servers.*]` (`$CODEX_HOME/config.toml`, project `.codex/config.toml`) | -| User Skill whitelist | Claude: `~/.claude/skills/`; Codex: `$CODEX_HOME/skills/`, `~/.agents/skills/`, project `.agents/skills/`; Pi: global/shared/project Pi skill locations plus explicit settings paths | -| Model prices | **Primary**: [models.dev](https://models.dev/api.json) (bare model names, matching CLI logs) → **Fallback**: [LiteLLM](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json) → built-in snapshot. Cached in `~/Library/Caches/tokenscope/`, refreshed every 24h | - -## Key processing - -- **Claude**: deduplicated by `message.id` (streaming/retries repeat usage); one message spanning multiple lines merges its tool calls and counts tokens once -- **Codex**: a dedicated adapter cross-checks exact per-response `last_token_usage` against the accumulated `total_token_usage` snapshot, so quota-only repeats and fork replays are not counted as new work; stable `(turn, total snapshot)` ids deduplicate persisted replays, and `cached_input_tokens` / `cache_write_input_tokens` are split out of the inclusive `input_tokens` for a four-category accounting comparable with Claude -- **Pi**: each persisted assistant response contributes its exact four-way `usage`; stable tree-entry ids deduplicate history copied by `/fork` or `/clone`; persisted request cost, reasoning, stop reason, tool errors, compactions, and model context windows feed the matching telemetry -- **Token split**: `input` (uncached) / `cache` (creation + read) / `output`; the UI folds cache into "In" by default with a separate "cached %" -- **Pricing**: exact id → normalized id (strip vendor prefix, `.`↔`p`, e.g. `glm-5.1`⇄`glm-5p1`); models.dev's official bare-name price wins. Pi's persisted request cost is preferred when available. Models without any price still count tokens but are labelled "no price" -- **Tool classification**: MCP calls are attributed from direct `mcp____*` names, Codex tool-search mappings, or Codex App custom calls, then checked against the owning agent's config; Claude Skill calls plus Codex/Pi `SKILL.md` reads are matched against that agent's user/project skill directories; everything else is ignored - -> Cost is an **estimate** based on public prices; subscription users should read it as "equivalent spend value". - -### Token types & cost formula - -Every assistant message's `usage` reports four **mutually exclusive** token counts (they never double-count the same token): - -| Stage | `usage` field | What it is | Price (relative to input) | -|-------|---------------|------------|---------------------------| -| **Input** (uncached) | `input_tokens` | New prompt tokens sent this turn | 1× | -| **Cache write** | `cache_creation_input_tokens` | Context written into the prompt cache | ~1.25× | -| **Cache read** (hit) | `cache_read_input_tokens` | Context replayed from the cache | ~0.1× | -| **Output** | `output_tokens` | Tokens the model generated | ~5× | - -Pi persists the same categories as `usage.input`, `usage.cacheWrite`, `usage.cacheRead`, `usage.output`; its optional `usage.reasoning` is already a subset of output and never added again. +# Windows: grab the setup .exe from Releases ``` -total = input + cache_creation + cache_read + output -# UI shows: In = input + cache_creation + cache_read, Out = output, cached % = cache_read / total -cost = input × price.input - + cache_creation × price.cache_creation - + cache_read × price.cache_read # cache hits billed at the discounted read rate - + output × price.output -``` +Downloads (`.dmg` / `.exe`) and update details: see [Releases](https://github.com/SunChJ/tokenscope-remix/releases). + +## Quick start -A cache hit is **not** billed as normal input — it uses the dedicated cheaper `cache_read` rate, which is why heavily-cached usage shows a huge token count but a modest cost. +1. Launch — an icon with today's token count appears in the menu bar / tray +2. **Left-click** toggles the dashboard; **right-click** for the menu (Provider Limits, Menu Bar Display, Dashboard Shortcut, language, …) +3. **Menu Bar Display** controls the quota summary next to the token count: Off / Compact / Detailed ## Develop ```bash pnpm install -pnpm tauri dev # launch the desktop app (requires the Rust toolchain) -``` - -Frontend-only preview (using the real-data snapshot `public/dev-dashboard.json`): - -```bash -pnpm dev # http://localhost:1420 -# refresh the snapshot: -cd src-tauri && cargo run --example dump > ../public/dev-dashboard.json -``` - -## Build & release - -```bash -pnpm tauri build # macOS: .app / .dmg; Windows: NSIS .exe → src-tauri/target/release/bundle/ -``` - -The build downloads and bundles the `hu` binary (`src-tauri/bin/build-hu.sh`). Keep version files in sync, then push: - -```bash -pnpm version:set 1.5.1 -pnpm version:check -git commit -am "release: v1.5.1" -git push origin main +pnpm tauri dev # desktop app (requires the Rust toolchain) +pnpm tauri build # .app / .dmg (macOS), .exe (Windows) ``` -A version change pushed to `main` is validated by GitHub Actions, tagged `vX.Y.Z`, built and published for macOS and Windows with signed updater artifacts and `latest.json` (requires `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets). +## Docs -## Structure - -``` -src/ React frontend - data.ts types + Tauri bridge + theme + formatting - charts.tsx chart primitives (bars / donut / sparkline / heatmap / segmented control) - App.tsx main panel -src-tauri/src/ - store.rs incremental multi-source JSONL ingest (Claude + Codex + Pi adapters) - parser.rs aggregation into scopes (All / per-agent; presets + custom ranges + heatmap) - pricing.rs models.dev / LiteLLM price loading and costing - quota_api.rs provider quota via bundled HappyUsage `hu` + native/auto-install fallbacks - config.rs user MCP / Skill whitelists (Claude + Codex + Pi) - model.rs data structures returned to the frontend - lib.rs Tauri commands + menu-bar tray -src-tauri/bin/ build-hu.sh — downloads the hu binary bundled into releases -``` - -## Bug log - -Notable bugs found during development — symptom, root cause, and fix — are collected in [docs/BUGFIXES.md](docs/BUGFIXES.md). +- Data sources & accounting model: [docs/DESIGN-usage-api.md](docs/DESIGN-usage-api.md), [docs/DESIGN-pi-adapter.md](docs/DESIGN-pi-adapter.md), [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md) +- Known bugs and fixes: [docs/BUGFIXES.md](docs/BUGFIXES.md) ## Acknowledgements -This project started as a remix of [tokenscope](https://github.com/HduSy/tokenscope) by [@HduSy](https://github.com/HduSy) — a beautifully built macOS menu-bar dashboard for Claude CLI usage. The clean Rust ingest/aggregation architecture and the panel design this project inherits are his work, and this repo remains under the original MIT license. Huge thanks! 🙏 - -Tokenscope Remix extends it into a multi-agent dashboard (see [docs/DESIGN-multi-agent.md](docs/DESIGN-multi-agent.md)) and is maintained independently: please file issues and feature requests here, not upstream. Provider quota uses [HappyUsage](https://github.com/SunChJ/happyusage) (`hu`) for credential handling and provider API calls. +A remix of [tokenscope](https://github.com/HduSy/tokenscope) by [@HduSy](https://github.com/HduSy) — the clean Rust ingest/aggregation architecture and panel design are his work; this repo stays under the MIT license. Provider quota uses [HappyUsage](https://github.com/SunChJ/happyusage) (`hu`). Issues and feature requests go to this repository. diff --git a/docs/DESIGN-usage-api.md b/docs/DESIGN-usage-api.md index eda9992..aa19348 100644 --- a/docs/DESIGN-usage-api.md +++ b/docs/DESIGN-usage-api.md @@ -2,36 +2,33 @@ ## Decision -Live provider subscription quota is collected through the **HappyUsage `hu` -CLI** instead of a self-hosted credential + HTTP stack. `hu` already owns: +Live provider subscription quota is collected **only** through the HappyUsage +`hu` CLI. `hu` owns credential discovery, OAuth refresh, and the provider API +calls (Claude via the Anthropic OAuth usage API, Codex via the ChatGPT +`wham/usage` endpoint). -- Credential discovery (Claude Code keychain / `.credentials.json`, - Codex CLI `auth.json`, …) and OAuth refresh, -- Provider API calls (Claude via the Anthropic OAuth usage API, Codex via the - ChatGPT `wham/usage` endpoint), -- Per-provider error handling. - -Tokenscope runs `hu usage --json` and parses the envelope. This -keeps one credential implementation instead of duplicating it in Rust. +There is deliberately **no fallback to locally computed quota** (session-log +`rate_limits`, `~/.claude.json` caches, or previously persisted snapshots): +when `hu` cannot serve a provider, that provider shows an explicit +**unavailable** state. Local logs count tokens; they do not describe +subscription limits. ## Architecture ``` refresh thread (every 5 min) └─ quota_api::reload() - ├─ bundled hu (Resources/bin/hu) → `hu usage claude --json` → claude ProviderLimit - │ └─ on failure: ~/.claude.json cachedUsageUtilization (local fallback) + ├─ bundled hu → `hu usage claude --json` → claude ProviderLimit ├─ bundled hu → `hu usage codex --json` → codex ProviderLimit - │ └─ on failure: native wham/usage call (Pi / Codex CLI OAuth) - └─ persist ProviderCache → provider_limits.json (no credentials) + └─ failed fetch ⇒ that provider is None (unavailable) parser::build_dashboard() └─ quota_api::shared() → provider_limits (dashboard + tray) - └─ Codex windows merge session-log quota_history for trend depth + └─ trend: live points only, carried across refreshes in memory ``` -`provider_limits.json` caches the last successful snapshot per provider so a -later offline refresh (or a missing `hu`) still has data. The cache holds -quota metadata only — never tokens. +The in-memory cache holds only the current process's last successful readings +so trend points can accumulate; it is never persisted and never treated as a +data source when a refresh fails. ## Bundling @@ -39,23 +36,22 @@ quota metadata only — never tokens. current OS/arch before `tauri build`; `bundle.resources` ships it as `Resources/bin/hu`. At runtime `lib.rs` injects `resource_dir()` into `quota_api`, which prefers the bundled binary, then system installs. If no -`hu` exists at all, `ensure_hu()` attempts a one-per-24h auto-install -(Homebrew tap → official install script → `go install`) before the native -Codex fallback. +`hu` exists at all (development builds), `ensure_hu()` attempts a +one-per-24h auto-install (Homebrew tap → official install script → +`go install`). ## Normalization | Provider | Source | Windows | |---|---|---| | Claude | `hu` envelope `quotas[]` | `session` → 5-hour, `weekly` → 7d | -| Claude (fallback) | `~/.claude.json` `cachedUsageUtilization` | `five_hour`, `seven_day` | | Codex | `hu` envelope `quotas[]` | primary → Weekly, `Spark` → Spark | -- HappyUsage hardcodes Claude's period labels, so they are trusted. -- Codex's `period` label is **not** trusted: `limit_window_seconds` can be - absent from the window payload (observed on live API responses), which makes - `hu` fall back to a misleading `5h`. The 5h Codex window is retired anyway, - so both Codex windows are classified as weekly here. +- Matching is case-insensitive and tolerates renamed quota names across + happyusage versions (`Spark` / `Spark_weekly`, `session` / `weekly`). +- Codex's `period` label is **not** trusted (`limit_window_seconds` can be + absent, which makes `hu` fall back to a misleading `5h`); the 5h Codex + window is retired anyway, so both Codex windows are classified as weekly. - `used_pct` is retained in storage; presentation converts to rounded percentage left (`29% left (resets 05:01 on 9 Aug)`). @@ -68,16 +64,6 @@ Codex fallback. - Detailed: `52.8M · Cl 5h0/W79 · Cx W29/S31`. - Dashboard: a global two-column card (Claude | Codex), each window showing remaining capacity, the reset date, and an optional burn-rate projection. - Provider limits never attach to a token-usage scope (Pi / Codex CLI remain - only usage sources). - -## Fallback chain - -1. Bundled `hu` → `hu usage --json` -2. System `hu` (user-installed, may be newer) -3. One-per-24h auto-install of `hu` (Homebrew → install script → `go install`) -4. Native wham/usage call for Codex (Pi `openai-codex` OAuth → Codex CLI OAuth) -5. Last successful `provider_limits.json` snapshot -6. Claude-only: `~/.claude.json` `cachedUsageUtilization` (updated by Claude - Code when it runs) -7. Codex trend: session-log `rate_limits` history merged into the live window + A provider with no `hu` snapshot shows **Unavailable**; if both are + unavailable the whole section says so. Provider limits never attach to a + token-usage scope (Pi / Codex CLI remain only usage sources). diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index 82e2441..ee6f8b9 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -160,42 +160,12 @@ pub struct HeatDay { pub level: u8, // 0..4 } -/// Codex provider rate-limit snapshot. The authenticated usage API is primary; -/// session-log rate_limits are the offline fallback. `as_of_ms` flags staleness. +/// Provider limit windows are rolling weekly (Claude also has a 5-hour +/// window). Same-cycle reset timestamps may drift a few seconds between `hu` +/// calls, so trend continuity allows a small tolerance. pub const WEEKLY_WINDOW_MINUTES: u64 = 7 * 24 * 60; pub const RESET_CYCLE_TOLERANCE_SECONDS: i64 = 5 * 60; -#[derive(Debug, Clone, Serialize, serde::Deserialize)] -pub struct Quota { - pub plan: String, - #[serde(rename = "primaryPct")] - pub primary_pct: f64, - #[serde(rename = "primaryMinutes")] - pub primary_minutes: u64, - #[serde(rename = "primaryResetsAt")] - pub primary_resets_at: i64, // unix seconds - #[serde(rename = "secondaryPct")] - pub secondary_pct: f64, - #[serde(rename = "secondaryMinutes")] - pub secondary_minutes: u64, - #[serde(rename = "secondaryResetsAt")] - pub secondary_resets_at: i64, // unix seconds - #[serde(rename = "asOfMs")] - pub as_of_ms: i64, -} - -impl Quota { - pub fn weekly_window(&self) -> Option<(f64, i64)> { - if self.primary_minutes == WEEKLY_WINDOW_MINUTES { - Some((self.primary_pct, self.primary_resets_at)) - } else if self.secondary_minutes == WEEKLY_WINDOW_MINUTES { - Some((self.secondary_pct, self.secondary_resets_at)) - } else { - None - } - } -} - pub fn same_reset_cycle(left: i64, right: i64) -> bool { left == right || (left > 0 @@ -244,13 +214,6 @@ pub struct Scope { pub week: PeriodReport, pub month: PeriodReport, pub heatmap: Vec, - pub quota: Option, // Codex provider quota; may be owned by Pi scope - #[serde(rename = "sparkQuota")] - pub spark_quota: Option, // codex_bengalfox only - #[serde(rename = "quotaTrend")] - pub quota_trend: Vec, - #[serde(rename = "sparkQuotaTrend")] - pub spark_quota_trend: Vec, } #[derive(Debug, Clone, Serialize)] diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs index 31530ee..6dbab8b 100644 --- a/src-tauri/src/parser.rs +++ b/src-tauri/src/parser.rs @@ -129,73 +129,6 @@ fn vendor_of(model: &str) -> &'static str { } } -fn window_trend( - history: &[crate::store::QuotaHistoryPoint], - limit_id: &str, - used_pct: f64, - resets_at: i64, - as_of_ms: i64, - live_points: &[QuotaTrendPoint], -) -> Vec { - // API reset timestamps can drift by a few seconds between calls. Merge - // same-cycle log history with the live points instead of starting a - // trend from scratch, otherwise the dashboard lingers in "Collecting". - let mut points: Vec = history - .iter() - .filter(|point| point.limit_id == limit_id && same_reset_cycle(point.resets_at, resets_at)) - .map(|point| QuotaTrendPoint { - ts_ms: point.ts_ms, - used_pct: point.used_pct, - }) - .chain(live_points.iter().cloned()) - .collect(); - points.push(QuotaTrendPoint { - ts_ms: as_of_ms, - used_pct, - }); - points.sort_by_key(|point| point.ts_ms); - points.dedup_by(|right, left| { - if right.ts_ms == left.ts_ms { - left.used_pct = right.used_pct; - true - } else { - false - } - }); - const MAX_POINTS: usize = 48; - if points.len() <= MAX_POINTS { - return points; - } - (0..MAX_POINTS) - .map(|index| { - let source = index * (points.len() - 1) / (MAX_POINTS - 1); - points[source].clone() - }) - .collect() -} - -/// Attach session-log Codex quota history to the matching provider windows so -/// the trend has depth even before the next `hu` refresh. -fn merge_log_trends(limits: &mut [ProviderLimit], store: &crate::store::Store) { - for limit in limits.iter_mut().filter(|limit| limit.provider == "codex") { - for window in &mut limit.windows { - let limit_id = if window.id == "spark" { - "codex_bengalfox" - } else { - "codex" - }; - window.trend = window_trend( - &store.quota_history, - limit_id, - window.used_pct, - window.resets_at, - window.as_of_ms, - &window.trend, - ); - } - } -} - pub fn build_dashboard() -> Dashboard { let _guard = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); @@ -207,10 +140,8 @@ pub fn build_dashboard() -> Dashboard { store.save(); } // Provider subscription limits (Claude / Codex) come from the `hu` CLI via - // quota_api; session-log Codex rate limits add trend depth. Provider limits - // are global — they never attach to a token-usage scope. - let mut provider_limits = quota_api::shared(); - merge_log_trends(&mut provider_limits, &store); + // quota_api. They are global — they never attach to a token-usage scope. + let provider_limits = quota_api::shared(); let telemetry_since_ms = store.telemetry_since_ms; // 2. Aggregate: apply current config + prices, slice by current time. @@ -510,10 +441,6 @@ fn build_scope( week, month, heatmap, - quota: None, - spark_quota: None, - quota_trend: Vec::new(), - spark_quota_trend: Vec::new(), } } @@ -1348,40 +1275,6 @@ mod tests { assert_eq!(pct_delta(0.0, 100.0), -100.0); } - #[test] - fn quota_trend_merges_api_points_with_same_cycle_log_history() { - let history = vec![ - crate::store::QuotaHistoryPoint { - ts_ms: 1_000, - limit_id: "codex".to_string(), - used_pct: 10.0, - resets_at: 20_000, - }, - crate::store::QuotaHistoryPoint { - ts_ms: 2_000, - limit_id: "codex".to_string(), - used_pct: 20.0, - resets_at: 20_010, - }, - crate::store::QuotaHistoryPoint { - ts_ms: 3_000, - limit_id: "codex".to_string(), - used_pct: 99.0, - resets_at: 30_000, - }, - ]; - let live = vec![QuotaTrendPoint { - ts_ms: 3_500, - used_pct: 25.0, - }]; - - let trend = window_trend(&history, "codex", 30.0, 20_020, 4_000, &live); - assert_eq!(trend.len(), 4); - assert_eq!(trend[0].used_pct, 10.0); - assert_eq!(trend[2].used_pct, 25.0); - assert_eq!(trend[3].used_pct, 30.0); - } - #[test] fn custom_range_is_inclusive_and_compares_equal_previous_range() { let events = vec![ diff --git a/src-tauri/src/quota_api.rs b/src-tauri/src/quota_api.rs index adc1802..b8346e9 100644 --- a/src-tauri/src/quota_api.rs +++ b/src-tauri/src/quota_api.rs @@ -2,29 +2,17 @@ //! //! `hu` owns credential discovery, OAuth refresh, and provider API calls. //! Tokenscope invokes `hu usage --json`, parses the envelope, and -//! caches the result. When `hu` is missing or fails, Codex falls back to the -//! native wham/usage call (Pi / Codex CLI OAuth) so users without HappyUsage -//! still get quota; Claude falls back to Claude Code's local usage cache. -//! Codex session-log rate limits remain a parser-level fallback. +//! keeps the result in memory. There is deliberately no local fallback: when +//! `hu` cannot serve a provider, that provider shows as unavailable. use crate::model::{same_reset_cycle, LimitWindow, ProviderLimit, QuotaTrendPoint, WEEKLY_WINDOW_MINUTES}; -use base64::Engine; -use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; use std::path::PathBuf; use std::process::Command; use std::sync::{OnceLock, RwLock}; -use std::time::Duration; -const CODEX_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage"; -const CODEX_TOKEN_URL: &str = "https://auth.openai.com/oauth/token"; -const CODEX_OAUTH_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; -const CACHE_FILE: &str = "provider_limits.json"; -const CACHE_VERSION: u32 = 2; - -#[derive(Clone, Default, Serialize, Deserialize)] -#[serde(default)] +#[derive(Clone, Default)] struct ProviderCache { claude: Option, codex: Option, @@ -32,40 +20,12 @@ struct ProviderCache { static CACHE: OnceLock> = OnceLock::new(); -fn cache_dir() -> Option { - let dir = dirs::cache_dir()?.join("tokenscope"); - let _ = fs::create_dir_all(&dir); - Some(dir) -} - -fn cache_path() -> Option { - Some(cache_dir()?.join(CACHE_FILE)) -} - -fn load_cache() -> ProviderCache { - let Some(path) = cache_path() else { - return ProviderCache::default(); - }; - fs::read_to_string(path) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .filter(|cache| cache.version == CACHE_VERSION) - .map(|cache| cache.cache) - .unwrap_or_default() -} - -#[derive(Serialize, Deserialize)] -struct CacheEnvelope { - version: u32, - cache: ProviderCache, -} - fn cache_lock() -> &'static RwLock { - CACHE.get_or_init(|| RwLock::new(load_cache())) + CACHE.get_or_init(|| RwLock::new(ProviderCache::default())) } -/// Last successfully fetched provider limits (disk + memory). Empty when no -/// provider has ever produced a usable snapshot. +/// Last successfully fetched provider limits (in-memory only). Empty when no +/// provider has ever produced a usable snapshot in this process. pub fn shared() -> Vec { let cache = cache_lock() .read() @@ -81,21 +41,6 @@ pub fn shared() -> Vec { limits } -fn save_cache(cache: &ProviderCache) { - let Some(path) = cache_path() else { return }; - let envelope = CacheEnvelope { - version: CACHE_VERSION, - cache: cache.clone(), - }; - let Ok(text) = serde_json::to_vec(&envelope) else { - return; - }; - let tmp = path.with_extension(format!("tmp-{}", std::process::id())); - if fs::write(&tmp, text).is_ok() && fs::rename(&tmp, path).is_err() { - let _ = fs::remove_file(tmp); - } -} - fn hu_executables() -> Vec { let mut candidates = Vec::new(); // 1. Bundled copy (app Resources/bin/hu) — the primary source so the app @@ -316,56 +261,6 @@ fn claude_from_hu(envelope: &Value) -> Option { }) } -/// Claude windows from Claude Code's local `~/.claude.json` usage cache. -/// Used only when `hu` cannot reach the API (e.g. expired OAuth token). -fn claude_from_cache_file() -> Option { - let home = dirs::home_dir()?; - let json: Value = serde_json::from_str( - &fs::read_to_string(home.join(".claude.json")).ok()?, - ) - .ok()?; - let cache = json.get("cachedUsageUtilization")?; - let fetched_at = cache.get("fetchedAtMs").and_then(Value::as_i64)?; - let utilization = cache.get("utilization")?; - let mut windows = Vec::new(); - for (key, id, label, duration) in [ - ("five_hour", "5h", "5-hour", 300), - ( - "seven_day", - "weekly", - "Weekly", - WEEKLY_WINDOW_MINUTES as i64, - ), - ] { - let Some(window) = utilization.get(key) else { continue }; - let Some(used_pct) = window.get("utilization").and_then(Value::as_f64) else { - continue; - }; - let resets_at = rfc3339_ms(window.get("resets_at")) / 1000; - if resets_at <= 0 { - continue; - } - windows.push(LimitWindow { - id: id.to_string(), - label: label.to_string(), - duration_minutes: duration as u64, - used_pct, - resets_at, - as_of_ms: fetched_at, - trend: Vec::new(), - }); - } - if windows.is_empty() { - return None; - } - Some(ProviderLimit { - provider: "claude".to_string(), - label: "Claude".to_string(), - plan: String::new(), - windows, - }) -} - /// Codex windows from the `hu` envelope. The primary pool and Spark are both /// rolling weekly windows today (the 5h window is retired); HappyUsage's quota /// names have changed across versions (`session`/`weekly`, `Spark`/`Spark_weekly`), @@ -465,9 +360,8 @@ fn merge_trend( } /// Refresh provider limits, installing `hu` first when missing. Each provider -/// is independent: a failure keeps the previous successful snapshot. When `hu` -/// is absent or its Codex call fails, the native wham/usage call is the next -/// fallback, and Claude falls back to Claude Code's local usage cache. +/// is independent: `hu` is the only data source, so a failed fetch leaves that +/// provider unavailable (no local log or cache fallback). pub fn reload() -> bool { let previous = cache_lock() .read() @@ -480,17 +374,16 @@ pub fn reload() -> bool { if let Some(claude) = run_hu("claude").and_then(|envelope| claude_from_hu(&envelope)) { next.claude = Some(claude); - } else if next.claude.is_none() { - // `hu` cannot refresh Claude (expired OAuth) — fall back to the local - // Claude Code usage cache so the card still shows the last reading. - next.claude = claude_from_cache_file(); + } else { + // `hu` does not serve Claude usage right now (expired OAuth, rate + // limit, missing subscription): leave it unavailable instead of + // surfacing stale local data. + next.claude = None; } if let Some(codex) = run_hu("codex").and_then(|envelope| codex_from_hu(&envelope)) { next.codex = Some(codex); - } else if next.codex.is_none() { - // No `hu` (or its Codex call failed): try the native usage API using - // the Pi / Codex CLI OAuth credentials. - next.codex = codex_from_native(); + } else { + next.codex = None; } let changed = next.claude.is_some() || next.codex.is_some(); @@ -498,7 +391,6 @@ pub fn reload() -> bool { return false; } merge_trend(&previous, &mut next); - save_cache(&next); if let Ok(mut cache) = cache_lock().write() { *cache = next; true @@ -507,407 +399,10 @@ pub fn reload() -> bool { } } -// ── Native Codex fallback (no HappyUsage) ──────────────────────────── - -#[derive(Clone)] -struct Credential { - access: String, - account_id: String, - refresh: String, -} - -fn pi_agent_dir() -> Option { - std::env::var_os("PI_CODING_AGENT_DIR") - .map(PathBuf::from) - .or_else(|| Some(dirs::home_dir()?.join(".pi").join("agent"))) -} - -fn jwt_account_id(token: &str) -> String { - let Some(payload) = token.split('.').nth(1) else { - return String::new(); - }; - let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload) else { - return String::new(); - }; - serde_json::from_slice::(&bytes) - .ok() - .and_then(|json| { - json.get("https://api.openai.com/auth")? - .get("chatgpt_account_id")? - .as_str() - .map(str::to_owned) - }) - .unwrap_or_default() -} - -/// Pi's own `openai-codex` OAuth credential (~/.pi/agent/auth.json). -fn read_pi_credential() -> Option { - let path = pi_agent_dir()?.join("auth.json"); - let json: Value = serde_json::from_str(&fs::read_to_string(path).ok()?).ok()?; - let auth = json.get("openai-codex")?; - if auth.get("type").and_then(Value::as_str) != Some("oauth") { - return None; - } - let access = auth.get("access")?.as_str()?.to_string(); - if access.is_empty() { - return None; - } - let account_id = auth - .get("accountId") - .and_then(Value::as_str) - .map(str::to_owned) - .filter(|id| !id.is_empty()) - .unwrap_or_else(|| jwt_account_id(&access)); - Some(Credential { - access, - account_id, - refresh: auth - .get("refresh") - .and_then(Value::as_str) - .unwrap_or("") - .to_string(), - }) -} - -fn decode_hex(text: &str) -> Option> { - let text = text - .strip_prefix("0x") - .or_else(|| text.strip_prefix("0X")) - .unwrap_or(text); - if text.is_empty() || !text.is_ascii() || !text.len().is_multiple_of(2) { - return None; - } - (0..text.len()) - .step_by(2) - .map(|index| u8::from_str_radix(&text[index..index + 2], 16).ok()) - .collect() -} - -fn parse_json_or_hex(text: &str) -> Option { - let text = text.trim(); - serde_json::from_str(text).ok().or_else(|| { - let bytes = decode_hex(text)?; - serde_json::from_slice(&bytes).ok() - }) -} - -fn codex_auth_paths() -> Vec { - if let Some(home) = std::env::var_os("CODEX_HOME") { - return vec![PathBuf::from(home).join("auth.json")]; - } - let Some(home) = dirs::home_dir() else { - return Vec::new(); - }; - vec![ - home.join(".config").join("codex").join("auth.json"), - home.join(".codex").join("auth.json"), - ] -} - -fn read_codex_credential() -> Option { - codex_auth_paths() - .into_iter() - .filter_map(|path| { - let json = parse_json_or_hex(&fs::read_to_string(&path).ok()?)?; - let tokens = json.get("tokens")?; - let access = tokens.get("access_token")?.as_str()?.to_string(); - if access.is_empty() { - return None; - } - let account_id = tokens - .get("account_id") - .and_then(Value::as_str) - .map(str::to_owned) - .filter(|id| !id.is_empty()) - .unwrap_or_else(|| jwt_account_id(&access)); - Some(Credential { - access, - account_id, - refresh: tokens - .get("refresh_token") - .and_then(Value::as_str) - .unwrap_or("") - .to_string(), - }) - }) - .next() -} - -fn pi_executables() -> Vec { - let mut candidates = Vec::new(); - if let Some(path) = std::env::var_os("PATH").and_then(|paths| { - std::env::split_paths(&paths) - .map(|dir| dir.join(if cfg!(windows) { "pi.exe" } else { "pi" })) - .find(|path| path.is_file()) - }) { - candidates.push(path); - } - for path in ["/opt/homebrew/bin/pi", "/usr/local/bin/pi"] { - let path = PathBuf::from(path); - if path.is_file() && !candidates.contains(&path) { - candidates.push(path); - } - } - candidates -} - -/// Ask Pi itself for a fresh token so its credential lock and persistence -/// stay authoritative; Tokenscope never rewrites Pi's auth.json. -fn refreshed_pi_credential(_previous: &Credential) -> Option { - for executable in pi_executables() { - let output = Command::new(executable) - .args([ - "auth", - "print-bearer-token", - "--provider", - "openai-codex", - "--model", - "gpt-5.5", - "--min-expiry", - "30m", - ]) - .output() - .ok()?; - if !output.status.success() { - continue; - } - let Ok(stdout) = String::from_utf8(output.stdout) else { - continue; - }; - let access = stdout.trim().to_string(); - if access.is_empty() { - continue; - } - let stored = read_pi_credential(); - let account_id = stored - .as_ref() - .map(|credential| credential.account_id.clone()) - .filter(|id| !id.is_empty()) - .unwrap_or_else(|| jwt_account_id(&access)); - return Some(Credential { - access, - account_id, - refresh: stored - .map(|credential| credential.refresh) - .unwrap_or_else(|| _previous.refresh.clone()), - }); - } - None -} - -fn persist_codex_credential( - path: &PathBuf, - previous_refresh: &str, - access: &str, - refresh: &str, -) -> bool { - let Some(mut json) = fs::read_to_string(path) - .ok() - .and_then(|text| parse_json_or_hex(&text)) - else { - return false; - }; - let Some(tokens) = json.get_mut("tokens").and_then(Value::as_object_mut) else { - return false; - }; - if tokens - .get("refresh_token") - .and_then(Value::as_str) - .unwrap_or("") - != previous_refresh - { - return false; - } - tokens.insert("access_token".to_string(), Value::String(access.to_string())); - tokens.insert("refresh_token".to_string(), Value::String(refresh.to_string())); - let account_id = jwt_account_id(access); - if !account_id.is_empty() { - tokens.insert("account_id".to_string(), Value::String(account_id)); - } - let Ok(bytes) = serde_json::to_vec_pretty(&json) else { - return false; - }; - let tmp = path.with_extension(format!("tmp-{}", std::process::id())); - if fs::write(&tmp, bytes).is_err() { - return false; - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o600)); - } - if fs::rename(&tmp, path).is_err() { - let _ = fs::remove_file(tmp); - return false; - } - true -} - -fn refreshed_codex_credential(previous: &Credential) -> Option { - if previous.refresh.is_empty() { - return None; - } - let response = ureq::post(CODEX_TOKEN_URL) - .timeout(Duration::from_secs(15)) - .set("Content-Type", "application/x-www-form-urlencoded") - .send_form(&[ - ("grant_type", "refresh_token"), - ("client_id", CODEX_OAUTH_CLIENT_ID), - ("refresh_token", previous.refresh.as_str()), - ]) - .ok()?; - let json: Value = response.into_json().ok()?; - let access = json.get("access_token")?.as_str()?.to_string(); - let refresh = json - .get("refresh_token") - .and_then(Value::as_str) - .unwrap_or(&previous.refresh) - .to_string(); - let path = codex_auth_paths().into_iter().find(|path| path.is_file())?; - if !persist_codex_credential(&path, &previous.refresh, &access, &refresh) { - return None; - } - Some(Credential { - account_id: jwt_account_id(&access), - access, - refresh, - }) -} - -fn refresh_credential(previous: &Credential) -> Option { - // Try the Pi CLI refresh first (most likely to have a working session), - // then the Codex CLI OAuth refresh. - refreshed_pi_credential(previous).or_else(|| refreshed_codex_credential(previous)) -} - -fn request_usage(credential: &Credential) -> Result<(Value, Option, Option), u16> { - let mut request = ureq::get(CODEX_USAGE_URL) - .timeout(Duration::from_secs(15)) - .set("Authorization", &format!("Bearer {}", credential.access)) - .set("Accept", "application/json") - .set("User-Agent", "Tokenscope"); - if !credential.account_id.is_empty() { - request = request.set("ChatGPT-Account-Id", &credential.account_id); - } - let response = match request.call() { - Ok(response) => response, - Err(ureq::Error::Status(status, _)) => return Err(status), - Err(_) => return Err(0), - }; - let primary = response - .header("X-Codex-Primary-Used-Percent") - .and_then(|value| value.parse().ok()); - let secondary = response - .header("X-Codex-Secondary-Used-Percent") - .and_then(|value| value.parse().ok()); - response - .into_json::() - .map(|body| (body, primary, secondary)) - .map_err(|_| 0) -} - -fn native_number(value: Option<&Value>) -> Option { - match value? { - Value::Number(number) => number.as_f64(), - Value::String(number) => number.parse().ok(), - _ => None, - } -} - -/// Native wham/usage decoding → the same ProviderLimit shape as `hu`'s Codex -/// envelope: primary pool becomes Weekly, `codex_bengalfox` becomes Spark. -fn codex_from_native() -> Option { - let credential = read_pi_credential().or_else(read_codex_credential)?; - let (body, primary_header, _) = request_usage(&credential) - .or_else(|status| { - if status != 401 { - return Err(status); - } - let refreshed = refresh_credential(&credential).ok_or(status)?; - request_usage(&refreshed) - }) - .ok()?; - let plan = body.get("plan_type").and_then(Value::as_str).unwrap_or(""); - let now_ms = now_ms(); - let mut windows = Vec::new(); - if let Some(rate_limit) = body.get("rate_limit") { - if let Some(primary) = rate_limit.get("primary_window") { - let used = primary_header - .or_else(|| native_number(primary.get("used_percent"))) - .unwrap_or(0.0); - let resets_at = primary - .get("reset_at") - .and_then(|value| value.as_i64()) - .or_else(|| { - native_number(primary.get("reset_after_seconds")) - .map(|seconds| now_ms / 1000 + seconds as i64) - }) - .unwrap_or(0); - windows.push(LimitWindow { - id: "weekly".to_string(), - label: "Weekly".to_string(), - duration_minutes: WEEKLY_WINDOW_MINUTES, - used_pct: used, - resets_at, - as_of_ms: now_ms, - trend: Vec::new(), - }); - } - } - if let Some(limits) = body - .get("additional_rate_limits") - .and_then(Value::as_array) - { - for entry in limits { - let is_spark = entry - .get("metered_feature") - .and_then(Value::as_str) - == Some("codex_bengalfox") - || entry - .get("limit_name") - .and_then(Value::as_str) - .is_some_and(|name| name.to_ascii_lowercase().contains("spark")); - if !is_spark { - continue; - } - if let Some(rate_limit) = entry.get("rate_limit") { - if let Some(primary) = rate_limit.get("primary_window") { - let used = native_number(primary.get("used_percent")).unwrap_or(0.0); - let resets_at = primary - .get("reset_at") - .and_then(|value| value.as_i64()) - .or_else(|| { - native_number(primary.get("reset_after_seconds")) - .map(|seconds| now_ms / 1000 + seconds as i64) - }) - .unwrap_or(0); - windows.push(LimitWindow { - id: "spark".to_string(), - label: "Spark".to_string(), - duration_minutes: WEEKLY_WINDOW_MINUTES, - used_pct: used, - resets_at, - as_of_ms: now_ms, - trend: Vec::new(), - }); - } - } - } - } - if windows.is_empty() { - return None; - } - Some(ProviderLimit { - provider: "codex".to_string(), - label: "Codex".to_string(), - plan: plan.to_string(), - windows, - }) -} - #[cfg(test)] mod tests { - use super::*; + use super::{claude_from_hu, codex_from_hu}; + use crate::model::WEEKLY_WINDOW_MINUTES; #[test] fn parses_claude_envelope_windows() { @@ -954,48 +449,6 @@ mod tests { assert_eq!(limit.windows[1].used_pct, 69.0); } - #[test] - fn parses_claude_local_cache_shape() { - let json = serde_json::json!({ - "cachedUsageUtilization": { - "fetchedAtMs": 1_785_485_070_985i64, - "utilization": { - "five_hour": {"utilization": 0, "resets_at": "2026-07-31T12:50:00Z"}, - "seven_day": {"utilization": 21, "resets_at": "2026-07-31T21:00:00Z"} - } - } - }); - let cache = json.get("cachedUsageUtilization").unwrap(); - let fetched_at = cache.get("fetchedAtMs").and_then(Value::as_i64).unwrap(); - let utilization = cache.get("utilization").unwrap(); - let mut windows = Vec::new(); - for (key, id, label, duration) in [ - ("five_hour", "5h", "5-hour", 300), - ("seven_day", "weekly", "Weekly", WEEKLY_WINDOW_MINUTES as i64), - ] { - let Some(window) = utilization.get(key) else { continue }; - let Some(used_pct) = window.get("utilization").and_then(Value::as_f64) else { - continue; - }; - let resets_at = rfc3339_ms(window.get("resets_at")) / 1000; - windows.push(LimitWindow { - id: id.to_string(), - label: label.to_string(), - duration_minutes: duration as u64, - used_pct, - resets_at, - as_of_ms: fetched_at, - trend: Vec::new(), - }); - } - assert_eq!(windows.len(), 2); - assert_eq!(windows[0].id, "5h"); - assert_eq!(windows[0].used_pct, 0.0); - assert_eq!(windows[1].id, "weekly"); - assert_eq!(windows[1].used_pct, 21.0); - assert_eq!(windows[1].duration_minutes, WEEKLY_WINDOW_MINUTES); - } - #[test] fn parses_codex_0211_names_and_ignores_missing_provider() { // happyusage 0.2.11 renames primary to `weekly` and Spark to @@ -1054,4 +507,5 @@ mod tests { }); assert!(claude_from_hu(&envelope).is_none()); } + } diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 9edb502..5b635fc 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -13,10 +13,10 @@ // codex — ~/.codex/sessions/**/*.jsonl (token_count turn deltas) // pi — ~/.pi/agent/sessions/**/*.jsonl (assistant messages) use crate::codex_adapter::{self, TokenCountOutcome}; -use crate::model::Quota; + use chrono::DateTime; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fs; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; @@ -63,20 +63,10 @@ pub struct TurnTelemetry { usage_ids: Vec, } -#[derive(Serialize, Deserialize, Clone, Default)] -pub struct QuotaHistoryPoint { - pub ts_ms: i64, - pub limit_id: String, - pub used_pct: f64, - pub resets_at: i64, -} - #[derive(Serialize, Deserialize, Default)] struct TelemetryCache { since_ms: i64, turns: Vec, - #[serde(default)] - quota_history: Vec, } #[derive(Serialize, Deserialize, Clone)] @@ -181,13 +171,9 @@ pub struct Store { // count its token usage only once. index: HashMap, manifest: Manifest, - // Latest general and model-specific Codex rate-limit snapshots. - pub codex_quota: Option, - pub codex_spark_quota: Option, projects_by_source: HashMap, pub telemetry_since_ms: i64, pub turns: Vec, - pub quota_history: Vec, telemetry_index: HashMap, } @@ -222,17 +208,8 @@ const STORE_VERSION: u32 = 13; // v3: during the v2 migration, fully read source files that contain Spark // usage. A session can switch away from Spark after its last Spark snapshot, // leaving its model context outside the usual 64 KiB quota-file tail. -// v4: an explicit `codex_bengalfox` limit id always identifies Spark. Real -// concurrent sessions can report that id while the active model context still -// says `gpt-5.6-sol`; trusting the model first temporarily put Spark's quota in -// the standard Codex cache. -const QUOTA_CACHE_VERSION: u32 = 4; const PROJECT_CACHE_VERSION: u32 = 1; const TELEMETRY_CACHE_VERSION: u32 = 1; -// One-time quota migration: tail the 32 newest files, and fully read only -// files which already contain Spark usage in the local event cache. -const RECENT_QUOTA_FILES: usize = 32; -const QUOTA_TAIL_BYTES: u64 = 64 * 1024; /// Atomically replace `path`'s contents: write a sibling temp file, then rename /// over the target (same-volume rename is atomic on Windows and Unix). Avoids @@ -389,51 +366,15 @@ fn project_ref(cwd: Option<&str>, fallback: &str) -> ProjectRef { } } -const CODEX_QUOTA_POOL: &str = "codex"; -const SPARK_QUOTA_POOL: &str = "codex_bengalfox"; - -fn is_codex_spark_model(model: &str) -> bool { - let model = model - .trim() - .rsplit('/') - .next() - .unwrap_or_default() - .to_ascii_lowercase(); - model == "gpt-5.3-codex-spark" || model.starts_with("gpt-5.3-codex-spark-") -} - -/// Assign a quota snapshot to its independent rate-limit pool. -/// -/// An explicit `codex_bengalfox` id is authoritative. The `codex` id is -/// ambiguous because Codex also emits it for Spark turns, so model identity -/// disambiguates that case. -fn codex_quota_pool(model: &str, rl: &serde_json::Value) -> Option<&'static str> { - let limit_id = rl.get("limit_id").and_then(|value| value.as_str()); - if limit_id == Some(SPARK_QUOTA_POOL) { - return Some(SPARK_QUOTA_POOL); - } - if !matches!(limit_id, None | Some(CODEX_QUOTA_POOL)) { - return None; - } - if is_codex_spark_model(model) { - return Some(SPARK_QUOTA_POOL); - } - Some(CODEX_QUOTA_POOL) -} - impl Store { /// Load persisted events + offset manifest (empty on first run). pub fn load() -> Self { let mut events: Vec = Vec::new(); let mut manifest = Manifest::default(); - let mut codex_quota = None; - let mut codex_spark_quota = None; let mut projects_by_source = HashMap::new(); let mut telemetry_since_ms = chrono::Utc::now().timestamp_millis(); let mut turns = Vec::new(); - let mut quota_history = Vec::new(); let mut version_ok = false; - let mut quota_cache_ok = false; let mut telemetry_cache_ok = false; if let Some(dir) = cache_dir() { // If the cache was written by an older parser, discard it so ingest @@ -460,19 +401,6 @@ impl Store { events = e; manifest = m; } - quota_cache_ok = fs::read_to_string(dir.join("quota_version")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - == Some(QUOTA_CACHE_VERSION); - if quota_cache_ok { - codex_quota = fs::read_to_string(dir.join("codex_quota.json")) - .ok() - .and_then(|t| serde_json::from_str::(&t).ok()); - codex_spark_quota = - fs::read_to_string(dir.join("codex_bengalfox_quota.json")) - .ok() - .and_then(|t| serde_json::from_str::(&t).ok()); - } let project_cache_ok = fs::read_to_string(dir.join("project_version")) .ok() .and_then(|text| text.trim().parse::().ok()) @@ -494,7 +422,6 @@ impl Store { { telemetry_since_ms = cache.since_ms; turns = cache.turns; - quota_history = cache.quota_history; telemetry_cache_ok = true; } } @@ -515,23 +442,11 @@ impl Store { events, index, manifest, - codex_quota, - codex_spark_quota, projects_by_source, telemetry_since_ms, turns, - quota_history, telemetry_index, }; - if version_ok && !quota_cache_ok { - store.rebuild_codex_quotas(); - if let Some(dir) = cache_dir() { - store.save_quota_cache(&dir); - // The quota trend uses the same pool id as the current - // snapshot, so persist the rebuilt history with it. - store.save_telemetry_cache(&dir); - } - } if version_ok && store.refresh_projects() { if let Some(dir) = cache_dir() { store.save_project_cache(&dir); @@ -558,7 +473,6 @@ impl Store { if let Ok(t) = serde_json::to_string(&self.manifest) { let _ = write_atomic(&dir.join("offsets.json"), t.as_bytes()); } - self.save_quota_cache(&dir); self.save_project_cache(&dir); self.save_telemetry_cache(&dir); let _ = write_atomic(&dir.join("version"), STORE_VERSION.to_string().as_bytes()); @@ -569,7 +483,6 @@ impl Store { let cache = TelemetryCache { since_ms: self.telemetry_since_ms, turns: self.turns.clone(), - quota_history: self.quota_history.clone(), }; if let Ok(text) = serde_json::to_string(&cache) { let _ = write_atomic(&dir.join("telemetry.json"), text.as_bytes()); @@ -590,128 +503,6 @@ impl Store { ); } - fn save_quota_cache(&self, dir: &std::path::Path) { - let save = |name: &str, quota: &Option| match quota { - Some(quota) => { - if let Ok(text) = serde_json::to_string(quota) { - let _ = write_atomic(&dir.join(name), text.as_bytes()); - } - } - None => { - let _ = fs::remove_file(dir.join(name)); - } - }; - save("codex_quota.json", &self.codex_quota); - save("codex_bengalfox_quota.json", &self.codex_spark_quota); - let _ = write_atomic( - &dir.join("quota_version"), - QUOTA_CACHE_VERSION.to_string().as_bytes(), - ); - } - - /// One-time migration from the old single-quota cache. Read only the tails - /// of recently active Codex logs; token_count snapshots occur frequently, - /// so this separates current quota buckets without rescanning usage history. - fn rebuild_codex_quotas(&mut self) { - self.codex_quota = None; - self.codex_spark_quota = None; - self.quota_history.clear(); - let spark_sources: HashSet = self - .events - .iter() - .filter(|event| is_codex_spark_model(&event.model)) - .map(|event| event.source.clone()) - .filter(|source| !source.is_empty()) - .collect(); - let Some(root) = codex_dir() else { - return; - }; - let mut files: Vec<_> = WalkDir::new(root) - .into_iter() - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .path() - .extension() - .map(|x| x == "jsonl") - .unwrap_or(false) - }) - .filter_map(|entry| { - let modified = entry.metadata().ok()?.modified().ok()?; - Some((modified, entry.into_path())) - }) - .collect(); - // Read the recent files oldest-first so quota history remains ordered. - // `update_codex_quota` still compares timestamps before replacing a - // current snapshot, so this is safe if filesystem mtimes are imperfect. - files.sort_by_key(|item| item.0); - let recent_start = files.len().saturating_sub(RECENT_QUOTA_FILES); - - for (index, (_, path)) in files.into_iter().enumerate() { - let key = path.to_string_lossy().to_string(); - let full_scan = spark_sources.contains(&key); - if index < recent_start && !full_scan { - continue; - } - let Ok(mut file) = fs::File::open(&path) else { - continue; - }; - let Ok(size) = file.metadata().map(|meta| meta.len()) else { - continue; - }; - let start = if full_scan { - 0 - } else { - size.saturating_sub(QUOTA_TAIL_BYTES) - }; - if file.seek(SeekFrom::Start(start)).is_err() { - continue; - } - let mut bytes = Vec::with_capacity((size - start) as usize); - if file.read_to_end(&mut bytes).is_err() { - continue; - } - let skip = if start == 0 { - 0 - } else { - bytes - .iter() - .position(|byte| *byte == b'\n') - .map(|i| i + 1) - .unwrap_or(bytes.len()) - }; - // Tails usually begin after their last turn_context, so seed them - // with the final model from normal ingest. Spark source files are - // read from their start, where each turn_context is available. - // Keep `source` blank so this quota-only migration cannot alter - // persisted turn telemetry. - let mut state = if full_scan { - FileState::default() - } else { - self.manifest.files.get(&key).cloned().unwrap_or_default() - }; - state.source.clear(); - for line in bytes[skip..].split(|byte| *byte == b'\n') { - let Ok(text) = std::str::from_utf8(line) else { - continue; - }; - // `session_meta` is what arms the forked-thread replay skip, so - // a full scan must feed it through too — otherwise `replaying` - // never gets set and the parent's restamped rate_limits (216 in - // one sampled fork, all stamped with the fork instant) would be - // ingested as fresh quota readings. Tails don't need it: they - // inherit the flag from the manifest and start well past the - // replay window anyway. - if text.contains("\"session_meta\"") - || text.contains("\"turn_context\"") - || text.contains("\"rate_limits\"") - { - let _ = self.parse_codex_line(text, &mut state); - } - } - } - } - /// Working directories seen in session headers. Config uses these to find /// project-scoped agent resources without crawling the user's home dir. pub fn project_dirs(&self) -> Vec { @@ -1144,22 +935,16 @@ impl Store { let TokenCountOutcome::Usage(usage) = outcome else { // Estimated context snapshots and unchanged-total quota // heartbeats are not new upstream usage. - if let Some(rl) = payload.get("rate_limits") { - self.update_codex_quota(rl, ts_ms, &state.model); - } return None; }; // A replay can carry a different rate-limit payload while still // naming the same upstream response. The stable (turn,total) - // usage id is the second line of defense and must precede quota - // and telemetry side effects. + // usage id is the second line of defense and must precede + // telemetry side effects. if !usage.event_id.is_empty() && self.index.contains_key(&usage.event_id) { return None; } - if let Some(rl) = payload.get("rate_limits") { - self.update_codex_quota(rl, ts_ms, &state.model); - } let context_window = payload .get("info") .and_then(|info| info.get("model_context_window")) @@ -1554,84 +1339,6 @@ impl Store { parse_claude_value(&value) } - - /// Keep the newest rate-limit snapshot. Spark and standard Codex models - /// consume separate pools. Honor an explicit Spark pool id first, then use - /// the active model to disambiguate snapshots carrying the shared `codex` - /// id. - fn update_codex_quota(&mut self, rl: &serde_json::Value, ts_ms: i64, model: &str) { - let Some(limit_id) = codex_quota_pool(model, rl) else { - return; - }; - let previous = if limit_id == CODEX_QUOTA_POOL { - self.codex_quota.as_ref() - } else { - self.codex_spark_quota.as_ref() - }; - if previous.map(|quota| quota.as_of_ms >= ts_ms).unwrap_or(false) { - return; - } - let win = |k: &str| rl.get(k); - let num = |w: Option<&serde_json::Value>, k: &str| { - w.and_then(|x| x.get(k)).and_then(|x| x.as_f64()).unwrap_or(0.0) - }; - let (p, s) = (win("primary"), win("secondary")); - // A snapshot with no windows at all carries no information — skip it. - if p.is_none() && s.is_none() { - return; - } - let quota = Quota { - plan: rl - .get("plan_type") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(), - primary_pct: num(p, "used_percent"), - primary_minutes: num(p, "window_minutes") as u64, - primary_resets_at: num(p, "resets_at") as i64, - secondary_pct: num(s, "used_percent"), - secondary_minutes: num(s, "window_minutes") as u64, - secondary_resets_at: num(s, "resets_at") as i64, - as_of_ms: ts_ms, - }; - self.record_quota_history(limit_id, "a); - if limit_id == CODEX_QUOTA_POOL { - self.codex_quota = Some(quota); - } else { - self.codex_spark_quota = Some(quota); - } - } - - fn record_quota_history(&mut self, limit_id: &str, quota: &Quota) { - let Some((used_pct, resets_at)) = quota.weekly_window() else { - return; - }; - let last = self - .quota_history - .iter() - .rev() - .find(|point| point.limit_id == limit_id); - if let Some(last) = last { - if quota.as_of_ms <= last.ts_ms { - return; - } - let same_value = (used_pct - last.used_pct).abs() < 0.1; - let recent = quota.as_of_ms - last.ts_ms < 15 * 60 * 1000; - if resets_at == last.resets_at && same_value && recent { - return; - } - } - self.quota_history.push(QuotaHistoryPoint { - ts_ms: quota.as_of_ms, - limit_id: limit_id.to_string(), - used_pct, - resets_at, - }); - if self.quota_history.len().is_multiple_of(256) { - let cutoff = quota.as_of_ms - 180 * 24 * 60 * 60 * 1000; - self.quota_history.retain(|point| point.ts_ms >= cutoff); - } - } } fn pi_usage_cost(usage: &serde_json::Value) -> Option { @@ -2109,88 +1816,15 @@ mod tests { events: Vec::new(), index: HashMap::new(), manifest: Manifest::default(), - codex_quota: None, - codex_spark_quota: None, projects_by_source: HashMap::new(), telemetry_since_ms: 0, turns: Vec::new(), - quota_history: Vec::new(), telemetry_index: HashMap::new(), } } #[test] - fn keeps_spark_and_standard_codex_quotas_separate_by_model() { - let mut store = empty_store(); - let snapshot = |limit_id: &str, used_percent: f64| { - serde_json::json!({ - "limit_id": limit_id, - "primary": { - "used_percent": used_percent, - "window_minutes": 10080, - "resets_at": 123 - } - }) - }; - - // Recent Spark JSONL events use `limit_id: codex`; model identity must - // still put that weekly usage in Spark's independent pool. - store.update_codex_quota(&snapshot("codex", 7.0), 100, "gpt-5.3-codex-spark"); - store.update_codex_quota(&snapshot("codex", 24.05), 200, "gpt-5.6-sol"); - // Explicit pool ids remain authoritative even when concurrent session - // context reports the same non-Spark model for both snapshots. - store.update_codex_quota( - &snapshot("codex_bengalfox", 24.1), - 300, - "gpt-5.6-sol", - ); - - assert_eq!(store.codex_quota.unwrap().primary_pct, 24.05); - assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 24.1); - assert_eq!(store.quota_history.len(), 3); - } - - #[test] - fn explicit_spark_limit_id_overrides_model_context() { - let explicit_spark = serde_json::json!({ - "limit_id": "codex_bengalfox", - "primary": { - "used_percent": 7.0, - "window_minutes": 10080, - "resets_at": 123 - } - }); - - assert_eq!( - codex_quota_pool("", &explicit_spark), - Some(SPARK_QUOTA_POOL) - ); - assert_eq!( - codex_quota_pool("chatgpt/gpt-5.3-codex-spark", &explicit_spark), - Some(SPARK_QUOTA_POOL) - ); - assert_eq!( - codex_quota_pool("gpt-5.6-sol", &explicit_spark), - Some(SPARK_QUOTA_POOL) - ); - } - - #[test] - fn routes_codex_limit_id_to_spark_after_a_spark_turn_context() { - let mut store = empty_store(); - let mut state = FileState::default(); - let spark_context = r#"{"timestamp":"2026-07-15T09:50:00Z","type":"turn_context","payload":{"model":"gpt-5.3-codex-spark","turn_id":"spark-turn"}}"#; - let spark_usage = r#"{"timestamp":"2026-07-15T09:51:13Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}},"rate_limits":{"limit_id":"codex","primary":{"used_percent":21,"window_minutes":10080,"resets_at":1784684425}}}}"#; - - assert!(store.parse_codex_line(spark_context, &mut state).is_none()); - assert!(store.parse_codex_line(spark_usage, &mut state).is_some()); - - assert!(store.codex_quota.is_none()); - assert_eq!(store.codex_spark_quota.unwrap().primary_pct, 21.0); - } - - #[test] - fn ignores_unchanged_codex_usage_but_keeps_its_new_quota() { + fn ignores_unchanged_codex_usage_heartbeats() { let mut store = empty_store(); let mut state = FileState { source: "/tmp/codex-heartbeat.jsonl".to_string(), @@ -2220,8 +1854,9 @@ mod tests { store.index.insert(first.id.clone(), store.events.len()); store.events.push(first); - // A new turn starts by publishing fresher quota with the previous - // positive last_token_usage and an unchanged accumulated total. + // A new turn starts by republishing the previous positive + // last_token_usage with an unchanged accumulated total: a heartbeat, + // not new upstream work. store.parse_codex_line(&context("turn-b"), &mut state); assert!(store .parse_codex_line( @@ -2230,7 +1865,6 @@ mod tests { ) .is_none()); assert_eq!(store.events.len(), 1); - assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 11.0); let second = store .parse_codex_line( @@ -2399,67 +2033,6 @@ mod tests { assert_eq!(fresh.in_tok, 300.0); } - // A replayed usage event must not update the quota either: it carries the - // parent's stale rate_limits restamped with the fork instant, so letting it - // through made a long-past 100% reading beat the real current one. - #[test] - fn replayed_rate_limits_do_not_overwrite_the_current_quota() { - let mut store = empty_store(); - let line = |pct: f64, ts: &str, total_input: u64, total_output: u64| { - format!( - r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"last_token_usage":{{"input_tokens":5,"cached_input_tokens":0,"output_tokens":1,"total_tokens":6}},"total_token_usage":{{"input_tokens":{total_input},"cached_input_tokens":0,"output_tokens":{total_output},"total_tokens":{total}}}}},"rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{pct},"window_minutes":10080,"resets_at":100}}}}}}}}"#, - total = total_input + total_output, - ) - }; - let context = |turn: &str, ts: &str| { - format!( - r#"{{"timestamp":"{ts}","type":"turn_context","payload":{{"model":"gpt-5.6-sol","turn_id":"{turn}"}}}}"# - ) - }; - - let mut parent = FileState { - source: "/tmp/p.jsonl".to_string(), - ..FileState::default() - }; - store.parse_codex_line(&context("cap", "2026-07-23T00:00:00Z"), &mut parent); - let capped = store - .parse_codex_line( - &line(100.0, "2026-07-23T00:00:01Z", 5, 1), - &mut parent, - ) - .expect("real reading"); - store.index.insert(capped.id.clone(), store.events.len()); - store.events.push(capped); - assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 100.0); - - // The window rolled over; a real later reading drops back to 9%. - store.parse_codex_line(&context("now", "2026-07-29T00:00:00Z"), &mut parent); - let fresh = store - .parse_codex_line( - &line(9.0, "2026-07-29T00:00:01Z", 10, 2), - &mut parent, - ) - .expect("real reading"); - store.index.insert(fresh.id.clone(), store.events.len()); - store.events.push(fresh); - assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 9.0); - - // A fork now replays the capped turn, restamped as "now". It must not - // drag the displayed quota back to 100%. - let mut child = FileState { - source: "/tmp/c.jsonl".to_string(), - ..FileState::default() - }; - store.parse_codex_line(&context("cap", "2026-07-29T00:05:00Z"), &mut child); - assert!(store - .parse_codex_line( - &line(100.0, "2026-07-29T00:05:00Z", 5, 1), - &mut child, - ) - .is_none()); - assert_eq!(store.codex_quota.as_ref().unwrap().primary_pct, 9.0); - } - // A forked/subagent thread replays the parent's whole history — usage, turn // telemetry, tool calls, even the parent's session_meta — at the head of its // rollout file, restamped with the fork instant. The parent file still holds diff --git a/src/App.tsx b/src/App.tsx index aa24fac..d1c994f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { check as checkUpdate, Update } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import appPackage from "../package.json"; import { - ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Quota, Theme, TH, + ContextStats, Dashboard, DateRange, PeriodReport, QuotaTrendPoint, RangeDashboard, ModelStat, ProjectStat, ReliabilityStats, Scope, Theme, TH, ProviderLimit, LimitWindow, fetchDashboard, fetchRangeDashboard, fmtInt, fmtMoney, fmtTokens, pct, themeForScope, } from "./data"; @@ -556,7 +556,16 @@ function ProviderLimitsCard({ limits, theme }: { limits: ProviderLimit[]; theme: const t = theme; const { locale, text } = useI18n(); const now = Date.now(); - if (limits.length === 0) return null; + // No usable snapshot from `hu` (missing subscription, expired OAuth, or the + // CLI not installed): show an explicit unavailable state instead of stale + // local data or nothing at all. + if (limits.length === 0) { + return ( +

    + {text.providerLimitsUnavailable} +
    + ); + } const status = (w: LimitWindow) => { const left = Math.round(Math.max(0, Math.min(100, 100 - w.usedPct))); @@ -582,8 +591,21 @@ function ProviderLimitsCard({ limits, theme }: { limits: ProviderLimit[]; theme: }; return ( -
    = 2 ? "1fr 1fr" : "1fr", gap: 16 }}> - {limits.map((limit) => { +
    + {["claude", "codex"].map((provider) => { + const limit = limits.find((l) => l.provider === provider); + if (!limit) { + // The provider is known but `hu` returned no snapshot for it. + return ( +
    +
    + + {provider === "claude" ? "Claude" : "Codex"} +
    +
    {text.providerUnavailable}
    +
    + ); + } const stale = now - limit.windows[0].asOfMs > 60 * 60 * 1000; const asOf = new Date(limit.windows[0].asOfMs).toLocaleTimeString(localeTag(locale), { hour: "2-digit", minute: "2-digit" }); return ( diff --git a/src/data.ts b/src/data.ts index 5ad512b..b302caa 100644 --- a/src/data.ts +++ b/src/data.ts @@ -36,13 +36,6 @@ export interface PeriodReport { agents: AgentSlice[]; } export interface HeatDay { date: string; tokens: number; level: number } -// Codex provider quota (authenticated usage API; token_count log fallback). -export interface Quota { - plan: string; - primaryPct: number; primaryMinutes: number; primaryResetsAt: number; - secondaryPct: number; secondaryMinutes: number; secondaryResetsAt: number; - asOfMs: number; -} // Provider subscription limits (Claude / Codex), independent of usage scopes. export interface LimitWindow { id: string; label: string; durationMinutes: number; @@ -59,8 +52,7 @@ export interface ProviderLimit { export interface Scope { id: string; label: string; color: string; day: PeriodReport; week: PeriodReport; month: PeriodReport; - heatmap: HeatDay[]; quota: Quota | null; sparkQuota?: Quota | null; - quotaTrend?: QuotaTrendPoint[]; sparkQuotaTrend?: QuotaTrendPoint[]; + heatmap: HeatDay[]; } export interface Dashboard { scopes: Scope[]; todayTokens: number; generatedAt: string; telemetrySinceMs?: number; diff --git a/src/i18n.tsx b/src/i18n.tsx index f94fef1..f1ac17b 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -61,6 +61,8 @@ const en = { weekly: "Weekly", window: "window", providerLimits: "Provider Limits", + providerLimitsUnavailable: "Provider quota unavailable", + providerUnavailable: "Unavailable", fiveHour: "5-hour", resetting: "resetting…", left: "left", @@ -192,6 +194,8 @@ const zh: Copy = { weekly: "每周", window: "窗口", providerLimits: "额度详情", + providerLimitsUnavailable: "暂时无法获取额度", + providerUnavailable: "不可用", fiveHour: "5 小时", resetting: "重置中…", left: "剩余", From 5812c901d45898ccae597517647387c3e415814a Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 22:51:37 -0700 Subject: [PATCH 58/67] chore: bump version to 1.5.3 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9123736..89139b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.5.2", + "version": "1.5.3", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7db4f74..5010cce 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.5.2" +version = "1.5.3" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 26dd0b8..dd8dda0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.5.2" +version = "1.5.3" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index eb58d59..5f5a448 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.2", + "version": "1.5.3", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From 488119e422645d9b6ead2b168644ef1e33fc5d13 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 23:19:23 -0700 Subject: [PATCH 59/67] fix: resolve hu version via releases redirect, not the rate-limited API build-hu.sh queried api.github.com for the latest release tag, which 403s on shared CI runner IPs (unauthenticated rate limit). Resolve the version from the releases/latest HTML redirect instead; HAPPYUSAGE_VERSION still overrides. This unblocks the macOS universal release job. --- src-tauri/bin/build-hu.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src-tauri/bin/build-hu.sh b/src-tauri/bin/build-hu.sh index f27c01e..c6b507a 100755 --- a/src-tauri/bin/build-hu.sh +++ b/src-tauri/bin/build-hu.sh @@ -9,8 +9,10 @@ REPO="SunChJ/happyusage" VERSION="${HAPPYUSAGE_VERSION:-}" if [ -z "$VERSION" ]; then - VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" + # Avoid the rate-limited GitHub API on shared CI runners: the HTML + # releases/latest page 302-redirects to the newest tag. + VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" \ + | sed -E 's#.*/tag/##' | sed 's#/$##')" fi [ -n "$VERSION" ] || { echo "failed to resolve latest version" >&2; exit 1; } From 0365b3099068054bb8901d112d0b49db9d04bb02 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Thu, 6 Aug 2026 23:23:43 -0700 Subject: [PATCH 60/67] ci: publish releases atomically via draft A platform failure must not leave a partial public release: v1.5.3 once published Windows assets while the macOS job failed. Build jobs now upload to a draft release; a publish job (needs: all platform jobs) flips it public and updates the Homebrew cask only when every asset succeeded. Also switch to the supported includeUpdaterJson input name. --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c11cf3b..d01aa02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -176,7 +176,7 @@ jobs: echo "RELEASE_BODY" } >> "$GITHUB_OUTPUT" - - name: Build and publish release + - name: Build and upload release (draft) uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -184,14 +184,35 @@ jobs: tagName: ${{ env.RELEASE_TAG }} releaseName: "Tokenscope ${{ env.RELEASE_TAG }}" releaseBody: ${{ steps.release_notes.outputs.body }} - releaseDraft: false + # Draft until every platform has uploaded: a failed job must not + # leave a partial public release (only its Windows assets once). + releaseDraft: true prerelease: false - uploadUpdaterJson: true + includeUpdaterJson: true updaterJsonPreferNsis: true args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json + publish: + needs: [prepare, release] + if: needs.prepare.outputs.publish == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + steps: + - uses: actions/checkout@v6 + + # Every platform uploaded its draft assets; flip the release public. + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release edit "$RELEASE_TAG" --draft=false + echo "Published $RELEASE_TAG" + - name: Update Homebrew Cask - if: matrix.os == 'macos-latest' env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | From ea60e4b368ce6b94ed0dec30e4e3e17bff971a76 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Fri, 7 Aug 2026 00:04:58 -0700 Subject: [PATCH 61/67] chore: bump version to 1.5.4 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 89139b1..f58b859 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.5.3", + "version": "1.5.4", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5010cce..7f13d99 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.5.3" +version = "1.5.4" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dd8dda0..41e0815 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.5.3" +version = "1.5.4" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5f5a448..9739dc0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.3", + "version": "1.5.4", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", From b31293a45f393e65680101a939aef2c98d4fd540 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Fri, 7 Aug 2026 00:15:01 -0700 Subject: [PATCH 62/67] ci: pre-create draft release to avoid the create-release race Two platform jobs concurrently creating the same tag's release race: the loser gets 403 "Resource not accessible by integration" (v1.5.4 failed this way when both jobs finished within seconds). prepare now creates the draft release once, before the matrix jobs, so each platform job finds it and uploads assets; publish flips it public only after all succeeded. --- .github/workflows/release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d01aa02..34e98b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,6 +86,23 @@ jobs: echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + # Create the release once, here, before the platform jobs run. Two + # concurrent "create release" calls for the same tag race (one gets a + # 403 "Resource not accessible by integration"); pre-creating a draft + # lets every platform job find it and upload its assets, and the + # publish job flips it public only after all of them succeeded. + - name: Create draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" \ + --draft \ + --title "Tokenscope $TAG" \ + --notes "Work in progress — assets are uploaded by the platform build jobs; the release becomes public once every platform succeeds." \ + --repo "${{ github.repository }}" + fi + release: needs: prepare if: needs.prepare.outputs.publish == 'true' From 74765e4d87568a133a104aefac43748fc08b0682 Mon Sep 17 00:00:00 2001 From: jingxing Date: Sat, 8 Aug 2026 00:00:54 +0800 Subject: [PATCH 63/67] fix: package architecture-matched hu on macOS --- .github/workflows/release.yml | 56 ++++++------ README-zh.md | 9 +- README.md | 9 +- docs/DESIGN-usage-api.md | 14 +-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/bin/build-hu.sh | 46 +++++----- src-tauri/src/lib.rs | 34 ++++++-- src-tauri/src/quota_api.rs | 160 +++------------------------------- src-tauri/tauri.conf.json | 18 +--- 11 files changed, 109 insertions(+), 243 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34e98b0..3c7c984 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,12 +113,14 @@ jobs: fail-fast: false matrix: include: + # Build one app per CPU architecture so each package contains the + # matching single-architecture HappyUsage binary. - os: macos-latest - target: universal-apple-darwin - rust-targets: aarch64-apple-darwin,x86_64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc - rust-targets: x86_64-pc-windows-msvc + target: aarch64-apple-darwin + rust-targets: aarch64-apple-darwin + - os: macos-latest + target: x86_64-apple-darwin + rust-targets: x86_64-apple-darwin runs-on: ${{ matrix.os }} env: RELEASE_TAG: ${{ needs.prepare.outputs.tag }} @@ -176,9 +178,8 @@ jobs: ## Install - **macOS**: download `Tokenscope_*_universal.dmg` (Apple Silicon + Intel). Unsigned — right-click → **Open** on first launch, or install via `brew install --cask sunchj/tokenscope/tokenscope`. + **macOS**: download `Tokenscope_*_aarch64.dmg` on Apple Silicon or `Tokenscope_*_x64.dmg` on Intel. Unsigned — right-click → **Open** on first launch, or install via `brew install --cask sunchj/tokenscope/tokenscope`. - **Windows**: download `Tokenscope_*_x64-setup.exe`. Unsigned — on first launch click **More info → Run anyway** to dismiss SmartScreen. BODY if [ -n "$gen" ]; then printf '\n%s\n' "$gen" @@ -202,11 +203,10 @@ jobs: releaseName: "Tokenscope ${{ env.RELEASE_TAG }}" releaseBody: ${{ steps.release_notes.outputs.body }} # Draft until every platform has uploaded: a failed job must not - # leave a partial public release (only its Windows assets once). + # leave a partial public release with only one macOS architecture. releaseDraft: true prerelease: false includeUpdaterJson: true - updaterJsonPreferNsis: true args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json publish: @@ -237,30 +237,32 @@ jobs: echo "HOMEBREW_TAP_TOKEN not set; skipping tap update." exit 0 fi - DMG_NAME="Tokenscope_${RELEASE_VERSION}_universal.dmg" - DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${RELEASE_TAG}/${DMG_NAME}" - - SHA256="" - for i in $(seq 1 10); do - if curl -fsSL "$DOWNLOAD_URL" -o /tmp/tokenscope.dmg; then - SHA256=$(shasum -a 256 /tmp/tokenscope.dmg | cut -d' ' -f1) - break - fi - echo "Retry $i: asset not ready yet..." - sleep 10 - done - if [ -z "$SHA256" ]; then - echo "::error::Release asset $DMG_NAME never became available" - exit 1 - fi + BASE_URL="https://github.com/${{ github.repository }}/releases/download/${RELEASE_TAG}" + fetch_sha() { + arch="$1" + name="Tokenscope_${RELEASE_VERSION}_${arch}.dmg" + for i in $(seq 1 10); do + if curl -fsSL "${BASE_URL}/${name}" -o "/tmp/tokenscope-${arch}.dmg"; then + shasum -a 256 "/tmp/tokenscope-${arch}.dmg" | cut -d' ' -f1 + return 0 + fi + echo "Retry $i: $name not ready yet..." >&2 + sleep 10 + done + echo "::error::Release asset $name never became available" >&2 + return 1 + } + SHA_ARM="$(fetch_sha aarch64)" + SHA_INTEL="$(fetch_sha x64)" git clone https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/SunChJ/homebrew-tokenscope.git /tmp/homebrew-tap cat > /tmp/homebrew-tap/Casks/tokenscope.rb <&2; exit 1; } -case "$(uname -s)" in - Darwin) OS_GO="darwin" ;; - Linux) OS_GO="linux" ;; - MINGW*|MSYS*|CYGWIN*) OS_GO="windows" ;; - *) echo "unsupported OS: $(uname -s)" >&2; exit 1 ;; +case "${TAURI_ENV_PLATFORM:-$(uname -s)}" in + darwin|Darwin) OS_GO="darwin" ;; + linux|Linux) OS_GO="linux" ;; + windows|Windows|MINGW*|MSYS*|CYGWIN*) OS_GO="windows" ;; + *) echo "unsupported OS: ${TAURI_ENV_PLATFORM:-$(uname -s)}" >&2; exit 1 ;; esac -case "$(uname -m)" in - x86_64|amd64) ARCH="amd64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; + +# beforeBuildCommand receives TAURI_ENV_ARCH from the requested Tauri target. +# Fall back to the host architecture for direct/local script invocations. +case "${TAURI_ENV_ARCH:-$(uname -m)}" in + x86_64|x86|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "unsupported architecture: ${TAURI_ENV_ARCH:-$(uname -m)}" >&2; exit 1 ;; esac -if [ "$OS_GO" = "windows" ]; then - EXT="zip" -else - EXT="tar.gz" -fi +if [ "$OS_GO" = "windows" ]; then EXT="zip"; else EXT="tar.gz"; fi ASSET="hu-${OS_GO}-${ARCH}.${EXT}" URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}" - TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT + echo "==> fetching ${URL}" if [ "$OS_GO" = "windows" ]; then curl -fsSL -o "$TMP/hu.zip" "$URL" - unzip -o -q "$TMP/hu.zip" -d "$TMP" + unzip -o -q "$TMP/hu.zip" -d "$TMP/extract" else + mkdir -p "$TMP/extract" curl -fsSL -o "$TMP/hu.tgz" "$URL" - tar -xzf "$TMP/hu.tgz" -C "$TMP" + tar -xzf "$TMP/hu.tgz" -C "$TMP/extract" fi -BIN_PATH="$(find "$TMP" -type f \( -name 'hu' -o -name 'hu.exe' \) | head -1)" +BIN_PATH="$(find "$TMP/extract" -type f \( -name 'hu' -o -name 'hu.exe' \) | head -1)" [ -n "$BIN_PATH" ] || { echo "hu binary not found in ${ASSET}" >&2; exit 1; } - cp "$BIN_PATH" hu chmod +x hu -echo "==> bundled hu ${VERSION}" +echo "==> bundled hu ${VERSION} (${OS_GO}/${ARCH})" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1060bc3..12e2203 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -140,6 +140,7 @@ struct TrayCopy { refresh: &'static str, check_updates: &'static str, provider_limits: &'static str, + provider_unavailable: &'static str, menu_bar_display: &'static str, off: &'static str, compact: &'static str, @@ -161,6 +162,7 @@ fn tray_copy(language: AppLanguage) -> &'static TrayCopy { refresh: "Refresh", check_updates: "Check for Updates…", provider_limits: "Provider Limits", + provider_unavailable: "Unavailable", menu_bar_display: "Menu Bar Display", off: "Off", compact: "Compact", @@ -180,6 +182,7 @@ fn tray_copy(language: AppLanguage) -> &'static TrayCopy { refresh: "刷新", check_updates: "检查更新…", provider_limits: "额度详情", + provider_unavailable: "不可用", menu_bar_display: "菜单栏显示", off: "关闭", compact: "紧凑", @@ -365,9 +368,16 @@ fn tray_label(dash: &Dashboard, display: MenuBarQuotaDisplay, language: AppLangu /// Menu-bar text for each provider row under Provider Limits, e.g. /// "Claude — 5h 64% · W 82%" (Claude) or "Codex — W 29% · S 31%" (Codex). -fn provider_menu_row(limit: Option<&model::ProviderLimit>, now_s: i64) -> String { +fn provider_menu_row( + limit: Option<&model::ProviderLimit>, + provider_label: &str, + unavailable: &str, + now_s: i64, +) -> String { let Some(limit) = limit else { - return String::new(); + // Empty native menu-item titles collapse the macOS submenu into the + // tiny blank scroller seen when `hu` cannot run. + return format!("{provider_label} — {unavailable}"); }; let mut sorted: Vec<&model::LimitWindow> = limit .windows @@ -441,8 +451,13 @@ fn update_provider_rows(app: &tauri::AppHandle, dash: &Dashboard) { }; let now_s = now_ms() / 1000; let (claude, codex) = provider_rows(&dash.provider_limits); - let claude_text = provider_menu_row(claude, now_s); - let codex_text = provider_menu_row(codex, now_s); + let language = app + .try_state::() + .and_then(|state| state.0.lock().ok().map(|prefs| prefs.language)) + .unwrap_or_default(); + let copy = tray_copy(language); + let claude_text = provider_menu_row(claude, "Claude", copy.provider_unavailable, now_s); + let codex_text = provider_menu_row(codex, "Codex", copy.provider_unavailable, now_s); let provider_claude = state.provider_claude.clone(); let provider_codex = state.provider_codex.clone(); let handle = app.clone(); @@ -1387,14 +1402,14 @@ pub fn run() { let provider_claude_i = MenuItem::with_id( app, "provider-claude", - provider_menu_row(claude_limit, now_s), + provider_menu_row(claude_limit, "Claude", copy.provider_unavailable, now_s), true, None::<&str>, )?; let provider_codex_i = MenuItem::with_id( app, "provider-codex", - provider_menu_row(codex_limit, now_s), + provider_menu_row(codex_limit, "Codex", copy.provider_unavailable, now_s), true, None::<&str>, )?; @@ -1797,7 +1812,7 @@ mod tests { assert_eq!(provider_summary(&limit, false, 1), "Cx29%"); assert_eq!(provider_summary(&limit, true, 1), "Cx W29%/S31%"); assert_eq!( - provider_menu_row(Some(&limit), 1), + provider_menu_row(Some(&limit), "Codex", "Unavailable", 1), "Codex — W 29% · S 31%" ); } @@ -1831,7 +1846,10 @@ mod tests { }; // The expired weekly window drops out; only the 5h window remains. assert_eq!(provider_summary(&limit, true, 1_000), "Cl 5h64%"); - assert_eq!(provider_menu_row(Some(&limit), 1_000), "Claude — 5h 64%"); + assert_eq!( + provider_menu_row(Some(&limit), "Claude", "Unavailable", 1_000), + "Claude — 5h 64%" + ); // Once every window is expired the provider disappears entirely. assert_eq!(provider_summary(&limit, false, 2_001), ""); } diff --git a/src-tauri/src/quota_api.rs b/src-tauri/src/quota_api.rs index b8346e9..6309acf 100644 --- a/src-tauri/src/quota_api.rs +++ b/src-tauri/src/quota_api.rs @@ -7,7 +7,6 @@ use crate::model::{same_reset_cycle, LimitWindow, ProviderLimit, QuotaTrendPoint, WEEKLY_WINDOW_MINUTES}; use serde_json::Value; -use std::fs; use std::path::PathBuf; use std::process::Command; use std::sync::{OnceLock, RwLock}; @@ -41,41 +40,6 @@ pub fn shared() -> Vec { limits } -fn hu_executables() -> Vec { - let mut candidates = Vec::new(); - // 1. Bundled copy (app Resources/bin/hu) — the primary source so the app - // is self-contained and the hu version ships with Tokenscope. - if let Some(path) = bundled_hu() { - candidates.push(path); - } - // 2. System installs (PATH + common locations; install.sh drops `hu` into - // ~/.local/bin, which may not be on the app's PATH at launch). - if let Some(path) = std::env::var_os("PATH").and_then(|paths| { - std::env::split_paths(&paths) - .map(|dir| dir.join(if cfg!(windows) { "hu.exe" } else { "hu" })) - .find(|path| path.is_file()) - }) { - if !candidates.contains(&path) { - candidates.push(path); - } - } - let mut extras = vec![ - PathBuf::from("/opt/homebrew/bin/hu"), - PathBuf::from("/usr/local/bin/hu"), - ]; - if let Some(home) = dirs::home_dir() { - extras.push(home.join(".local").join("bin").join("hu")); - extras.push(home.join("bin").join("hu")); - extras.push(home.join("go").join("bin").join("hu")); - } - for path in extras { - if path.is_file() && !candidates.contains(&path) { - candidates.push(path); - } - } - candidates -} - /// Directory injected by lib.rs at startup: `app.path().resource_dir()`. static BUNDLE_DIR: OnceLock> = OnceLock::new(); @@ -92,120 +56,25 @@ fn bundled_hu() -> Option { path.is_file().then_some(path) } -const HAPPYUSAGE_INSTALL_URL: &str = - "https://raw.githubusercontent.com/SunChJ/happyusage/main/scripts/install.sh"; -const INSTALL_RETRY_MS: i64 = 24 * 60 * 60 * 1000; -static LAST_INSTALL_ATTEMPT_MS: std::sync::atomic::AtomicI64 = - std::sync::atomic::AtomicI64::new(0); - -fn now_ms() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_millis() as i64) - .unwrap_or(0) -} - -/// Make sure `hu` exists, installing it once per 24h when missing. Returns -/// true when a usable executable is present afterwards. Runs on the -/// background refresh thread; a slow brew install only delays this refresh. -fn ensure_hu() -> bool { - if !hu_executables().is_empty() { - return true; - } - let now = now_ms(); - let last = LAST_INSTALL_ATTEMPT_MS.load(std::sync::atomic::Ordering::Relaxed); - if now - last < INSTALL_RETRY_MS { - return false; - } - let installed = try_install_hu(); - LAST_INSTALL_ATTEMPT_MS.store(now, std::sync::atomic::Ordering::Relaxed); - installed -} - -fn try_install_hu() -> bool { - // 1. Homebrew: tap + install (covers macOS and Linux with brew). - if let Some(brew) = find_on_path("brew") { - let tap_ok = Command::new(&brew) - .args(["tap", "SunChJ/happyusage"]) - .status() - .map(|status| status.success()) - .unwrap_or(false); - if tap_ok - && Command::new(&brew) - .args(["install", "hu"]) - .status() - .map(|status| status.success()) - .unwrap_or(false) - && !hu_executables().is_empty() - { - return true; - } - } - // 2. Official install script (needs curl + sh): fetch, run, then re-scan. - if let Some(curl) = find_on_path("curl") { - if let Ok(script) = Command::new(&curl).args(["-fsSL", HAPPYUSAGE_INSTALL_URL]).output() { - if script.status.success() { - let tmp = std::env::temp_dir().join(format!( - "happyusage-install-{}.sh", - std::process::id() - )); - if fs::write(&tmp, &script.stdout).is_ok() { - let ok = Command::new("sh") - .arg(&tmp) - .status() - .map(|status| status.success()) - .unwrap_or(false); - let _ = fs::remove_file(tmp); - if ok && !hu_executables().is_empty() { - return true; - } - } - } - } - } - // 3. Go toolchain as a last resort. - if let Some(go) = find_on_path("go") { - if Command::new(&go) - .args(["install", "github.com/SunChJ/happyusage/cmd/hu@latest"]) - .status() - .map(|status| status.success()) - .unwrap_or(false) - && !hu_executables().is_empty() - { - return true; - } - } - false -} - -fn find_on_path(name: &str) -> Option { - let paths = std::env::var_os("PATH")?; - std::env::split_paths(&paths) - .map(|dir| dir.join(if cfg!(windows) { format!("{name}.exe") } else { name.to_string() })) - .find(|path| path.is_file()) -} - /// Run `hu usage --json` and return the provider envelope /// (with `ok: true`). The CLI owns credential handling and HTTP timeouts; /// Tokenscope's refresh runs on a background thread, so a slow run only /// delays that provider's next refresh. fn run_hu(provider: &str) -> Option { - for executable in hu_executables() { - let output = Command::new(executable) - .args(["usage", provider, "--json"]) - .output() - .ok()?; - if !output.status.success() { - continue; - } - let json: Value = serde_json::from_slice(&output.stdout).ok()?; - let envelope = json.get("provider")?; - if envelope.get("ok").and_then(Value::as_bool) != Some(true) { - continue; - } - return Some(envelope.clone()); + // Tokenscope owns its HappyUsage version. Never fall back to a binary from + // PATH/Homebrew because its version and output schema are not controlled. + let executable = bundled_hu()?; + let output = Command::new(executable) + .args(["usage", provider, "--json"]) + .output() + .ok()?; + if !output.status.success() { + return None; } - None + let json: Value = serde_json::from_slice(&output.stdout).ok()?; + let envelope = json.get("provider")?; + (envelope.get("ok").and_then(Value::as_bool) == Some(true)) + .then(|| envelope.clone()) } fn rfc3339_ms(value: Option<&Value>) -> i64 { @@ -369,9 +238,6 @@ pub fn reload() -> bool { .unwrap_or_default(); let mut next = previous.clone(); - // First reload attempt: make sure `hu` exists (auto-install when missing). - ensure_hu(); - if let Some(claude) = run_hu("claude").and_then(|envelope| claude_from_hu(&envelope)) { next.claude = Some(claude); } else { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9739dc0..2bc0cb5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.4", + "version": "1.5.5", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", @@ -35,34 +35,24 @@ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRGREQ5QzhCQjc0QUIxNTYKUldSV3NVcTNpNXpkVDJvWTFpMUx6WEZialVXSXdyeDNSZnM5ZnMzK29NTThPUk5CVDVtWG02SjYK", "endpoints": [ "https://github.com/SunChJ/tokenscope-remix/releases/latest/download/latest.json" - ], - "windows": { - "installMode": "passive" - } + ] } }, "bundle": { "active": true, - "targets": ["app", "dmg", "nsis"], + "targets": ["app", "dmg"], "createUpdaterArtifacts": true, "resources": ["bin/hu"], "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" + "icons/icon.icns" ], "macOS": { "minimumSystemVersion": "10.15", "hardenedRuntime": true, "signingIdentity": "-" - }, - "windows": { - "nsis": { - "installerIcon": "icons/icon.ico", - "installMode": "currentUser" - } } } } From fd2e4b6f83f7b7000aeb9125b66216dd2bab48e9 Mon Sep 17 00:00:00 2001 From: jingxing Date: Sat, 8 Aug 2026 00:02:39 +0800 Subject: [PATCH 64/67] fix(ci): pass release tag to draft creation --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c7c984..a99814b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,6 +94,7 @@ jobs: - name: Create draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} run: | if ! gh release view "$TAG" >/dev/null 2>&1; then gh release create "$TAG" \ From 7076fd4ee832be4f6404472447f8cc5cb343e444 Mon Sep 17 00:00:00 2001 From: jingxing Date: Sat, 8 Aug 2026 00:49:20 +0800 Subject: [PATCH 65/67] release: prepare v1.5.6 --- .github/workflows/release.yml | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 10 ++++++++-- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 24 +++++++++++++++++------- 7 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a99814b..961a97b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -268,7 +268,7 @@ jobs: desc "Menu-bar dashboard for local AI coding agent token usage (Claude Code, Codex)" homepage "https://github.com/${{ github.repository }}" - depends_on macos: ">= :catalina" + depends_on macos: :catalina app "Tokenscope.app" postflight do diff --git a/package.json b/package.json index 7d35f0e..08b441e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.5.5", + "version": "1.5.6", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ff0d797..8205777 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.5.5" +version = "1.5.6" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fb4cff4..fd72b41 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.5.5" +version = "1.5.6" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 12e2203..f8abff4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1247,9 +1247,15 @@ pub fn run() { // so provider quota works without any system install. quota_api::set_bundle_dir(app.path().resource_dir().ok()); - // Menu-bar–only app: no Dock icon, runs in the background. + // Menu-bar–only app: use the accessory policy and explicitly hide + // the Dock tile. The explicit visibility call also covers `tauri + // dev`, whose unbundled debug process can briefly start as a + // regular foreground app before setup runs. #[cfg(target_os = "macos")] - app.set_activation_policy(tauri::ActivationPolicy::Accessory); + { + app.set_activation_policy(tauri::ActivationPolicy::Accessory); + app.set_dock_visibility(false); + } // Holds the latest tray-icon rect so show_popover can anchor the panel. // Captured in the tray click handler on every platform — see diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2bc0cb5..3c03c57 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.5", + "version": "1.5.6", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index d1c994f..75772b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -590,8 +590,17 @@ function ProviderLimitsCard({ limits, theme }: { limits: ProviderLimit[]; theme: return `${burnPerDay >= 0 ? "+" : ""}${burnPerDay.toFixed(1)}%${text.perDay} · ${horizon} ${text.toCap}`; }; + const hasClaude = limits.some((limit) => limit.provider === "claude"); + const hasCodex = limits.some((limit) => limit.provider === "codex"); + const onlyOneAvailable = hasClaude !== hasCodex; + const columns = onlyOneAvailable + ? hasClaude + ? "minmax(0, 1fr) max-content" + : "max-content minmax(0, 1fr)" + : "1fr 1fr"; + return ( -
    +
    {["claude", "codex"].map((provider) => { const limit = limits.find((l) => l.provider === provider); if (!limit) { @@ -627,9 +636,9 @@ function ProviderLimitsCard({ limits, theme }: { limits: ProviderLimit[]; theme:
    -
    - {status(w)} - {note && {note}} +
    + {status(w)} + {note && {note}}
    ); @@ -981,14 +990,15 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen const slices = P.agents; // Keep the hero's agent split on the same model scope as its Total. The // historical chart below remains the complete period view. - const heroSlices = selectedModel && slices.length > 0 + const heroSlices = (selectedModel && slices.length > 0 ? slices.map((slice) => ({ ...slice, tokens: models .filter((model) => model.name === selectedModel.name && model.agent === slice.id) .reduce((sum, model) => sum + model.tokens, 0), - })).filter((slice) => slice.tokens > 0) - : slices; + })) + : slices + ).filter((slice) => slice.tokens > 0); // animated Total tokens: counts up from 0 on each open / period / scope // / model switch; held at 0 while hidden so it never flashes. const animTotal = useCountUp(totalTokens, `${viewKey}:${scope.id}:${totalModel}:${openGen}`, active); From f79c647ce11ec9fce3293b1f8a015fa7f279cd36 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 10 Aug 2026 14:17:11 +0800 Subject: [PATCH 66/67] feat: show period-over-period growth as a ratio and per-agent tokens in scope chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delta badge renders ▲/▼ with a multiple of the previous period (63x instead of 6240%), exact ratio kept in the tooltip - Agent chips now display each source's tokens for the current period, with its own growth ratio on hover --- src/App.tsx | 43 +++++++++++++++++++++++++++++++++---------- tokenscope-panel.html | 11 +++++++++-- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 75772b6..4d21618 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,15 +51,24 @@ function useCountUp(target: number, resetKey: string, active: boolean, duration return val; } +// Period-over-period change rendered as a ratio of the previous period +// (e.g. ▲63x) — a percentage like 6240% is far less readable. The display +// rounds; the exact ratio stays available in the tooltip. +function fmtMultiple(m: number): string { + const s = m >= 100 ? m.toFixed(0) : m >= 10 ? m.toFixed(1) : m.toFixed(2); + return `${parseFloat(s)}x`; +} + function Delta({ v, theme }: { v: number; theme: Theme }) { const { text } = useI18n(); const up = v >= 0; + const multiple = 1 + v / 100; // Usage/cost going up is "bad" → red; going down is "good" → green. const col = up ? "#e0795f" : theme.accent; return ( - - {up ? "▲" : "▼"}{Math.abs(Math.round(v))}% + {up ? "▲" : "▼"}{fmtMultiple(multiple)} ); } @@ -373,18 +382,28 @@ function ShortcutEditor({ current, theme, dark, onClose, onSaved }: // Agent filter chips (All / Claude / Codex / Pi …). Rendered only when several // sources have data; a single-source install never sees them. -function AgentChips({ scopes, value, theme, onSelect }: - { scopes: Scope[]; value: string; theme: Theme; onSelect: (id: string) => void }) { +function AgentChips({ scopes, value, theme, onSelect, reportOf }: + { scopes: Scope[]; value: string; theme: Theme; onSelect: (id: string) => void; + reportOf: (s: Scope) => PeriodReport | undefined }) { const t = theme; const { text } = useI18n(); return ( -
    +
    {scopes.map((s) => { const on = s.id === value; + const rep = reportOf(s); + const tokens = rep?.metrics.totalTokens ?? 0; + const delta = rep?.metrics.deltaTokens; + const tip = rep + ? `${s.id === "all" ? text.all : s.label} · ${fmtTokens(tokens)}` + + (delta !== undefined && Math.round(delta) !== 0 + ? ` · ${delta >= 0 ? "▲" : "▼"}${fmtMultiple(Math.abs(1 + delta / 100))} ${text.comparedPrevious}` + : "") + : s.label; return ( -
    onSelect(s.id)} style={{ - display: "inline-flex", alignItems: "center", gap: 6, - font: `600 10.5px ${t.ui}`, letterSpacing: ".02em", padding: "4px 11px", +
    onSelect(s.id)} title={tip} style={{ + display: "inline-flex", alignItems: "center", gap: 5, + font: `600 10.5px ${t.ui}`, letterSpacing: ".02em", padding: "4px 10px", borderRadius: 20, cursor: "pointer", userSelect: "none", color: on ? t.segOnText : t.segOffText, background: on ? t.segOnBg : t.segBg, @@ -393,7 +412,10 @@ function AgentChips({ scopes, value, theme, onSelect }: }}> {s.color && } - {s.id === "all" ? text.all : s.label} + {s.id === "all" ? text.all : s.label} + + {fmtTokens(tokens)} +
    ); })} @@ -1197,7 +1219,8 @@ function Panel({ dash, dark, themePref, onToggleTheme, onToggleLanguage, openGen )} {/* provider filter — only providers with usage in this period */} {visibleScopes.length > 1 && ( - + )} {/* hero */}
    diff --git a/tokenscope-panel.html b/tokenscope-panel.html index 4f5d8f7..638c837 100644 --- a/tokenscope-panel.html +++ b/tokenscope-panel.html @@ -547,14 +547,21 @@ // SHARED PANEL PIECES // ===================================================================== // Usage/cost going up is "bad" → red; going down is "good" → green. +// Rendered as a ratio of the previous period (▲63x) instead of a percentage. +function fmtMultiple(m) { + const s = m >= 100 ? m.toFixed(0) : m >= 10 ? m.toFixed(1) : m.toFixed(2); + return `${parseFloat(s)}x`; +} + function Delta({ v, theme }) { const up = v >= 0; + const multiple = 1 + v / 100; const col = up ? '#e0795f' : theme.accent; return ( - - {up ? '▲' : '▼'}{Math.abs(Math.round(v))}% + {up ? '▲' : '▼'}{fmtMultiple(multiple)} ); } From 49214fef45b060223d079248974fcb2ee39789a6 Mon Sep 17 00:00:00 2001 From: SamsonCJ Date: Mon, 10 Aug 2026 14:17:13 +0800 Subject: [PATCH 67/67] release: prepare v1.5.7 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 08b441e..8920a2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tokenscope", - "version": "1.5.6", + "version": "1.5.7", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8205777..790b621 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4267,7 +4267,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenscope" -version = "1.5.6" +version = "1.5.7" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fd72b41..a69717b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokenscope" -version = "1.5.6" +version = "1.5.7" description = "Menu-bar dashboard for local AI coding agents (Pi, Claude Code, Codex) token usage" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3c03c57..7837c8e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Tokenscope", - "version": "1.5.6", + "version": "1.5.7", "identifier": "com.tokenscope.app", "build": { "beforeDevCommand": "pnpm dev",