fix(claudecode): skip char-based token estimation for bare transcript files - #9
Closed
t1000040 wants to merge 202 commits into
Closed
fix(claudecode): skip char-based token estimation for bare transcript files#9t1000040 wants to merge 202 commits into
t1000040 wants to merge 202 commits into
Conversation
Co-authored-by: astkaasa <2796928+astkaasa@users.noreply.github.com>
Co-authored-by: astkaasa <2796928+astkaasa@users.noreply.github.com>
* feat(cli): add yesterday date filter * style(cli): fix rustfmt and clippy violations in yesterday filter Run cargo fmt --all over the yesterday-filter changes (18 rustfmt violations) and allow clippy::too_many_arguments on get_date_range_label_for_date, which takes 8 parameters to keep the date injection testable. Also fix three pre-existing clippy::len_zero violations in the gjc scanner tests (len() >= 1 -> !is_empty()) so that cargo clippy --all-targets -- -D warnings passes on this branch; CI runs clippy without --all-targets, which is why main stayed green. Constraint: cargo fmt --check and clippy --all-targets -D warnings must pass locally Rejected: refactoring get_date_range_label_for_date into a params struct | out of scope for a lint fix Confidence: high Scope-risk: narrow * docs: document --yesterday date filter Add the --yesterday shortcut to the Date Filtering section in README.md and its ko/ja/zh-cn translations, matching the existing --today/--week/--month example style. Confidence: high Scope-risk: narrow --------- Co-authored-by: Junho Yeo <i@junho.io>
* feat(kimi): support kimi-code alongside legacy kimi-cli
- Parse kimi-code wire.jsonl format (usage.record lines) from
~/.kimi-code/sessions/{workspace}/{session}/agents/{agent}/wire.jsonl
- Route parser selection via is_kimi_code_path() in lib.rs
- Scanner discovers both ~/.kimi/sessions (legacy) and ~/.kimi-code/sessions
- Usage query reads credentials from ~/.kimi-code first, fallback to ~/.kimi
- Respect KIMI_CODE_HOME environment variable for custom data directories
- Update README to reflect Kimi Code support
* fix(kimi): respect KIMI_CODE_HOME in parser routing and docs
- is_kimi_code_path() now checks KIMI_CODE_HOME env var in addition
to the literal .kimi-code path segment, fixing mis-routing for
custom installation directories.
- Add serial_test-guarded test for custom KIMI_CODE_HOME paths.
- Update README (EN/ZH/KO) to document the KIMI_CODE_HOME override.
* docs(readme.ja): add kimi-code data path and wire format
Mirror the kimi-code documentation added to README.md, README.ko.md,
and README.zh-cn.md in junhoyeo#682, which missed the Japanese translation:
client table row, cross-platform path table row, and the Kimi Code
wire.jsonl format section.
* fix(kimi): count only turn-scoped usage.record lines
kimi-code tags every usage.record with usageScope: "turn" for per-step
LLM calls made inside a user turn, and "session" for non-turn
bookkeeping such as context compaction rollups. Upstream's own tooling
(apps/vis context-projector) treats a missing usageScope as
session-scoped, so the parser now requires an explicit "turn" before
counting a record.
Constraint: all public kimi-code releases always write usageScope on usage.record lines
Rejected: counting records with a missing usageScope | upstream attributes them to the session scope, not a turn
Rejected: also counting session-scoped compaction usage | review verdict for junhoyeo#682 was to count turn-scoped usage only
Confidence: high
Scope-risk: narrow
Not-tested: real wire.jsonl files containing compaction-emitted session-scoped records
* style: rustfmt kimi additions and fix clippy len_zero in scanner tests
cargo fmt over the kimi-code changes from junhoyeo#682 plus the merged
scanner.rs, and replace three .len() >= 1 assertions on the Gjc scan
results (from junhoyeo#685 on main, only linted under --all-targets) with
!is_empty() so cargo clippy --all-targets -- -D warnings passes.
---------
Co-authored-by: Junho Yeo <i@junho.io>
The Commit coverage badge step pushed without pulling first, so any commit landing on main during the ~5 minute coverage run rejected the push and failed the whole workflow (seen twice on 2026-06-09 during a 4-PR merge train). Retry up to 3 times with pull --rebase; -X theirs keeps the newest badge when two coverage runs race on the svg itself. Constraint: badge must be committed from the run's own checkout, which is stale by design Rejected: stefanzweifel/git-auto-commit-action | adds a third-party action for a 3-line fix Confidence: high Scope-risk: narrow
* feat(grok): add Grok Build usage parsing * feat(grok): add Grok Build subscription usage * fix(grok): handle missing metadata and billing edge cases * fix(grok): avoid cross-account merge in agent billing fallback The `grok agent --no-leader stdio` billing fallback runs without credential context, so its metrics cannot be attributed to a specific account. When auth.json holds multiple credentials, the fallback's metrics were merged with plan/email taken from `plan_only`, which may come from a different credential. Skip the fallback entirely when more than one credential exists. Also remove the dead always-true `if metrics.is_empty()` guard around the first fetch in `fetch_network_usage` (metrics is freshly created). Constraint: agent billing RPC exposes no account identity to match against Rejected: omit plan/email on the agent path | loses correct plan/email in the common single-account case Confidence: high Scope-risk: narrow Not-tested: live grok agent RPC behavior (network-dependent) --------- Co-authored-by: Junho Yeo <i@junho.io>
…llable login Five fixes plus a guard nit for the codex multi-account feature (junhoyeo#696): - Click hitboxes for [Use]/[Remove]/[Confirm] (and the action bar) are now clamped to the Usage panel's bottom edge. Rows clipped by the renderer no longer leave invisible buttons over the footer where two stray clicks could confirm-remove an account. - `remove_account` only edits tokscale's credential store. It no longer rewrites the codex CLI's auth.json with the next account's tokens (a silent re-login) nor deletes auth.json when removing the last account. Store-side active-account repair is kept; CLI/TUI messages now say tracking stopped and the codex login is unchanged. - The `codex login` TUI worker polls `try_wait` against a shared child slot instead of blocking on `wait()`. [Dismiss] while running kills the child, and TUI exit kills it too, so the OAuth port is always released. - Re-import identity dedup scans all stored accounts, so an account stored under a collision-suffixed id (acct_x-2) updates in place instead of accumulating acct_x-3, acct_x-4, ... - Import write paths bail on an unknown store version (checked on the raw JSON, since a future schema may not deserialize) instead of clobbering a store written by a newer tokscale. Read paths still treat it as absent. - The `u` key in the Usage tab routes through `refresh_usage()` so it respects the in-flight guard like `r` and the [Refresh] button. Constraint: Usage tab has no scrolling; rows past the panel are clipped, so hitboxes must be too Constraint: codex CLI treats auth.json as its login state; tokscale remove must not mutate it Rejected: scrollable Usage tab | larger UX change than the bug warrants Rejected: blocking kill via child.wait() in worker only | TUI exit would still orphan the child Rejected: bail on any unparseable store in write paths | would block recovery from corrupted v1 stores Confidence: high Scope-risk: narrow Directive: store active account may now diverge from the codex CLI login after removing the active account — this is intentional; [Use]/switch is the only path that writes auth.json Not-tested: real `codex login` child kill on Windows (unix-only test coverage)
…eams Behavior-preserving cleanup of PR junhoyeo#696 slop: - Delete `save_current_account_as_active` (byte-identical twin of `import_current_account`) and fold the login worker's two-step snapshot-then-import into one `import_login_auth_file` in the command layer. The snapshot step's previously swallowed error is now an explicit decision: non-fatal, but surfaced as a warning line in the login panel. - Render the Usage tab from a `mem::take`d buffer instead of cloning the whole subscription list every frame. - Delete the `usage_fetcher` fn-pointer injection seam (only ever held one value); use cfg(test) branching at the spawn site directly. - Collapse the seven identical single-output `fetch_X` Vec-coercion wrappers in usage/mod.rs into a `Fetch::Single`/`Fetch::Multi` adapter. - Delete the five test-only `_in_home` pass-throughs in codex.rs; tests call the `_at_path` variants directly. - Polish: one `account_sort_key` shared by the three account comparators, `auth_write_path` derived from `current_auth_paths()`, single-use `home_dir()` inlined away, `same_token_identity`'s repeated match arms collapsed into a `field_identity` helper. Rejected: trait-based UsageProvider abstraction | enum of two fn-pointer shapes is all that exists Rejected: keeping login warning silent | masks store divergence when the snapshot fails Confidence: high Scope-risk: narrow
… threading The date-filter plumbing passed seven parallel scalars (today, yesterday, week, month, since, until, year) through eight function signatures, and main() destructured DateRangeFlags into those scalars in eight copy-pasted blocks. PR junhoyeo#693 (--yesterday) pushed get_date_range_label_for_date over clippy's argument limit and silenced it with a new #[allow(clippy::too_many_arguments)]. Thread &DateRangeFlags through instead: - build_date_filter, build_date_filter_for_date, normalize_year_filter, get_date_range_label, get_date_range_label_for_date now take &DateRangeFlags. - run_models_report, run_monthly_report, run_hourly_report take &DateRangeFlags and resolve since/until/year internally; the eight destructuring blocks in main() are deleted. - The new allow on get_date_range_label_for_date is deleted, as are the pre-existing allows on run_monthly_report and run_hourly_report (both now at 6 args). run_models_report keeps its allow (9 args remain). - Tests rewritten from positional bool tuples to readable struct literals. BEHAVIOR CHANGE (intended): the shortcut flags --today/--yesterday/--week/ --month now declare clap conflicts with each other and with --since/--until/--year. Previously such combinations were accepted and one side was silently ignored by first-match precedence (today > yesterday > week > month > since/until; any shortcut silently nulled --year). They now fail at argument parsing with an ArgumentConflict error. --since/--until/ --year remain combinable with each other, as before, since all three are forwarded to the core report options together. Constraint: scope limited to crates/tokscale-cli/src/main.rs; tui::run, run_graph_command, run_submit_command, run_time_metrics_report keep their since/until/year parameters Constraint: README date-filter docs show flags only individually, so no doc updates were needed Rejected: converting to a DateFilter enum (Today|Yesterday|...|All) after parsing | larger diff for the same clarity, and clap conflicts already make the struct's invalid states unrepresentable at the CLI boundary Rejected: deleting normalize_year_filter outright | still the documented precedence safety net for programmatically built DateRangeFlags Confidence: high Scope-risk: narrow Not-tested: scripts in the wild relying on previously-ignored flag combinations (e.g. --today --year 2024) now exit 2 at parse time
…parsing Replace the KIMI_CODE_HOME env read inside is_kimi_code_path() with a structural check: kimi-code always writes <root>/sessions/WORKSPACE/SESSION/agents/AGENT/wire.jsonl while legacy kimi-cli writes <root>/sessions/GROUP/UUID/wire.jsonl, so the grandparent component being `agents` identifies the format regardless of scan root. This removes the only production env read among the sessions/* parsers and drops the serial_test requirement from kimi.rs. Also merge the field-identical KimiCodeUsage into TokenUsage via serde aliases (snake_case names stay canonical, camelCase added as aliases — purely additive for legacy parsing), extract the shared clamp/skip-zero logic into TokenUsage::to_breakdown(), collapse the duplicated load_or_parse_source routing branch in lib.rs into a single fn-pointer call, tighten kimi-code test assertions, repoint the model-normalization test at normalize_kimi_code_model with edge cases, and fix the "| Kimi Code|" spacing and "``` json" fence typos across all four READMEs. Constraint: behavior must be preserved exactly; slop cleanup of PR junhoyeo#682 only Constraint: scanner walks both kimi roots matching file_name == "wire.jsonl" at any depth, so routed paths always carry the client-created layout Rejected: keep env read in parser | only parser with a production env read; ignored use_env_roots and forced serial tests Rejected: pass format flag from scanner to parser | larger plumbing change for the same routing decision Confidence: high Scope-risk: narrow Directive: the agents/ grandparent check relies on kimi-code's on-disk layout; if kimi-code ever changes its session tree, update is_kimi_code_path and the module docs together Not-tested: a legacy kimi-cli session whose GROUP directory is literally named "agents" (would now route to the kimi-code parser; not a real kimi-cli layout)
Rename widgets::scrollbar_state to viewport_scrollbar_state so the helper is no longer shadowed by its own result binding at all 8 call sites. Drop the redundant .max(1) at the hourly call site (the helper already clamps viewport_len internally), rename the scrollbar_state_* tests to scrollbar_position_* to match the fn they exercise, replace the tautological wide-math test with hardcoded expected values, and add coverage for the public helper's zero-viewport guard. Confidence: high Scope-risk: narrow
…ggregation (junhoyeo#329) * feat(cli): add UUID v4 device-id generation and persistence * feat(api): accept device-id in submit route for per-device dedup * feat(api): add GET /api/me/stats endpoint for cross-device aggregation * feat(cli): send X-Device-Id header on submit * test(api): extend legacy migration and backward compat test coverage * feat(tui): add remote stats fetch and cache module * feat(tui): integrate remote stats into existing views with sync indicator * fix: address review issues in remote fetch and device-id handling - Add #[serde(default)] to fetched_at_secs to prevent deserialization failure when server response omits this local-only metadata field - Soft-fail cache write errors so a cache save failure no longer discards valid freshly-fetched remote stats - Preserve DataSource::Remote when background local reload completes, preventing the local data source from overriding the remote view - Skip background local reload when remote data is already active - Use || instead of ?? for X-Device-Id fallback to handle empty-string headers correctly alongside helpers.ts which already used || * fix(tui): scope remote cache to authenticated account and propagate fetch to live session - Store cached_for_user in remote stats cache and reject it when the current account differs, preventing a previous account's aggregated totals from appearing as synced after a token or account switch - Load credentials before loading remote cache so the username is available for the scope check - Add a dedicated remote_rx channel so background-fetched remote stats are delivered to the running TUI instead of only warming disk cache, making multi-device aggregation visible immediately in the same session * fix(tui): scope remote cache by API URL and reject empty username entries - Add cached_for_api_url field to RemoteStats for server-scoped caching - Validate API URL in load_cached_remote_stats to prevent cross-server data leaks - Reject cache entries with empty cached_for_user when expected_user is provided - Prevents stale data from wrong backend and legacy cache bypass * fix(tui): prevent local reload from overwriting remote data - Skip local data update when in Remote mode to preserve authoritative remote state - Ensures UI label (Synced) matches displayed data source - Shows Remote data unchanged status instead of silently overwriting * fix(tui): normalize API URL comparison in remote cache - Trim trailing slashes from cached and expected API URLs - Prevents false cache invalidation for equivalent URLs - Fixes mismatch between https://api.example.com and https://api.example.com/ * fix(tui): gate remote cache lookup on credential presence - Skip remote cache lookup when user is logged out - Prevents unauthorized display of cached remote account stats - Ensures remote data is only shown to authenticated users * feat(api): add /api/me/stats aggregated multi-device endpoint Rebuild PR junhoyeo#329's stats endpoint on main's relational device model: per-day totals are GROUP BY date sums over daily_breakdown (which is scoped per submitted_device_id), so the response aggregates across all of the user's devices. Returns schemaVersion 1, overall totals, per-day rows (tokens/input/output/cost), device count, and per-device last-submit timestamps. Auth reuses the exact bearer-token path POST /api/submit uses (getBearerToken + authenticatePersonalToken, touchLastUsedAt: false); browser session cookies are intentionally not accepted. Read-only — no writes on any path. Refs junhoyeo#699 Constraint: response shape must stay consumable by old CLIs, hence explicit schemaVersion field Constraint: no schema/migration changes — main's submitted_devices model is authoritative Rejected: reuse sourceBreakdown JSON parsing from the original PR | superseded by relational daily_breakdown rows; SQL sums are simpler and match /api/users/[username]/devices Rejected: cookie session fallback | endpoint exists for the CLI; submit's bearer-only contract keeps auth surface minimal Confidence: high Scope-risk: narrow Co-authored-by: blpeng2 <blpeng2@users.noreply.github.com> * feat(tui): show server-side aggregated multi-device stats Rebuild PR junhoyeo#329's TUI half on today's TUI architecture: - tui/remote.rs: fetch GET /api/me/stats with the stored CLI token (auth::resolve_api_token + auth::get_api_base_url, so TOKSCALE_API_URL and TOKSCALE_API_TOKEN behave exactly like submit), persist a ~1h-TTL cache under <cache_dir>/remote-stats-cache.json scoped by account and API server, and reject unknown schemaVersions. - App: cache-first init_remote_stats at startup, then on_tick-driven background refresh over the same channel+poll pattern the usage tab uses (thread + mpsc + try_recv), throttled to one attempt per 5min so offline/logged-out sessions never spin. - Footer status row: always-visible data-source indicator — "local" vs "local+remote (N devices)" — plus the all-devices aggregate (tokens/cost) when remote stats are available, styled via the theme (accent/muted), replacing the original PR's emoji + DataSource-enum approach that replaced local data wholesale. Every failure path degrades silently to local-only: no blocked render, no error states, no status spam. Refs junhoyeo#699 Constraint: TUI render must never block on the network; remote data is additive, not a replacement for local data Constraint: tests must stay hermetic — fetch path is cfg(not(test)) and cache tests pin TOKSCALE_CONFIG_DIR Rejected: PR junhoyeo#329's DataSource enum swapping UsageData wholesale | remote payload no longer carries per-model/client breakdowns, and silently replacing local rows misrepresents what is on screen Rejected: refetch on every tick once stale | hammers the API when offline; 5min retry throttle bounds it Confidence: high Scope-risk: narrow Directive: remote stats consumers must treat remote_stats == None as normal (logged out/offline), never as an error Not-tested: live end-to-end fetch against a running server (unit tests cover cache scoping/TTL/schema gating only) Co-authored-by: blpeng2 <blpeng2@users.noreply.github.com> --------- Co-authored-by: blpeng2 <264722857+blpeng2@users.noreply.github.com> Co-authored-by: Junho Yeo <i@junho.io> Co-authored-by: blpeng2 <blpeng2@users.noreply.github.com>
* fix(cli): detect musl correctly under Bun and Alpine Bun's process.report has no glibcVersionRuntime and an empty sharedObjects list, and musl's ldd rejects --version (it prints "musl libc" to stderr and exits non-zero), so the launcher fell through to the glibc default on Alpine and selected the gnu binary. Detection now also checks Bun's release.sourceUrl build flavor, the musl dynamic loader at /lib/ld-musl-*.so.1, and parses stdout/stderr captured from the failed ldd probe. A new TOKSCALE_LIBC=musl|gnu env var forces the result when detection cannot. Fixes junhoyeo#697 Constraint: launcher must stay dependency-free and work under both Node and Bun Rejected: parsing ldd stderr alone | Bun never reaches ldd when spawn shims differ; loader-file check is sturdier and runs first Confidence: high Scope-risk: narrow Not-tested: arm64 musl images (logic is arch-independent; verified x64 musl/glibc via Docker) * fix(cli): don't treat a coexisting musl loader as the host libc The loader-file check ran before the ldd probe, so a glibc host with the musl package installed (e.g. Debian with musl-tools) was detected as musl under Bun, where process.report is inconclusive. The ldd probe now runs first and short-circuits on explicit musl/glibc/gnu mentions; the loader scan is a last resort that prefers the glibc loader when both are present, since musl can coexist on glibc hosts but not the reverse. Constraint: Bun's process.report cannot distinguish the host libc on glibc systems Rejected: keeping loader check first and excluding known-coexistence paths | any allowlist of paths is fragile across distros; probe order fixes the class of problem Confidence: high Scope-risk: narrow Not-tested: glibc hosts where ldd --version mentions neither glibc nor gnu (falls through to loader scan, which prefers ld-linux) * fix(cli): break loader-coexistence ties with the distro marker Preferring the glibc loader unconditionally mis-detects Alpine with gcompat installed (it ships an ld-linux-* stub) as gnu in the rare case the loader scan is reached. A lone loader still wins outright; when both loaders exist, /etc/alpine-release decides, which resolves both coexistence directions (Debian+musl package -> gnu, Alpine+gcompat -> musl). Constraint: the loader scan only runs when process.report and ldd are both inconclusive Rejected: parsing /etc/os-release for musl-based distro IDs | more moving parts for the same rare branch; alpine-release covers the dominant musl distro Confidence: high Scope-risk: narrow Not-tested: non-Alpine musl distros with glibc compat stubs and no ldd (still resolve gnu; TOKSCALE_LIBC=musl covers them)
junhoyeo#701) * fix(frontend): allow the deployment's own origin in the CSRF allowlist Self-hosted deployments on custom domains got 401 "Not authenticated" on every cookie-authenticated mutation (creating API tokens, group management) because the CSRF origin allowlist defaulted to the hosted domains and the CSRF_ALLOWED_ORIGINS escape hatch was undocumented. The origin derived from NEXT_PUBLIC_URL — which self-hosters already set for OAuth redirects — is now always included in the allowlist, even when CSRF_ALLOWED_ORIGINS overrides the defaults. Both variables are now documented in .env.example. Fixes junhoyeo#695 Constraint: CSRF check must never widen to attacker-controllable input (request Host/X-Forwarded-* headers) Rejected: same-origin comparison against the request Host header | Host can be wrong behind misconfigured proxies and is not operator-declared intent Rejected: docs-only fix | NEXT_PUBLIC_URL already declares the deployment origin, requiring a second var for the same value is a footgun Confidence: high Scope-risk: narrow Directive: NEXT_PUBLIC_URL origin is intentionally allowed even when CSRF_ALLOWED_ORIGINS is set — do not make the env var fully replace it * fix(frontend): drop nonexistent tokscale.dev from CSRF defaults The domain is not ours, so allowlisting it lets whoever registers it mount CSRF attacks against cookie sessions. The existing allow-test now asserts rejection instead. Confidence: high Scope-risk: narrow * fix(frontend): only derive CSRF origin from http(s) NEXT_PUBLIC_URL new URL("mailto:...").origin is the literal string "null", and browsers send Origin: null from sandboxed iframes, so a non-HTTP NEXT_PUBLIC_URL would have allowlisted a CSRF vector. Non-http(s) schemes are now ignored like malformed URLs. Constraint: the opaque origin "null" must never enter the allowlist Confidence: high Scope-risk: narrow
…unhoyeo#702) * feat(frontend): show per-device usage on profile and settings Surface the per-device aggregates that already exist server-side: - Public profile (/u/[username]) gains a "Devices" section rendered from GET /api/users/[username]/devices, fetched server-side in page.tsx in parallel with the existing profile fetch. Hidden entirely when a user has no recorded devices; tolerates fetch failure by omitting the section instead of failing the page. - Settings gains a "Devices" section listing each device with a usage summary (tokens, cost, active days, last submit) and an inline rename flow wired to PATCH /api/settings/devices/[deviceId]. Client-side validation mirrors the server's RenameBodySchema (<=120 chars, no control characters); empty input clears the custom name back to the deviceDisplayLabel fallback. - New formatRelativeTime helper in lib/format.ts (with tests) for the "last submit" timestamps on both surfaces. Constraint: No new API routes; settings reuses the public devices endpoint with the session user's username since /api/me/stats is bearer-token-only (CLI) and unusable from a cookie session. Constraint: Public devices endpoint returns the fallback display label, not the raw null name, so the rename input detects fallback labels and pre-fills empty instead. Rejected: GET /api/settings/devices listing route | public endpoint already returns everything the settings UI needs. Rejected: date-fns formatDistanceToNow for relative time | verbose output ("about 3 hours ago") and untestable without injection; a 20-line helper with an injectable clock fits the existing lib/format.ts pattern. Confidence: high Scope-risk: narrow Not-tested: hydration of relative timestamps when the 60s ISR cache serves a stale page (suppressHydrationWarning covers the text node). * style(frontend): tokenize stray hardcoded colors on profile and settings Replace the profile page's hardcoded #10121C background with var(--color-bg-default) (same value, token form) and the settings token icon's stale #737373 neutral gray with var(--color-fg-muted), matching the design tokens both pages already use everywhere else. Deliberately not changed: the landing page uses its own bespoke navy palette (#01070f / #10233e / #0073ff accent) that is disjoint from the globals.css token system the app pages share; retheming profile and settings onto it would be a site-wide restructure, not a surgical fix. Confidence: high Scope-risk: narrow * fix(frontend): address device UI review findings Three review findings on the device breakdown UI: 1. Stale rename on public profile: the profile page's devices fetch now carries the same `user:<lowercased-username>` cache tag the rename PATCH revalidates, and revalidateUsernamePaths additionally flushes /api/users/[username]/devices (called from the PATCH route too) so a rename is visible immediately instead of after the 60s ISR window. 2. Style duplication: the card/header/row/metric-cell styled primitives shared by ProfileModels and ProfileDevices now live in components/profile/listStyles.ts; both tables consume them so the layouts cannot drift. Rendered output is unchanged. 3. Rename prefill clearing legitimate names: the public devices API now exposes the raw nullable `customName` alongside the resolved `displayName`, and the settings rename input prefills from `customName ?? ""` instead of comparing the resolved label against the fallback string. Constraint: dedup must keep profile tables pixel-identical Rejected: sharing MetricText/CostText too | ProfileModels has an extra 390px breakpoint the devices table lacks Rejected: styled(ListMetricCell).attrs for fixed widths | styled-components v6 attrs typing rejects omitted required props Confidence: high Scope-risk: narrow Not-tested: end-to-end ISR invalidation on Vercel (verified via unit-level revalidate assertions only)
…ng (junhoyeo#634) * fix(pricing): parse modern Claude version from id instead of hardcoding `normalize_model_name` matched each opus/sonnet/haiku minor version with a hardcoded branch and a catch-all `claude-opus-4`. Every new release (4.5 → 4.6 → 4.7 → 4.8) needed a new branch; a missing one let ids like `aws.claude-opus-4-8` fall through to the catch-all, which OpenRouter then resolved to legacy `anthropic/claude-opus-4` ($15/$75 per M instead of $5/$25) — a ~3x overcharge. This repeated for 4.7 (junhoyeo#580) and again for 4.8. The modern Claude line (major >= 4) follows the regular `claude-{family}-{major}[-{minor}]` scheme, so parse the version straight from the id and build the canonical key dynamically. New minor releases (4.9, 5.0, …) now resolve to their own pricing key with no code change — pricing values still come entirely from the upstream datasets. Boundary contract from the old matcher is preserved: `opus-4-60` → `claude-opus-4` (two-digit minor degrades to major), `opus-14-6` → None (two-digit major is not the modern line), undelimited `opus4`/`opus-4x` → None. The irregular legacy 3.x naming keeps its explicit branches. Tests: - test_normalize_future_minor_versions_resolve_without_hardcoding — 4.9/5.0 and cross-family (sonnet/haiku) parse without a hardcoded entry - test_normalize_modern_claude_boundaries — locks the boundary contract - 4.8 regression tests mirroring the 4.7 set (short/dot/aws forms + cost band) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style: wrap haiku condition to satisfy cargo fmt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(pricing): enforce never-degrade across all Claude families and majors Generalize the opus-only version handling in PricingLookup so every Claude family (opus, sonnet, haiku, fable) gets the same guarantees: - normalize_claude_opus_4_minor -> normalize_claude_family_minor: family/major/minor parsed from the id in both orders (sonnet-4.7, claude-4-6-sonnet), so reversed-order sonnet/haiku ids resolve to their canonical key instead of cross-family fuzzy fallbacks. - normalize_claude_family_bare_major: bare modern majors (claude-opus-5, claude-sonnet-5, fable-5) normalize to a canonical key that resolves only via an exact dataset hit. - contains_delimited_modern_major_minor: the 4.x major-minor None-guard in normalize_model_name now covers majors 4-9, so 4-60 / 5-0 / dated forms stay unresolved instead of degrading. - resolves_unsafe_claude_version (was resolves_different_claude_opus_4_ minor): vetoes cross-family resolutions (bedrock sonnet ids billed as opus), cross-version resolutions for any family (sonnet-4-7 -> sonnet-4.6, haiku-4-6 -> haiku-4.5 / 3.5-haiku), and any modern-Claude resolution for ids whose version could not be parsed. Bare major 4 stays unpinned to preserve existing claude-opus-4 fuzzy behavior. Whether a version is "known" remains dataset-driven, exactly as it was for opus: the canonical key either exact-matches the pricing dataset or the id resolves unpriced. No hardcoded minor lists were added. Reworks PR junhoyeo#634 on top of main's mechanisms instead of merging its parallel parser. Constraint: unknown minors/majors must resolve unpriced, never to a cheaper or different price Constraint: all of main's existing pricing tests must pass unchanged Rejected: PR junhoyeo#634's original generic parser | degrades two-digit minors (opus-4-60 -> claude-opus-4) and drops reversed-order coverage Rejected: extending the hardcoded 4.5/4.6/4.7 allowlist in has_unrecognized_claude_four_minor | recreates the new-minor-release maintenance trap PR junhoyeo#634 set out to remove Rejected: pinning bare major 4 ids (claude-opus-4) to exact-only | changes long-standing dated/regional 4.x resolution covered by existing tests Confidence: high Scope-risk: moderate Directive: requested_claude_version deliberately unpins bare major 4; do not "complete" it to all majors without auditing dated-id (claude-opus-4-20250514) and regional-key resolution Not-tested: provider-hinted lookups (lookup_with_provider) against cross-family adversarial datasets; models.dev reseller keys that legitimately embed reversed-order ids (cortecs/claude-4-6-sonnet) are covered only via real-data probing --------- Co-authored-by: xiaochaozheng <xiaochaozheng@meituan.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Junho Yeo <i@junho.io>
…icing sources (junhoyeo#707) * fix(pricing): block brand-token fuzzy matches and prefer canonical sources A post-junhoyeo#634 audit of lookup.rs against the real Anthropic catalog found two mispricing bugs: 1. Retired `claude-2.1`/`claude-2.0` (in historical logs, absent from every dataset) eroded to bare `claude` in try_strip_unknown_suffix (the "2.1" segment failed the all-digits version check) and then fuzzy-matched `anthropic/claude-opus-4.7-fast` at $30/$150. Fixed by (a) generalizing the stripper guard's version-segment recognition to digits-with-optional-dot and refusing strips that erode a claude id to a bare brand token, and (b) blocklisting bare `claude` and `anthropic` from fuzzy matching outright. 2. `claude-opus-4-6-fast` resolved to Models.dev reseller `venice/claude-opus-4-6-fast` ($36/$180) instead of canonical `anthropic/claude-opus-4.6-fast` ($30/$150) because the models.dev model-part pass ran before the separator-normalized OpenRouter exact pass in lookup_auto. Fixed by reordering so canonical separator-normalized passes run first, with models.dev kept as the long-tail fallback; the models.dev model-part index now also picks a deterministic provider (anthropic/ first, then shorter key, then lexicographic) instead of HashMap-iteration-order roulette. Reorder safety diff over the real cached datasets (197 probed ids): only the intended resolutions changed (claude-2.x/claude/anthropic -> None; opus-4-6/4-7-fast -> canonical; identical-price namespace moves; one previously nondeterministic equal-length tie made deterministic). Constraint: ids absent from all datasets must resolve unpriced, never to another model's price Constraint: existing junhoyeo#634 lookup contract must hold (only the assertion pinning the bug itself changed: is_fuzzy_eligible("claude") is now false by design) Rejected: hardcoding claude-2.x ids | the guard must cover future family-less shapes Rejected: pure lexicographic provider choice for shared model parts | would silently move 161 model parts to different-priced providers; anthropic-then-shortest-then-lex preserves historical winners Confidence: high Scope-risk: moderate Directive: lookup_auto pass ordering is load-bearing — canonical (LiteLLM/OpenRouter) separator-normalized exact passes must stay ahead of the models.dev model-part pass Not-tested: providers whose models.dev keys differ only by case * fix(pricing): respect provider hints and priced-key preference in lookup fallbacks Addresses three junhoyeo#707 review findings: 1. Provider-hint bypass: the canonical-source reorder let the unscoped separator-normalized OpenRouter pass return anthropic/... before the provider-scoped models.dev pass could honor a hint like `venice`. Provider-scoped models.dev passes (raw and version-normalized) now run before the unscoped normalized fallback whenever a hint is present, so the reorder only preempts models.dev for unhinted lookups. 2. Bare-brand-token coverage: `anthropic` joins `claude`/`claude-2.x` in the must-resolve-to-None assertions (catalog invariant test and the in-module fuzzy-blocklist regression test). 3. Unpriced-key shadowing: the models.dev model-part index now skips entries without any usable pricing, so the anthropic-first preference cannot pick an unpriced anthropic/<model> row over a priced reseller row and bill usage at zero. The loader already guarantees priced entries (models_dev::cost_to_pricing), but new_with_models_dev is public, so the index guards itself. Constraint: unhinted resolutions must keep the canonical-first behavior the PR introduced (bug 2) Rejected: gating the unscoped raw exact litellm/openrouter passes on the hint too | pre-existing behavior outside this PR's regression, broader blast radius Rejected: filtering zero-cost (0.0) entries from the model-part index | 0.0 is deliberately valid pricing for free models (is_valid_price_value) Confidence: high Scope-risk: narrow Directive: provider-scoped passes must stay ahead of unscoped normalized fallbacks in lookup_auto; both new regression tests fail if either guard is removed Not-tested: hinted lookups where the hint matches multiple models.dev providers sharing a model part * fix(pricing): pin provider hints ahead of unscoped OpenRouter model-part dbec203 pinned provider-hinted lookups ahead of the separator-normalized fallbacks, but the unscoped OpenRouter exact pass still ran first and its model-part fallback could leak: for a dotted id like claude-opus-4.6-fast, exact_match_openrouter matches anthropic/claude-opus-4.6-fast by model-part and returns before the provider-scoped models.dev pass — so a venice-hinted lookup got the canonical price instead of venice's own key. The hyphenated form fell through correctly (its model-part is dotted), which is why dbec203's test missed this. Split exact_match_openrouter into _full_key (the id's own canonical key, which still wins under a hint) and _model_part (matches any provider sharing the model-part, which a hint must override). In lookup_auto the provider- scoped models.dev pass now runs between them. Unhinted lookups are unchanged (full_key.or(model_part) == the old combined match). Constraint: an exact full-key match is the id's own key and stays first even under a hint Constraint: a hint for a provider with no matching key must fall through to the canonical resolution, not None Rejected: gating the unscoped litellm/openrouter full-key passes on the hint too | full-key is the id's own key; broader blast radius for no correctness gain Confidence: high Scope-risk: narrow Directive: provider-scoped models.dev must stay between OpenRouter full-key and model-part passes in lookup_auto; the dotted-id regression test fails if it moves * fix(pricing): block generic "model"/"router" tokens from fuzzy match Harvesting the distinct model ids in real local session data surfaced three ids (`model-zero-usage-v1`, `model-nonzero-usage-v1`, `test-model`) that mispriced to `azure_ai/model_router` ($0.14/MTok). Root cause: after suffix/prefix stripping their only fuzzy-eligible remnant is the bare word `model`, which substring-matches the priced key `azure_ai/model_router` (and `router` matches `kilo/switchpoint/router`). These are generic English words carrying no model identity — the same defect class as the already- blocked bare brand tokens (`claude`, `anthropic`). Add `model` and `router` to FUZZY_BLOCKLIST so such ids stay unpriced (never-degrade) instead of billing at an unrelated key's rate. Exact-key matches (e.g. the real `azure/model-router` id) are unaffected. Constraint: must not change resolution of any real model id — before/after resolution over the full harvested local id set differs only on the three noise ids (now None). Rejected: raise MIN_FUZZY_MATCH_LEN | `model`/`router` are >=5 chars, would not be caught and longer generic words would still slip through. Confidence: high Scope-risk: narrow Not-tested: ids that strip to a generic word longer than `router` * test(pricing): drive catalog regression from real local model ids The catalog invariant test used a hardcoded ~16-id list. Replace its core with the DISTINCT model ids actually harvested from local session data (committed as tests/fixtures/local_model_ids.txt — model-id strings plus family/version/price-band expectations only, NO usage counts or user data). - anthropic_catalog_resolves_to_correct_family_version_and_price now drives every harvested claude-* id (regional/suffix/provider forms incl. vertex_ai/llmgateway/github-copilot variants) through the real cached datasets, asserting family token, major-minor version token, and price within +/-25%. A few hardcoded provider/regional forms remain as guards. - Add real_local_models_all_resolve_sanely: iterates the FULL harvested set (gpt/gemini/grok/glm/kimi/claude) and asserts each id resolves with a matching vendor token or is a documented acceptable-None, and that NO id resolves to a cross-family key. Both stay #[ignore]d (skip-graceful when cache absent). The generic-token misprice fix is covered by a non-ignored unit test in lookup.rs. Constraint: fixture must contain only model-id strings + pricing metadata, no usage counts / session ids / paths / user-identifying content. Confidence: high Scope-risk: narrow Directive: when adding a new model id to the fixture, audit its real resolution first — the version column uses the dashed spelling the matched KEY carries (e.g. claude-3-5 keys need "3-5", not "3.5"). * test(pricing): flag any acceptable-None resolution, not just input-priced The real-data catalog test's acceptable-None gate keyed off input_cost_per_token.is_some(), so a documented-None id resolving to a key with input: None but output: Some(..) would have passed silently. Mirror the sibling catalog test: flag ANY resolution for an acceptable-None id, and surface both input and output prices in the failure message. Issue identified by cubic. Confidence: high Scope-risk: narrow
* feat(jcode): add support for Jcode * fix(jcode): handle journal fallback timestamps and read errors
* fix(kiro): preserve auto labels in snapshots Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(scanner): discover Kiro globalStorage snapshots Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(kiro): ignore unknown snapshot roles Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(scanner): format kiro globalstorage roots Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore(kiro): apply rustfmt for lint Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(kiro): attribute globalStorage workspaces and disambiguate snapshot dedup keys globalStorage snapshots were unattributed (workspace_key/label hardcoded None) and shared a dedup_key of `<file_stem>:globalstorage`, so two workspaces each containing e.g. execution.chat collapsed into one key. Extract the workspace folder (the path segment after kiro.kiroagent/), run it through the same normalize_workspace_key / workspace_label_from_key helpers the file/sqlite paths use, and namespace both session_id and dedup_key by workspace. Confidence: high Scope-risk: narrow Directive: globalStorage and kiro-cli sqlite dedup_keys are structurally disjoint by design (`<ws>/<stem>:globalstorage` vs `<conversation_id>:<turn_index>`) and the surfaces are physically distinct (IDE vs CLI); do NOT add cross-source dedup without first proving a real overlap. * style: apply rustfmt --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Junho Yeo <i@junho.io>
Keep repeated child session metadata from resetting fork replay state after the child turn has started. Add parser and aggregate-path coverage for Codex user fork sessions, and bump the source message cache schema so stale empty entries are invalidated. Co-authored-by: Junho Yeo <i@junho.io>
* feat(sessions): add Command Code as a tracked source Command Code (commandcode.ai) stores session transcripts under ~/.commandcode/projects but does not persist token usage on disk — the CLI computes usage in memory and sends it to its backend. Estimate usage from the transcript: input from the cumulative conversation context preceding each assistant turn and output from the assistant's own content, at ~4 characters per token (matching tokscale's other estimated sources), counted from each message's canonical JSON serialization so structured tool args/results are included. Canonicalize the configured model id from ~/.commandcode/config.json (e.g. "MiniMaxAI/MiniMax-M3-Free" -> "MiniMax-M3") so pricing resolves to the real paid model rather than the free-promo entry or a fuzzy mismatch. The estimate approximates tokens processed and intentionally does not match Command Code's server-reported usage (which reflects tool-output truncation and auxiliary model runs absent from the transcript). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(commandcode): document and pin cumulative-input estimation behavior Directive: input estimation is intentionally an upper bound; changing re-sent context to cache_read is a maintainer decision needing real billing data Confidence: high Scope-risk: narrow * style: apply rustfmt --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Junho Yeo <i@junho.io>
* feat: add MiMo Code client support MiMo Code (github.com/XiaomiMiMo/MiMo-Code) is a TypeScript fork of OpenCode, released V0.1.0 on June 10, 2026. It stores session data in SQLite at ~/.local/share/micode/mimocode.db with a schema nearly identical to OpenCode's. Changes: - Add MiMoCode client definition (ClientId=28) in clients.rs - New session parser micode.rs (adapted from opencode.rs) with SQLite support and 6 unit tests - Add discover_micode_dbs() in scanner.rs for DB discovery - Integrate MiMo Code parsing pipeline in lib.rs - Add Micode variant to ClientFilter in main.rs - Add TUI entry with hotkey 'j' - Add frontend support (types, display name, logo, brand color) All 1710 tests pass. * docs: add MiMo Code client to README in all languages * chore: format micode changes * fix(micode): correct logo org/repo links, doc timestamps, optional cache, add tests Align MiMo Code logo and repo links on the real github.com/XiaomiMiMo org, fix README time.created examples to 13-digit epoch ms (parser convention, matching OpenCode), make the cache field optional so cache-less assistant messages are not silently dropped, drop the unused sessionID JSON field, and remove a redundant sort/dedup. Adds parser and scanner-filename tests. Confidence: high Scope-risk: narrow Not-tested: real mimocode.db timestamp unit (verified against OpenCode fork convention, no sample DB available) --------- Co-authored-by: Junho Yeo <i@junho.io>
junhoyeo#713) * feat(sessions): read Antigravity CLI usage from local SQLite databases The Antigravity CLI (the terminal agent that stores its data under `~/.gemini/antigravity-cli/`) was never counted. tokscale only knew two Gemini-family sources: the Gemini CLI (scans `~/.gemini/tmp/*.{json,jsonl}`) and Antigravity (pulls usage from a running IDE language server over RPC and caches it under the config dir). The Antigravity CLI fell into neither bucket, so its on-disk usage was invisible — `tokscale antigravity sync` found the filesystem candidates but cached zero because its only artifact path still requires a live language-server RPC connection. This adds Antigravity CLI as a first-class local scan source so its usage updates automatically like every other file-based source — no RPC, no `antigravity sync`. A new `antigravity-cli` client globs `~/.gemini/antigravity-cli/conversations/*.db` (honoring `GEMINI_CLI_HOME`) and a new parser reads each conversation database directly. Each `gen_metadata` row is one generation encoded as the same `GeneratorMetadata` protobuf the IDE returns over `GetCascadeTrajectoryGeneratorMetadata`. The repository has no `.proto`/prost decoder (the IDE path receives JSON because the language server does the proto-to-JSON conversion), so the parser ships a tiny dependency-free wire-format reader and pulls only the fields it needs. The field numbers were reverse-engineered from real databases and cross-checked across 6 sessions / 140 turns: `chatModel.#19` is the response model, `usage.#5`/`#9`/`#10` are cacheRead/output/thinking (verified by the invariant `#9 + #10 == #3`, the stored total output), `#11` is the responseId used for dedup, and input combines the fixed system-prompt count `#1` with the newly-processed input `#2`. The session timestamp and workspace come from `trajectory_metadata_blob`. Adding the new `ClientId` variant fans out to the usual registration points: the scanner gains a `*.db` glob arm (which naturally rejects `.db-wal`/`.db-shm` sidecars), both local-parse dispatch paths gain a branch, and the CLI `ClientFilter`, client labels, TUI picker, and frontend source maps gain entries. The deprecated per-client boolean flags intentionally do not, since `antigravity-cli` is reachable only via the canonical `--client antigravity-cli`. Closes junhoyeo#712. * fix(sessions): handle file:// authority/UNC paths and test Antigravity CLI wiring Addresses the cubic review on junhoyeo#713. `file_uri_to_path` previously stripped `file://` and only special-cased the leading slash before a Windows drive letter, so a non-empty authority (`file://host/share/...`, the UNC form) lost its host and collapsed into a bare path. It now treats an empty-authority remainder as before (`/C:/x` → `C:/x`, `/home/x` kept) and reconstructs a non-empty authority as a UNC path (`host/share/x` → `//host/share/x`) so `normalize_workspace_key` preserves the `//` prefix. A unit test covers the Windows-drive, POSIX, UNC, and percent-encoded-CJK cases. The new `AntigravityCli` client wiring is now asserted in `test_client_as_str`, `test_client_key`, and `test_client_from_key` (display name "Antigravity CLI", hotkey `f`, and the reverse hotkey mapping). * style: rustfmt antigravity_cli.rs * fix(antigravity-cli): add gemini-3-flash-a pricing alias and harden parser tests Map the raw #19 responseModel `gemini-3-flash-a` onto the priced `gemini-3-flash-preview` so Antigravity CLI cost no longer resolves to 0. Add alias-resolution, #9/#10==#3 field-mapping invariant, and malformed-protobuf bounds tests. Constraint: must not weaken the junhoyeo#707 brand-token fuzzy-match guard in lookup.rs Confidence: high Scope-risk: narrow --------- Co-authored-by: Junho Yeo <i@junho.io>
…unhoyeo#857) * feat(profile): make embed configuration truthful and compact Replace the oversized embed editor with a viewport-contained preview-first workflow, remove promotional filler, use descriptive template labels, and derive visible controls plus URL parameters from the renderer capability matrix. Constraint: Preserve every public embed query id and API contract without adding dependencies Rejected: Keep every control visible in every view | several combinations are ignored by the 3D and compact renderers Confidence: high Scope-risk: moderate Reversibility: clean Directive: Add renderer capabilities to embedDialogOptions before exposing future controls Tested: 5 embed option tests, 12 embed route tests, scoped ESLint and Prettier Not-tested: Full production build deferred to the final branch verification * feat(profile): make usage history explorable at a glance The profile now uses one compact service language for identity, model-level stacked usage, contribution density, token mix, supporting facts, and device history. Daily contribution cells provide viewport-safe hover detail and a pinned client/model breakdown without losing keyboard access. Constraint: Preserve existing profile API, authentication, and submission contracts while using no new frontend dependency. Rejected: Reuse the shared local-only chart renderer | it cannot represent model-level stacked history or the profile interaction contract. Confidence: high Scope-risk: moderate Directive: Keep tooltip sorting independent from stable stack order, and keep Token mix immediately below Contributions. Tested: 35 focused profile unit tests, changed-file ESLint, Prettier, diff check, real-data browser interaction, and visual-verdict score 94/100. Not-tested: Full frontend suite and production build are deferred to the final branch verification pass. * fix(profile): keep model shades consistent with the TUI Model colors now use the same family and version precedence as the terminal UI, so Fable receives the primary source shade before Opus, Sonnet, Haiku, and remaining families. Shade ranking remains independent from area stack order and value-sorted tooltip rows. Constraint: Preserve each source base color and the existing chart ordering contracts. Rejected: Rank shades by spend or token volume | this made the same model family change color as the selected metric or range changed. Confidence: high Scope-risk: narrow Directive: Update frontend and TUI family precedence together if a new named Claude tier is introduced. Tested: 20 focused aggregation/color tests, changed-file ESLint, Prettier, and diff check. Not-tested: Cross-platform pixel comparison of browser and terminal color rendering. * fix(profile): keep wide analytics dense and geometrically correct The desktop profile now uses a 1280px responsive canvas with a full-width usage chart and a compact activity/details split below it. Active chart points render in CSS pixel space, so the intentionally stretched plotting SVG cannot distort circular markers. Constraint: Token mix must remain directly below Contributions, and tablet/mobile must retain the same single-column reading order. Rejected: Preserve the 896px content ceiling | it cramped the area chart and wasted wide-screen space. Rejected: Compensate SVG circle radii with a fixed ratio | the ratio changes with every responsive container size. Confidence: high Scope-risk: narrow Directive: Keep active markers outside the preserveAspectRatio=none SVG unless their geometry is dynamically compensated. Tested: 33 focused profile tests, changed-file ESLint, Prettier, diff check, zero-overflow checks at 1440px and 390px, wide-layout visual score 97/100, and marker visual score 99/100. Not-tested: Full frontend suite and production build are deferred to the final branch verification pass. * feat(embed): make profile cards purposeful without decorative chrome All eight 2D renderers now share one restrained card grammar while keeping explicit information roles: overview, token focus, command readout, contribution-first, rank-first, activity signals, detailed facts, and compact ledger. Generic template-taxonomy copy, gradients, glows, patterns, metaphor chrome, and novelty decoration are removed. Constraint: Preserve template IDs, query options, XML safety, light/dark palettes, contribution titles, and the independent 3D renderer. Rejected: Make every template the same metric card with different labels | it reduced choice to cosmetic duplication. Rejected: Redesign the 3D renderer into the flat 2D grammar | its contribution geometry is an intentional supported view. Confidence: high Scope-risk: moderate Directive: Keep new templates within the shared surface/header/footer and semantic-color grammar, but give each one a distinct data hierarchy. Tested: 106 focused renderer/route/3D tests, changed-file ESLint, Prettier, diff check, xmllint on ten actual-data SVGs, and embed gallery visual score 94/100. Not-tested: GitHub README rendering across every third-party Markdown host. * fix(embed): preview live renderers at a readable scale The modal now loads its preview from the current origin while keeping copied snippets canonical to tokscale.ai, so local iteration shows the renderer actually being edited. Redundant preview chrome is removed, 2D and 3D receive aspect-appropriate stage heights, and 3D continues to expose only controls its renderer consumes. Constraint: Copied Markdown, HTML, and image URLs must remain canonical production links. Rejected: Point the entire embed builder at the current origin | that would leak localhost URLs into copied snippets. Confidence: high Scope-risk: narrow Directive: Keep preview URL construction separate from canonical snippet generation. Tested: 41 focused modal/route/3D tests, changed-file ESLint, Prettier, diff check, actual-data 2D and 3D browser previews, and modal visual score 96/100. Not-tested: Clipboard APIs in non-Chromium browsers. * fix(profile): keep contribution model totals internally consistent Nested model snapshots can carry a stale aggregate alongside newer token components, so the contribution detail now follows the usage chart reconciliation rule and keeps the larger truthful value. Constraint: Preserve valid reported aggregates when they exceed the component sum. Rejected: Always replace the aggregate with the component sum | older payloads may include token categories the current schema does not expose. Confidence: high Scope-risk: narrow Directive: Reconcile nested token aggregates with Math.max anywhere profile detail consumes both representations. Tested: Regression reproduced before the fix; 60 frontend test files and 507 tests passed afterward, plus scoped ESLint, Prettier, and diff check. Not-tested: Historical payload variants that omit both aggregate and components. * feat(profile): keep contribution context visible beside wider usage analytics The profile now uses independent desktop stacks: a 650px Contributions and full selected-day breakdown column on the left, with a wider usage chart, usage facts, and token mix column on the right. Both 2D and accessible inline 3D views share palette and range-owned selection state, while the standalone breakdown expands without nested scrolling. Constraint: Preserve actual profile API data, existing period routes, mobile responsiveness, and the supported 3D contribution view without adding dependencies. Rejected: Stretch Contributions to match the usage chart row | it produced a square card with excessive empty space and a small calendar. Rejected: Reuse the 3D embed card or local canvas | neither preserves the profile range, detailed selection, or keyboard interaction contract. Confidence: high Scope-risk: moderate Directive: Keep contribution selection owned by the active range so prior dates never revive after period navigation. Tested: 60 frontend test files and 510 tests, production build and TypeScript, scoped ESLint and Prettier, actual-data profile/API smoke tests, Lifetime-to-7d-to-Lifetime selection, 2D/3D pointer and keyboard interactions, zero nested/page overflow, and visual verdict 98/100. Not-tested: Screen-reader announcements across every browser and assistive-technology combination. * feat(profile): strengthen identity and supporting data hierarchy Give the profile identity a clearer visual anchor while keeping the desktop header compact. Usage details now reads as the same class of service card as the surrounding datasets, and primary totals have an explicit left-aligned scan path. Constraint: Preserve the compact profile layout and add no dependencies. Rejected: Oversized promotional rank treatment | It would compete with the usage data and break the restrained service language. Confidence: high Scope-risk: narrow Directive: Keep rank as the only accented identity metadata; do not turn adjacent profile facts into badges. Tested: 60 frontend test files and 510 tests; production build and TypeScript; scoped ESLint and Prettier; desktop and mobile actual-data screenshots; computed avatar sizes, metric alignment, and card geometry; visual verdict 98/100. Not-tested: Every third-party avatar aspect ratio. * fix(profile): keep headline metrics compact and left anchored Limit the desktop summary to four fixed compact tracks so totals scan as one group instead of stretching across the full analytics canvas. Container queries preserve the existing two-column mobile treatment when the card itself is narrow. Constraint: Preserve the full-width profile card boundary and existing mobile hierarchy. Rejected: Shrink the entire summary element to fit-content | This leaves an unfinished empty surface under the right side of the profile header. Confidence: high Scope-risk: narrow Directive: Keep headline metric tracks compact even if the outer analytics canvas grows. Tested: 60 frontend test files and 510 tests; Next production build and application TypeScript; scoped ESLint and Prettier; desktop and mobile actual-data screenshots; 592px desktop used width, 2-column mobile layout, zero horizontal overflow; visual verdict 99/100. Not-tested: Standalone tsc --noEmit remains blocked by pre-existing mock tuple errors in groupMemberRoleRoute.test.ts lines 159-160. * fix(profile): make compact contribution charts reliably tappable Map taps on packed 2D and 3D chart whitespace to the nearest rendered day, so mobile users can select dates without hitting four-pixel cells precisely. Coarse-pointer view and palette controls expose 44px effective targets while retaining their compact visual dimensions. Constraint: Preserve the dense year overview, existing keyboard navigation, and direct cell selection semantics. Rejected: Enlarge every contribution cell to a standalone touch target | A full year would no longer fit without horizontal scrolling or loss of context. Confidence: high Scope-risk: narrow Directive: Keep chart-level selection limited to the nearest rendered cell within 24px and exclude direct cell targets from the bubbling handler. Tested: 60 frontend test files and 511 tests; production build and application TypeScript; scoped ESLint and Prettier; real coarse-pointer taps on 2D gaps and 3D whitespace at 320px; keyboard and direct-click regression audit; zero overflow at 320px, 390px, and 430px; visual verdict 99/100. Not-tested: Physical Mobile Safari touch-event behavior. * fix(navigation): keep collapsed mobile links out of focus Connect the hamburger to its controlled menu state and mark the collapsed dropdown inert, preventing invisible links from appearing in the accessibility tree or keyboard order. The compact icon retains its visual size while exposing a 44px coarse-pointer target. Constraint: Preserve the existing dropdown animation and mobile navigation appearance. Rejected: Conditionally unmount the dropdown | It removes the current close animation and adds unnecessary state-transition churn. Confidence: high Scope-risk: narrow Directive: Keep aria-expanded, aria-hidden, and inert synchronized when changing mobile navigation state. Tested: Production build and application TypeScript; scoped ESLint; closed/open accessibility snapshots; hidden-link Tab-order check; 390px menu visual check; zero horizontal overflow. Not-tested: Physical screen-reader announcement phrasing. * fix(profile): restore legible contribution intensity on dark surfaces The profile now derives a monotonic dark-surface ramp from the existing graph palettes, restores the token-linear 3D height range, persists palette choice through shared settings, and aligns preview swatches with the square legend. Constraint: Contribution cards remain fixed to the compact dark service surface. Rejected: Reuse light-canvas grades directly | high-intensity grades lose contrast on the dark contribution surface Rejected: Quantile or logarithmic intensity scaling | would diverge from main’s max-relative token semantics Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep #191f2b synchronized with --service-surface-muted when changing the profile surface theme. Tested: Frontend test suite (511 passed); focused contribution tests after final assertions (13 passed); scoped ESLint and Prettier; Next.js production build; desktop, 390px, and 320px actual-data browser QA Not-tested: Repository-wide ESLint remains blocked by existing errors in ViewSelector and Footer; Safari before 16.2 side-face color-mix fallback * refactor(embed): make profile widgets read as one compact service The public cards now share a restrained surface, identity hierarchy, fitted typography, and the profile contribution palette while each template keeps one distinct data job. The 3D card uses the same token-derived calendar semantics and stable public dimensions. Constraint: Preserve public template IDs, query parameters, XML escaping, and intrinsic card dimensions Rejected: Template-specific decorative themes | gradients, fake chrome, and metaphor styling made the widgets feel unrelated to the profile service Confidence: high Scope-risk: moderate Directive: Keep future templates within the shared shell and derive contribution color and height from scoped tokens Tested: 563 frontend tests, changed-file ESLint, Quick Look renders for all templates, desktop/mobile browser captures, production build Not-tested: Pixel output in every third-party README renderer * fix(embed): keep contribution cards scoped and truthful Contribution facts now use the exact trailing UTC year, inclusive profile thresholds, and token-derived intensity. Required contribution views return an uncached error card when their source is unavailable instead of publishing believable zero activity, while optional facts can still degrade safely. Constraint: The database query keeps an alignment buffer so the renderer must exclude off-window rows from every visible fact Rejected: Treat fetch failures as empty arrays | it converts an outage into false zero-usage data and allows stale caching Confidence: high Scope-risk: narrow Directive: Preserve the distinction between a successful empty contribution result and an unavailable contribution source Tested: Route failure/cache contracts, exact threshold boundaries, off-window spike regressions, full 563-test frontend suite, production build * refactor(profile): make embed configuration compact and responsive The configurator now gives the live card the primary pane, keeps settings dense on desktop, uses one document scroll on mobile, and shows only controls that map to the selected renderer. Accent previews match rendered output and 2D layout state no longer leaks into 3D number formatting. Constraint: Preserve keyboard focus trapping, existing URL generation, and coarse-pointer target sizes Rejected: Separate template gallery and setup prose | duplicated the preview and pushed the actionable controls below the fold Confidence: high Scope-risk: narrow Directive: Add a capability entry before exposing any new renderer option in the dialog Tested: Desktop 1440x1100 and mobile 390x844 actual-data captures, keyboard accessibility snapshot, 563 frontend tests, changed-file ESLint, production build * fix(profile): keep empty analytics and rolling ranges truthful Reviewing the public profile and embed flows exposed edge cases where empty contribution arrays collapsed requested graphs, hidden synthetic usage distorted visible model shares, and separate server/client clocks could disagree about the rolling-year range. Normalize those boundaries and add regression coverage so the rendered metrics remain truthful for malformed, empty, and time-sensitive inputs. Constraint: Preserve canonical embed URLs and existing profile period semantics while making helper inputs safe for non-canonical usernames and non-finite values. Rejected: Resolve the hydration mismatch in the browser with a client-only guard | that would hide the mismatch instead of making the server and client render the same data. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep the server-provided chartRange authoritative for any future client-rendered rolling windows. Tested: 568 frontend Vitest tests; TypeScript check; targeted ESLint; Prettier check; production build. Not-tested: Live visual review against production data after this follow-up commit. Related: junhoyeo#857 * fix(profile): keep contribution cells visibly square The compact contribution calendar uses very small cells at wide desktop widths, so even a modest border radius reads as a field of dots instead of the familiar square contribution grid. Keep the interactive 2D cells square while retaining the existing focus and selection rings. Constraint: Preserve the existing grid geometry, colors, keyboard interactions, and isometric view. Rejected: Increase cell size to hide the rounded corners | that would change the responsive density and still fail at narrower widths. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep 2D contribution cells square unless the product intentionally changes the contribution-grid language. Tested: Profile contribution calendar Vitest suite (14 tests); Prettier check. Not-tested: Live Vercel visual capture. Related: junhoyeo#857 junhoyeo#859 junhoyeo#860 * fix(profile): preserve full rolling year across leap days Clamp the prior-year rolling range to the last valid day in its month so a Feb 29 request does not silently begin on Mar 1 and omit Feb 28 activity. Add a route regression test covering the leap-day boundary. Constraint: Rolling profile ranges must remain calendar-date based and deterministic in UTC Rejected: Rely on Date#setUTCFullYear alone | JavaScript normalizes invalid February 29 targets into March Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep leap-day behavior covered whenever rolling chart range logic changes Tested: bunx vitest run __tests__/api/usersProfile.test.ts; bunx prettier; git diff --check Not-tested: Full frontend suite and live preview after this follow-up Related: junhoyeo#857 junhoyeo#859 junhoyeo#860 * feat(profile): add compact contribution date ranges Public profiles now offer a rolling recent-year contribution graph alongside complete calendar-year views, with compact rounded cells, accessible range controls, inert future dates, and selected-day scrolling on narrow screens. The page now reuses cached server-side profile and device loaders so protected Vercel previews render their own deployment data without an anonymous self-fetch being redirected to the login page. Constraint: Vercel Deployment Protection returns an HTML login page to anonymous server self-fetches, even when the final response status is 200 Rejected: Forward request cookies to the preview fetch | request-dependent caching remains fragile and cannot serve non-interactive render paths Confidence: high Scope-risk: moderate Directive: Keep profile and device page loaders on the shared server path; do not restore server-side HTTP self-fetches without validating protected previews Tested: bun run test (575 tests); targeted ESLint; next build; contribution range browser interaction checks; git diff --check Not-tested: Authenticated Vercel preview browser smoke test; lifetime daily-detail payload growth under unusually long profile histories * fix(profile): preserve mixed client model attribution Same-day submissions can mix legacy modelId records with nested model breakdowns for the same client. Materialize the legacy totals before merging the next record so nested client models retain every source attribution in contribution detail and usage views. Constraint: Daily rows are ordered only by date, so legacy/current submission order is not stable Rejected: Keep a single client modelId fallback | it cannot represent multiple models after same-day aggregation Confidence: high Scope-risk: narrow Directive: Materialize legacy client totals before adding incoming totals whenever nested model attribution is introduced Tested: usersProfile API regression test; targeted ESLint; git diff --check Not-tested: Live mixed-version production submission corpus * fix(profile): keep inert contribution cells non-interactive Future dates remain visible in a current-year graph but are intentionally disabled. Their 3D cubes now retain a noninteractive hit marker, preventing the SVG's forgiving nearest-cell gesture from selecting an adjacent past date. Constraint: Current-year calendars visibly include future dates while input must stop at the authoritative today cutoff Rejected: Remove the SVG nearest-cell fallback | it would regress forgiving touch selection in the 3D graph Confidence: high Scope-risk: narrow Directive: Any visually rendered but inert graph cell must intercept its own hit target before a nearest-cell fallback runs Tested: profile contribution graph data tests; targeted ESLint; git diff --check Not-tested: Authenticated browser click on a protected Vercel preview
* feat(frontend): unify analytics routes around a compact service shell Introduce shared ranking controls, fact strips, mobile rows, group identity marks, and a compact footer so profiles, leaderboards, and groups can reuse one production UI language. Constraint: Preserve existing API, authentication, and data contracts without new dependencies Rejected: Reuse the black-hole hero and animated globe footer | promotional chrome obscures application data and breaks the compact profile language Confidence: high Scope-risk: moderate Directive: Keep service tokens additive and reuse RankingUI before creating route-local control or metric variants Tested: Integrated frontend suite (566 tests), ESLint, production build, and visual verdict 94/100 Not-tested: Authenticated group creation and invite acceptance with a live GitHub session * feat(leaderboard): keep ranking context visible at every viewport Remove the marketing hero, compact the aggregate and control hierarchy, preserve decisive metrics in linked mobile rows, and derive Users/Groups links from live URL state so active filters survive navigation. Constraint: Keep the existing 50-row API pagination and persisted sort preference Rejected: Hide cost, time, or submissions on mobile | losing comparison context makes the ranking misleading Confidence: high Scope-risk: moderate Directive: URL sort precedence and live view-link preservation are intentional; verify both before changing navigation state Tested: Actual-data range, sort, search, pagination, view navigation, 320px/390px overflow, frontend suite, ESLint, and production build Not-tested: Signed-in current-user rank card against a live browser session * feat(groups): make scoped rankings compact and complete Replace generic gradient cards with deterministic group identity, preserve role and usage context in mobile rankings, add the missing scoped pagination controls, and align discovery, create, join, and invite states with the compact service shell. Constraint: Reuse existing group endpoints, role permissions, and invite contracts unchanged Rejected: Keep desktop tables inside horizontal mobile scrollers | core rank, role, cost, and token facts need to remain visible together Confidence: high Scope-risk: moderate Directive: Keep invite management after the ranking and retain current-user emphasis when authenticated Tested: Actual public group data, discovery navigation, scoped range/sort/search and empty states, invalid invite state, 320px/390px overflow, frontend suite, ESLint, and production build Not-tested: Owner/admin invite creation and member leave actions with authenticated production identities * perf(leaderboard): remove non-comparative submission metadata Submission volume and freshness do not help users compare ranked usage, so global and scoped leaderboards now query and return only the identity, ranking, usage, scope, and pagination facts their screens consume. Constraint: Profile and embed surfaces still use submission counts and freshness metadata and remain unchanged. Rejected: Hide only the visible columns | would retain unused SQL aggregates, scalar subqueries, response bytes, and duplicate client types. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep profile diagnostics separate from ranking payloads; COUNT(*) is valid here because submissions.user_id has a unique constraint. Tested: 565 frontend tests; focused leaderboard tests 22/22; ESLint with zero errors; Next.js production build; actual-data API shape checks; desktop and 390px global/group smoke tests; visual verdict 96/100. Not-tested: Compatibility with undocumented third-party consumers of the public leaderboard JSON fields removed here. * fix(frontend): preserve distinct leaderboard users and shared view styles All-time pagination counts one row per user even when a user has multiple submissions, while the leaderboard view selector reuses the shared segmented control without losing full-page link semantics. Constraint: Public leaderboard totals must remain user-based after submission metadata removal Rejected: Keep a second selector style implementation | It would drift from the shared compact controls Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep navigation controls on the shared segmented styles when adding leaderboard views Tested: Frontend targeted ESLint; all-time and view-selector Vitest tests; full frontend Vitest suite (566 tests) Not-tested: Full TypeScript check remains blocked by three pre-existing repository errors
* feat(leaderboard): drop estimated time as a public metric Estimated active time cannot reliably represent human coding time: parallel agents, overlapping sessions, tool/runtime durations, and device overlap can inflate it far past wall-clock time. Rather than publish an untrustworthy comparison behind a disclaimer, remove it from public comparative surfaces. - Remove time from SortBy, LeaderboardSortBy, and VALID_SORT_BY so stale ?sortBy=time links fall back to tokens. - Remove totalActiveTimeMs from public leaderboard, group leaderboard, and profile payloads, SQL aggregates, and UI. - Retain the submissions and daily_breakdown columns, submit validation, and /local self-inspection because deployed CLIs still submit this data and a defensible metric could return later. Constraint: Existing database columns are immutable after apply, so removal must be display-only. Constraint: In-the-wild CLIs still POST totalActiveTimeMs, so submit validation must continue accepting it. Rejected: Keep time on profiles behind a methodology tooltip | a disclaimer would still publish an untrustworthy value. Rejected: Drop the time columns entirely | destructive migration and breaks older CLI submissions without public benefit. Confidence: high Scope-risk: moderate Directive: Do not re-add a public time sort until active time is interval-unioned at submit time rather than summed across overlapping sessions. Tested: 62 frontend Vitest files / 583 tests; ESLint with zero errors; standalone TypeScript; Next.js production build; git diff --check. Not-tested: Third-party consumers reading the removed totalActiveTimeMs fields from public JSON. * fix(leaderboard): honor token fallback for retired sort links Explicitly invalid sortBy query values represent retired links and must not be replaced by a persisted cost preference during SSR or hydration. Resolve them to the token ranking while retaining cookie preferences only when the URL omits sortBy, and cover the distinction with focused resolver tests. Constraint: Retired public sort links must resolve consistently across server and client rendering Rejected: Let the cookie win for invalid URLs | stale links would silently show a different ranking than the documented token fallback Confidence: high Scope-risk: narrow Directive: Preserve the absent-versus-invalid distinction when adding leaderboard sort values Tested: 62 frontend Vitest files, 574 tests; targeted ESLint; git diff --check Not-tested: Frontend TypeScript remains blocked by pre-existing groupMemberRole tuple errors and the existing hero-bg PNG module declaration error
…nt logos (junhoyeo#863) * feat(profile): model legend, today tooltip, newest-first toggle, client logos Four polish fixes on the public profile activity dashboard: - Usage over time: the legend listed clients/providers while the chart is model-based. Show the user's top models (by the selected metric) with a "+N more" cutoff via a new pure `selectLegendModels` helper. - Contributions: hovering the newest ("today") cell showed no tooltip because it is the default-selected cell and the hover was suppressed for the selected cell. Show the tooltip for any in-range cell (2D + 3D); selection/keyboard behavior is unchanged. - Contributions: add a localStorage-persisted "Newest first" toggle (2D) that mirrors the calendar so recent weeks sit on the left. Default follows layout — reversed on desktop (>=1360px, the 2-column dashboard), chronological on mobile. Hydration-safe: server + first paint render chronological, then the resolved value applies after mount. - Day breakdown: replace the colored client dot in the "Clients and models" headers with the client logo (SourceLogo), falling back to the dot when a client has no logo. Constraint: reversal must not desync month markers or keyboard nav — both route through the same display-ordered week structure as the cells Rejected: CSS scaleX(-1) flip | mirrors month-label text and breaks scroll math Rejected: apply reversal to the 3D isometric view | geometry mirror is risky and the request targets the 2D card; 3D stays chronological Confidence: high Scope-risk: moderate Directive: keep displayCells/displayMonthMarkers derivation the single source for both render and keyboard nav so reversed mode can't desync Not-tested: real-DOM hydration path (no jsdom); covered via pure-helper tests * fix(profile): re-scroll on reverse toggle; keep keyboard nav chronological Addresses two review findings on the "Newest first" toggle: - The auto-scroll effect didn't depend on `reversed`, so toggling the mirror left scrollLeft at the old edge and the now-leftmost newest weeks could sit off-screen. Add `reversed` to the effect deps so the newest cell is scrolled back into view after a toggle. - Keyboard navigation was fed the reversed display order, which scrambled the Arrow/Home/End semantics (ArrowRight crossed into the previous week, Home/End inverted). Navigate the chronological `calendar.cells` instead: arrows inspect adjacent calendar days and Home/End hit the true range boundaries regardless of the visual mirror, honoring the documented a11y contract. Directive: "Newest first" is display-only — do not route keyboard/date nav through displayCells; keep it chronological Confidence: high Scope-risk: narrow * fix(profile): mark client-header logo decorative for screen readers The client logo in the "Clients and models" headers rendered with alt={sourceId}, so assistive tech announced the source name twice — once from the image and once from the adjacent visible client name (the prior colored dot was decorative). Add an opt-in `decorative` prop to SourceLogo (empty alt) and use it here so the row keeps a single accessible label. Confidence: high Scope-risk: narrow
…junhoyeo#864) The reverse-render toggle from junhoyeo#863 landed on the Contributions calendar, but the request targeted the Usage over time chart — the card that sits in the right-hand column of the >=1360px dashboard grid. Remove the calendar mirroring entirely and mirror the usage chart's time axis instead. The whole per-day pipeline (dates, series values, daily totals) is reversed once in reverseUsageChartData, so pointer hit-testing, keyboard inspection, tooltips, and the date-range labels all follow visual order with no special cases. Trailing averages are computed on chronological data before the mirror; per-provider cost lookups map the visual index back into the chronological days array. Constraint: SSR/first paint must stay chronological (no hydration mismatch); the resolved default applies only after mount Constraint: 30d trailing average must be computed pre-reversal Rejected: reversing only the SVG x-mapping | every index consumer (tooltip, keyboard, provider costs) would need its own mirror logic Rejected: sharing one localStorage key with the removed calendar toggle | stale "0"/"1" from the mistaken feature would silently override the new responsive default Confidence: high Scope-risk: narrow Directive: keep chartData display-ordered; anything indexing the chronological `days` array must go through chronologicalActiveIndex Not-tested: manual toggling while a committed (pinned) inspection is open on a coarse-pointer device
…n panel (junhoyeo#865) Two P1 defects in the contribution graph's client filter: Bug 1 — filter chips inverted on first click. An empty `clientFilter` is the "show all" sentinel and every chip renders active, but the toggle ignored that convention: `[].includes(client)` is false, so clicking a highlighted chip selected only that client instead of deselecting it — the opposite of the aria-pressed affordance. Toggling now expands an empty filter to the full available-clients set first, and normalizes back to `[]` when the result covers every client or none, reusing the existing show-all sentinel instead of inventing a new one. Bug 2 — breakdown panel showed stale pre-filter data. `selectedDay` was a snapshot object set only on click, so after a client-filter change the heatmap/stats re-derived but the open panel kept its old clients/totals. The panel now stores the selected date and resolves the day object from the live `yearContributions` via useMemo, so it always reflects current data and closes when the date drops out of the filtered set. Toggle and day-resolution logic extracted into pure helpers (`toggleClientFilter`, `resolveSelectedDay`) with vitest coverage. Constraint: empty clientFilter must remain the show-all sentinel used by StatsPanel and the Clear/Show-all controls Rejected: add a distinct "none selected" sentinel | would fork show-all semantics across the UI Rejected: re-resolve selectedDay via useEffect | derived useMemo can't go stale and needs no extra render Confidence: high Scope-risk: narrow Not-tested: full component render (no jsdom harness for these components); logic covered via extracted pure helpers
* fix(profile): repair four adversarially-found UI defects Address four accessibility/interaction bugs found while auditing the profile contribution graph and usage chart: - Future contribution cells (in-range but unselectable in the current year) looked interactive: they kept the pointer cursor, showed a hover tooltip, yet clicking was a silent no-op. Gate the tooltip on `cell.selectable` (both 2D and 3D, hover and focus), disable the 2D button for unselectable cells, and gate the 3D pointer-enter on `interactive`. Today stays selectable so its tooltip/click survive. - The usage chart SVG `<desc>` read the date range backwards under the desktop "Newest first" default because it used the display-ordered dates. Build the from/to span from `chronologicalChartData` and append ", displayed newest first" so AT users know the visible axis is mirrored. Visible DateRange labels stay display-ordered. - The usage legend's "+N more" undercounted plotted bands: it only counted hidden model series, ignoring the non-model buckets (blank-model, provider-remainder, daily-remainder, series-remainder) the chart also draws. Count every plotted series (nonzero total) the legend omits; keep legend entries models-only. - A keyboard-focused contribution cell's tooltip was stranded when the mouse crossed a second cell and then left it: the tooltip cleared while the focused cell's aria-describedby still pointed at it. On pointer leave, re-anchor the tooltip to the focused cell when focus rests on a registered cell; otherwise clear as before. Escape-to-close intact. Constraint: no DOM test framework in this package — component-only behavior is covered via the existing pure data-layer invariants (selectable flag) and new hiddenCount unit tests. Rejected: gate future cells on `inRange` alone | leaves them non-inert Rejected: count zero-total bands in "+N more" | never visibly drawn Confidence: high Scope-risk: narrow Not-tested: live screen-reader announcement of the mirrored axis note * fix(profile): keep Escape-dismissed tooltips closed and show +N legend for bucket-only charts Two review findings on the audit-fix branch: the pointer-leave re-anchor resurrected tooltips the user had just dismissed with Escape, and a chart whose only bands are non-model buckets rendered no legend at all despite a positive hidden-series count. Constraint: re-anchoring must still restore the focused cell's tooltip in the normal hover-away case (the original stranded-aria-describedby bug) Rejected: tracking an explicit escape-dismissed flag | the open-tooltip state already encodes exactly when restoring is legitimate Confidence: high Scope-risk: narrow
…ations (junhoyeo#861) * docs(readme): drop redundant Supported/Status table columns and sync translations Both the Clients table and the Supported Platforms table carried a trailing column that read "✅ Yes" / "✅ Supported" for every single row. That column is redundant — a client or platform is only listed because it is supported — so it is removed from both tables across all four language READMEs. While there, the ja/ko/zh translations were re-synced against the English source of truth (they had drifted behind recent additions): - Added missing clients OpenCodeReview, CodeBuddy, WorkBuddy to the Clients table, the Windows data-locations table, and the multi-platform feature list. - Added the missing Autosubmit section and its Table of Contents entry to all three translations. - Added the Minutely-tab subsection (ja), the Warp/Oz data-source section (ja, ko), and the Cline data-source section (ko, zh). - Fixed Kiro/Cline row ordering in the Korean Clients table to match English. - Collapsed a duplicated "Multi-platform support" bullet in the English README (a merge artifact where one copy listed CodeBuddy and the other WorkBuddy) into a single line covering all three. All four files now agree on heading counts (16 h2 / 77 h3 / 9 h4) and client count (39). Constraint: English README.md is the source of truth for the translations Rejected: Keep the Supported/Status column | fully redundant with table membership Confidence: high Scope-risk: narrow Not-tested: GitHub-rendered markdown (validated pipe/column/heading counts locally) * docs(readme): fix duplicated Minutely-tab section in Japanese README The previous sync commit added a `#### Minutely タブの有効化` subsection to README.ja.md, but the Japanese file already carried that description as inline prose (the old pre-refactor English shape) directly under the Configuration section — so the content ended up present twice. Root cause: the heading-count diff used to detect drift saw "ja is missing one h4" and a new subsection was added, without noticing the content already existed inline without a heading. English (and ko/zh) keep a single `#### Enabling the Minutely tab` subsection. Fix: keep the established inline translation and promote it to the `#### Minutely タブの有効化` subsection, and drop the duplicate block. ja now matches English/ko/zh: exactly one Minutely subsection between the Configuration prose and "Cache directory layout". Heading parity is unchanged (16/77/9). Constraint: English README.md is the source of truth for the translations Rejected: Keep the freshly-added block, drop the inline one | prefer preserving the pre-existing reviewed translation Confidence: high Scope-risk: narrow
…hoyeo#867) The frontend source palette rendered Codex blue while the TUI renders OpenAI model series with green. Use the same green base color for Codex client surfaces and assert that the profile usage chart resolves the expected shade. Constraint: Frontend groups usage by coding client while the TUI groups colors by model vendor Rejected: Limit the change to the profile chart | Codex uses the shared source palette across frontend surfaces Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the Codex client base color aligned with the TUI OpenAI green unless color semantics change Tested: bun run --cwd packages/frontend lint; bun run --cwd packages/frontend typecheck; bun run --cwd packages/frontend test Not-tested: Manual browser screenshot
Co-authored-by: astkaasa <2796928+astkaasa@users.noreply.github.com>
Expose the Codex app-server account snapshot through an opt-in command so backend aggregates are visible without conflating them with locally parsed usage. The command uses the documented handshake against the active Codex authentication, bounds protocol input, and always tears down its subprocess. It avoids token reads, persistence, pricing, reports, exports, submissions, and leaderboards. Constraint: Account activity has different scope and semantics from local event totals Rejected: Add a TokenBreakdown row or reconcile totals | upstream equivalence is not established Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep account activity outside local totals unless upstream defines a stable reconciliation contract Tested: cargo check -p tokscale-cli; cargo clippy -p tokscale-cli --all-targets -- -D warnings; cargo test -p tokscale-cli Not-tested: Live authenticated account/usage/read request Related: junhoyeo#855
Use tabular figure spacing for the mobile-only cost value so equal-length amounts retain consistent visual widths, matching the token value in the same leaderboard row. Constraint: Desktop hides this cost value above the mobile breakpoint Rejected: Switch all leaderboard figures to a monospace stack | tabular figures preserve the existing typeface Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep adjacent leaderboard numeric values on tabular figures when adjusting their typography Tested: frontend typecheck; frontend lint (6 pre-existing warnings); leaderboardPresentation Vitest; git diff --check Not-tested: Manual browser visual check
Usage over time now remains oldest-left unless a visitor explicitly enables the persisted Newest first preference. This removes the viewport-dependent reversal that made the default differ on wide profiles. Constraint: Existing saved Newest first choices must remain effective after mount Rejected: Remove the preference toggle | Visitors need to retain their explicit visual-order choice Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep implicit chart order chronological; reverse only for an explicit user preference Tested: Frontend ESLint and typecheck; frontend Vitest suite (601 tests) Not-tested: Browser interaction test with real localStorage; SSR regression covers the unset initial render Related: junhoyeo#868
…eo#875) VS Code stores per-request token counts in workspaceStorage chatSessions JSONL files. Parse kind=0 (initial state) and kind=2 (array appends) to extract promptTokens, completionTokens, resolvedModel, and reasoning tokens from toolCallRounds. Scans ~/Library/Application Support/Code/User/workspaceStorage/*/chatSessions/ on macOS and equivalent paths on Linux/Windows. Deduplicates against OTEL and desktop app sessions by dedup_key and session_id+timestamp.
Use a categorical palette so input, output, cache reads, cache writes, and reasoning tokens remain legible in both the segmented bar and the metric list. Constraint: The compact dark profile surface needs colors with clear category separation without changing its layout or theme contract Rejected: Retain a single blue accent ramp | adjacent token categories remain difficult to distinguish Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep token categories color-distinct; do not collapse them back into one hue family Tested: bun run lint (six existing warnings), bun run typecheck, bun run test (601 tests) Not-tested: Visual screenshot comparison skipped at user request
Collect OTel parent edges before the attributes gate so attribute-less intermediary spans keep nested invoke_agent chains connected to their root. Scope parent and invoke span identities by (traceId, spanId) and keep root traversal within the current trace, preventing reused span IDs from making the wrong invoke span appear nested or root. Add hermetic regressions for both hierarchy failure modes, including per-record sub-agent precedence and the existing single-invoke path. Bump the Copilot parser cache version from 3 to 4 so existing shards cannot replay pre-fix agent attribution; the cache regression covers stale-v3 rejection with an unchanged source fingerprint, true-parser rebuild, and save/reload correctness.
The repository now records the requested merge-commit default for gh workflows, so pull request history preserves branch topology unless a user explicitly selects a different strategy. Constraint: Merge strategy must remain overrideable for exceptional repository or release workflows Rejected: Keep squash as the default with ad hoc exceptions | the intended policy would remain easy to miss Confidence: high Scope-risk: narrow Reversibility: clean Directive: Use gh pr merge --merge for ordinary pull request merges unless the user specifies another strategy Tested: AGENTS.md policy review Not-tested: No runtime behavior; documentation-only change
Preserves local-calendar usage on submission so users east of UTC do not lose valid current-day activity before the API evaluates it. Constraint: The API is the authoritative future-date validator and permits a bounded UTC buffer Rejected: Client-side UTC truncation | silently dropped valid local-day usage Confidence: high Scope-risk: narrow Directive: Keep future-date validation server-authoritative Tested: UTC+14 local-date CLI regression test Not-tested: Live API submission
Attributes Codex token deltas to non-overlapping intervals so performance metrics do not repeatedly charge elapsed turn time. Constraint: Codex token-count timestamps may be missing, equal, or out of order Rejected: Measure every delta from turn start | overlaps durations and inflates totals Confidence: high Scope-risk: narrow Directive: Advance the duration cursor only for accepted token snapshots Tested: Duration, invalid-timestamp, incremental-parse, and cache-invalidation tests Not-tested: Production Codex transcript corpus
Adds client and model search directives so leaderboard users can narrow results by submitted usage metadata. Constraint: Period data stores source metadata in daily JSON breakdowns while all-time data uses submission arrays Rejected: Separate search inputs | would fragment the existing leaderboard search interface Confidence: medium Scope-risk: moderate Directive: Keep directive semantics identical across period and all-time leaderboard paths Tested: Frontend Vitest and migration replay checks Not-tested: Multi-directive all-time semantic regression
All-time leaderboard queries now OR repeated client or model directives, then combine the two directive categories with AND. Directive values use the same literal LIKE escaping as free-text search so underscore matches remain consistent across period and all-time views. Constraint: Period data filters in memory while all-time data filters through PostgreSQL Rejected: AND every directive condition | contradicted documented same-type OR semantics Confidence: high Scope-risk: narrow Directive: Keep directive matching equivalent across global and group leaderboard periods Tested: Frontend tests, typecheck, and lint Not-tested: Live production PostgreSQL query plans
… files Transcripts under ~/.claude/transcripts/ written by third-party tools (e.g. OpenCode) contain tool_output content but no Claude API usage metadata. The char-based estimation fallback was counting these outputs as input tokens, causing double-counting against the originating client's own parser. Only suppress estimation for files directly under a transcripts/ directory with no project/workspace context. Project transcripts and cc-mirror variants continue to estimate as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix double-counting of token usage for files under
~/.claude/transcripts/that are written by third-party tools (e.g. OpenCode with kiro/claude models).Problem
When OpenCode uses Claude-family models via Kiro, it writes transcript JSONL files to
~/.claude/transcripts/. These files containtool_resultentries withtool_output.outputcontent but no Claude APIusagemetadata (noassistantmessages withusageblocks).The claudecode parser's char-based token estimation fallback (
estimate_tokens_from_chars) was treating these tool outputs as input tokens, producing ghost "claude/unknown" entries. This caused:claudeclientmodel=unknown,provider=unknown,output=0,messageCount=0rows appearing in reportsFix
Detect "bare transcripts" — files directly inside a
transcripts/directory with no project/workspace context and no cc-mirror metadata. For these files, skip the char-based estimation path fortool_resultentries. Only suppress estimation for bare transcripts; project-level transcripts and cc-mirror variants continue to estimate as before.Verification
tokscale-coretests pass (including the existingtest_wrapper_transcript_without_usage_is_skipped)test_bare_transcript_with_tool_outputs_is_not_estimatedconfirming the fixtest_project_transcript_with_tool_outputs_is_estimatedconfirming project-level estimation is preserved