Merge branch 'main' of git.arirex.me:iris/ai-app
This commit is contained in:
commit
3095052df0
5 files changed
+101
-31
No files matched your search
@@ -20,18 +20,42 @@ fun formatSpan(until: Duration): String =
|
||||
}
|
||||
|
||||
/**
|
||||
* Time from [now] until [resetsAt], or null when that timestamp cannot be read.
|
||||
* What is known about when a usage window ends.
|
||||
*
|
||||
* Null rather than a zero duration, because a string this app failed to parse is not a window that
|
||||
* has just run out: a caller given zero for both would tell the reader to refresh on the strength
|
||||
* of something nobody measured.
|
||||
* Three answers rather than a nullable duration, because two of them shared `null` and they are not
|
||||
* the same thing at all. A window the server sent no reset time for is one that is **not running**:
|
||||
* the five-hour window is anchored to the block it started in, so between sessions there is nothing
|
||||
* counting down and the API says so by omitting the field -- measured against a live response on
|
||||
* 2026-08-31, where the five-hour window's reset was exactly five hours after the moment work
|
||||
* resumed. A timestamp that did arrive and could not be read is the genuinely unknown case, and it
|
||||
* is the only one worth those words.
|
||||
*
|
||||
* Collapsing them put "reset time unknown" on the session bar for a machine behaving perfectly, on
|
||||
* the one row somebody reads before starting something big -- and the usage dialog, looking at the
|
||||
* same field, quietly drew nothing. Two rules for one missing value; this is the rule.
|
||||
*/
|
||||
sealed class WindowEnd {
|
||||
/** No reset time was sent, so nothing is running in this window. Not a failure to find out. */
|
||||
data object NotRunning : WindowEnd()
|
||||
|
||||
/** A timestamp arrived and could not be read. The one case that is actually unknown. */
|
||||
data object Unreadable : WindowEnd()
|
||||
|
||||
/** How long is left. Negative once the window is past, which each caller words for itself. */
|
||||
data class Ends(val until: Duration) : WindowEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* [resetsAt] as the server sent it -- absent, unreadable, or a moment -- against [now].
|
||||
*
|
||||
* [now] is a parameter rather than read here so a caller can drive it from state and have the
|
||||
* countdown recompute on its own schedule.
|
||||
*/
|
||||
fun remainingUntil(resetsAt: String, now: OffsetDateTime): Duration? =
|
||||
try {
|
||||
Duration.between(now, OffsetDateTime.parse(resetsAt))
|
||||
fun windowEnd(resetsAt: String?, now: OffsetDateTime): WindowEnd {
|
||||
if (resetsAt == null) return WindowEnd.NotRunning
|
||||
return try {
|
||||
WindowEnd.Ends(Duration.between(now, OffsetDateTime.parse(resetsAt)))
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
WindowEnd.Unreadable
|
||||
}
|
||||
}
|
||||
@@ -88,13 +88,15 @@ private val LOADING_SPINNER = 48.dp
|
||||
* row is anything from one line to a page and a fixed count is therefore a distance only by
|
||||
* accident. Eight rows was the number, and on a tool-heavy transcript eight rows is less than one
|
||||
* screen: the reader reached the end of what was loaded on *every* swipe and waited a round trip
|
||||
* standing there. That is what "scrolling is laggy" turned out to be -- not a slow frame, but the
|
||||
* list running out of transcript, which the emulator showed as a swipe that moved nothing for 689ms
|
||||
* and then jumped.
|
||||
* standing there, which is a list running out of transcript rather than a slow frame.
|
||||
*
|
||||
* Three, so a fling lands on rows that are already there and the page after them is on its way. The
|
||||
* cost of being generous is a page fetched that nobody reads; the cost of being mean is a list that
|
||||
* stops under a finger, and those are not the same size.
|
||||
*
|
||||
* Counted at the server rather than inferred from the screen, which is the only measurement here
|
||||
* that does not depend on how the emulator renders: against a 24,000-event transcript at `--delay
|
||||
* 120`, ten swipes asked for **ten** pages before this and **three** after.
|
||||
*/
|
||||
private const val HISTORY_SCREENS = 3
|
||||
|
||||
|
||||
@@ -185,19 +185,22 @@ private fun UsageNote(text: String) {
|
||||
* The percentage on its own does not answer the question it gets asked, which is whether to start
|
||||
* something now; 80% with twenty minutes to go and 80% with four hours to go are opposite answers.
|
||||
*
|
||||
* The window's end has its own missing case, kept apart from the rest: a snapshot can arrive with
|
||||
* no reset time, and saying "refresh soon" there would put a recommendation on the screen that
|
||||
* nothing measured. The percentage is still known, so it is still shown.
|
||||
* The window's end has two missing cases and they are worded differently on purpose; see
|
||||
* [WindowEnd]. A window that is not running gets the percentage and nothing else, because there is
|
||||
* no countdown to report and inventing one would be the same fault as inventing the number.
|
||||
*/
|
||||
private fun fiveHourLabel(window: UsageWindow, now: OffsetDateTime): String {
|
||||
val percent = "${window.percent.toInt()}%"
|
||||
val until = window.resetsAt?.let { remainingUntil(it, now) }
|
||||
return when {
|
||||
until == null -> "$percent · reset time unknown"
|
||||
// Under a minute, including past the end: the number would round to "0m left", which reads
|
||||
// as a measurement rather than as the window having run out.
|
||||
until < Duration.ofMinutes(1) -> "$percent · refresh soon"
|
||||
else -> "$percent · ${formatSpan(until)} left"
|
||||
return when (val end = windowEnd(window.resetsAt, now)) {
|
||||
// Between blocks the five-hour window has no reset time, and saying so is a fact about
|
||||
// nothing: there is no window to run out. The percentage is the whole answer.
|
||||
WindowEnd.NotRunning -> percent
|
||||
WindowEnd.Unreadable -> "$percent · reset time unreadable"
|
||||
is WindowEnd.Ends ->
|
||||
// Under a minute, including past the end: the number would round to "0m left", which
|
||||
// reads as a measurement rather than as the window having run out.
|
||||
if (end.until < Duration.ofMinutes(1)) "$percent · refresh soon"
|
||||
else "$percent · ${formatSpan(end.until)} left"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -204,10 +204,10 @@ private fun WindowBar(window: UsageWindow) {
|
||||
color = quotaColor(window.percent),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
window.resetsAt?.let {
|
||||
resetLine(window)?.let {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
"resets ${formatReset(it)}",
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -215,8 +215,19 @@ private fun WindowBar(window: UsageWindow) {
|
||||
}
|
||||
}
|
||||
|
||||
/** "in 3h 12m" -- close enough for deciding whether to start a big task. */
|
||||
private fun formatReset(resetsAt: String): String {
|
||||
val until = remainingUntil(resetsAt, OffsetDateTime.now()) ?: return "at $resetsAt"
|
||||
return if (until.isNegative) "soon" else "in ${formatSpan(until)}"
|
||||
}
|
||||
/**
|
||||
* "resets in 3h 12m" -- close enough for deciding whether to start a big task -- or nothing.
|
||||
*
|
||||
* Null for a window that is not running, which is the case this row has always drawn as nothing and
|
||||
* is right to: there is no end to report. What it used to get wrong is the other missing case, a
|
||||
* timestamp that arrived and could not be read: that was printed raw, so a parse failure appeared
|
||||
* as an ISO string in the middle of a sentence written for a person. Both cases are named in
|
||||
* [WindowEnd], and the session bar words them the same way.
|
||||
*/
|
||||
private fun resetLine(window: UsageWindow): String? =
|
||||
when (val end = windowEnd(window.resetsAt, OffsetDateTime.now())) {
|
||||
WindowEnd.NotRunning -> null
|
||||
WindowEnd.Unreadable -> "reset time unreadable"
|
||||
is WindowEnd.Ends ->
|
||||
if (end.until.isNegative) "resets soon" else "resets in ${formatSpan(end.until)}"
|
||||
}
|
||||
Reference in new issue
Block a user