Skip to content

v3.1.0 — additional limits, a token that stays fresh, and a way to sign in again - #24

Merged
weltern merged 42 commits into
mainfrom
develop
Sep 27, 2026
Merged

weltern merged 42 commits into
mainfrom
develop

Conversation

@weltern

@weltern weltern commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Everything on develop since v3.0.1: 35 commits. APP_VERSION is 3.1.0.

New

  • Additional limits. Settings › Display › Additional limits lists every scoped limit the usage API reports, such as Weekly · Fable. A ticked one gets its own bar, with a reset countdown, in the full, compact and mini views, and joins the approaching-limit alerts at its group's threshold. Past 100% it reads OVER LIMIT, and its alert says "reached 100% of its limit": whether a model's own cap spills onto paid credits is unverified, so neither claims it. Off by default. It adds no API calls, because the data comes from the usage request the Stats page already makes.
  • The token stays fresh when Claude Code runs from the desktop app. The desktop app never renews ~/.claude/.credentials.json, so Clawdmeter does. It renews about 30 minutes before expiry, under Claude Code's own refresh lock, and re-reads the file under the lock, standing down if the token has already rotated. It uses the current token endpoint and the stored scopes, with a Clawdmeter User-Agent. The endpoint answers User-Agent: anthropic with a 429 every time.
  • "Sign in again". When Claude rejects the token or its refresh token, auto-refresh stops and Settings offers Sign in again, which opens claude auth login --claudeai in a terminal on Windows, macOS and Linux. The new token is picked up on the next poll.
  • "When usage can't be read" alert (on by default). It fires once when the token expires, is rejected or is missing, and once when usage can be read again. Network and API errors don't trigger it.
  • The failure badge names the problem (Token expired / Sign in again / No token found / Offline / API error). A failed poll used to clear the badge.

Changed

  • Session shelf: mascots are sized from the window height rather than the session count, and the row scrolls horizontally once they no longer fit. Short windows show text-only tiles (below a 64px mascot), as before.
  • Dead sessions no longer come back to the shelf. Recency is read from the transcript's last event, not the file's modified time, which a maintenance rewrite could bump.
  • The idle mascot scales to fit rather than growing the window.
  • Tooltips follow the theme.

Fixed

  • Drop-down and spin-box arrows were solid 8×5 blocks on Windows and macOS. Qt's stylesheet engine doesn't draw the CSS border-triangle trick. They're now cached per-user images in the theme's colours.
  • Compact rows: the status dot sat about 30px from its label.
  • Linux: underscores in session labels were clipped ("list_issues" read as "list issues"), and the compact row's "·" separator rendered near-black on a light-mode desktop.

Release review (the mandatory gate)

A code review over main..develop found 1 Medium and 4 Low issues. All are fixed except one accepted Low:

  • Medium: "Sign in again" was disabled as "token still valid" when Claude rejected a token whose file still showed a future expiry (a revoked token, or a 403). The badge and the alert were sending the user to it. Fixed in dfdc81e, with a regression test.
  • Low: only OAuth's invalid_grant now counts as a dead refresh token. Measured against the live endpoint: a dead token returns 400 invalid_grant, while a malformed request returns 400 invalid_request_error, which now retries rather than telling everyone to sign in.
  • Low: additional-limit reset times now go through one shared ISO parser (src/isotime.py). A bare fromisoformat rejects Z on Python 3.10, which the Linux release build uses.
  • Low: the README now documents the new alert and the fact that macOS never auto-refreshes.
  • Accepted, one exception: the refresh lock's timestamp isn't refreshed while it's held. It could only matter if Clawdmeter's refresh and Claude Code's overlapped within seconds, which the 30-minute versus 5-minute lead times make very unlikely.

