Skip to content

chore: sync fork with upstream (7 commits) - #43

Merged
cte13 merged 23 commits into
mainfrom
chore/sync-upstream-2.5.3
Sep 2, 2026
Merged

cte13 merged 23 commits into
mainfrom
chore/sync-upstream-2.5.3

Conversation

@cte13

@cte13 cte13 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Brings in upstream changes up to v2.5.3 (commit cb4fba7):

Preserves fork-specific behavior (slow polling on display sleep, antigravity error display, test network gates).

ite476 and others added 23 commits August 29, 2026 14:46
…) (chattymin#228)

The OAuth cache returned a still-unexpired token without re-reading
credentials.json, so /login to a second Team email left the 5h bars
on the previous account. Peek the file before the cache hit; Keychain
stays off the auto path. Same class in the Antigravity token cache.
…tymin#211)

* fix: mark only current evolution stage as raising

* docs: clarify Pokédex raising badge semantics

* docs: correct Raising badge width note
…oating pet (chattymin#212)

* fix: cap the sprite frame rate by decimating frames, not stretching them

The always-visible surfaces (menu bar sprite, floating pet) capped their frame
rate by raising each frame's delay -- max(floor, delay). That keeps the frame
count and stretches the whole loop, so a 55-frame Gen-V sprite (55 x 0.05s =
2.75s) played back over 22s at the 0.4s floor: 1/8 speed. The original rationale
("2.5fps and 5fps are indistinguishable at 22px") only held for the frame rate;
it missed that playback slowed 8x. Two-frame bob animations hid the defect,
because holding and decimating are the same thing there.

GIFDecoder.capFrameRate decimates instead: it emits one frame per accumulated
floor-length span, so the loop length matches the original and only the frame
rate drops. Same wakeup budget, correct speed, and fewer frames to composite
(55 -> 6 at a 0.4s floor).

Timer.tolerance drops from 0.5x to 0.1x for the same symptom. Tolerance only ever
fires late, so 0.5x declared that the loop may stretch by up to 1.5x -- next to
the popover, which runs with tolerance .zero, that made the menu bar look slower
on its own.

The menu bar cap also moves into a named constant. It was an inline
max(0.4, delay) while the pet's cap was a tested constant, so the cap could
vanish from the menu bar without any test noticing; both surfaces are now
asserted together in testAlwaysVisibleSurfacesAreCapped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: let users pick animation quality for the menu bar sprite and floating pet

The frame-rate cap on the always-visible surfaces was a single hardcoded value,
and no single value fits: the per-frame cost is a status-bar recomposite, so how
smooth it looks and how much battery it costs both vary by device and by sprite.
A 22px menu bar hides the low frame rate that is clearly visible on a 96px
floating pet.

This adds a three-preset setting -- Power saver (0.4s / ~2.5fps), Balanced (0.2s /
~5fps), Smooth (0.1s / ~10fps) -- shared by both surfaces, in all four UI
languages. No preset can disable the cap; 0 (native fps) is not reachable from
the UI, which keeps the idle-wakeup guarantee that the cap exists.

The default is Power saver: it is the same frame rate the app used before this
setting existed, so an existing user's battery profile does not change and the
smoother settings are opt-in. Measured idle CPU for reference: 1.8% at Balanced,
5.1% at Smooth.

The floor is part of the frame-cache identity (AppDelegate.menuSpriteKey,
SpriteView.frameTaskID) because cached frames are already decimated to a specific
rate -- without it, changing the setting would not take effect until the next
evolution. The menu bar also observes the setting explicitly: observeStore only
tracks menuTitle, so otherwise a change would not show until the next usage poll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ero (chattymin#197)

* feat(cursor): fetch usage from dashboard API when local tokenCount is zero

Cursor's state.vscdb bubbles often store 0/0 token counts since early 2026,
so the SQLite scan under-reports. When the user is signed into Cursor, read
the same unofficial dashboard export the web UI uses (with sub::jwt cookie auth
from cursorAuth/accessToken), fall back to local bubbles or Downloads CSV
exports, and avoid merging dashboard rows with stale bubble estimates.

API and in-memory cursor reload cache TTL follow Settings → refresh interval
(1/2/5/15 min or manual = refetch on every refresh()).

Closes chattymin#196

* refactor(cursor): remove CSV export and local file fallbacks

Dashboard JSON API is the sole authoritative source when signed in; keep SQLite bubble scan only as offline fallback.

* fix(cursor): satisfy CI env guard and ISO8601 date parsing in tests

Route CURSOR_USAGE_API through UsageEnvironment and reuse CompanionStore state dir for the API cache path. Parse fractional-second ISO dates in CursorUsageTests.

* fix(cursor): extract AppStatePaths for nonisolated cache directory

CompanionStore.defaultURL() is MainActor-isolated; share PTB_STATE_DIR resolution via AppStatePaths for Cursor API disk cache.

* fix(cursor): harden dashboard usage fallback

Keep cached usage account-scoped, reject incomplete pagination, preserve authoritative empty responses, and make paginated event IDs unique.

* fix(cursor): harden API fetch, cache, logging, and pagination tests

Add explicit fetch failure reasons and HTTP status logging, ephemeral URLSession, request/page deadlines, account-scoped cache TTL and coveredSince checks, safer pagination/id handling, and SQLite fallback when stale cache is non-authoritative.

* test(cursor): fix transport test closures for CI

Avoid throwing closures in injected transport and compare empty fetch results by count.

* test(cursor): use Sendable page recorder in pagination test

Fix Swift 6 concurrency error when capturing mutable state in injected transport.

* fix(cursor): accept legacy disk cache without accountIdentifier

Migrate pre-account-scoped cache files instead of ignoring them on upgrade.

* fix(cursor): send epoch-ms range and read usageEventsDisplay

The dashboard endpoint was never exercised while the CSV export was the
primary source, so two request/response mismatches stayed hidden until
CSV was removed:

- startDate/endDate were sent as ISO8601, which the endpoint answers with
  HTTP 500. It expects epoch milliseconds as strings.
- Events are returned under usageEventsDisplay, not usageEvents/events,
  so parsing failed even once the request succeeded.

Pagination now also uses totalUsageEventsCount when the response carries
no pagination object. Verified live: 239 events fetched.

* docs(cursor): sync ko/ja READMEs with dashboard API data source

The English README gained the cursor.com data source row and the updated
Privacy host list, but the Korean and Japanese translations still listed
state.vscdb as Cursor's only source and claimed seven outbound hosts.
CLAUDE.md requires all three READMEs to stay in sync.

Also notes that the offline disk cache is account-scoped and capped at
6 h, so readers know how stale a fallback reading can be.

---------

Co-authored-by: Patrick.Park <patrick.park@buzzvil.com>
* fix: read Kiro CLI 2.20+ JSONL sessions instead of only data.sqlite3

Kiro stopped writing new turns to the SQLite store. Scan ~/.kiro/sessions
(cli/*.jsonl and v3 messages.jsonl) while keeping the legacy database, and
register KIRO_HOME / KIRO_CLI_HOME so the GUI app can see those overrides.

* fix: keep every v3 Kiro turn when timestamps are missing

Flat messages.jsonl lines share session.json createdAt, so millis ids
collapsed under dedupKeepMax. Key v3 entries by turn index and count
tool_call args as output, matching the writer-shaped parser.

---------

Co-authored-by: Patrick.Park <patrick.park@buzzvil.com>
* Add oh-my-pi (omp) usage provider

Parse pi-format session JSONL under ~/.omp/agent/sessions recursively:
every assistant message's usage event is summed (rewound branches are
already billed; subagent sessions count since their tokens are not
folded into the parent), plus compaction/branch_summary envelope usage,
mirroring parsePiFile's semantics. Per-event cost.total is trusted over
the pricing table when reported; OMP_CODING_AGENT_DIR overrides the
agent home (verified against the omp binary's env vars). bridge/
conversion copies are excluded: they mirror sessions whose usage is
already counted at the original source (verified against a real bridge
file duplicating a -Projects/ session).

Also make testHermesAcceptsMillisecondStartedAt timezone-independent
(it hardcoded a local day that only holds at UTC+0/east) and cover the
previously uncovered omp reader branches.

* Use omp as the provider display name

The tool ships as `omp` (omp.sh, the `omp` binary, Homebrew `can1357/tap/omp`),
so the provider tab and Settings now show that name instead of `OMP`. The README
tables keep the repository name alongside it as `omp (oh-my-pi)` for discovery.

---------

Co-authored-by: Patrick.Park <patrick.park@buzzvil.com>
* Add Trendshift badges to README

* Update Korean README with Trendshift badges

* Update README.ja.md
… prompt) (chattymin#241)

* feat: fetch official limits with a claude.ai session key (no Keychain prompt)

Auto-polling never reads the Keychain by design, so once the cached token
expires and ~/.claude/.credentials.json is absent, official limits freeze until
the user presses "Refresh limit token cache" -- and that press raises the
password dialog on unnotarised builds, where an "Always Allow" grant does not
survive the next code-signature change.

This adds an opt-in second credential path. Paste a claude.ai sessionKey cookie
under Settings -> Advanced and limits come from
claude.ai/api/organizations/{org}/usage, which returns the same schema as
api.anthropic.com/api/oauth/usage -- so LimitStatus is reused unchanged and no
popover row changes shape. The path touches no Keychain, so auto-polling keeps
limits current and there is no ACL grant to lose on the next update. Without a
key nothing changes: ChainedLimitsProvider falls through to the existing OAuth
path and no file is created.

Organization selection is not "the first one". A claude.ai account also returns
orgs the user merely logged into, and API-only orgs that answer 403, so picking
first yields a permanent and plausible-looking 0%. Candidates are filtered to
chat-capable orgs, probed in parallel, and the one with usage is selected and
cached. A cached org that later returns 403 triggers rediscovery.

A dead key answers 403, not 401 -- verified by flipping one character of a
stored key in a running build, where the usage endpoint and /api/organizations
both returned 403. Since /api/organizations is not organization-scoped, a 403
there means the key itself can see nothing, so it maps to sessionKeyInvalid.
Per-organization 403s keep their existing meaning and still trigger rediscovery.

Expiry carries its source (limitsAuthExpiry: .oauth | .sessionKey) rather than a
single boolean, because the two remedies are opposites: a session key must be
re-copied from the browser, while OAuth only needs a Keychain re-read. Collapsed
into one flag, a session-key user is told to run Claude Code once -- which does
nothing for a dead cookie -- and the retry button reads the Keychain, raising
the very prompt the session key exists to avoid. The settings badge reads
"Expired" while the dead key is still stored, and the banner opens Settings with
the Advanced disclosure already expanded, since the key field lives inside it.

The key is stored as an owner-only (0600) plaintext file, not in the Keychain:
an app-owned Keychain item is the very class of defect this removes, because a
cdhash change invalidates its ACL and the prompts return. The settings copy
states the storage model and that a browser logout invalidates the key.

Note for anyone testing this: curl cannot verify these endpoints. Cloudflare
answers it with its own 403 challenge regardless of the cookie, so it will
confirm any theory brought to it. Only URLSession gives a real answer; the unit
tests seam out the HTTP layer instead.

Refs chattymin#240

* docs: list claude.ai in the outbound-host section (en/ko/ja)

The session-key path adds a twelfth outbound host, and the privacy section
enumerates every host the app talks to.

* fix: keep a configured session key from falling back into the Keychain

A stored-but-rejected key used to pass allowKeychainPrompt straight through to
the OAuth fallback, so pressing Refresh raised the very password dialog the key
exists to avoid. The remedy for a dead key is re-pasting it, not the Keychain,
and the expiry banner already routes there. Users with no key keep today's
behaviour, which the second test locks.

---------

Co-authored-by: Patrick.Park <patrick.park@buzzvil.com>
Buying an egg discarded the active Pokémon without touching `dex`, so a
species that was only backed by that companion vanished from the Pokédex.
That was the one path where the collection screen could shrink, which
contradicts the promise it makes.

`buyEgg` now records the released companion as a `DexEntry` carrying
`releasedAt`. The Pokédex folds `state.dex`, so species retention follows
automatically; only the capture log distinguishes the two, with a
`Released` badge next to the existing `Raising` one.

Three rules the record follows:

- Only reached forms are credited (`pathIDs.prefix(stageIndex + 1)`).
  Using `plannedPathIDs` would make buying an egg a shortcut to filling
  the Pokédex with evolutions never reached.
- Shiny follows `currentIsShiny`, so releasing a disguised Ditto does not
  reveal it early.
- `collectedFinals` is untouched — a released companion was not raised to
  its final form, so it must not count toward completion or branch
  weighting.

`releasedAt == nil` means "graduated", so existing saves need no
migration. A representative pinned to a released species now survives the
release, because the species is still owned.
Add native German copy, official Pokémon terminology, locale detection, and regression coverage so German users get a complete localized experience.

Co-authored-by: Patrick.Park <patrick.park@buzzvil.com>
…ram (chattymin#243)

kSecMatchLimitAll cannot be combined with kSecReturnData; macOS rejects that
pair with errSecParam (-50) during parameter validation, before it looks for
any item. The query added in chattymin#232 therefore never succeeded for anyone whose
credentials live only in the Keychain — an approved ACL, a re-login, or a
single matching item made no difference. Measured on a real install: 24
consecutive -50 failures, zero successes.

Enumerate accounts with attributes only, then read each candidate's data with
kSecMatchLimitOne, preserving chattymin#232's intent of picking the entry that carries
claudeAiOauth. Falls back to an unscoped single read when no account attribute
comes back.

The old tests exercised extractDataItems with synthetic arrays, so
SecItemCopyMatching was never on the test path and the defect sat one layer
above what they covered. The new guard aims the production queries at a service
name that cannot exist -- nothing matches, so no ACL or prompt is involved --
and asserts the framework does not answer errSecParam. extractDataItems is
removed along with its tests; it has no production caller left.
…hattymin#247)

The Keychain credential carries refreshToken and refreshTokenExpiresAt next to
a ~5h accessToken, which makes "renew with it and stop touching the Keychain
every few hours" an obvious idea. It is the wrong one: Claude Code's refresh
handler replaces its stored refreshToken with the value from the response, so
the token rotates. An external app renewing on its own would leave Claude Code
holding a dead token and force a re-login, and the only way around that is
writing into someone else's credential store, which the app-owned-Keychain rule
already forbids.

Antigravity can do this because Google does not rotate — its cache reuses the
same refreshToken across renewals. The asymmetry is the providers' token
semantics, not a gap on the Claude side.

Rotation is inferred from the replace-on-store behaviour rather than tested; the
cost of being wrong is breaking the user's primary tool, so the experiment was
not run. The decision rests on the asymmetry instead: at best it is a weaker
version of what the session key already provides, at worst Claude Code breaks.

Also records that adding a silent-first Keychain read on the user-action path
buys nothing, so the Antigravity pattern is not worth porting there either.
… per model (chattymin#225)

* feat: attribute Pi/OMP usage to the real model and break the day down per model

* fix: gate the Pi per-model breakdown to Pi and carry it through the provider

---------

Co-authored-by: Kirill Korikov <11762090+yourconscience@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…saver (chattymin#250)

Low Power Mode used to drop the menu bar sprite to a two-frame bob, and
the check only ran while building frames — toggling the mode mid-session
never rebuilt anything, so an already-running GIF kept its full frame
rate, and a bob built during Low Power survived after leaving it.

The Power saver preset is within +0.5 recomposites/s of the bob, so cap
the effective frame floor there instead of dropping the GIF: the stored
animationQuality is never mutated, the cap is derived at the consumption
site, and a power-state observer rebuilds frames the moment the mode
toggles (the effective floor is part of menuSpriteKey).
…mage (chattymin#255)

Assigning `button.image` redraws the entire NSStatusBarButton on every
animation frame — and that redraw includes the two-line attributed title, not
just the 22px sprite. At the default 5fps the text re-render is always on.

Route frame pixels through a dedicated sublayer's `contents` instead: an
already-uploaded bitmap is swapped without entering the view drawing path.
`button.image` keeps a transparent placeholder so AppKit still owns
`imagePosition`, title layout and status item width; it is only reassigned when
the sprite size changes.

Measured (A/B probe — same timer, 5fps, two-line title, 60s x 2 rounds):

  button.image + CATransaction   2.00 ms/frame
  button.image alone             3.51 ms/frame
  layer.contents                 0.27 ms/frame

Live app before/after (`sample`, main thread):

  timer path                235/17212 (1.37%)  ->  33/16581 (0.20%)
  CA::Transaction::commit   163                ->  14
  _NSViewDrawRect           105                ->  0

This is a different axis from the existing transition-suppression rule, which
still holds and is kept: assigning outside a CATransaction measured worse
(3.51 ms), not better.

If the conversion ever fails the code falls back to `button.image`, which would
make the saving disappear while the animation still looks correct. A test pins
the conversion for every sprite shape and bob phase, and asserts that nil is
actually reachable so the guard cannot silently stop being able to fail.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#256)

ccusage was removed in b762be9 (shipped in v2.1.0) and the app has parsed local
logs directly ever since. Four comments still describe the refresh path as
spawning it, which no longer matches what the code does: a refresh parses local
logs and queries limits, and the only subprocess left is codex.

These are load-bearing comments — they explain why polling is suspended on
display sleep and why App Nap is disabled — so a reader takes them at face
value. During a battery investigation they sent me looking for a per-refresh
ccusage spawn that does not exist.

The other ccusage mentions are deliberate and kept: ModelPricing (the pricing
table was reverse-derived from it), Models (the JSON shapes originated there),
LocalUsageProvider / LocalUsageReader ("replaces ccusage" context) and
scripts/parity-check.sh (an intentional cross-check tool).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.