feat(desktop): Luke's avatar video moves to the top of the Call tab, and the call card goes back to audio only

Merged
CH
5d ago
+502-571

Follow-up to the avatar feature: the floating call window's video tile was too small. Luke's avatar now plays as a large 16:9 video at the top of the panel's Call tab. The floating call card goes back to its audio-only 264x92 size.

What moves

  • The Keyframe session runs in the panel's renderer.
    • callAvatarAtom (#9b's scoped rendererRuntime.atom), the bundleAtom load, the feed mapping, the AVATAR_REPORT reports and the autoplay retry are unchanged. They run in the panel now.
    • The session stays mounted at the renderer root, so it keeps running whatever tab is showing and while the panel is hidden.
    • The avatar's <audio> is split out of the tile as AvatarVoice. PanelAvatarVoice mounts it beside <App /> in index.tsx, so a tab change never cuts the voice.
  • The tile is drawn only in the Call tab.
    • CallTabVideo (renderer/call/panel-avatar.tsx) draws CallVideoTile, now video-only, through a new video slot on PlanCall.
    • It appears only on the call's own plan, and only while that call is in progress and avatarTiled holds (callTabVideoStatus in call-card-model.ts).
    • The CSS is resized to the tab's width with the transcript's 16px gutter, and the drag region is gone.
  • Main targets the panel.
    • PanelManager holds the relay, renamed CallAvatarRelay → PanelAvatarRelay (main/window/panel-avatar.ts).
    • It sends onAvatarSession and onAvatarFeed to the primary panel's webContents, and only after that panel's ready-to-show. It re-hands the standing grant when the panel draws.
    • AVATAR_REPORT and VOICE_SET_VIDEO are accepted from the panel sender only.
    • avatar-link.ts and avatar-acts.ts take panel: Pick<PanelManager, …> in place of callWindow.
  • The call window is restored to its pre-#9a shape. call-window.ts, call-window-presence.ts, call.html (back to connect-src 'none') and CallWindowCard are back to what they were. CALL_VIDEO_BOUNDS, callWindowFrame, fitVideo and avatarTiled are gone from them.
  • CSP. connect-src wss://*.livekit.cloud https://*.livekit.cloud moves to index.html.
    • The preview logs show one avatar-started line on 2026-10-14 with host: keyframelabs-n1yiobta.livekit.cloud.
    • I kept the wildcard, which the task allowed when narrowing isn't tighter. livekit-client 2.22.4 (RegionUrlProvider) fails a refused join over to regional hosts it gets from the project's /settings/regions. Those hosts are other *.livekit.cloud names that no tighter CSP source can express. The HTML comment says so.
  • Sentry and session replay.
    • The Sentry beforeBreadcrumb and beforeSend scrub already runs in every renderer, the panel included, and is unchanged.
    • The panel is the window that records session replay. So SESSION_REPLAY_MASKING now sets capture_performance: false and enable_recording_console_log: false explicitly. A refused LiveKit join's URL and its console line carry the token in the query string. Both were already off by default with the preloaded remote config; now they are asserted. Both names are added to the test that checks them against the installed bundle.
  • AGENTS.md. The Keyframe carve-out now names "the panel window" instead of "the floating call window". Nothing else in that sentence changed.
  • Docs. PRIVACY.md, apps/web/server/README.md and the voiceVideo setting's detail text now say where the video is shown.

Hidden panel and audio: every window, panels included, is built from hardenedWebPreferences, which already sets backgroundThrottling: false and autoplayPolicy: "no-user-gesture-required". That is the same setup the hidden voice window plays Luke's voice under today. No window settings changed.

Tests

  • avatar-acts.test.ts: reports and toggles are accepted from the panel. A report or toggle from the floating call window or the voice window is refused and changes nothing. The grant and the feed reach the panel's page.
  • panel-avatar.test.ts (moved from call-window-avatar.test.ts): the grant and feed reach the panel's page and never the call window's or the voice window's. A panel that draws later gets the standing grant and not the stale feed.
  • call-window-presence.test.ts: rewritten for the card alone. The card stands only while a call is in progress and no panel has the keyboard. A late blur changes nothing.
  • call-video-tile.test.ts: the tile draws no audio. AvatarVoice plays its track, follows the speaker toggle both ways, and releases the track once it is gone.
  • call-card-model.test.ts: callTabVideoStatus draws the tile only on the call's own plan, while the call is in progress, for STARTING, LIVE and restartable ENDED.

./scripts/check.sh exits 0.

Not verified

  • Mac evidence pending. CI could not verify this, and this Linux VM can't run verify.sh or run.sh --preview. The user will retest on the Mac: the tile's look at the top of the Call tab, the hidden panel still playing the avatar's audio, and the card back at 264x92.
  • No live Keyframe session was opened. Untested against a live session:
    • the panel joining the room under the new CSP;
    • LIVE on the first frame while the panel is hidden or on another tab;
    • lip-synced audio from a hidden panel;
    • the handover that mutes the voice window's element.
  • Known limits:
    • The grant goes to the primary panel only. Luke has one panel window, keyed by display.
    • A panel that reloads mid-call is handed the standing grant again on did-finish-load, as the call window re-handed it to a renderer it stood back up. A panel renderer that crashes and is not reloaded still leaves the avatar reading as up. The panel has no crash-recovery path of its own; that is out of scope here.

šŸ¤– Generated with Claude Code