fix(web,desktop): on a live call, Luke's work stands under the line that asked for it, his written answer is not drawn twice, and "Worked for" is set like "Working"
Dean's Call tab, mid-call: each turn's work (the "6 tool calls" fold and its written answer) landed at the bottom of the live call, after later speech, and the brain's written reply repeated what Luke had just said aloud.
Cause
- On the call standing now, the Call tab draws the voice window's own caption lines. Their row ids come from the window's own counter (
live-call.ts, "matched to the record by nothing"). - A planning turn names its ask by the service's ledger ids (#2368). No heard line carries those ids, so every live turn fell through to
slotOf, which places a turn at the end of the live call, and kept it there as lines arrived. - An unanchored turn was drawn as unspoken, so
unsaidnever stripped its written answer, and the answer showed beside what Luke said aloud.
Fix
- The tie on a live call is where the line began. Both ledgers group the same Live API transcript deltas on the call's own timeline, which
live-captions.tsalready names as the key the panel matches the record by.PlanWorkTurngains an optionalaskedAt: { voiceSessionId, fromMs }[], read with the ask rows (spokenAskRowsOfnow returns each row'svoice_session_idandfrom_ms).- Each line in the voice window's report carries its
startMs/endMs. call-thread.tsanchors a turn under the developer's heard line, on that call, whose span holds where the ask began. Record lines still match by row id, as before.
- The duplicate goes with the anchor. Once a turn stands under the line that asked for it, it is drawn as spoken and its written reply is left out (design A, #2378: Luke's message is what he said). A turn no line on screen asked for keeps its words, as before.
- Review flagged that a reply silenced by a newer ask is never said aloud, so this PR does not widen "spoken" beyond anchored turns.
- An anchored reply that was silenced is still hidden, as it was before this PR, because the work frame doesn't say a reply was silenced. That is a follow-up.
- "Worked for …" is set like "Working…": both lines share
STATUS_LINE_CLASS(12.5px,text-muted-foreground), the finished line without the shimmer. Its margin plus the message's gap equals the room above a working line. This applies everywhereWorkedFordraws (the Call tab and agent tabs).- It also fixes a margin bug the Working line already had: its
m-0overrode the conversation'sspace-ygap, so on a live call the next spoken line touched it.
- It also fixes a margin bug the Working line already had: its
An older service sends no askedAt, and an older desktop ignores it. Both fall back to today's behaviour.
Tests (each fails without the change)
call-thread.test.ts: a live call heard under window ids, where the asks are named by service ids plusaskedAt. The work stands under each asking line, ahead of Luke's reply, and stays there as later lines arrive and later work finishes. Luke's overlapping reply never takes the anchor.plan-call.test.ts: the same through the rendered tab, with the written answer drawn once, as Luke said it.working-time.test.ts: the finished line is set in the Working line's status classes, without the shimmer.plan-work.test.ts(PGlite): a plan's work read back carries each ask line's call andfrom_ms.voice-view.test.tsand the orchestrator test: a reported line carries its span, and the guard refuses a line without one.- The
live-contract-planWorkFrameSchema.jsongolden is re-recorded underLUKE_UPDATE_FIXTURES=1. Its only change is the new optionalaskedAtarray. It is the service-to-desktop frame, not a schema a model is shown.
Evidence
Rendered from the real PlanCall in headless Chrome (Linux), with Dean's call replayed: the first turn's work sits under "Hey, can you check…", ahead of "Hold on…". The second turn is still working under "…make a fully interactive page?", ahead of "Okay, I'll take a look…". "Worked for 39s · …" is set like "Working… 3m 2s".
| Light | Dark |
|---|---|
![]() | ![]() |
CI could not verify the Mac UI, and ./scripts/verify.sh was not run: this was done on a Linux VM. LUKE-159 is the manual Mac pass. ./scripts/check.sh exits 0.
🤖 Generated with Claude Code

