Skip to content

feat(session): send a message while the agent is still replying - #13

Open
Adam-Dalloul wants to merge 2 commits into
xintaofei:mainfrom
Adam-Dalloul:feat/mid-turn-send
Open

Adam-Dalloul wants to merge 2 commits into
xintaofei:mainfrom
Adam-Dalloul:feat/mid-turn-send

Conversation

@Adam-Dalloul

Copy link
Copy Markdown

Stacks on #12, which added the receive half. The diff shows both commits until that one lands. Mirrors desktop #637 and #640.

The composer refuses to send while a turn is streaming, so the phone can only watch a turn that desktop and web can talk into. This adds the send half.

The affordance follows the delivery channel the server reports. native_steering_available and feedback_tool_available already ride the attach snapshot, so the client decodes them and offers a mid-turn send only when one is true, with Stop still in place. Copy is per channel, since a native push lands in the running turn while a pulled note is only read at the agent's next check. A delivered note shows up in the transcript; a pending one gets no user turn anywhere, so it reports itself in the notice banner. A note that comes back pending on a session we believed was native downgrades the copy for the rest of the session.

Images take neither channel. The pull tool is text only, and a server without the block support from #640 ignores the field rather than rejecting it, so an attachment sent that way would vanish with no error. A draft holding one is kept whole and sent as an ordinary prompt once the turn ends; the turn-end race takes the same route. There is no message queue here, so the composer is the queue and the draft stays visible and editable either way.

Not compiled. I have no Xcode or Swift toolchain on this machine and the repo has no CI or test target, so I stuck to the idioms already in these files and checked every symbol and signature against the tree, but a build is the only real proof.

A message sent while the agent is still replying never appeared in the
transcript. The client does not decode feedback_submitted at all, so the event
falls into the AcpEvent .unknown catch-all and the handler drops it. The
message only surfaces once the turn ends and the transcript reconciles with the
server, which projects a mid-turn user_message_chunk as its own user turn. And
because no user turn lands between the two halves of the reply, the answer to
that message keeps streaming into the same assistant bubble, so two separate
replies read as one run-on paragraph.

Decode the event, and when the note is already delivered on submission (the
native steering channel, where the agent has the text as a user message rather
than as a check_user_feedback tool result), splice it into the live turn at the
point it arrived. That closes the run the agent was writing and starts the
reply to it in a fresh one, which is the same split the server's own projection
makes, so the live view and a reload agree. It is idempotent by note id, since
the submit is broadcast to every attached client including the sender, and it
is ignored outside a running turn, where there is nothing to split and
appending would graft the message onto a finished reply. The waiting shimmer
moves to the tail of the turn, so interrupting the agent no longer leaves the
transcript looking stalled.

The turn's transcript is replaced wholesale by the server's copy when it
settles, so nothing doubles on reload or on re-entering the session. The
unreconciled fallback splits its snapshot the same way rather than folding a
whole interrupted reply into one assistant turn.

iOS itself still cannot send mid-turn: send() refuses while a turn is in flight
and the compose button is Stop. What this fixes today is the cross-client case,
a message sent from desktop or web while the phone is watching the same turn.
The composer refuses to send during a turn: send() guards on !isInFlight and the
action button is Stop. So the phone can only watch a running turn, while desktop
and web can talk into it. This adds the send half; the receive half (rendering a
mid-turn message in the transcript) is the change this stacks on.

Delivery is the server's decision, not ours. A connection advertises
native_steering_available and feedback_tool_available in its attach snapshot, and
submit_session_feedback refuses a session that has neither. The snapshot already
carries both flags on the wire, so decode them and let them decide whether the
affordance exists at all: no channel, no button, and the bar keeps its Stop-only
form. The flags only ever upgrade, since a snapshot read while the agent was
still coming up reports false, and they reset with the connection they describe.
The one downgrade the server signals is a note that comes back pending on a
session we believed was native, which proves it rerouted to the pull tool; that
latches, so a stale snapshot cannot restore the promise.

The two channels do different things and the composer says which. A native push
lands in the reply being written and comes back delivered, so it appears in the
transcript where it interrupted. A pulled note is only read when the agent next
checks, gets no user turn there or on reload, and would otherwise vanish without
a trace, so it reports itself in the notice banner. The line above the field
carries the same distinction before the tap rather than after it. The whole
affordance is also gated on the server's own Live feedback toggle, which is what
that setting describes.

Images take neither channel. The pull tool delivers plain text, and while the
native wire can carry a full draft, a server without that support ignores the
field rather than rejecting it, which would drop the image with no error
anywhere. A draft holding one is therefore kept whole and sent as an ordinary
prompt when the turn ends. The turn-end race takes the same route: the server
answers no active turn, nothing was recorded, and the draft goes as a normal
message the moment the stream catches up. There is no message queue here, so the
composer is the queue, and the draft stays visible and editable either way.
Every other failure keeps the draft and says what went wrong.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant