feat(panel): a call's agent work folds under what was said, and its divider is plain

Merged
DA
5d ago
+622-63

A follow-up in the Unified agents rollout, from Dean's first real try on the Mac.

Why

Any agent can take a call now (#4300, #4307). On a call, the agent tab drew the agent's turn in full (reasoning, tool rows, "Worked for", its written answer) and then the voice's spoken line, which says the same thing again. Every answer read twice. The old Call tab folded the brain's work under what was said. The call divider also wore a gray band.

What changed

  • The spoken reply leads, and the work folds under it. The voice's reply already names the turn it read aloud: metadata.read_from is the turn's journal id (#4246, #4260, #4307). spokenMessages (coding-agent-model.ts) moves each such reply up to its turn. The tab draws the developer's line, then the voice's lines, then the turn folded closed (said). The fold holds the reasoning, calls, written answer and "Worked for …". It opens on a click.
  • The fold is the old Call tab's. It reuses the said fold in turn-rows.ts. That fold used to drop a spoken turn's words, and left a turn with no call unfolded. Now a finished spoken turn folds all of itself, and its words are counted as messages (1 tool call, 1 message). The agent's written answer is not what the voice said, only a longer version of it, so it stays one press away instead of being dropped. A turn that is still moving draws as before.
  • Typed lines on a call (#4307) fold the same way, since their replies carry read_from too. callOfLine now also counts a line typed on the call (channel: typed with a voice_session_id), so the typed line stands inside the call's divider instead of above it.
  • Outside a call nothing changes. A row is moved only when a voice line names a turn on the page.
  • The divider is plain. The sticky band on the well colour now belongs only to the Code tab's feed (.checkpoint-log .checkpoint). In the agent transcript the call divider, live or ended, is a thin rule with the day and time.

Not covered

  • A reply written without read_from stays where it was said, unfolded. The service leaves it off for words written more than 2 minutes after the turn settled, or before it settled. Linking those would need the service to put turnId on the agent page's rows, and the service is out of scope.
  • callThread (@luke/voice/call-thread) didn't fit here: it matches by the asked/askedAt that only the old task-conversation page carried. It stays as it is.

Tests

  • New agent-call-fold.test.ts mounts the whole AgentTab:
    • A call's turn is drawn folded closed under the spoken line, and opening the fold shows its reasoning, call, written answer and "Worked for 11s".
    • A typed call line folds the same way, inside the call's divider.
    • A turn outside a call is unchanged. This one is a guard, so it passes on main too.
    • The divider, live and ended, has no background and isn't sticky, checked with tasks.css loaded.
  • The other three tests fail on main.
  • turn-rows.test.ts: the spoken-turn cases are updated to the new fold, and cases are added for a call-less spoken turn and for a message that arrived mid-turn.
  • Ran locally:
    • vitest over packages/voice/src, packages/panel/src/tasks and turn-rows: 144 files, all green.
    • Typecheck of voice, features and panel.
    • biome and oxlint on the changed files.
  • pnpm check:affected was OOM-killed in this sandbox (@luke/service typecheck), so CI is the gate.

Screenshots

Dark mode, synthetic data, rendered from the panel's AgentTab in a throwaway harness. The harness isn't in this PR. It shows a call with one spoken ask, then a line typed on the call.

Before · after (folded) · after (first fold opened):

The divider, live and ended. Before, it sits on the well's gray band. After, it's a plain rule:

Made with Luke


Devin Review