Tested

  • Windows: 738 tests pass.
  • Ubuntu 22.04: the suite passes, 3 tests skipped.
  • CI (develop, the last completed run, at 0124ffd): Windows and Ubuntu tests, plus a launch test on Arch, Debian 12, Fedora 40 and Ubuntu 22.04/24.04.
  • macOS (M2 and Intel): the suite shows the same 16 test failures and 4 files that crash without a display as before any of this work. All are test-side platform assumptions, not app failures, and unchanged by this branch.
  • Release installers, all built from 3ec0425 (identical files to this PR's head d656f54). Each contains the new isotime module and older builds don't, and every checksum verifies:
    • Windows Clawdmeter.exe (v3.1.0): click-tested.
    • macOS .dmg and .zip: both Apple-chip and Intel code in every file (96/96). Runs natively on an M2 (macOS 26.6.1) and as x86_64 on Intel (macOS 15.7.7). A copy marked as downloaded is rejected by Gatekeeper, as expected for an ad-hoc-signed app; the README's "Open Anyway" steps cover it.
    • Linux tarball and AppImage: built on Ubuntu 22.04 (glibc 2.35), and all three launch forms stay running.

🤖 Generated with Claude Code

weltern and others added 30 commits August 22, 2026 16:56
The shelf keyed a session's recency off its file mtime, so a maintenance pass that rewrites a long-dead transcript without adding content put it back on the shelf. Judge recency by the last real event in the transcript instead, and shorten the active window to 5 minutes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xr9KLS575DHx4NEMUBYpx
Mascots hold a readable size regardless of session count and the row scrolls when they overflow, instead of shrinking to nothing past a few sessions. Size follows window height up to a 250px cap, and the row centres vertically so a tall or maximised window has no dead band below the mascots. Also themes the shelf tooltip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xr9KLS575DHx4NEMUBYpx
Add a QToolTip rule to the base stylesheet so tooltips follow the active palette and update on theme switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xr9KLS575DHx4NEMUBYpx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xr9KLS575DHx4NEMUBYpx
The usage API reports limits beyond the 5h and weekly windows, such as a
weekly limit on one model (Weekly · Fable). Until now they only appeared
on the Stats page. Settings > Display > ADDITIONAL LIMITS now lists every
one the account has reported, and a ticked limit gets its own bar in the
full, compact and mini views and joins the approaching-limit alerts at the
threshold of its group.

- scoped_windows.py (new, Qt-free) parses them from limits[], including
  their reset time and a surface scope as well as a model one, and holds
  the last-known list so a failed usage request doesn't blank the bars.
  The usage and profile requests now raise on an error status so that
  case is distinguishable from "no limits".
- One keyed row helper (ScopedRows) serves all three views. Compact and
  mini settle the rows' layouts before re-fitting, and the full window
  re-fits the session shelf once its rows change.
- A limit crossing 100% reads OVER LIMIT on its bar and "reached 100% of
  its limit" in the alert, not OVERAGE: whether a model's own limit
  spills onto paid credits is unverified.
- Ticked limits are opt-in and remembered by key; a remembered limit the
  API has stopped reporting is marked in Settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With no sessions the empty-state mascot was a fixed 240px, so an
additional-limit row it had no room for forced the window taller
(572 -> 627px at one row) -- and did so on its own when the last session
ended with a limit ticked. It now scales to the height it is given,
capped at 240px, the way the session shelf's mascots already fit. A
window with room looks exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Only the usage request raises on an error status. The profile request
  raising too meant a profile-only failure threw away the spend and
  scoped windows that usage had returned.
- A limit the API sends without a group is labelled by name alone
  instead of being guessed as "Weekly".
- README: the new bars, Settings > Display > Additional limits, and the
  approaching alert covering them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eekly · Fable) on the dashboard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every probe failure collapsed into the status string "error", and
_apply_status_badge had no branch for it, so it fell through to the else
branch and CLEARED the badge. An expired OAuth token therefore rendered
as the 5h / 7d bars frozen on their last good values, with the only
signal anywhere being a tray tooltip reading "Clawdmeter - error".

That is how a token which expired at 02:42 went unnoticed for 34 hours:
the session shelf reads transcripts off disk and needs no auth, so the
window went on looking alive while the half that needs a token was dead.

