A pass over every Kotlin file, fixing each place a rule the codebase had
already learned was applied to only one member of its set:
- Notifications: checkSelfPermission(POST_NOTIFICATIONS) on Android 12 and
below answers "denied" for a permission that does not exist there, so
every notification on API 24-32 was silently dropped. Version-guarded;
before 13 the app-level switch is the whole answer.
- The permission-mode list was written three times and had drifted: the
import screen was missing "plan". One list in Api.kt now.
- The import screen's delete refetched the whole list through a loading
spinner -- the exact fault the session list's delete already fixed and
documented. It now removes only the deleted row.
- The import screen truncated paths at a hardcoded 40 characters; it now
uses StartEllipsis against the row's real width, like the models screen.
- warm() bypassed the partsOf cache built for it, re-scanning every loaded
message per page, and warmed a multi-block memory note under keys no row
looks up. It now mirrors transcriptUnits through the same caches.
- The transcript's data model (TranscriptItem, foldEvent, joinPages, warm)
moved out of SessionScreen.kt into TranscriptItems.kt: pure folding with
no screen in it, changing for unrelated reasons in the same file.
- One image fetch/decode/failed block was written twice; it is
rememberSessionBitmap in SessionImage.kt now.
- Lint is fully clean: android.media.ExifInterface replaced with the
androidx one (the framework copy lacks the hostile-image parsing fixes,
and these images arrive from outside the phone), highlights bumped to
1.1.0, and the notification fix above closed InlinedApi.
- Dead weight out: an unused act() onFailure parameter, and five orphaned
or misattached doc comments (UserBubble carried SessionImage's doc).
Verified: ktfmt, compileDebugKotlin, lintDebug (0 issues), server suite
(93 passed), clippy and rustfmt clean; exercised on the emulator against a
real imported transcript -- paging, tool groups, block rendering, no
crashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five-hour window's reset time is absent between blocks, because the
window is anchored to the block it started in and there is nothing to reset
until one is running. Measured against a live response: the reset came back
as exactly five hours after work resumed, and the weekly windows in the same
response carried the identical microsecond, so both are computed from one
now() at request time. The weeklies always have a reset because a week is
always running -- which is why only the five-hour row looked wrong.
`remainingUntil` returned null for that and for a timestamp it could not
parse, so the session bar announced "reset time unknown" about a machine
behaving perfectly, on the one row somebody reads before starting something
big. The usage dialog, looking at the same field, drew nothing at all and
printed a raw ISO string when a parse did fail. One missing value, two
rules, and neither of them right.
`WindowEnd` names the three answers and both callers go through it. A window
that is not running shows its percentage and no countdown, in the bar as
well as the dialog; an unreadable timestamp says so in words rather than
showing itself.
Looked at both on the emulator, the second by making the server drop the
field: 15% with "4h 43m left" when a block is running, "13%" alone when none
is, and the dialog's five-hour row with no reset line beside weeklies that
have one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar read "31% of 5h", which is the one thing about the window a
reader already knows. What decides whether to start something now is how
long what is left has to last: 80% with twenty minutes to go and 80%
with four hours to go are opposite answers, and the second number was a
screen away on the usage screen.
It now reads "31% - 2h 36m left", and the countdown is driven by a clock
the refresh loop advances rather than computed at draw time. A
percentage that comes back unchanged is an equal value, so Compose skips
the recomposition -- a "left" recomputed only when the quota happens to
move would have sat at a stale figure for hours while looking live.
A window can arrive with no reset time, so that keeps its own wording:
"reset time unknown" rather than "refresh soon", which would be a
recommendation nothing measured. Under a minute, including past the end,
is "refresh soon" -- "0m left" reads as a measurement.
The span arithmetic was already on the usage screen, so it moves into
`ResetCountdown.kt` and both callers supply their own sentence. That
screen still reads "resets in 2h 37m" and "resets in 5d 21h", checked on
the emulator alongside the bar it was not part of changing.
The fill is blue rather than the scheme's primary: the bar sits under
every session header, on a screen somebody opened to do something else,
and it reports a quantity rather than a verdict. The usage screen is
still where the same number turns yellow and then red, for a reader who
went there to be told where the limits are.
Also declares this project's resources for Dev Updater, whose
declaration schema changed in d27b5a3: `resources.ron` says ai-app keeps
its state as `ai-app`, so the Uninstall dialog offers the real
directories instead of saying it cannot tell where they are. Only the
name, because both XDG places are the conventional ones. What that
dialog's config toggle would delete includes the CA under `certs`, which
strands every phone running an APK pinned to it -- noted where somebody
would be standing when it matters.