- Five of one's own at most, so the grid is whole: at the limit the add
tile goes, and it comes back once one is removed. The store holds the
limit too.
- An added image is refused if it isn't an image, is animated, or can't
be decoded; reduced to 1920px on its longest edge; and laid on an opaque
ground, since a transparent pixel would be a hole in the background.
- Animation is read from the file, as a still and an animated WebP share
a type. Chromium has no selected track until the decoder's tracks are
ready; without waiting for them an animated GIF got through.
- A refusal says why, in place of the notice, and is scrolled into view.
It can be dismissed, is gone once the menu closes, and the next shows
even if it reads the same. What was in force stays.
- End to end on Chromium and Firefox, from what the device then keeps: an
animated GIF is refused with its message, a 4000x3000 image is kept at
1920x1440, and a half transparent one is kept opaque. All three fail
with the preparation bypassed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- An add tile after the pictures opens the file picker, and the image
chosen joins the backgrounds on offer without being put on; it is in
force once chosen, like a shipped one. The tile is a command rather
than a choice, so a menu item beside the radios, and it isn't offered
where effects can't run.
- Kept in IndexedDB on this device and never sent anywhere, behind a
small interface, so the unit tests use an in-memory store; the
IndexedDB store itself is tested against fake-indexeddb, a new dev
dependency.
- The picker takes the focus, which the menu reads as a click elsewhere,
so the menu holds itself open until a file is chosen or the picker is
cancelled.
- A chosen image the device no longer keeps, as after its storage is
cleared, falls back to no effect once the device has said what it
keeps.
- Switching is checked between any two kinds of effect. End to end on
Chromium and Firefox: an added image shows in the preview once chosen;
once it is in force, no request carries the file or the copy kept,
which a deliberate upload fails; chosen, it is in force again after a
reload, gone once storage is cleared, and can be added again.
- Kept as given here; what can be added is checked in the commit after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A remembered effect this browser can't run is cleared rather than kept,
as the menu already showed no effect in force there.
- A camera turned off and on again with an effect chosen gets the same
pipeline as its track arrives, neither rebuilt nor switched again.
- End to end, on Chromium and Firefox: the effect chosen before a call is
still chosen, and worn, in the next call after leaving; and a second
tab still shuts the first down with the existing "Opened in another
tab" error while an effect runs.
- Switching camera and sharing a screen are checked by hand: they need
two cameras and a screen, which the fake devices don't give yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The standalone app at five window sizes, from a large desktop to a
phone-shaped 360x640, on Chromium and Firefox: the camera menu stays
inside the window, which is the call area there, and the last effect can
still be scrolled to and chosen.
- As a component in a host page, and as a widget in Element Web: a
picture chosen reaches the preview, so the pipeline's model and
WebAssembly load from those builds, and in a small container the last
effect is reached and chosen.
- Compound portals the menu to the page and takes no container for it, so
there it has none of the component's scoped styles, as the microphone
menu has none.
- The colour checks the three share live in playwright/utils.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A host in the call records every frame of every video track it receives
from the moment the track arrives. A guest with a picture chosen joins,
or turns the camera on in the call from off at joining or from off in
the call; none of the guest's frames may be closer to the camera image
than to the picture. A guest with no effect fails it.
- The preview's track stopping destroys the shared pipeline, so the call's
track builds it again. That build is held back as long as the slower
route takes, and publishing still waits for it; each check that holds
the model back also checks that it did.
- The first build survives the preview camera turned off and on while it
runs, which is what running builds one at a time is for.
- The camera's colour is taken once the preview shows it; taken earlier it
was black, which let frames of the room pass as the picture. The camera
checks wait for the call itself, as the lobby has a camera button of
its own.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The first effect chosen in a session shows a spinner on its own tile,
in place of the tick, until a frame carrying it is drawn. Later changes
show nothing, having nothing to prepare.
- Ended by the frame, not the promises, which resolve while the segmenter
is still being built. The transformer reports the first frame that went
through the segmenter; one passed through untouched doesn't count.
- The pipeline publishes the wait with its state, and both footer view
models take it as an input.
- The lobby tests' provider mock gains the observable hook, built once: a
fresh observable per render rebuilt the footer on every render.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Where the browser has no MediaStreamTrackProcessor, effects draw each
frame through a canvas: slower, and the first build stalls the page.
They stay offered and choosable there, with a notice under the grid
that says so and describes the group.
- Decided in the view model, beside whether effects can run at all, so
the lobby and the call give the same answer.
- The notice stays within the menu's width however long the sentence,
which the story checks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The camera menu's blur toggle becomes a Background effects section
under the cameras: no effect, blur and the two shipped backgrounds as
one radio choice of tiles, three across. It scrolls with the device
list, so it stays reachable in a short call, and choosing keeps the
menu open. The lobby gets it too, as both render the same footer.
- The footer's view model gives the options, the notice and the actions;
the footer only names them.
- The tile is its own component, not a restyled Compound MenuItem. On
desktop it is Radix's radio item, from the copy Compound uses, so the
arrow keys walk the tiles with the device rows; a unit check fails if
Compound ever resolves another copy. In the phone drawer, which is
outside Radix, it is a plain menuitemradio button.
- Where the browser can't run effects the tiles are shown, disabled and
explained by a notice the group is described by. No effect stays
choosable and shows as in force. The notice is in the scrolling list,
so a short call doesn't push the menu out of it.
- Section headings are raised, or the positioned tiles scroll over the
one they sit under. Tiles clip with overflow: clip; with hidden they are
scroll containers, which focus scrolls into view without their border.
- End to end on Chromium and Firefox: a shipped background chosen in the
lobby shows in the preview, and None after Blur draws nothing, counted
in WebGL draws (Blur: about 1400 a second on Chromium, 2300 on
Firefox).
- The Quick Audio Menu's test of the camera menu keeps its name and now
finds blur among the effects rather than as a toggle.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A catalogue of what can be chosen: no effect, blur, and two shipped
backgrounds, the app's own gradients flattened onto its canvas colour.
Their ids are what a saved choice stores, so they are named for the
pictures.
- One setting for the choice, typed as the ids an effect can have and
defaulting to blur for anyone who had it on. A stored value we no
longer ship, or one that isn't even a string, reads as no effect: it
comes from storage another build or a hand may have written.
- The pipeline follows it, switching in place to a picture as readily as
to blur, and the camera menu's blur toggle reads and writes it.
- So does Settings' blur checkbox: a picture reads as blur off, and
checking it replaces the picture with blur. The old on/off setting is
only read, to carry blur over. First tests for the Settings modal,
which fail with the checkbox on the old setting.
- The transformer's own init loads a picture chosen before it was built,
as the base init does and it had not: the first effect always is, and
the picture was drawn black.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Brings design's blur icon, which the background effects grid uses.
- Of the icons the release redrew, guest and zoom-in, Element Call uses
neither, and none of the icons it removed are used here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The menu asked the browser; the pipeline asked the browser and for a
desktop. On a phone whose browser can run them, blur was offered and
then refused: the toggle moved and the video did not change.
- Both now ask one function, and it asks the browser alone. The platform
was the wrong measure: a phone on the fast route held 99% of its frame
rate and was refused, a desktop on the slow one held 73% and was
allowed.
- The in-call device switcher is still withheld on a phone; that is the
device menu's own rule and is unchanged.
- The provider tests run as a phone, and fail with the desktop test back.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- One pipeline, shared by the pre-join preview and the call, attached the
first time an effect is wanted and never detached after; turning blur
off switches it to a disabled mode in place. Main dropped the processor
whenever blur went off, and a pipeline taken off the camera is destroyed
and re-primed, so each time blur came back one unprocessed frame went
out. Built once, the priming frame is spent once.
- Still driven by the existing blur setting: nothing a user sees changes.
- Its logic is BackgroundEffects in src/state/, which the provider only
builds, so the tests drive it without rendering anything.
- Disabled drops the blur radius too. The SDK's disabled mode keeps it,
and with a radius every frame is still segmented and thrown away; with
neither a radius nor a picture, frames pass untouched.
- Switches run one at a time and skip any overtaken, so an earlier,
slower switch can't land after a later choice.
- Builds, rebuilds and teardowns of the pipeline run one at a time
(OneStepPipeline). The preview's and the call's tracks attach and stop
it independently, and the SDK's wrapper holds one track's streams: two
builds at once overwrite each other's, and a stop during a build does
nothing.
- Every camera track starts with the pipeline on: the room's capture
defaults follow it, and a camera turned off before the first effect is
replaced on turning it on, since a stopped track can't take one. One
attached after publishing lets the room through while it is built.
- BlurBackgroundTransformer becomes BackgroundEffectTransformer: it also
sits disabled, and its init applies that state as the SDK's would.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- nodePolyfills rewrites any module that mentions process, and MediaPipe's
WASM loader does. The loader is a classic script that sets
self.ModuleFactory; as a module it sets nothing, so under pnpm dev the
segmenter failed with "ModuleFactory not set". Blur on main fails the
same way there. Production is unaffected: the loader ships as a static
asset.
- The plugin's transform now skips that loader. Checked with main's own
BlurBackgroundTransformer: it initialises with this and fails without.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The camera and microphone menus were as wide as their longest device
name, so a long one widened the menu over more of the call: 340px for a
long speaker name. Both are now 296px, as design sets them, and a long
name wraps.
- Narrower only where the call area is, so the menu still fits inside it:
268px in a 300px call. Taken from the observation of the call area that
already bounds the list's height.
- Held only for the dropdown. On Android and iOS Compound renders the menu
as a drawer as wide as the screen; menuIsDrawer follows its rule.
- Once the list is long enough to scroll, Safari paints it above the
outline the menu is framed with, and the frame vanished down both sides
of it: 384px of it on the many-devices microphone menu, in WebKit. The
list now keeps a border width clear of the frame, so the headings and
the meter inside it no longer need their own.
- Guarded with a long speaker name, which fails at 340px on main's sizing;
a unit test run as a phone, which fails with the width held there; and
the many-devices story's inset, which fails on the old stylesheet, since
the stories run in Chromium, which never showed the Safari bug.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Give the microphone level its own observable
* Add a microphone level meter
* Show speakers and microphones in the quick audio menu
* Wire speaker selection through the call footer
* Add stories for the meter and the device menu
* Add end-to-end specs for the quick audio menu
* Keep the footer while a menu opened from it is open
* Use the shared audio capture stub in the lobby test
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Say what earns a comment its place
- Add a comments section to the code style: no comment by default, the reason
only, and the kinds that go stale.
- Point to it from the PR checklist in AGENTS.md.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes the following warning:
[warn] (!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:
- import "./vite.config" without a file extension (vitest.config.ts:3:35). Add the file extension
Set `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.
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>