_poll_once now classifies what failed rather than flattening it:
401/403 is auth-expired, another non-2xx is http-error, and a transport
failure is offline. The badge and the tray tooltip render from one table,
so they cannot disagree, and the wording splits on what the user should
do about it - "Token expired" names an action, "Offline" says to wait.

Settings already had this wording on the Connection tab; the main window
was the only surface that stayed silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three faults let an expired token stay expired for 34 hours.

The refresh only fired once the token was already dead, so there was
always a window where every poll 401'd and recovery depended on the
token endpoint answering right at that moment. needs_refresh() now opens
a 30-minute window before expiry. It is the same one refresh per token
lifetime, just taken while there is still a working token to fall back
on. is_expired() keeps its old 120s skew and its old meaning, because
Settings asks a different question - is it broken NOW - and widening
that would have the panel calling a healthy token expired.

Every failure then looked alike. A 429 and a refused refresh token both
came back as "refresh failed", so the cooldown doubled to a 15-minute
ceiling and kept trying either way. RefreshOutcome splits them:
THROTTLED backs off to a four-hour ceiling instead of feeding a rate
limiter it may already be behind, REJECTED stops entirely because a dead
refresh token cannot be retried into working, and UNREACHABLE keeps the
short ceiling since that is just the network. The block self-clears when
the credentials file's expiry changes, so signing in again resumes
polling with nothing having to notify the poller.

And there was no way to sign in again. reauth.py hands off to
`claude auth login --claudeai` rather than reimplementing the flow:
its authorize endpoint, redirect URI and scopes are documented nowhere
we can verify, and this is the worst place in the app to be guessing.
It adds no dependency either - the credentials file Clawdmeter reads is
Claude Code's, so with no CLI there was never a token to read. The
Connection tab gets a Sign in again button, always present and disabled
with a reason when the token is healthy, matching the refresh button
beside it. On macOS it stays enabled: it is the one remedy that works
there, and the note above it already says so.

A 401 whose refresh token is dead now reads "Sign in again" rather than
"Token expired" - same symptom, different thing for the user to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A mutation run caught this: swapping needs_refresh() back for
is_expired() in _maybe_auto_refresh left the whole suite green. The
new rule was tested in isolation and nothing checked that the poller
actually calls it, which is the one place it matters.

The control assertion is the point - is_expired() is False at twenty
minutes out, so the old behaviour could not have produced this call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The session shelf reads transcripts off disk and needs no token, so the
window goes on looking alive while the half that needs one is dead. That
is the real reason an expired token went unnoticed for 34 hours: nothing
was broken enough to notice from across the room, and the app never said
anything.

AuthNotifier is edge-triggered, which is the requirement rather than a
nicety - at a 30-second poll that stretch would otherwise have been
around four thousand notifications.

Only auth failures count. A dropped network or a 500 clears itself and
is not something to act on, so alerting on those would teach you to
ignore the channel. For the same reason recovery has to be a genuine OK
sample: an offline poll arriving during an outage must not read as "all
better now", and a test covers exactly that false positive.

It is deliberately NOT primed on first sight, unlike ApproachingNotifier.
Starting silent would reproduce the original failure for anyone who
launches the app after the token has already died - the moment it is
most worth saying something.

Delivery goes through _deliver_alert alongside the reset and approaching
alerts, so it honours whatever channels are already configured,
including a push-only setup - which is the one that reaches someone who
is not looking at the window.

The Settings toggle defaults ON, unlike the approaching alert. That one
is a judgement call about how close is too close; this one means the
dashboard has stopped telling the truth and cannot fix itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A mutation run caught it: dropping no-token from AUTH_FAILURE_STATUSES
left the suite green, because the test parametrised over that same set.
Removing a status just removed a case. The list is written out by hand
now, with a separate assertion that the set matches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the shape of the existing known-issue docs: the timeline it was
confirmed against, why the window still looked alive, why nothing
renewed the token once the desktop app became the daily driver, and
what each part of the fix does.

