package com.example.aiapp import java.time.Duration import java.time.OffsetDateTime // How long is left in a usage window. Shared by the session bar and the usage screen: the // arithmetic is the same in both, so everything here returns the span or the state on its own and // leaves the wording to the caller. /** * "1d 4h", "3h 12m", "12m" -- the span alone, with no leading or trailing words. * * Rounded **up** to the whole minute, rather than truncated as it was. A window with 3h 12m 50s * left is nearer four minutes past the twelve than it is to twelve, and truncating also parks the * figure on a minute it has already spent. One rule, so the session bar and the usage dialog cannot * round a shared measurement two different ways. */ fun formatSpan(until: Duration): String { val up = if (until.seconds % 60 == 0L && until.nano == 0) until else until.plusMinutes(1) return when { up.toHours() >= 24 -> "${up.toDays()}d ${up.toHours() % 24}h" up.toHours() > 0 -> "${up.toHours()}h ${up.toMinutes() % 60}m" else -> "${up.toMinutes()}m" } } /** * What is known about when a usage window ends. * * 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. A timestamp that did arrive and could * not be read is the genuinely unknown case. * * 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. */ 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 windowEnd(resetsAt: String?, now: OffsetDateTime): WindowEnd { if (resetsAt == null) return WindowEnd.NotRunning return try { WindowEnd.Ends(Duration.between(now, OffsetDateTime.parse(resetsAt))) } catch (_: Exception) { WindowEnd.Unreadable } }