Show the model and mode the session has, not the ones it was asked for

Picking either from the phone wrote the choice straight into the
session's state and then sent the request. Asking and having are
different things, and the difference is not rare: `auto` is a permission
mode the CLI accepts on the command line, silently resolves to
`default`, and refuses outright over the control channel -- "auto mode
unavailable for this model" -- so a session spawned in auto was in
default and one switched to auto stayed where it was, with the phone
reporting auto in both cases.

So the drivers report what they are set to and the manager follows that.
Measured, because the confirmations are not uniform: a model change
answers success with no value, so what was asked is remembered until the
answer arrives; a mode change echoes the mode it became, and that answer
wins over the request; and `init` names both -- resolving `haiku` to
claude-haiku-4-5-20251001 -- which also covers a session adopted from a
terminal that set them outside this app. A driver that cannot change
either already says so with an error, and now that error is the whole
story rather than a note beside a display that changed anyway.

The config keeps the requested value, deliberately: that answers a
different question, which is what to launch this session with next time.

Two things fall out. Control request ids are random rather than the
clock, because two in the same second shared an id and something now
looks them up. And the phone shortens a resolved name for the button --
`haiku-4-5` -- since the full one is what the CLI reports and roughly
twice the room that row has once Stop is in it.
This commit is contained in:
iris committed 2026-08-29 15:36:53 -04:00
1 parent 404066fa7d
commit 3eccf7e443
8 files changed
+359 -29

No files matched your search

@@ -48,6 +48,14 @@ sealed class SessionEvent {
data class Status(val state: String) : SessionEvent()
/**
* What the session is set to, as the session itself reports it.
*
* Either field alone: the two are confirmed separately and by different things. Asking for a
* change is not having one, so this -- not the request -- is what the pickers show.
*/
data class Settings(val model: String?, val permissionMode: String?) : SessionEvent()
data class UsageDelta(val tokens: Long) : SessionEvent()
/**
@@ -108,6 +116,11 @@ fun parseSeqEvent(json: String): SeqEvent {
"peerMessage" ->
SessionEvent.PeerMessage(body.getString("from"), body.getString("text"))
"status" -> SessionEvent.Status(body.getString("state"))
"settings" ->
SessionEvent.Settings(
model = body.optString("model").ifEmpty { null },
permissionMode = body.optString("permissionMode").ifEmpty { null },
)
"usageDelta" -> SessionEvent.UsageDelta(body.getLong("tokens"))
"compacted" ->
SessionEvent.Compacted(
@@ -0,0 +1,25 @@
package com.example.aiapp
/**
* A model's name as a person reads it.
*
* Providers answer with their own full identifier -- Claude Code resolves `haiku` to
* `claude-haiku-4-5-20251001` and reports that, which is the honest answer to "what is this session
* using" and far too long for a button in a row that also has to hold Stop and Send.
*
* So the two ends that identify nothing are dropped and nothing else is: the vendor prefix, which
* is the same on every model this app can show, and the release date, which distinguishes builds of
* one model rather than one model from another. What is left is the part somebody chose --
* `haiku-4-5` -- and anything that does not look like that is returned untouched, since a name this
* does not recognise is a name it has no business editing.
*
* A display decision, not a correction: the full name is what the session reports and what a reader
* is shown when there is room for it.
*/
fun modelLabel(model: String?): String {
val name = model?.takeIf { it.isNotBlank() } ?: return "default"
return name.removePrefix("claude-").replace(DATED_SUFFIX, "")
}
/** A trailing `-YYYYMMDD`, which is how these identifiers carry their release date. */
private val DATED_SUFFIX = Regex("""-\d{8}$""")
@@ -228,7 +228,7 @@ private fun SessionCard(
listOfNotNull(
session.provider,
"on ${session.setupName}",
session.model,
session.model?.let { modelLabel(it) },
)
.joinToString(" · "),
style = MaterialTheme.typography.bodySmall,
@@ -233,6 +233,8 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
}
is SessionEvent.PeerMessage ->
items + TranscriptItem.PeerNote(entry.seq, event.from, event.text)
// Screen-level state, not transcript rows -- see SessionScreen.
is SessionEvent.Settings -> items
is SessionEvent.Status -> items
is SessionEvent.Error -> items + TranscriptItem.ErrorMsg(entry.seq, event.message)
is SessionEvent.Image ->
@@ -346,6 +348,12 @@ fun SessionScreen(
when (val event = entry.event) {
is SessionEvent.UsageDelta -> totalTokens += event.tokens
else -> {
// What the session says it is set to now, which is the only thing that
// says it: picking from either menu asks, and the answer comes back here.
if (event is SessionEvent.Settings) {
event.model?.let { model = it }
event.permissionMode?.let { permissionMode = it }
}
if (event is SessionEvent.Status) {
// Started here, or nowhere. `ready` is what separates the live stream from
// the page of history the screen opens with, and a compaction found in that
@@ -651,7 +659,9 @@ fun SessionScreen(
listOfNotNull(
summary.provider,
"on ${summary.setupName}",
summary.model,
// What the session says it is set to now, which is the same fact
// the picker below shows and has to be the same answer.
model?.let { modelLabel(it) },
if (totalTokens > 0) "$totalTokens tok" else null,
)
.joinToString(" · "),
@@ -931,10 +941,13 @@ fun SessionScreen(
) {
if (offeredModels.isNotEmpty()) {
PickerButton(
current = model ?: "default",
current = modelLabel(model),
options = offeredModels,
// Not set here. The button follows what the session reports it
// is set to, which arrives a moment later and is sometimes a
// different answer -- a name the CLI resolved, or no change at all
// on a provider whose model is fixed when it starts.
onPick = { chosen ->
model = chosen
act { setSessionModel(settings, summary.id, chosen) }
},
)
@@ -943,7 +956,6 @@ fun SessionScreen(
current = permissionMode,
options = PERMISSION_MODES,
onPick = { chosen ->
permissionMode = chosen
act { setSessionPermissionMode(settings, summary.id, chosen) }
},
)