Records two things worth not relearning: claude auth status reports
loggedIn true against a token that 401s, because it reads presence and
not validity; and the 429 seen from the token endpoint came from probes
with a deliberately invalid refresh token, so it is consistent with
throttling but does not prove it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two of them were the branch telling the user something untrue about its
own behaviour, which is the failure mode the branch exists to remove.

The delivery channels vanished for anyone running only the auth alert.
_sync_notify_subtoggles counted the reset and approaching toggles but
not the new one, and its handler never called the sync at all - so
turning the other two off hid the "how" controls while the auth alert
went on firing toasts through settings nobody could see any more.

Settings still said "wait for auto-refresh" in the state where this
branch deliberately switches auto-refresh off for good. The poller owns
that state, so it is now handed to the panel per sample and the line
names the one remedy that still works. Same defect the macOS branch of
that function already guards against, in the branch that has the users.

403 no longer claims the token expired. It is forbidden - a revoked
token or an org policy - and asserting an expiry pointed the user at a
refresh that cannot help. It reads as "sign in again", which is true
under either reading.

The blocked-refresh state is its own boolean instead of being inferred
from a nullable expiry. A credentials file with no refreshToken - the
case that produces a REJECTED in the first place - plausibly has no
expiresAt either, so the block silently failed to engage on exactly the
input that triggers it. It only avoided becoming a retry loop because
needs_refresh independently also returns False on an unknown expiry;
two functions had to keep agreeing for that to hold.

auth_notify no longer imports poller. Three string constants were
dragging PySide6 and httpx into a module whose docstring promises
neither, and making it unimportable from poller - where the statuses
live and where a future caller belongs. The literals are guarded by the
name-match test that already existed, plus a subprocess probe that
proves nothing heavy gets pulled in. That probe was checked against
poller itself, which reports all three.

is_expired takes a blocking flag and the panel passes False. That call
sat eleven lines under a comment explaining that a blocking macOS
Keychain read there had hung the app before it drew anything, and
blocked anyway.

reauth drops the --email parameter. No caller ever passed one and the
address is unavailable at the moment it would be needed.

Two fixes had no test that could fail on them - the handler's call to
_sync_notify_subtoggles, and the panel's blocking=False - so both got
behavioural tests that drive the real signal and record the real kwarg
rather than asserting on source text. Both carry a control assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A mutation run caught it: deleting the set_reauth_needed call from
_on_sample left the suite green. The wording branch was tested, but
nothing proved the panel is ever told which state it is in - so it
would have gone on offering to wait for a refresh that had stopped.

An AST walk rather than a substring search, so it sees a real call and
not a mention in a comment, and survives a reflow. The control test
beside it proves the walk can find a call that is definitely there and
does not find one that is not - a broken walk and a missing call look
identical otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updates the counts, names all four mutation survivors and what each
had in common - a fix nothing could fail on - and records that the
badge and the Sign in again button were both exercised against the
real expired token rather than only against fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… renew it before it does, and offer a way back in

An expired OAuth token stopped the 5h/7d windows updating for 34 hours
without the window saying anything. The shelf reads transcripts off disk
and needs no token, so half the app went on looking alive while the half
that needs one was dead — and the Claude desktop app, unlike the CLI,
never writes the credentials file Clawdmeter reads, so nothing renewed it.

Four things, one per commit, plus the review fixes:

- Every probe failure used to collapse into the status "error", which the
  badge had no branch for and therefore drew as nothing. Failures now name
  themselves, and the badge and tray tooltip render from one table.
- The refresh only fired once the token was already dead. It now renews 30
  minutes ahead — the same one refresh per token lifetime, taken while
  there is still a working token to fall back on.
- A failing refresh retried at four attempts an hour forever. Backoff now
  keys off the cause: throttled backs off for hours, a refused refresh
  token stops entirely, and network trouble keeps the short ceiling.
