Skip to content

fix(claudecode): skip char-based token estimation for bare transcript files - #9

Closed
t1000040 wants to merge 202 commits into
mainfrom
fix/claude-transcripts-no-usage-double-count
Closed

fix(claudecode): skip char-based token estimation for bare transcript files#9
t1000040 wants to merge 202 commits into
mainfrom
fix/claude-transcripts-no-usage-double-count

Conversation

@t1000040

Copy link
Copy Markdown

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 contain tool_result entries with tool_output.output content but no Claude API usage metadata (no assistant messages with usage blocks).

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:

  • Double-counting: usage already tracked by the OpenCode parser was re-counted under the claude client
  • Phantom entries: model=unknown, provider=unknown, output=0, messageCount=0 rows appearing in reports

Fix

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 for tool_result entries. Only suppress estimation for bare transcripts; project-level transcripts and cc-mirror variants continue to estimate as before.

Verification

  • All 1187 existing tokscale-core tests pass (including the existing test_wrapper_transcript_without_usage_is_skipped)
  • Added test_bare_transcript_with_tool_outputs_is_not_estimated confirming the fix
  • Added test_project_transcript_with_tool_outputs_is_estimated confirming project-level estimation is preserved

astkaasa and others added 30 commits June 10, 2026 06:03
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: update copyright year to use current year dynamically

* fix(footer): dynamically update copyright year in footer

Signed-off-by: Wilson Wu <iwilsonwu@gmail.com>

---------

Signed-off-by: Wilson Wu <iwilsonwu@gmail.com>
* 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>
github-actions Bot and others added 29 commits July 12, 2026 10:09
…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.
@t1000040 t1000040 closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.