Repository navigation
chore: sync fork with upstream (7 commits) - #43
Merged
Merged
Conversation
…) (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>
… breakdown, and animation quality
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>
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).