Stop and start a session's process from the composer

The composer's second button now says what pressing it would do to the
process behind the session, in one place that is always there: an orange
pause while a turn is running (interrupt, the process stays), a red stop
when it is not (end the process), a green play when it has exited (start it
again on the same conversation). Send is disabled while there is nothing to
send, rather than pressable and silent.

Behind it, two routes. `stop` signals the recorded process and says nothing
else -- the driver's own reader already reports a death correctly, and
announcing it here would be a guess ahead of the measurement. `start`
replaces the driver and nothing else, so the transcript, the pump and every
open phone's stream stay where they were and there is still one writer of
the transcript; it is refused unless the session is known to have exited,
since starting on `Unknown` is the two-CLIs-on-one-conversation fault.

That last rule found a bug in the launch path: a relaunched session took its
status from the transcript, so one whose process had died before a backend
restart reported `exited` while the launch had just started a new process --
which refuses every command and offers a phone the chance to start a second
CLI on a live conversation. A launch that leaves a process running now says
idle.

The icon font moves to the Mono face, where every glyph is one em square, so
two icon buttons are the same width without either being told one; the
proportional advances ran 0.46 to 0.92 em and Send came out visibly wider
than Stop. GLYPH_SIZE comes down to match, since a glyph that fills its em
draws bigger at the same point size.

Verified against a stand-in CLI on the emulator: idle -> stop -> exited ->
start -> idle, a turn interrupted from the pause button, and both buttons
measured at 171x105 device pixels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-30 12:41:36 -04:00
1 parent 317ad29d85
commit 6154cb1949
13 files changed
+562 -79

No files matched your search

@@ -213,13 +213,14 @@ val overLimitColor: Color
@Composable get() = MaterialTheme.colorScheme.error
/**
* The composer's three buttons, coloured by what pressing one does rather than by where it sits.
* The composer's buttons, coloured by what pressing one does rather than by where it sits.
*
* Green sends now, blue sends later, red takes the running turn away. The pair of greens and the
* pair of reds elsewhere in this file are deliberate near-collisions worth naming: [runningColor]
* is green because a session is working, and [failedColor] is red because one fell over -- those
* are *states*, and these are *actions*. A reader never has to tell them apart, because nothing
* here is a state and nothing there is pressable.
* Green makes something happen now, blue makes it happen later, orange takes back what is in
* flight, red ends the process. The near-collisions with the states above are deliberate and worth
* naming rather than collapsing: [runningColor] is green because a session is working,
* [failedColor] is red because one fell over, [awaitingColor] is the same orange because a session
* is waiting on somebody -- those are *states*, and these are *actions*. A reader never has to tell
* them apart, because nothing here is a state and nothing there is pressable.
*/
val sendColor: Color
@Composable get() = Mocha.Green
@@ -228,10 +229,30 @@ val sendColor: Color
val queueColor: Color
@Composable get() = Mocha.Blue
/** Interrupting the running turn -- the one button here that takes something away. */
/**
* Interrupting the running turn: the work stops and the session stays.
*
* Orange rather than red because of how much it takes: only what is in flight. The process is still
* there holding the conversation, and the next message starts a turn as though nothing had
* happened. Red is spent on [stopColor], which is the same button in the same place when what it
* would end is the session's process.
*/
val pauseColor: Color
@Composable get() = Mocha.Peach
/** Ending the session's process -- the one button here that takes something away. */
val stopColor: Color
@Composable get() = Mocha.Red
/**
* Starting the process again, on the conversation it left.
*
* The same green as [sendColor] on purpose: both mean "this happens now", and they are never the
* same button -- the process button only offers to start when there is nothing running to stop.
*/
val startColor: Color
@Composable get() = Mocha.Green
/**
* A filled button in one of the action colours above.
*