Read a timeout in units, share one usage answer, and let a tilde mean home
The composer's settings row is outlined bubbles opening round menus, and the message box is a TextFieldValue so anything put into it without being typed -- a draft, a share, a slash command -- leaves the cursor at the end. A tap that puts a text selection away no longer also collapses the card the text was drawn in: every open and close on the session screen goes through one guard that spends such a press on the selection. The usage bar and the usage dialog were two polls of one measurement and disagreed for up to a minute at a time; they are one feed now, and the countdown rounds up to the minute in the one place both read. A working directory typed as ~/repos/ai-app was four literal characters on the local transport and as an argument on both, so the existence check refused every home-relative path. It is checked by entering the directory now, expanded for a local spawn the way the remote shell expands it, and stored short so the phone draws what somebody would write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2970a6cf9a
commit
32b2a4871a
16 files changed
+414
-99
No files matched your search
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -55,28 +56,63 @@ sealed class SessionUsage {
|
||||
private const val REFRESH_MS = 60_000L
|
||||
|
||||
/**
|
||||
* One machine's rate limits, polled.
|
||||
* One poll of every machine's limits, and the handle to ask again.
|
||||
*
|
||||
* Hoisted out of [SessionUsageBar] because two things on a session's screen show this same answer
|
||||
* -- the bar, and the colour of the button that opens the usage dialog. Fetching it twice would
|
||||
* cost two round trips to say one thing, and the two copies would disagree for up to a minute at a
|
||||
* time, which is the interface contradicting itself about a number somebody is deciding on.
|
||||
* A screen shows this answer in more than one place -- the bar under the session header, the colour
|
||||
* of the button beside it, and the dialog that button opens -- and each of those used to fetch for
|
||||
* itself. Two fetches say one thing twice and then disagree about it: the bar's copy can be a whole
|
||||
* refresh interval old when the dialog opens with a fresh one, so the header read 42% while the
|
||||
* screen over it read 47%, about a number somebody is deciding on. One feed per screen, and
|
||||
* [refresh] moves both.
|
||||
*/
|
||||
class UsageFeed(
|
||||
val snapshots: LoadState<List<UsageSnapshot>>,
|
||||
/**
|
||||
* A fetch is outstanding. Only ever true over an answer already shown; see [rememberUsageFeed].
|
||||
*/
|
||||
val refreshing: Boolean,
|
||||
/** Ask the backend again now. The dialog's refresh button; the poll does it on its own. */
|
||||
val refresh: () -> Unit,
|
||||
) {
|
||||
/** What [setup]'s own limits came back as. See [usageFor] for why the states are these. */
|
||||
fun forSetup(setup: String): SessionUsage =
|
||||
when (val state = snapshots) {
|
||||
is LoadState.Loading -> SessionUsage.Waiting
|
||||
is LoadState.Error -> SessionUsage.Unavailable(state.message)
|
||||
is LoadState.Loaded -> usageFor(state.value, setup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The one poll of the machines' rate limits, polled and refreshable.
|
||||
*
|
||||
* Hoisted out of [SessionUsageBar] because everything on a session's screen that reports on usage
|
||||
* has to be reporting the same measurement; see [UsageFeed].
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSessionUsage(settings: ServerSettings, setup: String): SessionUsage {
|
||||
var usage by remember(setup) { mutableStateOf<SessionUsage>(SessionUsage.Waiting) }
|
||||
LaunchedEffect(setup) {
|
||||
fun rememberUsageFeed(settings: ServerSettings): UsageFeed {
|
||||
var snapshots by remember { mutableStateOf<LoadState<List<UsageSnapshot>>>(LoadState.Loading) }
|
||||
var refreshing by remember { mutableStateOf(true) }
|
||||
// Bumped to ask again now. The poll below restarts from the new value, so a manual refresh
|
||||
// also resets the countdown to the next one rather than leaving one due immediately after.
|
||||
var asked by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(asked) {
|
||||
while (true) {
|
||||
usage =
|
||||
refreshing = true
|
||||
// Replaces the answer only once the next one is in hand: dropping back to Loading
|
||||
// would blank a bar somebody is reading for the length of a round trip, and what was
|
||||
// on screen is still the last thing the machine actually said.
|
||||
snapshots =
|
||||
try {
|
||||
usageFor(withContext(Dispatchers.IO) { fetchUsage(settings) }, setup)
|
||||
LoadState.Loaded(withContext(Dispatchers.IO) { fetchUsage(settings) })
|
||||
} catch (e: ApiException) {
|
||||
SessionUsage.Unavailable(e.message ?: "couldn't reach the backend")
|
||||
LoadState.failed(e)
|
||||
}
|
||||
refreshing = false
|
||||
delay(REFRESH_MS)
|
||||
}
|
||||
}
|
||||
return usage
|
||||
return remember(snapshots, refreshing) { UsageFeed(snapshots, refreshing) { asked++ } }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user