- There was no way to sign in again. The Connection tab hands off to
  `claude auth login` rather than reimplementing an OAuth flow whose
  endpoints are documented nowhere verifiable.

And it says so once when usage goes blind, once when it comes back,
through the channels already configured for reset alerts.

Verified against the real failure rather than fixtures: the badge rendered
against the genuinely expired token, and the sign-in button completed a
browser login after which the poller recovered on its own. 691 tests,
29/29 mutants killed over three runs. Write-up in
docs/token-expiry-blind-spot-2026-09-20.md.

A code review of the branch found seven issues, all fixed in aa7635f.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two platform gaps the Windows work left behind.

macOS can never refresh its own token - the credential lives in the
login Keychain and writing the rotated one back is not implemented - so
_refresh_blocked could never be set there, and every expiry rendered as
"Token expired". That is a refresh which is never coming, on the one
platform where it is true of EVERY expiry rather than only a rejected
one: the same false promise the Settings line was fixed for, still on
the badge. It now reads "Sign in again", which is the only thing that
does work there, and which the Sign in again button now offers.

The condition behind that is one function, auto_refresh_supported(),
called by both the "should I even try" gate and the wording decision so
the two cannot drift. It replaces the copy of the expression that the
poller had inline. CLAUDE_CREDENTIALS_PATH still opts a Mac back into
the file path, and therefore back into refreshing, as it always did.

The Linux handoff passed argv to xfce4-terminal's -e, which wants a
single quoted string - the terminal opens and never runs the command.
It uses -x now, which takes the remainder of the command line. The
Debian x-terminal-emulator alternative moved from first choice to last
resort, since what it actually points at is unknown.

Both of those failed the same way: Popen succeeded, so the user was told
to "finish in the window that just opened" about a window that had done
nothing. _survived() now waits briefly and distinguishes a terminal
still in the foreground, one that handed off to a server and exited 0,
and one that rejected its arguments and died - and on the last, moves on
to the next candidate rather than reporting success. The same check
covers osascript on macOS.

The AppleScript command is escaped properly too: shlex for the shell
inside Terminal, then backslash and quote escaping for the AppleScript
literal wrapping it, which a path containing a double quote would
otherwise have ended early.

None of the macOS or Linux spawn paths has ever run on its own platform.
The tests cover the selection logic, the flags, the fall-through and the
escaping with fakes; whether a real konsole accepts -e and a real
Terminal accepts the script is still unproven and needs the VMs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI covers Linux and Windows; there is no macOS job, so nothing there
has been exercised by anything but reasoning and fakes. Names what is
inert on macOS and why, and which spawn paths have never run on their
own platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Minor, not patch. The 3.0.2 sitting on develop was chosen in August for
the shelf rework and themed tooltips; two feature sets have landed
since - the scoped additional-limit windows, and the token resilience
work with its own Settings control, notification type and badge states.

No side effect beyond the About dialog, the exe file version, the
User-Agent and the update check, which now compares 3.1.0 against the
v3.0.1 release and correctly finds nothing newer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Claude desktop app hands Claude Code its own token and never writes
~/.claude/.credentials.json, so the file only stays fresh while the CLI
is in use. Clawdmeter now refreshes the way the CLI does:

- refresh 5 min before expiry instead of after it lapses
- use the current token endpoint (platform.claude.com), falling back to
  the legacy console host, and send the stored scopes
- take Claude Code's .oauth_refresh.lock and re-read the file under it,
  standing down if the tokens were already rotated, so the two never
  spend the same refresh token
- on a 401 with a valid-looking expiry, re-read / force a refresh and
  re-poll right away instead of waiting out the expiry
