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:
1 parent
317ad29d85
commit
6154cb1949
13 files changed
+562
-79
No files matched your search
@@ -1358,30 +1358,44 @@ fun SessionScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
if (running) {
|
||||
// The same filled shape as the button beside it, not an outlined one: these
|
||||
// are two things you can do about the turn that is running, and weighting one
|
||||
// of them as secondary said they were a primary action and its qualifier.
|
||||
// What separates them is the colour and the mark, which is what they mean.
|
||||
Button(
|
||||
onClick = { act { interruptSession(settings, summary.id) } },
|
||||
colors = actionButtonColors(stopColor),
|
||||
) {
|
||||
// A filled square, which is what stop has looked like since tape decks.
|
||||
Glyph(
|
||||
STOP_GLYPH,
|
||||
colour = LocalContentColor.current,
|
||||
modifier = Modifier.semantics { contentDescription = "Stop" },
|
||||
)
|
||||
// The same filled shape as the button beside it, not an outlined one: these are
|
||||
// two things you can do about the session, and weighting one of them as secondary
|
||||
// said they were a primary action and its qualifier. What separates them is the
|
||||
// colour and the mark, which is what they mean.
|
||||
//
|
||||
// Always here, rather than arriving with the turn as it used to. A control that
|
||||
// comes and goes makes its own presence the signal, and its absence could not say
|
||||
// whether there was nothing to do; a button that is always in the same place also
|
||||
// cannot push Send off the end of the row by turning up.
|
||||
val process =
|
||||
when {
|
||||
running -> ProcessAction.Pause
|
||||
status == "exited" -> ProcessAction.Start
|
||||
else -> ProcessAction.Stop
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = { act { process.perform(settings, summary.id) } },
|
||||
colors = actionButtonColors(process.colour()),
|
||||
) {
|
||||
Glyph(
|
||||
process.glyph,
|
||||
colour = LocalContentColor.current,
|
||||
modifier = Modifier.semantics { contentDescription = process.label },
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
// The paper plane, with a clock on it while a turn is in flight: sending then
|
||||
// queues the message for the next tool boundary rather than starting a turn of
|
||||
// its own, and the two have to be told apart at a glance. The label says the same
|
||||
// thing to a screen reader, which has nothing else to read.
|
||||
//
|
||||
// Disabled while there is nothing to send, rather than pressable and silent:
|
||||
// `send` has always returned early on an empty composer, so the button promised
|
||||
// something it would not do, and the only feedback was the ripple. Disabled and
|
||||
// not hidden, for the reason the button beside it is always here.
|
||||
Button(
|
||||
onClick = { send() },
|
||||
enabled = input.isNotBlank() || pendingAttachments.isNotEmpty(),
|
||||
colors = actionButtonColors(if (running) queueColor else sendColor),
|
||||
) {
|
||||
Glyph(
|
||||
@@ -1402,6 +1416,39 @@ fun SessionScreen(
|
||||
/** What pressing Send does right now, said the same way to the eye and to a screen reader. */
|
||||
private fun sendLabel(running: Boolean) = if (running) "Queue" else "Send"
|
||||
|
||||
/**
|
||||
* What the composer's process button would do if it were pressed now.
|
||||
*
|
||||
* One value rather than four parallel conditions over the status, because the mark, the colour, the
|
||||
* name a screen reader is given and the request that goes out are four halves of one decision. A
|
||||
* button drawn as a pause that terminates the CLI is the worst bug available here, and separate
|
||||
* branches over the same condition are how that happens -- these three each have to cover every
|
||||
* case, and the compiler says so.
|
||||
*/
|
||||
private enum class ProcessAction(val glyph: String, val label: String) {
|
||||
/** A turn is running: take it back, and leave the process holding the conversation. */
|
||||
Pause(PAUSE_GLYPH, "Pause"),
|
||||
/** Nothing is running, but the process behind the session is: end it. */
|
||||
Stop(STOP_GLYPH, "Stop"),
|
||||
/** The process is gone: start it again, on the conversation it left. */
|
||||
Start(PLAY_GLYPH, "Start"),
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProcessAction.colour() =
|
||||
when (this) {
|
||||
ProcessAction.Pause -> pauseColor
|
||||
ProcessAction.Stop -> stopColor
|
||||
ProcessAction.Start -> startColor
|
||||
}
|
||||
|
||||
private fun ProcessAction.perform(settings: ServerSettings, sessionId: String) =
|
||||
when (this) {
|
||||
ProcessAction.Pause -> interruptSession(settings, sessionId)
|
||||
ProcessAction.Stop -> stopSession(settings, sessionId)
|
||||
ProcessAction.Start -> startSession(settings, sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* An inline transcript image, fetched (authenticated, pinned) from the session's files route. The
|
||||
* bitmap is remembered per ref, so scrolling doesn't refetch.
|
||||
|
||||
Reference in new issue
Block a user