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:
irisandClaude Opus 5 committed 2026-09-03 19:56:36 -04:00
1 parent 2970a6cf9a
commit 32b2a4871a
16 files changed
+414 -99

No files matched your search

@@ -0,0 +1,38 @@
package com.example.aiapp
/**
* A span of milliseconds, written the way somebody reads it.
*
* A tool's timeout arrives as `480000`, which nobody reads as eight minutes. The rule has two
* halves, because a short span and a long one are read for different things. Under a minute the
* question is "roughly how long", so only the largest unit is shown and a fraction of it carries
* the rest -- `2.5s`, `30ms`. At a minute or more the question is "how long exactly", so every unit
* that has something in it is written out -- `5d 12h 4m`. Units that are empty are left out rather
* than written as zero, since the labels say which is which and `5d 0h 4m` is only longer.
*
* Sub-second precision is dropped past a minute: nothing that takes days is measured in
* milliseconds, and carrying them would make the common case the widest one.
*/
fun formatMillis(ms: Long): String {
if (ms < 0) return "-" + formatMillis(-ms)
if (ms < 1000) return "${ms}ms"
if (ms < 60_000) {
val tenths = (ms + 50) / 100
val whole = tenths / 10
val rest = tenths % 10
return if (rest == 0L) "${whole}s" else "$whole.${rest}s"
}
val seconds = ms / 1000
val parts =
listOf(
"d" to seconds / 86_400,
"h" to seconds / 3600 % 24,
"m" to seconds / 60 % 60,
"s" to seconds % 60,
)
return parts.filter { it.second > 0 }.joinToString(" ") { "${it.second}${it.first}" }
}
/** [text] as a span when it is a whole number of milliseconds, and unchanged when it is not. */
fun formatMillisText(text: String): String =
text.trim().toLongOrNull()?.let { formatMillis(it) } ?: text