feat(session): send a message while the agent is still replying - #13
Open
Adam-Dalloul wants to merge 2 commits into
Open
Adam-Dalloul wants to merge 2 commits into
Adam-Dalloul wants to merge 2 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_availableandfeedback_tool_availablealready 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.