- remember refresh tokens the endpoint rejected and stop resending them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsZHk76Z7ym9WENv2U2MMy
Develop's fix/token-expired-badge covers the same desktop-app diagnosis
(early refresh, RefreshOutcome backoff, rejected-token block, Sign in
again). Keep that structure and layer on what it lacked: the current
token endpoint and stored scopes, Claude Code's refresh lock with a
BUSY outcome, standing down when the tokens were rotated under us, and
an immediate refresh + re-poll on a 401. The per-run dead-token set is
dropped in favour of the poller's REJECTED block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsZHk76Z7ym9WENv2U2MMy
Refresh the token like Claude Code does: current endpoint, shared lock, recover on 401
The OAuth token endpoint answers every request whose User-Agent is exactly
"anthropic" with HTTP 429, whatever the refresh token. Measured live with a
fake token: "anthropic" 429 on both hosts, while "Anthropic", httpx's default
and "Clawdmeter/3.1.0" all reach the real token check (400 invalid_grant).
Clawdmeter has sent "anthropic" since auto-refresh first shipped, so it read
every refresh as THROTTLED and backed off, and the token stayed expired.

Send Clawdmeter/<version>, as the update check already does. With the fix
the same real credentials refreshed (200) and the usage poll came back
allowed. The new test asserts on the header actually sent and fails with
the old value restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "can't read usage" alert fired once per outage. When that outage began
as a plain expiry, its message said Clawdmeter was refreshing the token; if
the refresh was then rejected, nothing ever corrected that, so a phone or
toast still said help was coming while only signing in could fix it.

It now alerts once more, "Clawdmeter needs you to sign in", on that one
change, at most once per outage, and re-arms after a recovery. Two of the
new tests fail on the previous behaviour; the once-per-outage test catches
the escalation repeating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checkbox read "Also alert when I cross 100% into overage", but it also
switches the additional limits' own "reached 100%" alert on and off. It now
reads "Also alert when a limit passes 100%", with a tooltip saying what 100%
means for each kind of window, and the README says the additional-limit
alert follows that box. Wording only; behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every sample told Settings whether it was a sign-in-again sample, so an
offline or API-error poll in the middle of a dead-refresh-token outage
cleared the flag, and the Connection tab went back to "wait for
auto-refresh" while none was coming. Now sign-in-again sets it, a working
poll clears it, and other failures leave it alone. The new offline and
API-error cases fail with the previous line restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every arrow in the app rendered as a solid 8x5 block, on macOS 26 and
Windows 11 alike. The stylesheet used the CSS border-triangle trick
(zero-size box, three borders), which Qt's stylesheet engine does not
support: it fills the whole border box.

The arrows are now small PNGs drawn once per colour (plus an @2x copy for
high-DPI screens) into a cache folder under the temp directory. The QSS
names a direction and a palette role, so they re-theme like everything
else. If the folder can't be written the arrow is dropped rather than
crashing startup.

build_qss(MIDNIGHT_SALMON) is no longer byte-identical to _BASE_QSS, since
the arrow placeholders now resolve to paths; the byte-identity guarantee
moves to the hex-swap step (_swap_hexes), which is what it always covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The glow behind the app icon was removed in 80a9745 (2026-07-29) because
it clashed with the rounded tile macOS 26 wraps every icon in, but the
design doc still described it and still asked for its alignment to be
checked. Record the bare slot, why, and the 2026-09-27 check on an Apple
Silicon Mac running macOS 26.6.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
weltern and others added 12 commits September 27, 2026 13:42
Code-review finding on the arrow fix. On Linux the temp dir is one /tmp
shared by every user: the first user to run the app owned the cache
folder, so nobody else could add a colour (their arrows silently
vanished); another local user could plant a symlink at the predictable
write path; and tmp cleaners could delete arrows mid-session. Windows and
macOS were unaffected (per-user temp dirs), but the per-user cache folder
(QStandardPaths GenericCacheLocation) fixes all three everywhere and needs
no application name, so it resolves the same before QApplication exists.

Also corrects test_theme.py's docstring, which still described build_qss()
as the byte-identical step and theme.py as Qt-free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the compact view the status dot sat ~30px from 'IDLE' (and, on
macOS, from every activity name). The row's target label is capped at
170px, so a wide row had slack left over, and with nowhere else to go Qt
handed it to the dot and activity labels.

