Repository navigation
Description
What happened
I sent messages to Claude Code from Chat while it worked, and I used Stop. Four things went wrong around the queue and Stop:
- A queued follow-up vanishes before its turn. A follow-up sent while Claude works waits above the composer as a "Next turn · Send now" row. At the end of the turn, the row says "Sending…" and goes. For 117 ms to 1.2 s the message is nowhere in Chat: no row, no sent bubble, no user turn. "Working…" goes away, and Stop turns into a disabled Send, for 670 to 871 ms. The chat area grows by 99 px when the row goes, so the transcript moves twice. Send now on a queued row does the same. A follow-up sent just as the turn ended gets nothing at all until its turn is read.
- After Stop, a new message brings the stopped one back. I sent a long request and pressed Stop before Claude answered. As designed, the message left Chat and came back into the composer. I replaced it with a new message and pressed Send. The stopped message came back into Chat as a sent bubble for 783 ms, above the new message, and then went away.
- Stop with a queued follow-up loses the stopped message. Claude worked on message A, and follow-up B waited as "Next turn". I pressed Stop before Claude answered. Claude put A back into its own input box. About 0.5 s later Ovis sent B: it cleared Claude's box with two Esc presses and typed B. A was then in no place: not in the transcript, not in Claude's box, not in the composer. Without a queued row, A comes back to the composer (fix(chat): steer Claude with Ctrl+Enter, guard pending writes, restore a stopped send #760).
- Clicking Stop drops the keyboard focus to the page. After the turn stops, keys typed go nowhere until I click the message box again. Send keeps the focus in the box (fix(chat): keep focus in the message box when Send is pressed #937).
Expected: a sent message shows in Chat from Send until its turn, a stopped message stays out of Chat and in the composer, and the focus stays in the message box.
Cause
Timelines from headless Chromium against an isolated herdr session, with Claude Code 2.1.316 (haiku). Times are from the page start. The messages are test prompts.
Part 1, the end-of-turn drain of a queued follow-up:
61070ms show pending-message "After that, reply with the single word QUEUEDOK" Sending…
61070ms hide chat-activity "Working…" ← and Stop goes
61456ms hide pending-messages ← the row goes
61682ms show chat-virtual-row "After that, reply with the single word QUEUEDOK" (226 ms with no message)
61798ms !blink-out chat-activity goneMs 728
Part 2, a new message after Stop:
36148ms STEP click Stop
36774ms hide chat-bubble "Write exactly 120 numbered lines…" ← the stopped message leaves Chat
42446ms STEP send "Reply with the single word RA"
43091ms show chat-bubble "Write exactly 120 numbered lines…" ← back
43557ms show chat-sent-waiting-list "Reply with the single word RA"
43874ms hide chat-bubble "Write exactly 120 numbered lines…" (shown 783 ms)
Parts 3 and 4, Stop with a queued follow-up (state of the page, sampled):
6884ms pend=["Reply with the single word PONG3"] composer="" focus=Stop
7290ms turn ends composer="" focus=body ← part 4
8682ms pend=[] composer="" ← B goes: Esc, Esc, paste
8999ms user rows: A's row now reads "Reply with the single word PONG3" ← part 3: A is gone
- No bubble for a message the bridge typed from the queue.
src/lib/useComposerSends.ts:149and:155record the waiting bubble only fordelivery === "immediate". A queued send that the bridge types at once (the turn ended before it arrived,server/core/pending.ts:1069) answers{ ok: true }with nopending, like an immediate send, but gets no bubble. The turn-end drain and Send now remove the row with asentreceipt (src/lib/machineSession.ts:77-78), and nothing records a bubble there either. Send now of a held copy (useComposerSends.ts:243) records none. Claude writes the user turn only after it takes the message in, so between the row and the turn the chat has nothing to show. feat(chat): show a sent message in Chat at once, before the transcript has it #2258 closed this gap for immediate sends only. - The cancelled turn is hidden only while the restored copy is read.
src/components/ChatView.tsx:2061-2066(shownTurns) drops the last user turn whilerestoredequals it.restoredcomes from the prompt poll (ChatView.tsx:1693, every 2 s,PROMPT_POLL_MSat:134). The new send empties Claude's box, so the next prompt read answersrestored: null. The conversation read still ends with the cancelled turn until Claude writes the new one, so the turn is drawn again until that read. - The queue drain clears the restored copy.
drainPendingtypes the queued row at once when the turn ends (server/core/pending.ts:726,:755).dispatchPendingTextcallsfreeClaudeBoxfor this automatic send too (pending.ts:582).freeClaudeBox(server/core/composer.ts:275-297) treats a box that holds the chat's own last message as clearable and presses Esc twice. That rule assumes that the composer already holds the copy. The composer gets it only from the next prompt poll, 2 s later, and the drain runs well before it. - Stop takes the focus. The Stop button (
src/components/Composer.tsx:1467-1476) has noonPointerDownthat prevents the default, and Send has one (Composer.tsx:1487). A click focuses Stop. When the turn stops, the same<button>node becomes a disabled Send (sendShown,Composer.tsx:1071), and the browser moves the focus of a disabled node to<body>.
Proposed fix
One PR, four small parts:
- Record the waiting bubble whenever the bridge says that it typed a chat message:
useComposerSends: for any send answered with nopending, not only for an immediate one. DESIGN.md already says that a send the bridge "accepts at once (a receipt with no pending item…)" follows the chat to its end.useComposerSends: after Send now of a held copy.MachineSession: for this connection's row removed assent. The text comes from the row before the store drops it. The time and the baseline come from the frame that showed the row assending, before its Enter. This one place covers the turn-end drain and Send now of a queued row.
ChatView: keep the cancelled turn out after a local send while the conversation read is the same page it was at the send (thesentOverpattern). The next read drops the turn from the active chain.freeClaudeBox: an automatic delivery never clears the restored copy. The queued row is held with theinput_draftnote. The next prompt read gives the stopped message back to the composer, and Send now still clears the copy and sends the follow-up. Claude Code does the same in its own terminal: after Esc it puts queued messages back into the input and does not send them.Composer: Stop gets the sameonPointerDownas Send.
Left open, because each needs a design decision:
- "Working…" and Stop still drop out for about 0.7 s between the two turns. herdr reports the agent idle in that gap. Holding the line while a row of the pane is
sendingis a choice about what "working" means. - A message sent with Send now while Claude still works waits until Claude takes it in at the end of the turn (118 s in one live run). The bubble expires after
WAITING_SEND_MS(60 s). Keeping it longer for a steer is a choice about how long a bubble waits. - Stop pressed from the keyboard still loses the focus when the button becomes a disabled Send. This also applies to Send today.
How often
- Part 1: every queued follow-up that the bridge types, in 3 of 3 live runs (gaps of 117, 226 and 336 ms in one setup, and 377 to 1186 ms in another).
- Part 2: when a prompt read lands between the send and the conversation read with the new turn. 1 of 1 clean live repeat, and 88 of 120 frames in a demo that holds the conversation read.
- Part 3: every time Stop comes before Claude's first output while a follow-up is queued.
- Part 4: every click on Stop.
Ovis version
main (feb3ae0 for my test runs, checked again on dac7c6d).
How it runs
herdr plugin
Server PC
macOS 26.5.2, Apple M4 (Mac mini), herdr 0.9.0
Device and browser
Headless Chromium at 1280×800. The cause is not specific to a device.
Agent and PC
Claude Code 2.1.316 (haiku), local PC
Activity
fix(chat): keep queued and stopped messages in place, and the focus a…
chore(release): 0.4.22 (#2473)