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

+48 -1
View File
@@ -298,6 +298,51 @@ Two consequences worth stating:
going away and means to come back) and `stop` (the session is being deleted,
so the process must not survive). Every driver owes exactly one of them.
### Stopping and starting a session's process (decided 2026-08-30)
If a session outlives the backend, the person holding the phone needs the
other direction too: **end the process without ending the session, and start
it again on the same conversation.** `POST /sessions/:id/stop` and
`/start`.
Three decisions worth not undoing:
- **Stop signals the recorded process and says nothing else.** It does not
go through the driver and it does not announce `Exited`. The record is the
session's rather than any dialect's, so signalling it here works for a
session whose driver is in no state to be asked and adds no trait method a
new driver could implement wrongly — and the driver's own reader already
reports the death correctly, draining the last output and recording the
status. Announcing it from here would be a guess arriving ahead of the
measurement, and wrong for the grace period a process that ignores SIGTERM
keeps running.
- **Start replaces the driver and nothing else.** The transcript, the event
pump and the SSE stream every open phone is reading stay where they were,
so starting a session again is not a reconnect for anybody watching, and
there is still exactly one writer of the transcript — which relaunching
the whole `LiveSession` would not be, since the old pump outlives its
session and an imported session's sync task would go on feeding it.
`LiveSession` and `Commands` therefore share one `Mutex<Arc<dyn Driver>>`
rather than each holding a copy.
- **Start is refused unless the session is *known* to have exited.**
`Unknown` means nobody could find out whether the process is alive, and
starting one on that is exactly the two-CLIs-on-one-conversation fault
`session::process` exists to prevent.
That last rule found a real bug in the launch path, which is where the phone
would have hit it: a relaunched session took its status from the transcript,
so one whose process had died before a backend restart reported `Exited`
while the launch it had just gone through was starting a new process. The
status now says `Idle` when a launch leaves a process running — `Exited`
there is not merely stale, it is the word that refuses every command and
invites somebody to start a second process against a live conversation.
On the phone this is one button in the composer, left of Send, whose mark and
colour say what pressing it would do now: an orange pause while a turn is
running (interrupt — the process stays), a red stop when it is not (end the
process), and a green play when it has exited (start it again). One button
rather than three that come and go, so its presence is never the signal.
### Importing refuses a session that is already open (decided 2026-08-29)
Claude Code keeps a descriptor per live session at
@@ -448,7 +493,9 @@ POST /sessions spawn {provider, host, model, cwd, permiss
GET /sessions/:id/events?after=N SSE: transcript replay from N, then live
POST /sessions/:id/message {text, attachment_ids}
POST /sessions/:id/answer {question_id, answer} (questions and permissions)
POST /sessions/:id/interrupt
POST /sessions/:id/interrupt stop the running turn; the process stays
POST /sessions/:id/stop end the process; the session and transcript stay
POST /sessions/:id/start run the process again, continuing the conversation
POST /sessions/:id/model {model}
POST /sessions/:id/compact (llama sessions)
POST /sessions/:id/attachments multipart upload → id (referenced by /message)