Answer a command a session can never run, and put attachments under the text

Investigating a `/clear` that did nothing. What I could measure says the
basic path is sound: the CLI honours `/clear` in stream-json mode -- it
emits `conversation_reset`, opens a fresh session id, and the model then
answers "NO CONTEXT" to a question about something it was told a moment
before -- and a `/clear` sent into a running turn is queued here and applied
at the boundary, with the model losing context, in two reproductions.

What the investigation did find is a command that can wait forever. Held
commands drain at the next idle, and a session whose process is gone has no
next idle, so `/clear` sent to one sat in the queue with a waiting bubble on
the phone that nothing could resolve and nothing anywhere saying why. The
*message* path has always answered this case -- a message to the same
session reports the exit at once -- which is what made the silence visible:
one session answered one and swallowed the other. A command owes the same
answer, since what makes it unanswerable is the same fact.

`Unknown` still waits. It means nobody could find out whether the process is
there and it resolves itself, so refusing on it would turn "we don't know"
into "it's gone".

`local_command` gets the `closed` check `send_user_message` has had all
along, for the window between the status being read and the line being
written -- a line into a fifo nothing is reading goes nowhere and looks
exactly like one that arrived.

Attachments now draw under the message text rather than above it: what
somebody wrote is what the bubble is, and it keeps the first line of every
bubble in the same place down the transcript whether or not there is an
image in it.
This commit is contained in:
iris committed 2026-08-30 01:05:24 -04:00
1 parent da65c1571f
commit f1a185a9cd
3 files changed
+84 -10

No files matched your search

@@ -1314,13 +1314,7 @@ private fun UserBubble(
modifier = Modifier.align(Alignment.CenterEnd).padding(start = 48.dp),
) {
Column(Modifier.padding(12.dp)) {
// Above the words, in the order they were composed: the picture was attached
// before the sentence about it was typed, and it is what the sentence refers to.
images.forEach { ref ->
SessionImage(settings, sessionId, ref)
Spacer(Modifier.height(4.dp))
}
// A message can be nothing but an attachment, and an empty line under a picture
// A message can be nothing but an attachment, and an empty line above a picture
// is a bubble with a gap in it for a sentence nobody wrote.
if (text.isNotEmpty()) {
Text(
@@ -1330,6 +1324,13 @@ private fun UserBubble(
else MaterialTheme.colorScheme.onPrimaryContainer,
)
}
// Under the words: what somebody wrote is what the bubble is, and the picture is
// what they attached to it. It also keeps the first line of every bubble at the
// same place down the transcript, whether or not there is an image in it.
images.forEachIndexed { index, ref ->
if (index > 0 || text.isNotEmpty()) Spacer(Modifier.height(4.dp))
SessionImage(settings, sessionId, ref)
}
}
}
}