Repository navigation
Description
What happened
A bug hunt on Ovis Chat (headless Chromium, a frame recorder, the demo and a live Claude Code pane) found four places where the chat's "Working…" state shows or goes at the wrong moment.
-
A finished turn opens again as "Working…". A turn ended and reads
› Worked for 2s · 1 other tool, with its answer under it. A background subagent then finishes, and Claude starts a new turn by itself. For 200–400 ms the old block reads∨ ● Working… · 1 other tooland opens its tool row. Then the new turn's first row lands, and the old block folds a second time. Live timeline (Claude Code 2.1.316, haiku):54492ms the turn ends: its block folds to "Worked for 2s · 1 other tool" (normal) 58342ms composer status BG -> RUN (Claude starts a turn by itself) 58557ms "Background task ended" row shows, with chat-activity "Working…" 58742ms the old block folds again: same text change as at 54492msIn the demo, a status push to
workingwith no new row reopened the turn every time. The row grew from 145 px to 274 px. -
"Working…" shows before a working pane's rows. A switch to a pane whose agent works draws a "Working…" work-block head next to "Loading conversation…". When the first read lands, the head goes and "Working…" comes back as the title of the running turn's own block, in the last row. In the demo it lasted one frame (18 ms) in 1 of 3 runs. A held first read shows the head for the whole read. On a real server, the first read of a chat takes about 150–500 ms.
-
"Working…" goes about 1 s before Claude's answer. At the end of a long answer, "Working…" and the Stop button go, and the chat moves 34 px. For a moment only the user's message shows, as if the turn ended with no answer. Then the answer is drawn. Live: a 984 ms gap in 1 of 12 turns (the conversation reads at 142240 ms returned unchanged, and the answer came with the read at 143256 ms). A second check saw a 133 ms gap in 1 of 3 80-line answers.
-
/clearin an empty Claude chat flashes "Conversation unavailable". The greeting and the centred composer go, the composer drops to the bottom, and the chat shows▸ Conversation unavailable — show terminal outputand "Working…". Then the greeting comes back. Live: 241 ms in the first run, and 549 ms and 295 ms in 2 of 5 later/clearruns typed in the terminal. The other 3 left herdr's status at rest.
Expected: a finished turn stays finished, "Working…" shows only for a turn in progress and only once the chat knows its rows, and a short status change after /clear leaves the greeting in place.
Cause
isLiveWorkTurn(src/lib/workBlocks.ts:179-182) calls the last assistant turn live whenever the pane's status isworking. The only exception issentOver(src/components/ChatView.tsx:1424), which a send from this browser sets. A turn that the agent starts by itself, or that starts from the terminal or another device, sets nothing. When herdr'sworkingstatus comes before the new turn's first row, the finished turn is the last one, soChatView.tsx:2070and:2195title it "Working…" and open it. The transcript already says the turn is over: its last part is a text with phasefinal_answer(Claude gives it only to anend_turnentry).- The activity line at
src/components/ChatView.tsx:2257has noloadedgate. The empty state at:2265has one. Before the first read,lastSplit(:2070) is null, soworkBlockLive(:2073) is false and the footer draws thePendingWorkHead. - For Claude Code, the activity is herdr's status (
ChatView.tsx:1230). The line at:2257and the composer's Stop button (src/components/Composer.tsx:1470) follow it. herdr can godonebefore a conversation read holds the answer. The edge read (ChatView.tsx:1432) starts at once, but nothing holds the line until a read made after the edge is drawn. /clearmakes herdr reportworkingfor a moment.showsGreeting(src/lib/greeting.ts:113-117) hides the greeting while the agent works, so the composer docks. On the server,onNotStarted(server/conversation.ts:1637-1647) sends a session with no transcript yet to the terminal fallback while the agent works or is blocked. Each part is on purpose (a first turn at work, a message just sent), but a/clearstarts both of them.
Proposed fix
One PR for 1 and 2:
isLiveWorkTurn: while the status isworking, a turn whose last part is afinal_answertext is not live. The fix is in the shared function, so every caller gets it. The pending "Working…" head under the rows stands for the new turn until its first row lands, as after a send.- That head waits a short time (800 ms) over a turn that ended on its answer. At a normal turn end, herdr's status trails the answer by 100–400 ms. A head drawn at once flashes under every answer for that time.
- The activity line waits for the first read (
loaded), as the empty state does. Until then "Loading conversation…" is the only line.
3 and 4 stay open:
- 3 needs a hold: on a
working→idleordoneedge with no answer drawn, keep "Working…" and the Stop button until a read begun after the edge is drawn, with a cap of about 2 s. The Stop button lives in the composer, so the hold crosses components. - 4 needs a change to the server's first-turn rule (a history that
/clearreplaced is not a first turn), or a short delay before a status-onlyworkinghides the greeting.
How often
1: when herdr's working status comes before the new turn's first row. 1 of 2 live runs with a background subagent. 2: on a switch to a working pane whose chat has no cached read. 3: rare, 1 of 12 turns. 4: 2 of 5 /clear runs typed in the terminal of an empty chat.
Ovis version
main (feb3ae0 for my test runs, checked again on 53542d6).
How it runs
herdr plugin
Server PC
macOS 26.5.2, Apple M4 (Mac mini), herdr 0.9.0, Claude Code 2.1.316
Device and browser
Headless Chromium at 1280×800. The causes are not specific to a device.
Activity
fix(chat): show the Working head only for a turn in progress (#2551)
chore(release): 0.4.22 (#2473)
fix(chat): keep a Claude turn at work until its answer is read, and t…