Hold a queued message below the indicator until the turn takes it

The backend records a message the moment it is sent, so one sent into a
running turn landed in the transcript at the time *we* spoke -- above the
working indicator, in among things the session had already read. It had
not read it. Showing it there says otherwise.

Messages sent while a turn is in flight are now held below the indicator,
drawn quieter, and take their place in the conversation when the turn
ends. That is an approximation and worth naming: the CLI injects a queued
message at a tool boundary, and tells nobody when it does, so the end of
the turn is the first moment anything here can honestly say the message
was taken. It errs toward "not yet read", which is the direction that
cannot mislead.

Also in this change, from the same pass over the screen: the app draws
above the gesture strip rather than under it, and the three status colours
that were literals in two other files -- an amber, a green and a red off
Material's defaults -- are now Catppuccin members in Theme.kt beside the
rest of the scheme.

The ordering is verified by construction rather than photographed: the
list is bottom-anchored, so the first item emitted is the lowest on
screen, and the queued block is emitted before the indicator. Staging a
real long-running turn to photograph cost four model turns and never
produced one, because the model kept declining to sleep -- which is its
own finding, and the reason the next change is a test command in the echo
driver.
This commit is contained in:
iris committed 2026-08-28 23:49:55 -04:00
1 parent f402a1f6c8
commit 9791afcfd6
5 files changed
+89 -20

No files matched your search

@@ -85,7 +85,15 @@ val AiAppColors =
scrim = Mocha.Crust,
)
/** "There is something here": a session that is running. */
/**
* What a session is doing, said in colour.
*
* Here rather than beside each screen that shows a status. These were separate literals in two
* other files -- an amber, a green and a red picked off Material's defaults -- so the same state
* was a slightly different colour depending which screen you looked at, and none of them belonged
* to this palette at all. A colour that carries meaning is part of the scheme, not a value typed
* where it happened to be needed.
*/
val runningColor: Color
@Composable get() = Mocha.Green
@@ -98,3 +106,15 @@ val runningColor: Color
*/
val failedColor: Color
@Composable get() = MaterialTheme.colorScheme.error
/** Waiting on a person: a question, a permission, a turn that is theirs. */
val awaitingColor: Color
@Composable get() = Mocha.Peach
/** Approaching a limit -- still fine, worth seeing. */
val warningColor: Color
@Composable get() = Mocha.Yellow
/** Past a limit. The scheme's error colour, for the reason [failedColor] gives. */
val overLimitColor: Color
@Composable get() = MaterialTheme.colorScheme.error