Compare commits

...
Author SHA1 Message Date
Johannes Marbach d5f72f464d Compact AGENTS.md and split hand-off out into a skill 2026-11-11 16:33:46 +01:00
renovate[bot] 96949a57da Update ghcr.io/element-hq/element-web:develop Docker digest to 8dc3767 (#4447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 14:47:22 +00:00
Johannes Marbach 2e05ef77a0 Merge pull request #4441 from element-hq/johannes/flakes
Try to fix flaky background tests
2026-11-11 15:37:31 +01:00
Johannes Marbach 73826ec4f9 Reformat 2026-11-11 15:29:03 +01:00
Johannes Marbach 1135c02e6f Merge branch 'main' into johannes/flakes 2026-11-11 13:53:30 +01:00
Johannes MarbachandTimo 9dc425b5f8 Apply batched suggestions from code review
Improve comments

Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
2026-11-11 13:52:05 +01:00
Johannes Marbach c1b94b62dd Merge pull request #4434 from element-hq/johannes/intent
Stabilise m.call.intent
2026-11-11 11:02:22 +01:00
Johannes Marbach cfd8312e46 Try to fix flaky background tests 2026-11-09 12:20:00 +01:00
10 changed files with 135 additions and 63 deletions
+35
View File
@@ -0,0 +1,35 @@
---
name: handoff
description: Implement the first cut of a task and stop for a direction check before the quality pass.
argument-hint: <the task>
disable-model-invocation: true
---
Task: $ARGUMENTS
Stop after the first implementation and hand it back before the quality pass.
## Before the hand-off
- Run the narrowest check that rules out an obviously broken hand-off: the test
file for the touched code, `tsc` on its own, a single story. Not the full
`pnpm lint`, not the whole suite.
- If a wider check is needed before feedback can be useful, keep it to the touched
code and say why it was needed.
- Leave wider refactors, extra tests and documentation polish out. They are
follow-up work, not part of the first hand-off.
## The hand-off
Stop and ask whether the direction is right. The message says:
- what was built and where, in a few lines,
- which check ran and what it returned, verbatim if it failed,
- what was left out on purpose,
- the decisions that could have gone another way.
Do not continue past this point on your own.
## After the user confirms
The skill ends here. Continue with the quality pass as usual.
+4
View File
@@ -44,3 +44,7 @@ storybook-static
# Nested checkout made by scripts/layered.sh
/matrix-js-sdk/
# Claude
/.claude/*
!/.claude/skills/
+25 -4
View File
@@ -16,19 +16,35 @@ host's page. It is the MatrixRTC reference implementation.
`@media`, global `i18next` or the `widget` global — take it from a provider.
- The change works standalone, as a widget and as a component. Say what you checked.
- Every gate below is green before you push.
- Hand off after the first implementation, before the quality pass. Then commit on
the user's word where the setup allows it, or hand them the message.
## Read before you
| … | … |
| -------------------------------- | ---------------------------------------------------------- |
| start a task of any size | [docs/agents/workflow.md](docs/agents/workflow.md) |
| touch call logic or a view model | [docs/agents/architecture.md](docs/agents/architecture.md) |
| write any code | [docs/agents/code-style.md](docs/agents/code-style.md) |
| write a test, story or e2e spec | [docs/agents/testing.md](docs/agents/testing.md) |
| open a PR | [CONTRIBUTING.md](CONTRIBUTING.md) |
| implement from a feature spec | [FEATURES_SPEC/AGENTS.md](FEATURES_SPEC/AGENTS.md) |
## Scratch files live in `agent-workspace/`
Git-ignored. One kebab-case subfolder per task, matching the branch topic:
`agent-workspace/<slug>/`.
| File | Purpose |
| --------------------------- | -------------------------------------------------------------------------------- |
| `plan.md` | The concrete approach — files, symbols, edit order — once direction is confirmed |
| `commit-msg.txt` | `git commit -F agent-workspace/<slug>/commit-msg.txt` |
| `pr-body.md` | `gh pr create --body-file agent-workspace/<slug>/pr-body.md` |
| `implementation-summary.md` | What was built, decisions, trade-offs |
| `NN-prompt.md` | Raw prompt text, numbered, when worth keeping |
- Nothing here is durable. The folder is ignored, so anything worth keeping is
promoted into the PR body or a doc before the task ends.
- Never write scratch files to the repo root. Stray `load_test_summary.md` and
`config.json_` files are what this folder prevents.
- Start `pr-body.md` from `.github/PULL_REQUEST_TEMPLATE.md` and fill every
section.
## Gates
@@ -42,6 +58,11 @@ pnpm i18n:check
`knip` fails on dead code; a file deliberately inert ahead of its consumer goes in
`knip.ts` `ignoreFiles` with a reason.
Ready means every gate is green and the change is covered at the layers
[testing.md](docs/agents/testing.md) asks for. Read the diff against
[code-style.md](docs/agents/code-style.md) first. Re-run the whole checklist after
any fix.
## A good PR
- One slice, under ~400 changed lines, green on its own, linked to its issue.
+2 -2
View File
@@ -19,10 +19,10 @@ services:
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
element-web:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:37d88b4426ff93a884b31c63d5319a7bcee32580f189c724e6b09f793a6de390
image: ghcr.io/element-hq/element-web:develop@sha256:72791f0b2541208f8f7edd9c6b5a7eaf88accd8ed1f0c4db1bc62bfe18eb90fe
element-web-1:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:37d88b4426ff93a884b31c63d5319a7bcee32580f189c724e6b09f793a6de390
image: ghcr.io/element-hq/element-web:develop@sha256:72791f0b2541208f8f7edd9c6b5a7eaf88accd8ed1f0c4db1bc62bfe18eb90fe
volumes:
# The other site opens the embedded package under test in place of the
# copy its image bundles (the first site opens the dev server's app.
+5 -1
View File
@@ -14,7 +14,11 @@ This folder contains documentation for setup, usage, and development of Element
Entry point: [AGENTS.md](../AGENTS.md).
- [Agent workflow](./agents/workflow.md)
- [Architecture](./agents/architecture.md)
- [Code style](./agents/code-style.md)
- [Testing](./agents/testing.md)
Opt-in skills in [`.claude/skills/`](../.claude/skills/):
- `/handoff <task>` — get the first implementation back for a direction check
before lint, the full suite and polish.
-50
View File
@@ -1,50 +0,0 @@
# Agent workflow
## Scratch files live in `agent-workspace/`
Git-ignored. One kebab-case subfolder per task, matching the branch topic:
`agent-workspace/<slug>/`.
| File | Purpose |
| --------------------------- | -------------------------------------------------------------------------------- |
| `plan.md` | The concrete approach — files, symbols, edit order — once direction is confirmed |
| `commit-msg.txt` | `git commit -F agent-workspace/<slug>/commit-msg.txt` |
| `pr-body.md` | `gh pr create --body-file agent-workspace/<slug>/pr-body.md` |
| `implementation-summary.md` | What was built, decisions, trade-offs |
| `NN-prompt.md` | Raw prompt text, numbered, when worth keeping |
- Nothing here is durable. The folder is ignored, so anything worth keeping is
promoted into the PR body, a doc, or a feature spec before the task ends.
- Never write scratch files to the repo root. Stray `load_test_summary.md` and
`config.json_` files are what this folder prevents.
- Start `pr-body.md` from `.github/PULL_REQUEST_TEMPLATE.md` and fill every
section.
- Commit subjects are plain imperative English. No conventional-commits prefixes.
- A `plan.md` and a feature spec sit at different altitudes; neither replaces the
other. A spec in `FEATURES_SPEC/` is durable and deliberately abstract —
behaviour, decisions, acceptance criteria — so the feature can be rebuilt against
a `main` nobody has seen yet. A plan is one slice of it landing on today's
`main`: the paths, symbols and edit order the spec's decisions and criteria must
not name. `FEATURES_SPEC/AGENTS.md` wins wherever the two genuinely overlap.
## Hand off before the quality pass
- Implement the change, run the narrowest check that rules out an obviously broken
handoff, then stop and ask whether the direction is right.
- Full `pnpm lint`, the whole suite, coverage and benchmarks come after the
direction is confirmed.
- Wider refactors, extra tests and documentation polish are follow-up work, not
part of the first handoff.
- If a check is needed before feedback, keep it to the touched code and say why.
## Commit and PR readiness
- Commit once the user confirms direction, or asks for one. Not before. Where the
setup has no git identity, a sandbox included, write the message to
`commit-msg.txt` and hand it over instead.
- Ready means every gate in [AGENTS.md](../../AGENTS.md#gates) is green and the
change is covered at the layers [testing.md](./testing.md) asks for. Read the diff
against [code-style.md](./code-style.md) first.
- Re-run the whole checklist after any fix. Only on green, either way.
- Once a human has started reviewing, fix forward — never force-push a
regeneration over a review in progress.
+1
View File
@@ -162,6 +162,7 @@ describe("createLocalMedia", () => {
media.release("camera");
expect(trackBehind(camera).stop).toHaveBeenCalled();
expect(media.tracks$.value).toEqual([microphone]);
expect(media.requests.map((r) => r.source)).toEqual(["microphone"]);
media.stop();
expect(trackBehind(microphone).stop).toHaveBeenCalled();
+1
View File
@@ -107,6 +107,7 @@ export function createLocalMedia(
if (source !== "microphone" && source !== "camera") return;
capturedTracks.get(source)?.stop();
capturedTracks.delete(source);
requests.delete(source);
publishCapturedTracks();
};
+34 -1
View File
@@ -20,6 +20,7 @@ import {
const frame = {} as VideoFrame;
const controller = {} as TransformStreamDefaultController<VideoFrame>;
const stats = { processingTimeMs: 1, segmentationTimeMs: 1, filterTimeMs: 0 };
describe("BackgroundEffectTransformer", () => {
afterEach(() => {
@@ -66,9 +67,10 @@ describe("BackgroundEffectTransformer", () => {
});
it("reports the first frame carrying an effect, once", async () => {
// The library reports every frame it draws.
vi.spyOn(BackgroundTransformer.prototype, "transform").mockImplementation(
async function (this: BackgroundTransformer) {
this.isFirstFrame = false;
this.options.onFrameProcessed?.(stats);
return Promise.resolve();
},
);
@@ -91,6 +93,37 @@ describe("BackgroundEffectTransformer", () => {
expect(transformer.onFirstFrame).not.toHaveBeenCalled();
});
it("keeps reporting drawn frames to whoever asked", async () => {
vi.spyOn(BackgroundTransformer.prototype, "transform").mockImplementation(
async function (this: BackgroundTransformer) {
this.options.onFrameProcessed?.(stats);
return Promise.resolve();
},
);
const onFrameProcessed = vi.fn();
const transformer = new BackgroundEffectTransformer({ onFrameProcessed });
await transformer.transform(frame, controller);
expect(onFrameProcessed).toHaveBeenCalledWith(stats);
});
it("never sends the first frame on untouched", async () => {
// The library copies its first frame through untouched, ahead of the
// drawn one, unless told the first frame is past
const seen: boolean[] = [];
vi.spyOn(BackgroundTransformer.prototype, "transform").mockImplementation(
async function (this: BackgroundTransformer) {
seen.push(this.isFirstFrame);
return Promise.resolve();
},
);
const transformer = new BackgroundEffectTransformer({});
expect(transformer.isFirstFrame).toBe(true);
await transformer.transform(frame, controller);
expect(seen).toEqual([false]);
});
it("passes frames on untouched once disabled, even after blurring", async () => {
const transformer = new BackgroundEffectTransformer({});
await transformer.update({ blurRadius: 15, backgroundDisabled: false });
+28 -5
View File
@@ -41,18 +41,41 @@ const wasmFileset: WasmFileset = { wasmLoaderPath, wasmBinaryPath };
* resource fetched from the public internet.
*/
export class BackgroundEffectTransformer extends BackgroundTransformer {
/** Called once, when the first frame carrying an effect has been drawn. */
/**
* Called once, when the first frame carrying an effect has been drawn.
* This is used for drewAFrame$ to show the correct loading state.
*/
public onFirstFrame: (() => void) | undefined;
private drewAFrame = false;
public constructor(opts: BackgroundOptions) {
super(opts);
// Called by the library for a frame it drew, never for one it passed on
// untouched.
this.options.onFrameProcessed = (stats): void => {
opts.onFrameProcessed?.(stats);
if (this.drewAFrame) return;
this.drewAFrame = true;
this.onFirstFrame?.();
};
}
public override async transform(
frame: VideoFrame,
controller: TransformStreamDefaultController<VideoFrame>,
): Promise<void> {
const priming = this.isFirstFrame;
// From the LK docs:
// > Render a copy of the first frame is rendered to the screen as soon as possible to act
// > as a less jarring initial state than a solid color while the synchronous work below
// > (segmentation + frame rendering) occurs.
// We do not want this copy (it leaks unblurred media).
// So we always set `isFirstFrame` to false -> never show an unprocessed frame.
// It does not have any impact other than setting the unprocessed frame while loading the processor.
//
// To not show the blank frame, we have our own mechanism for hiding the loading overlay:
// `onFirstFrame` is what the EC UI will listen to and only then show the video feed.
this.isFirstFrame = false;
await super.transform(frame, controller);
// Cleared only once a frame has been through the segmenter; a frame
// passed through untouched leaves it set.
if (priming && !this.isFirstFrame) this.onFirstFrame?.();
}
/**