feat(desktop): the call window is handed the avatar's grant and voice feed alone, stands at the video tile's size while the avatar is up, and reports its status

Merged
CH
5d ago
+628-38

Plan item 9a. main: call window avatar plumbing, stacked on #2472.

What changes

  • Seams on CallWindow: showAvatarSession(grant | undefined) and feedAvatar(feed). They send on two new one-shot channels, app:avatar-session and app:avatar-feed, to the floating call window's own webContents and nowhere else.
    • The sending lives in call-window-avatar.ts (CallAvatarRelay), which does not touch Electron.
    • It holds the standing grant so that a page which draws after the grant arrived still gets it. That covers a window opened by the first STARTING before its renderer loaded, and a renderer that died mid-call.
    • Feed messages are not held; a chunk that found no page has already expired.
    • The grant is never written to app state and never broadcast.
  • Presence and size:
    • CallWindowPresence keeps the floating window up while the avatar is STARTING or LIVE, even when a panel has the keyboard.
    • CallWindow resizes to CALL_VIDEO_BOUNDS (336Ɨ196: #5's 320Ɨ180 tile plus 8 pt of shadow room on each side). The tile grows up from the card's bottom-left corner, and is clamped back onto the work area if growing pushed it off.
    • The window holds the card's own place apart from the frame, so a clamped tile gives the card back where it was. Only a drag moves the saved placement; a move the window made itself is not kept.
    • The "may reach no host" comment now names the one host: the LiveKit server the grant points to. The CSP itself changes in #9b.
  • ACT_KIND.AVATAR_REPORT (avatar.report, payload {status, endReason?}) writes state.voice.avatar.
    • endReason is accepted with ENDED and nowhere else.
    • The router row refuses any sender but call.
  • AGENTS.md "Never": adds the approved carve-out. The Keyframe session token for a call's own avatar may reach the floating call window, and never the voice window, a Gateway answer or event, app state, a counted event, a trace or a fixture.
  • New door @sidecar/hosted/avatar-grant: it holds the grant schema {serverUrl, participantToken, agentIdentity}, its bounds and avatarGrantFromWire. avatar.started in live-contract.ts is now that grant plus type and requestId, so the call window never sees requestId. It's a separate subpath because reading the grant through the barrel put all of live-contract.ts into the renderer bundle, 3.5 KB gzipped over its ceiling. Through the door the renderer grows by about 0.3 KB. The started frame's JSON Schema golden is unchanged.

Tests

  • avatar-acts.test.ts:
    • a panel's or the voice window's AVATAR_REPORT is refused and writes nothing;
    • the call window's report becomes state.voice.avatar;
    • a reason with a status other than ENDED, or ENDED without a reason, is refused.
  • call-window-avatar.test.ts:
    • the grant, the feed and the closing undefined reach the call window's page, and the panel and voice pages receive nothing;
    • a page that draws later gets the standing grant but not the missed feed;
    • after a close it gets nothing.
  • call-window-presence.test.ts:
    • the video stays up through panel focus and comes down with ENDED or the end of the call;
    • the tile grows from the card's bottom-left corner and clamps to the work area, and the card comes back to its own place.

Not in this PR

  • No caller yet. #11 binds showAvatarSession and feedAvatar to the host's grant and the voice window's feed, and writes the status from the host side.
  • #9b runs the Keyframe session in the call window: it subscribes to these channels, adds the CSP connect-src, and sends AVATAR_REPORT.
  • #9b's subscription has to be in place by the call page's first paint (ready-to-show), which is when the held grant is re-sent.

Evidence

./scripts/check.sh exits 0 locally.

CI could not verify this; Mac evidence pending. The verify.sh run on a Mac is owed: the window's refit and presence change are Mac UI, and CI builds nothing for the Mac.

šŸ¤– Generated with Claude Code


Open workspace in Conductor