- In a short window the footer fades out when the pointer leaves the window.
The chevron menus are portalled to the document, so they stayed behind, their
trigger faded out beneath them.
- The footer already stays for keyboard focus. A menu's focus is outside it, so
the trigger's aria-expanded tracks it instead.
- Pre-existing: the camera chevron does the same on main today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Two participants, for what only a real call can show: a device switch that
neither drops the media session nor rejoins, and a meter that keeps moving
while muted while the peer is still shown the mute rather than left to infer
it from silence.
- The component harness covers what only a host page shows: every device
reachable in a small container, the list sized against the call rather than
the window, and the menu following the call area when the host resizes it.
- Synthetic devices are patched onto the media element and the audio context
alike, so a browser with no hardware still enumerates and still routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- One story per state that matters: silence, three volumes of speech, denied
permission, no device, many devices, and a selection still settling.
- The geometry the design rests on is asserted rather than eyeballed — the bars'
shape at any width, the icon's two centre lines with the rows above it,
headings that stay while their section is in view, every row reachable by the
keyboard clear of both, and each section headed by a rule edge to edge.
- Each story supplies its own microphone and its own call-sized root, so what it
renders is what the app renders. Browser flags that fake a device were tried
and rejected: they made the suite pass while the same story showed no
microphone in a browser without them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The footer view model now carries the output devices, the current output and
the callback that changes it, alongside the ones it already carried for input.
- That callback is withheld where the platform cannot route audio to a chosen
device, and its absence is what renders the section disabled — an absent
action rather than a flag the view has to interpret.
- An empty output list stays a list rather than becoming an absence, so the
section is still drawn where a platform enumerates nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Two labelled sections, speaker above microphone, with radio selection in both,
the pattern the settings list already uses.
- A device that cannot be chosen is shown disabled rather than hidden, so the
menu keeps one shape across browsers; where a platform lists no output at all,
a disabled Default stands in that section instead of leaving a bare heading.
- The list is bounded by the measured call area rather than the window, so it
stays usable where Element Call is a component in a corner of a host's page,
and follows that area when the host resizes it.
- It scrolls with its section headings pinned and the meter held at its foot,
and keeps both clear of whichever row the keyboard has just reached.
- The focus ring is shown only when the keyboard moved the focus, tracked on the
menu rather than the document, so two calls on one page cannot answer for each
other.
- While a requested device has not arrived, nothing in either section can be
picked, so a second request cannot overtake the first. That holds only while
the device is still offered and only within the menu it was asked in: one
removed before the switch lands never arrives, and waiting for it would leave
every device unselectable for the rest of the call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Draws the level as lit bars, and each failure state as its own message with a
next action rather than as a meter that has stopped.
- A bar and the space beside it keep one size wherever the meter is drawn; what
the width decides is how many bars there are. Bars spread to fill would be a
different shape in every place, and would close into one block where the space
ran short.
- Announces the level on a fixed scale, so it does not rest on hue or on how
many bars happened to fit.
- The hook is a bridge and nothing more: the capture belongs to the state layer.
- The capture is held by a component of its own, so a level that does change
redraws the meter rather than whatever is rendered beside it. A level arrives
many times a second; held by the menu it would reconcile every device row on
the way to the bars. A test counts those renders, because nothing else would
notice the hook moving back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Opens a short-lived capture of a named device and reports a smoothed level.
- Owns the capture, the audio context and the frame loop, and releases all three
idempotently, including when cleanup lands before getUserMedia has resolved.
- Reports denied permission and a missing device as states of their own, so a
caller never has to read silence as an error.
- Gives the microphone back when the audio graph fails to build. That can fail
after getUserMedia has resolved, and the capture would otherwise outlive its
own failure: the device open and its in-use light on, behind a meter saying
it is unavailable.
- Shares the capture stub through the test utilities, silence and a signal
alike, so the meter's tests and its stories drive the same microphone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
As of the removal of 'Legacy' mode, there can no longer be a difference between the transport you advertise in your membership and the transport you publish media on, so the local transport code can be simplified considerably. For instance, the function which gets the local transport can simply return a promise rather than being reactive.
I kept the local transport as an Observable in other modules so that they could easily be tested with existing marble tests.
LiveKit's livekit-ice logger records each local ICE candidate, ICE
candidate errors and ICE connection state changes, but only at debug,
which we cap to info unless extended LiveKit logs are switched on. That
means a 'could not establish pc connection' rageshake carries no way to
tell 'no relay candidates at all' from 'relay candidates that never
paired'. Keep that one logger at debug regardless of the toggle.
* Add AGENTS.md documenting the repo's conventions
Codifies what is currently tribal knowledge or only discoverable by reading CI
config: the view model / view contract, the rule that nothing reads the page,
Clean Code ordering, the three test layers, and reuse-before-you-build.
The root AGENTS.md is the always-loaded minimum and routes to docs/agents/, so
an agent loads only the detail its task needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Allow either hand at the commit, and measure continuous redraws
Committing from the agent rules out running it where there is no git
identity, a sandbox included. Either is now fine: commit on the user's
word where the setup allows it, or write the message and hand it over.
Redrawing continuously is the other addition. A level meter reported a
fresh value every animation frame, so its whole menu reconciled sixty
times a second, silence included, and nobody noticed until review. A PR
that adds anything of the kind now says what it costs, or better, counts
the redraws — commits rather than render calls, since React runs a
component it then discards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "has no matching matrix call member" warning fired on every re-render
(i.e. every active speaker update) and also for the local participant's own
track, which is never rendered here anyway. Skip local tracks and warn once
per unexpected identity.