A zero-stretch spacer at the end of the line now takes only what the
capped target cannot. A stretch-1 spacer would also close the gap, but it
splits the slack with the target and elides file names sooner; the test
covers both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed compact rows, DMG doc matches the art

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Ubuntu 22.04 "list_issues" read as "list issues". ScrollingLabel was
exactly fm.height() tall and drew into its own rect, and DejaVu Sans
(Ubuntu's default UI font) draws '_' below its own descent: at 10px it is
11px tall with the baseline at 9, and the underscore sits on row 11.

The label is now tall enough for the lowest descender, and draws
top-aligned in its full height so that extra row is inside the drawText()
rect (which clips). Both halves are needed; the render test fails with
either one alone. On Windows none of the sizes the app uses change
height (Segoe UI leaves room), so layouts there are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "·" between a session's activity and its target never had a colour
rule, unlike the dot and activity beside it, so it fell back to the
system palette: near-black on a light-mode desktop (seen on Ubuntu),
nearly invisible on the dark row. It now uses the theme's muted text
colour and follows theme switches with the rest of the compact sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code-review finding on de64d90. The height check measured "_gjpqy", but
on macOS's SF the tight box of g/y/p sits a row or two below where they
actually draw, so every Mac label grew 1px (10px 12->13, 13px bold
16->17) although the old height already showed them in full - measured
by rendering each glyph old vs new. Only '_' genuinely overflows (on
DejaVu), so measure just that.

Now, at the sizes the app uses, heights are unchanged on Windows and
macOS; on Ubuntu the 10px label gains the row the underscore needs, and
the 12/13px bold labels gain 1px they did not strictly need (the tight
box is conservative) - harmless, Linux only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the compact separator render on Linux

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release-review finding. A revoked token (401 whose refresh is then
rejected) or a 403 still carries a future expiresAt. The poller correctly
reported "sign in again" and the badge and alert sent the user to
Settings, but Settings gated its button and status line on the file's
expiry alone: Sign in again was disabled as "still valid", and the line
read "Valid for ~4h 59m - refreshes automatically".

The button now also opens when the poller says sign-in is needed, and
that verdict outranks the expiry in the status line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release-review finding. Every HTTP 400 from the token endpoint was read
as "the refresh token is dead": auto-refresh stopped and the user was
told to sign in again. But the endpoint also answers a request it can't
parse with a 400, under a different body - measured 2026-09-27 against
the live endpoint: a fake refresh token gives
400 {"error": "invalid_grant", ...}, a missing grant_type gives
400 {"type": "error", "error": {"type": "invalid_request_error", ...}}.
After an endpoint change that would have stopped refreshing for every
user and sent them to a sign-in that cannot fix it.

A 400 now counts as REJECTED only with invalid_grant; any other 400 is
ERROR and keeps retrying with backoff. 401 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release-review finding. scoped_windows parsed resets_at with a bare
datetime.fromisoformat(), which on Python 3.10 - the Linux release build
- rejects a trailing 'Z' and a millisecond fraction. Today's API shape
(six-digit fraction, +00:00) parses everywhere, but any other form would
have read "reset time not reported" on Linux alone.

transcript.parse_iso_ts already handled these, but transcript imports Qt
and scoped_windows is Qt-free, so the parser moves verbatim into a small
isotime module both import; transcript re-exports the name, so existing
callers are unchanged. Verified on Python 3.10: the old code fails the
'Z' and '.543Z' cases, the new code passes all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release-review finding. The "When usage can't be read" alert is on by
default but was not mentioned anywhere, and the token paragraph promised
renewal 30 minutes before expiry without saying macOS never refreshes
(the Keychain is read-only to Clawdmeter; Settings already says so). Also
says what Sign in again actually does: opens `claude auth login`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@weltern
weltern merged commit 42adc1f into main Sep 27, 2026
18 checks passed
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.

2 participants