fix(web,desktop): the Call tab stands a call's first ask under its turn, folds a spoken turn's words and footer with its work, and a goodbye's turn settles rather than fails

Merged
DA
6d ago
+368-66

Summary

Dean's 3:38 PM call showed three faults on the Call tab, each traced to its cause against the store's own rows for that call (turns f1103ef1 and 98d6730e, conversation 57cb2809).

  • Order. The brain turn that answered Dean's first ask sank to the end of the call. Every call opens with the explore ask (voice/live-brain.ts's explore), keyed by the call's session and naming no line, and the brain opens a turn for it at once; the developer's first ask is then steered into that running turn, so its ask row is newer than the turn's start. #2417's rule in spokenAskRowsOf dropped every ask made after its turn started, meaning the follow-ups a parked turn takes, and so dropped the first ask of every call too: the turn named no line, found no anchor, and stood by its start time after the call. noSteeredAsk now counts an ask as a follow-up only where the turn already had a delegation (an ask naming a voice session) before it. The explore ask is no delegation, so the first ask steered into its turn is the turn's ask.
  • Brain text. An unanchored turn is drawn as unspoken, so its written words showed as a message. With the turn anchored, today's code dropped its words whole. Now a spoken turn's words run with its calls (turn-rows.ts): inside the open group while it moves, inside the fold once it has finished, with its "Worked for" line (TranscriptRows's trailer), the fold's line counting calls, subagents and coding agents and no message, and no Copy under the message. A spoken turn with nothing but words draws nothing, as before. The end-call turn is a brain turn under the ask that asked for it, so it folds the same way: one line, "1 tool call".
  • "Failed". The end-call turn's row was failed / model, "The model did not return a response". The tool's words tell the model to call it first and add nothing; eve then asked the model for a step after the call, got an empty reply, reissued the call once with a nudge, got empty again, and failed the turn. The hang-up was never involved. END_CALL_TOOL now declares eve's endsTurn, carried through HostedToolDeclaration to the defineTool in eve/tools/brain.ts, so the turn settles on the call. The tool's description is unchanged, so no golden moves.

Tests, each failing without its change: plan-work.test.ts plays the explore turn, the steered first ask and a later steered follow-up through the relay on PGlite and reads back the turn's asked lines; turn-rows.test.ts reads a spoken turn's rows moving and finished; plan-call.test.ts mounts the sequence and reads the fold's line, its opened body, and its place between the ask and Luke's goodbye; hosted-brain-host-tools.test.ts reads which planning tool is declared to end its turn.

Evidence

  • Platform-independent checks: ./scripts/check.sh exit 0 on this Linux workspace.
  • macOS Electron verification (./scripts/verify.sh): not run. This is a Linux cloud workspace, so CI could not verify the Mac UI and verify.sh was not run; LUKE-159 is the manual Mac pass.

Visual evidence

The Call tab rendered in headless Chrome from a throwaway page over the renderer's own PlanCall and stylesheet, fed Dean's sequence (synthetic words), before on main's renderer with the data the service sent that day, after on this branch with the data it now sends.

DarkLight
Beforebefore darkbefore light
Afterafter darkafter light
After, folds openedafter dark openafter light open
  • Fixture screenshots inspected: not attached (no Mac).

Physical-device evidence

  • Screenshot or screen recording: not attached
  • Device/display configuration: not recorded

Invariants

  • ./scripts/check.sh green; renderer/UI PRs also ./scripts/verify.sh with evidence inspected. (CI) — verify.sh not run, see above.
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI)
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root AGENTS.md "Testing")
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/oxlint/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/oxlint/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives)
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed. (manual: vitest run --reporter=json) — four tests added, none removed.
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays. — nothing replaced.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording) — none names one.
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module (@effect/platform-node, effect/unstable/sql) behind a barrel the renderer or a web function opens. (CI: existing renderer node: grep plus a new grep for those specifiers in renderer bundles)

Notes

  • Blockers or follow-up verification: the end-call fix is at the eve boundary (endsTurn), which no hermetic test can run end to end; the next live call with a goodbye is the check that its turn settles rather than fails.

šŸ¤– Generated with Claude Code